mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 15:03:26 +00:00
resolve merge conflicts
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -6,6 +6,7 @@
|
||||
|
||||
## Secrets Manager team files ##
|
||||
bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev
|
||||
apps/web/src/app/secrets-manager/ @bitwarden/team-secrets-manager-dev
|
||||
|
||||
## Auth team files ##
|
||||
apps/browser/src/auth @bitwarden/team-auth-dev
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"loginOrCreateNewAccount": {
|
||||
"message": "Log in or create a new account to access your secure vault."
|
||||
},
|
||||
"inviteAccepted": {
|
||||
"message": "Invitation accepted"
|
||||
},
|
||||
"createAccount": {
|
||||
"message": "Create account"
|
||||
},
|
||||
@@ -68,6 +71,12 @@
|
||||
"masterPassHint": {
|
||||
"message": "Master password hint (optional)"
|
||||
},
|
||||
"joinOrganization": {
|
||||
"message": "Join organization"
|
||||
},
|
||||
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||
"message": "Finish joining this organization by setting a master password."
|
||||
},
|
||||
"tab": {
|
||||
"message": "Tab"
|
||||
},
|
||||
@@ -3037,6 +3046,10 @@
|
||||
"message": "Unlock your account to view matching logins",
|
||||
"description": "Text to display in overlay when the account is locked."
|
||||
},
|
||||
"unlockYourAccountToViewAutofillSuggestions": {
|
||||
"message": "Unlock your account to view autofill suggestions",
|
||||
"description": "Text to display in overlay when the account is locked."
|
||||
},
|
||||
"unlockAccount": {
|
||||
"message": "Unlock account",
|
||||
"description": "Button text to display in overlay when the account is locked."
|
||||
@@ -3061,6 +3074,22 @@
|
||||
"message": "Add new vault item",
|
||||
"description": "Screen reader text (aria-label) for new item button in overlay"
|
||||
},
|
||||
"newLogin": {
|
||||
"message": "New login",
|
||||
"description": "Button text to display within inline menu when there are no matching items on a login field"
|
||||
},
|
||||
"addNewLoginItem": {
|
||||
"message": "Add new vault login item",
|
||||
"description": "Screen reader text (aria-label) for new login button within inline menu"
|
||||
},
|
||||
"newCard": {
|
||||
"message": "New card",
|
||||
"description": "Button text to display within inline menu when there are no matching items on a credit card field"
|
||||
},
|
||||
"addNewCardItem": {
|
||||
"message": "Add new vault card item",
|
||||
"description": "Screen reader text (aria-label) for new card button within inline menu"
|
||||
},
|
||||
"bitwardenOverlayMenuAvailable": {
|
||||
"message": "Bitwarden auto-fill menu available. Press the down arrow key to select.",
|
||||
"description": "Screen reader text for announcing when the overlay opens on the page"
|
||||
@@ -3734,6 +3763,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cardNumberEndsWith": {
|
||||
"message": "card number ends with",
|
||||
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
|
||||
},
|
||||
"loginCredentials": {
|
||||
"message": "Login credentials"
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ export type WebsiteIconData = {
|
||||
export type FocusedFieldData = {
|
||||
focusedFieldStyles: Partial<CSSStyleDeclaration>;
|
||||
focusedFieldRects: Partial<DOMRect>;
|
||||
filledByCipherType?: CipherType;
|
||||
tabId?: number;
|
||||
frameId?: number;
|
||||
};
|
||||
@@ -50,13 +51,26 @@ export type InlineMenuPosition = {
|
||||
list?: InlineMenuElementPosition;
|
||||
};
|
||||
|
||||
export type NewLoginCipherData = {
|
||||
uri?: string;
|
||||
hostname: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type NewCardCipherData = {
|
||||
cardholderName: string;
|
||||
number: string;
|
||||
expirationMonth: string;
|
||||
expirationYear: string;
|
||||
expirationDate?: string;
|
||||
cvv: string;
|
||||
};
|
||||
|
||||
export type OverlayAddNewItemMessage = {
|
||||
login?: {
|
||||
uri?: string;
|
||||
hostname: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
addNewCipherType?: CipherType;
|
||||
login?: NewLoginCipherData;
|
||||
card?: NewCardCipherData;
|
||||
};
|
||||
|
||||
export type CloseInlineMenuMessage = {
|
||||
@@ -91,6 +105,7 @@ export type OverlayPortMessage = {
|
||||
command: string;
|
||||
direction?: string;
|
||||
inlineMenuCipherId?: string;
|
||||
addNewCipherType?: CipherType;
|
||||
};
|
||||
|
||||
export type InlineMenuCipherData = {
|
||||
@@ -178,7 +193,7 @@ export type InlineMenuListPortMessageHandlers = {
|
||||
autofillInlineMenuBlurred: () => void;
|
||||
unlockVault: ({ port }: PortConnectionParam) => void;
|
||||
fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
addNewVaultItem: ({ port }: PortConnectionParam) => void;
|
||||
addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
|
||||
@@ -187,5 +202,5 @@ export type InlineMenuListPortMessageHandlers = {
|
||||
export interface OverlayBackground {
|
||||
init(): Promise<void>;
|
||||
removePageDetails(tabId: number): void;
|
||||
updateOverlayCiphers(): Promise<void>;
|
||||
updateOverlayCiphers(updateAllCipherTypes?: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -322,6 +322,7 @@ describe("OverlayBackground", () => {
|
||||
it("removes the page details and port key for a specific tab from the pageDetailsForTab object", async () => {
|
||||
await initOverlayElementPorts();
|
||||
const tabId = 1;
|
||||
portKeyForTabSpy[tabId] = "portKey";
|
||||
sendMockExtensionMessage(
|
||||
{ command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() },
|
||||
mock<chrome.runtime.MessageSender>({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }),
|
||||
@@ -705,6 +706,13 @@ describe("OverlayBackground", () => {
|
||||
type: CipherType.Card,
|
||||
card: { subTitle: "subtitle-2" },
|
||||
});
|
||||
const cipher3 = mock<CipherView>({
|
||||
id: "id-3",
|
||||
localData: { lastUsedDate: 222 },
|
||||
name: "name-3",
|
||||
type: CipherType.Login,
|
||||
login: { username: "username-3", uri: url },
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
@@ -751,16 +759,53 @@ describe("OverlayBackground", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => {
|
||||
it("queries all cipher types, sorts them by last used, and formats them for usage in the overlay", async () => {
|
||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||
|
||||
await overlayBackground.updateOverlayCiphers();
|
||||
|
||||
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
|
||||
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||
new Map([
|
||||
["inline-menu-cipher-0", cipher2],
|
||||
["inline-menu-cipher-1", cipher1],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("queries only login ciphers when not updating all cipher types", async () => {
|
||||
overlayBackground["cardAndIdentityCiphers"] = new Set([]);
|
||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher3, cipher1]);
|
||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||
|
||||
await overlayBackground.updateOverlayCiphers(false);
|
||||
|
||||
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
|
||||
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||
new Map([
|
||||
["inline-menu-cipher-0", cipher1],
|
||||
["inline-menu-cipher-1", cipher3],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("queries all cipher types when the card and identity ciphers set is not built when only updating login ciphers", async () => {
|
||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||
|
||||
await overlayBackground.updateOverlayCiphers(false);
|
||||
|
||||
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
|
||||
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [CipherType.Card]);
|
||||
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
|
||||
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
|
||||
new Map([
|
||||
["inline-menu-cipher-0", cipher2],
|
||||
@@ -771,6 +816,7 @@ describe("OverlayBackground", () => {
|
||||
|
||||
it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => {
|
||||
overlayBackground["inlineMenuListPort"] = mock<chrome.runtime.Port>();
|
||||
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id });
|
||||
cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]);
|
||||
cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1);
|
||||
getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab);
|
||||
@@ -780,21 +826,6 @@ describe("OverlayBackground", () => {
|
||||
expect(overlayBackground["inlineMenuListPort"].postMessage).toHaveBeenCalledWith({
|
||||
command: "updateAutofillInlineMenuListCiphers",
|
||||
ciphers: [
|
||||
{
|
||||
card: cipher2.card.subTitle,
|
||||
favorite: cipher2.favorite,
|
||||
icon: {
|
||||
fallbackImage: "",
|
||||
icon: "bwi-credit-card",
|
||||
image: undefined,
|
||||
imageEnabled: true,
|
||||
},
|
||||
id: "inline-menu-cipher-0",
|
||||
login: null,
|
||||
name: "name-2",
|
||||
reprompt: cipher2.reprompt,
|
||||
type: 3,
|
||||
},
|
||||
{
|
||||
card: null,
|
||||
favorite: cipher1.favorite,
|
||||
@@ -810,7 +841,7 @@ describe("OverlayBackground", () => {
|
||||
},
|
||||
name: "name-1",
|
||||
reprompt: cipher1.reprompt,
|
||||
type: 1,
|
||||
type: CipherType.Login,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -884,6 +915,7 @@ describe("OverlayBackground", () => {
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "autofillOverlayAddNewVaultItem",
|
||||
addNewCipherType: CipherType.Login,
|
||||
login: {
|
||||
uri: "https://tacos.com",
|
||||
hostname: "",
|
||||
@@ -899,6 +931,29 @@ describe("OverlayBackground", () => {
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a new card cipher", async () => {
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "autofillOverlayAddNewVaultItem",
|
||||
addNewCipherType: CipherType.Card,
|
||||
card: {
|
||||
cardholderName: "cardholderName",
|
||||
number: "4242424242424242",
|
||||
expirationMonth: "12",
|
||||
expirationYear: "2025",
|
||||
expirationDate: "12/25",
|
||||
cvv: "123",
|
||||
},
|
||||
},
|
||||
sender,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher");
|
||||
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkIsInlineMenuCiphersPopulated message handler", () => {
|
||||
@@ -929,8 +984,9 @@ describe("OverlayBackground", () => {
|
||||
|
||||
it("returns true if the overlay login ciphers are populated", async () => {
|
||||
overlayBackground["inlineMenuCiphers"] = new Map([
|
||||
["inline-menu-cipher-0", mock<CipherView>()],
|
||||
["inline-menu-cipher-0", mock<CipherView>({ type: CipherType.Login })],
|
||||
]);
|
||||
await overlayBackground["getInlineMenuCipherData"]();
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{ command: "checkIsInlineMenuCiphersPopulated" },
|
||||
@@ -2029,12 +2085,16 @@ describe("OverlayBackground", () => {
|
||||
sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender);
|
||||
await flushPromises();
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey });
|
||||
sendPortMessage(listMessageConnectorSpy, {
|
||||
command: "addNewVaultItem",
|
||||
portKey,
|
||||
addNewCipherType: CipherType.Login,
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(tabsSendMessageSpy).toHaveBeenCalledWith(
|
||||
sender.tab,
|
||||
{ command: "addNewVaultItemFromOverlay" },
|
||||
{ command: "addNewVaultItemFromOverlay", addNewCipherType: CipherType.Login },
|
||||
{ frameId: sender.frameId },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
@@ -54,6 +55,8 @@ import {
|
||||
CloseInlineMenuMessage,
|
||||
InlineMenuPosition,
|
||||
ToggleInlineMenuHiddenMessage,
|
||||
NewLoginCipherData,
|
||||
NewCardCipherData,
|
||||
} from "./abstractions/overlay.background";
|
||||
|
||||
export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
@@ -69,6 +72,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private inlineMenuCiphers: Map<string, CipherView> = new Map();
|
||||
private inlineMenuPageTranslations: Record<string, string>;
|
||||
private inlineMenuPosition: InlineMenuPosition = {};
|
||||
private cardAndIdentityCiphers: Set<CipherView> | null = null;
|
||||
private currentInlineMenuCiphersCount: number = 0;
|
||||
private delayedCloseTimeout: number | NodeJS.Timeout;
|
||||
private startInlineMenuFadeInSubject = new Subject<void>();
|
||||
private cancelInlineMenuFadeInSubject = new Subject<boolean>();
|
||||
@@ -132,7 +137,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(),
|
||||
unlockVault: ({ port }) => this.unlockVault(port),
|
||||
fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port),
|
||||
addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
|
||||
addNewVaultItem: ({ message, port }) => this.getNewVaultItemDetails(message, port),
|
||||
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
|
||||
redirectAutofillInlineMenuFocusOut: ({ message, port }) =>
|
||||
this.redirectInlineMenuFocusOut(message, port),
|
||||
@@ -220,7 +225,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* Queries all ciphers for the given url, and sorts them by last used. Will not update the
|
||||
* list of ciphers if the extension is not unlocked.
|
||||
*/
|
||||
async updateOverlayCiphers() {
|
||||
async updateOverlayCiphers(updateAllCipherTypes = true) {
|
||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
if (this.focusedFieldData) {
|
||||
@@ -235,9 +240,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
}
|
||||
|
||||
this.inlineMenuCiphers = new Map();
|
||||
const ciphersViews = (
|
||||
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
|
||||
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||
const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes);
|
||||
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
|
||||
this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
|
||||
}
|
||||
@@ -249,6 +252,51 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the decrypted ciphers within a user's vault based on the current tab's URL.
|
||||
*
|
||||
* @param currentTab - The current tab
|
||||
* @param updateAllCipherTypes - Identifies credit card and identity cipher types should also be updated
|
||||
*/
|
||||
private async getCipherViews(
|
||||
currentTab: chrome.tabs.Tab,
|
||||
updateAllCipherTypes: boolean,
|
||||
): Promise<CipherView[]> {
|
||||
if (updateAllCipherTypes || !this.cardAndIdentityCiphers) {
|
||||
return this.getAllCipherTypeViews(currentTab);
|
||||
}
|
||||
|
||||
const cipherViews = (
|
||||
await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "")
|
||||
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||
|
||||
return cipherViews.concat(...this.cardAndIdentityCiphers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries all cipher types from the user's vault returns them sorted by last used.
|
||||
*
|
||||
* @param currentTab - The current tab
|
||||
*/
|
||||
private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise<CipherView[]> {
|
||||
if (!this.cardAndIdentityCiphers) {
|
||||
this.cardAndIdentityCiphers = new Set([]);
|
||||
}
|
||||
|
||||
this.cardAndIdentityCiphers.clear();
|
||||
const cipherViews = (
|
||||
await this.cipherService.getAllDecryptedForUrl(currentTab.url, [CipherType.Card])
|
||||
).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
|
||||
for (let cipherIndex = 0; cipherIndex < cipherViews.length; cipherIndex++) {
|
||||
const cipherView = cipherViews[cipherIndex];
|
||||
if (cipherView.type === CipherType.Card && !this.cardAndIdentityCiphers.has(cipherView)) {
|
||||
this.cardAndIdentityCiphers.add(cipherView);
|
||||
}
|
||||
}
|
||||
|
||||
return cipherViews;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips out unnecessary data from the ciphers and returns an array of
|
||||
* objects that contain the cipher data needed for the inline menu list.
|
||||
@@ -260,6 +308,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) {
|
||||
const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex];
|
||||
if (this.focusedFieldData?.filledByCipherType !== cipher.type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
inlineMenuCipherData.push({
|
||||
id: inlineMenuCipherId,
|
||||
@@ -273,6 +324,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
});
|
||||
}
|
||||
|
||||
this.currentInlineMenuCiphersCount = inlineMenuCipherData.length;
|
||||
return inlineMenuCipherData;
|
||||
}
|
||||
|
||||
@@ -1062,7 +1114,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"),
|
||||
toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"),
|
||||
listPageTitle: this.i18nService.translate("bitwardenVault"),
|
||||
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"),
|
||||
unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewAutofillSuggestions"),
|
||||
unlockAccount: this.i18nService.translate("unlockAccount"),
|
||||
fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"),
|
||||
username: this.i18nService.translate("username")?.toLowerCase(),
|
||||
@@ -1070,6 +1122,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
noItemsToShow: this.i18nService.translate("noItemsToShow"),
|
||||
newItem: this.i18nService.translate("newItem"),
|
||||
addNewVaultItem: this.i18nService.translate("addNewVaultItem"),
|
||||
newLogin: this.i18nService.translate("newLogin"),
|
||||
addNewLoginItem: this.i18nService.translate("addNewLoginItem"),
|
||||
newCard: this.i18nService.translate("newCard"),
|
||||
addNewCardItem: this.i18nService.translate("addNewCardItem"),
|
||||
cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1100,16 +1157,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* Triggers adding a new vault item from the overlay. Gathers data
|
||||
* input by the user before calling to open the add/edit window.
|
||||
*
|
||||
* @param addNewCipherType - The type of cipher to add
|
||||
* @param sender - The sender of the port message
|
||||
*/
|
||||
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
|
||||
if (!this.senderTabHasFocusedField(sender)) {
|
||||
private getNewVaultItemDetails(
|
||||
{ addNewCipherType }: OverlayPortMessage,
|
||||
{ sender }: chrome.runtime.Port,
|
||||
) {
|
||||
if (!addNewCipherType || !this.senderTabHasFocusedField(sender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
void BrowserApi.tabSendMessage(
|
||||
sender.tab,
|
||||
{ command: "addNewVaultItemFromOverlay" },
|
||||
{ command: "addNewVaultItemFromOverlay", addNewCipherType },
|
||||
{
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
@@ -1120,18 +1181,60 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* Handles adding a new vault item from the overlay. Gathers data login
|
||||
* data captured in the extension message.
|
||||
*
|
||||
* @param addNewCipherType - The type of cipher to add
|
||||
* @param login - The login data captured from the extension message
|
||||
* @param card - The card data captured from the extension message
|
||||
* @param sender - The sender of the extension message
|
||||
*/
|
||||
private async addNewVaultItem(
|
||||
{ login }: OverlayAddNewItemMessage,
|
||||
{ addNewCipherType, login, card }: OverlayAddNewItemMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
if (!login) {
|
||||
if (!addNewCipherType) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeInlineMenu(sender);
|
||||
const cipherView: CipherView = this.buildNewVaultItemCipherView({
|
||||
addNewCipherType,
|
||||
login,
|
||||
card,
|
||||
});
|
||||
|
||||
if (cipherView) {
|
||||
this.closeInlineMenu(sender);
|
||||
await this.cipherService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a new cipher view with the provided vault item data.
|
||||
*
|
||||
* @param addNewCipherType - The type of cipher to add
|
||||
* @param login - The login data captured from the extension message
|
||||
* @param card - The card data captured from the extension message
|
||||
*/
|
||||
private buildNewVaultItemCipherView({ addNewCipherType, login, card }: OverlayAddNewItemMessage) {
|
||||
if (login && addNewCipherType === CipherType.Login) {
|
||||
return this.buildLoginCipherView(login);
|
||||
}
|
||||
|
||||
if (card && addNewCipherType === CipherType.Card) {
|
||||
return this.buildCardCipherView(card);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new login cipher view with the provided login data.
|
||||
*
|
||||
* @param login - The login data captured from the extension message
|
||||
*/
|
||||
private buildLoginCipherView(login: NewLoginCipherData) {
|
||||
const uriView = new LoginUriView();
|
||||
uriView.uri = login.uri;
|
||||
|
||||
@@ -1146,13 +1249,30 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = loginView;
|
||||
|
||||
await this.cipherService.setAddEditCipherInfo({
|
||||
cipher: cipherView,
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
return cipherView;
|
||||
}
|
||||
|
||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
/**
|
||||
* Builds a new card cipher view with the provided card data.
|
||||
*
|
||||
* @param card - The card data captured from the extension message
|
||||
*/
|
||||
private buildCardCipherView(card: NewCardCipherData) {
|
||||
const cardView = new CardView();
|
||||
cardView.cardholderName = card.cardholderName || "";
|
||||
cardView.number = card.number || "";
|
||||
cardView.expMonth = card.expirationMonth || "";
|
||||
cardView.expYear = card.expirationYear || "";
|
||||
cardView.code = card.cvv || "";
|
||||
cardView.brand = card.number ? CardView.getCardBrandByPatterns(card.number) : "";
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "";
|
||||
cipherView.folderId = null;
|
||||
cipherView.type = CipherType.Card;
|
||||
cipherView.card = cardView;
|
||||
|
||||
return cipherView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1209,7 +1329,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* @param sender - The sender of the message
|
||||
*/
|
||||
private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) {
|
||||
return this.senderTabHasFocusedField(sender) && this.inlineMenuCiphers.size > 0;
|
||||
return this.senderTabHasFocusedField(sender) && this.currentInlineMenuCiphersCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1477,6 +1597,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
portName: isInlineMenuListPort
|
||||
? AutofillOverlayPort.ListMessageConnector
|
||||
: AutofillOverlayPort.ButtonMessageConnector,
|
||||
filledByCipherType: this.focusedFieldData?.filledByCipherType,
|
||||
});
|
||||
void this.updateInlineMenuPosition(
|
||||
{
|
||||
|
||||
@@ -104,7 +104,7 @@ export default class TabsBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.overlayBackground.updateOverlayCiphers();
|
||||
await this.overlayBackground.updateOverlayCiphers(false);
|
||||
|
||||
if (this.main.onUpdatedRan) {
|
||||
return;
|
||||
@@ -134,7 +134,7 @@ export default class TabsBackground {
|
||||
await Promise.all([
|
||||
this.main.refreshBadge(),
|
||||
this.main.refreshMenu(),
|
||||
this.overlayBackground.updateOverlayCiphers(),
|
||||
this.overlayBackground.updateOverlayCiphers(false),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum";
|
||||
import AutofillScript from "../../models/autofill-script";
|
||||
@@ -18,6 +19,7 @@ export type AutofillExtensionMessage = {
|
||||
isFocusingFieldElement?: boolean;
|
||||
authStatus?: AuthenticationStatus;
|
||||
isOpeningFullInlineMenu?: boolean;
|
||||
addNewCipherType?: CipherType;
|
||||
data?: {
|
||||
direction?: "previous" | "next" | "current";
|
||||
forceCloseInlineMenu?: boolean;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
/**
|
||||
* Represents a single field that is collected from the page source and is potentially autofilled.
|
||||
*/
|
||||
@@ -106,4 +108,6 @@ export default class AutofillField {
|
||||
rel?: string | null;
|
||||
|
||||
checked?: boolean;
|
||||
|
||||
filledByCipherType?: CipherType;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background";
|
||||
|
||||
@@ -14,6 +15,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
|
||||
theme: string;
|
||||
translations: Record<string, string>;
|
||||
ciphers?: InlineMenuCipherData[];
|
||||
filledByCipherType?: CipherType;
|
||||
portKey: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu 1`] = `
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that does not have a fill by cipher type 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
@@ -13,7 +13,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
|
||||
class="inline-menu-list-button-container"
|
||||
>
|
||||
<button
|
||||
aria-label="addNewVaultItem, opensInANewWindow"
|
||||
aria-label=", opensInANewWindow"
|
||||
class="add-new-item-button inline-menu-list-button"
|
||||
id="new-item-button"
|
||||
tabindex="-1"
|
||||
@@ -45,13 +45,112 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
newItem
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that should be filled by a card cipher 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
<div
|
||||
class="no-items inline-menu-list-message"
|
||||
>
|
||||
noItemsToShow
|
||||
</div>
|
||||
<div
|
||||
class="inline-menu-list-button-container"
|
||||
>
|
||||
<button
|
||||
aria-label=", opensInANewWindow"
|
||||
class="add-new-item-button inline-menu-list-button"
|
||||
id="new-item-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .49h16v16H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu that should be filled by a login cipher 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
<div
|
||||
class="no-items inline-menu-list-message"
|
||||
>
|
||||
noItemsToShow
|
||||
</div>
|
||||
<div
|
||||
class="inline-menu-list-button-container"
|
||||
>
|
||||
<button
|
||||
aria-label=", opensInANewWindow"
|
||||
class="add-new-item-button inline-menu-list-button"
|
||||
id="new-item-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .49h16v16H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of login ciphers 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
@@ -87,7 +186,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
website login 1
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
class="cipher-subtitle"
|
||||
title="username1"
|
||||
>
|
||||
username1
|
||||
@@ -156,7 +255,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
website login 2
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
class="cipher-subtitle"
|
||||
title="username2"
|
||||
>
|
||||
username2
|
||||
@@ -297,7 +396,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
website login 4
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
class="cipher-subtitle"
|
||||
title="username4"
|
||||
>
|
||||
username4
|
||||
@@ -367,7 +466,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
website login 5
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
class="cipher-subtitle"
|
||||
title="username5"
|
||||
>
|
||||
username5
|
||||
@@ -437,7 +536,439 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
website login 6
|
||||
</span>
|
||||
<span
|
||||
class="cipher-user-login"
|
||||
class="cipher-subtitle"
|
||||
title="username6"
|
||||
>
|
||||
username6
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 6, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the views for a list of card ciphers 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
<ul
|
||||
class="inline-menu-list-actions"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, username1"
|
||||
aria-label="fillCredentialsFor website login 1"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 1"
|
||||
>
|
||||
website login 1
|
||||
</span>
|
||||
<span
|
||||
class="cipher-subtitle"
|
||||
title="username1"
|
||||
>
|
||||
username1
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 1, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, username2"
|
||||
aria-label="fillCredentialsFor website login 2"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon bwi bw-icon"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 2"
|
||||
>
|
||||
website login 2
|
||||
</span>
|
||||
<span
|
||||
class="cipher-subtitle"
|
||||
title="username2"
|
||||
>
|
||||
username2
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 2, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, "
|
||||
aria-label="fillCredentialsFor "
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon bwi bw-icon"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view , opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, username4"
|
||||
aria-label="fillCredentialsFor website login 4"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M18.026 17.842c-1.418 1.739-3.517 2.84-5.86 2.84a7.364 7.364 0 0 1-3.431-.848l.062-.15.062-.151.063-.157c.081-.203.17-.426.275-.646.133-.28.275-.522.426-.68.026-.028.101-.075.275-.115.165-.037.376-.059.629-.073.138-.008.288-.014.447-.02.399-.016.847-.034 1.266-.092.314-.044.566-.131.755-.271a.884.884 0 0 0 .352-.555c.037-.2.008-.392-.03-.543-.035-.135-.084-.264-.12-.355l-.01-.03a4.26 4.26 0 0 0-.145-.33c-.126-.264-.237-.497-.288-1.085-.03-.344.09-.73.251-1.138l.089-.22c.05-.123.1-.247.14-.355.064-.171.129-.375.129-.566a1.51 1.51 0 0 0-.134-.569 2.573 2.573 0 0 0-.319-.547c-.246-.323-.635-.669-1.093-.669-.44 0-1.006.169-1.487.368-.246.102-.48.216-.68.33-.192.111-.372.235-.492.359-.93.96-1.48 1.239-1.81 1.258-.277.017-.478-.15-.736-.525a9.738 9.738 0 0 1-.19-.29l-.006-.01a11.568 11.568 0 0 0-.198-.305 2.76 2.76 0 0 0-.521-.6 1.39 1.39 0 0 0-1.088-.314 8.302 8.302 0 0 1 1.987-3.936c.055.342.146.626.272.856.23.42.561.64.926.716.406.086.857-.061 1.26-.216.125-.047.248-.097.372-.147.309-.125.618-.25.947-.341.26-.072.581-.057.959.012.264.049.529.118.8.19l.36.091c.379.094.782.178 1.135.148.374-.032.733-.197.934-.623a.874.874 0 0 0 .024-.752c-.087-.197-.24-.355-.35-.47-.26-.267-.412-.427-.412-.685 0-.125.037-.2.09-.263a.982.982 0 0 1 .303-.211c.059-.03.119-.058.183-.089l.036-.016a3.79 3.79 0 0 0 .236-.118c.047-.026.098-.056.148-.093 1.936.747 3.51 2.287 4.368 4.249a7.739 7.739 0 0 0-.031-.004c-.38-.047-.738-.056-1.063.061-.34.123-.603.368-.817.74-.122.211-.284.43-.463.67l-.095.129c-.207.281-.431.595-.58.92-.15.326-.245.705-.142 1.103.104.397.387.738.837 1.036.099.065.225.112.314.145l.02.008c.108.04.195.074.268.117.07.042.106.08.124.114.017.03.037.087.022.206-.047.376-.069.73-.052 1.034.017.292.071.59.218.809.118.174.12.421.108.786v.01a2.46 2.46 0 0 0 .021.518.809.809 0 0 0 .15.35Zm1.357.059a9.654 9.654 0 0 0 1.62-5.386c0-5.155-3.957-9.334-8.836-9.334-4.88 0-8.836 4.179-8.836 9.334 0 3.495 1.82 6.543 4.513 8.142v.093h.161a8.426 8.426 0 0 0 4.162 1.098c2.953 0 5.568-1.53 7.172-3.882a.569.569 0 0 0 .048-.062l-.004-.003ZM8.152 19.495a43.345 43.345 0 0 1 .098-.238l.057-.142c.082-.205.182-.455.297-.698.143-.301.323-.624.552-.864.163-.172.392-.254.602-.302.219-.05.473-.073.732-.088.162-.01.328-.016.495-.023.386-.015.782-.03 1.168-.084.255-.036.392-.099.461-.15.06-.045.076-.084.083-.12a.534.534 0 0 0-.02-.223 2.552 2.552 0 0 0-.095-.278l-.01-.027a3.128 3.128 0 0 0-.104-.232c-.134-.282-.31-.65-.374-1.381-.046-.533.138-1.063.3-1.472.035-.09.069-.172.1-.249.046-.11.086-.21.123-.31.062-.169.083-.264.083-.312a.812.812 0 0 0-.076-.283 1.867 1.867 0 0 0-.23-.394c-.21-.274-.428-.408-.577-.408-.315 0-.788.13-1.246.32a5.292 5.292 0 0 0-.603.293 1.727 1.727 0 0 0-.347.244c-.936.968-1.641 1.421-2.235 1.457-.646.04-1.036-.413-1.31-.813-.07-.103-.139-.21-.203-.311l-.005-.007c-.064-.101-.125-.197-.188-.29a2.098 2.098 0 0 0-.387-.453.748.748 0 0 0-.436-.18c-.1-.006-.22.005-.365.046a8.707 8.707 0 0 0-.056.992c0 2.957 1.488 5.547 3.716 6.98Zm10.362-2.316.003-.192.002-.046c.01-.305.026-.786-.232-1.169-.036-.054-.082-.189-.096-.444-.014-.243.003-.55.047-.9a1.051 1.051 0 0 0-.105-.649.987.987 0 0 0-.374-.374 2.285 2.285 0 0 0-.367-.166h-.003a1.243 1.243 0 0 1-.205-.088c-.369-.244-.505-.46-.549-.629-.044-.168-.015-.364.099-.61.115-.25.297-.511.507-.796l.089-.12c.178-.239.368-.495.512-.745.152-.263.302-.382.466-.441.18-.065.416-.073.77-.03.142.018.275.04.397.063.274.837.423 1.736.423 2.671a8.45 8.45 0 0 1-1.384 4.665Zm-4.632-12.63a7.362 7.362 0 0 0-1.715-.201c-1.89 0-3.621.716-4.965 1.905.025.54.12.887.24 1.105.13.238.295.34.482.38.2.042.484-.027.905-.188l.328-.13c.32-.13.681-.275 1.048-.377.398-.111.833-.075 1.24 0 .289.053.59.132.871.205l.326.084c.383.094.694.151.932.13.216-.017.326-.092.395-.237.039-.083.027-.114.014-.144-.027-.062-.088-.136-.212-.264l-.043-.044c-.218-.222-.567-.578-.567-1.142 0-.305.101-.547.262-.734.137-.159.308-.267.46-.347Z"
|
||||
fill="#777"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 4"
|
||||
>
|
||||
website login 4
|
||||
</span>
|
||||
<span
|
||||
class="cipher-subtitle"
|
||||
title="username4"
|
||||
>
|
||||
username4
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 4, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, username5"
|
||||
aria-label="fillCredentialsFor website login 5"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 5"
|
||||
>
|
||||
website login 5
|
||||
</span>
|
||||
<span
|
||||
class="cipher-subtitle"
|
||||
title="username5"
|
||||
>
|
||||
username5
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="view website login 5, opensInANewWindow"
|
||||
class="view-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
|
||||
fill="#175DDC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 .113h20v19.773H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="inline-menu-list-actions-item"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
class="cipher-container"
|
||||
>
|
||||
<button
|
||||
aria-description="username, username6"
|
||||
aria-label="fillCredentialsFor website login 6"
|
||||
class="fill-cipher-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="cipher-icon"
|
||||
style="background-image: url(https://jest-testing-website.com/image.png);"
|
||||
/>
|
||||
<span
|
||||
class="cipher-details"
|
||||
>
|
||||
<span
|
||||
class="cipher-name"
|
||||
title="website login 6"
|
||||
>
|
||||
website login 6
|
||||
</span>
|
||||
<span
|
||||
class="cipher-subtitle"
|
||||
title="username6"
|
||||
>
|
||||
username6
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { createInitAutofillInlineMenuListMessageMock } from "../../../../spec/autofill-mocks";
|
||||
import {
|
||||
createAutofillOverlayCipherDataMock,
|
||||
createInitAutofillInlineMenuListMessageMock,
|
||||
} from "../../../../spec/autofill-mocks";
|
||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||
|
||||
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
||||
@@ -69,7 +73,33 @@ describe("AutofillInlineMenuList", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("creates the views for the no results inline menu", () => {
|
||||
it("creates the views for the no results inline menu that should be filled by a login cipher", () => {
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("creates the views for the no results inline menu that should be filled by a card cipher", () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillInlineMenuListMessageMock({
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
ciphers: [],
|
||||
filledByCipherType: CipherType.Card,
|
||||
portKey,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("creates the views for the no results inline menu that does not have a fill by cipher type", () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillInlineMenuListMessageMock({
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
ciphers: [],
|
||||
filledByCipherType: undefined,
|
||||
portKey,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -80,7 +110,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
addVaultItemButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{ command: "addNewVaultItem", portKey },
|
||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
||||
"*",
|
||||
);
|
||||
});
|
||||
@@ -91,7 +121,37 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||
});
|
||||
|
||||
it("creates the view for a list of ciphers", () => {
|
||||
it("creates the view for a list of login ciphers", () => {
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("creates the views for a list of card ciphers", () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillInlineMenuListMessageMock({
|
||||
filledByCipherType: CipherType.Card,
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
type: CipherType.Card,
|
||||
card: "Visa, *4234",
|
||||
login: null,
|
||||
icon: {
|
||||
imageEnabled: true,
|
||||
icon: "bw-id-card card-visa",
|
||||
},
|
||||
}),
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
type: CipherType.Card,
|
||||
card: "*2234",
|
||||
login: null,
|
||||
icon: {
|
||||
imageEnabled: true,
|
||||
icon: "bw-id-card card-visa",
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import "@webcomponents/custom-elements";
|
||||
import "lit/polyfill-support.js";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||
import { buildSvgDomElement } from "../../../../utils";
|
||||
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons";
|
||||
import {
|
||||
InitAutofillInlineMenuListMessage,
|
||||
AutofillInlineMenuListWindowMessageHandlers,
|
||||
InitAutofillInlineMenuListMessage,
|
||||
} from "../../abstractions/autofill-inline-menu-list";
|
||||
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
|
||||
|
||||
@@ -21,6 +22,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
private cipherListScrollIsDebounced = false;
|
||||
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
|
||||
private currentCipherIndex = 0;
|
||||
private filledByCipherType: CipherType;
|
||||
private readonly showCiphersPerPage = 6;
|
||||
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
||||
{
|
||||
@@ -46,6 +48,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* @param authStatus - The current authentication status.
|
||||
* @param ciphers - The ciphers to display in the inline menu list.
|
||||
* @param portKey - Background generated key that allows the port to communicate with the background.
|
||||
* @param filledByCipherType - The type of cipher that fills the current field.
|
||||
*/
|
||||
private async initAutofillInlineMenuList({
|
||||
translations,
|
||||
@@ -54,6 +57,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
authStatus,
|
||||
ciphers,
|
||||
portKey,
|
||||
filledByCipherType,
|
||||
}: InitAutofillInlineMenuListMessage) {
|
||||
const linkElement = await this.initAutofillInlineMenuPage(
|
||||
"list",
|
||||
@@ -62,6 +66,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
portKey,
|
||||
);
|
||||
|
||||
this.filledByCipherType = filledByCipherType;
|
||||
|
||||
const themeClass = `theme_${theme}`;
|
||||
globalThis.document.documentElement.classList.add(themeClass);
|
||||
|
||||
@@ -157,10 +163,10 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
newItemButton.tabIndex = -1;
|
||||
newItemButton.id = "new-item-button";
|
||||
newItemButton.classList.add("add-new-item-button", "inline-menu-list-button");
|
||||
newItemButton.textContent = this.getTranslation("newItem");
|
||||
newItemButton.textContent = this.getNewItemButtonText();
|
||||
newItemButton.setAttribute(
|
||||
"aria-label",
|
||||
`${this.getTranslation("addNewVaultItem")}, ${this.getTranslation("opensInANewWindow")}`,
|
||||
`${this.getNewItemAriaLabel()}, ${this.getTranslation("opensInANewWindow")}`,
|
||||
);
|
||||
newItemButton.prepend(buildSvgDomElement(plusIcon));
|
||||
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
||||
@@ -172,12 +178,45 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
this.inlineMenuListContainer.append(noItemsMessage, inlineMenuListButtonContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the new item text for the button based on the cipher type the focused field is filled by.
|
||||
*/
|
||||
private getNewItemButtonText() {
|
||||
if (this.filledByCipherType === CipherType.Login) {
|
||||
return this.getTranslation("newLogin");
|
||||
}
|
||||
|
||||
if (this.filledByCipherType === CipherType.Card) {
|
||||
return this.getTranslation("newCard");
|
||||
}
|
||||
|
||||
return this.getTranslation("newItem");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aria label for the new item button based on the cipher type the focused field is filled by.
|
||||
*/
|
||||
private getNewItemAriaLabel() {
|
||||
if (this.filledByCipherType === CipherType.Login) {
|
||||
return this.getTranslation("addNewLoginItem");
|
||||
}
|
||||
|
||||
if (this.filledByCipherType === CipherType.Card) {
|
||||
return this.getTranslation("addNewCardItem");
|
||||
}
|
||||
|
||||
return this.getTranslation("addNewVaultItem");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event for the new item button.
|
||||
* Sends a message to the parent window to add a new vault item.
|
||||
*/
|
||||
private handeNewItemButtonClick = () => {
|
||||
this.postMessageToParent({ command: "addNewVaultItem" });
|
||||
this.postMessageToParent({
|
||||
command: "addNewVaultItem",
|
||||
addNewCipherType: this.filledByCipherType,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -267,10 +306,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
"aria-label",
|
||||
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
|
||||
);
|
||||
fillCipherElement.setAttribute(
|
||||
"aria-description",
|
||||
`${this.getTranslation("username")}, ${cipher.login.username}`,
|
||||
);
|
||||
this.addFillCipherElementAriaDescription(fillCipherElement, cipher);
|
||||
fillCipherElement.append(cipherIcon, cipherDetailsElement);
|
||||
fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
|
||||
fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent);
|
||||
@@ -278,6 +314,44 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
return fillCipherElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an aria description to the fill cipher button for a given cipher.
|
||||
*
|
||||
* @param fillCipherElement - The fill cipher button element.
|
||||
* @param cipher - The cipher to add the aria description for.
|
||||
*/
|
||||
private addFillCipherElementAriaDescription(
|
||||
fillCipherElement: HTMLButtonElement,
|
||||
cipher: InlineMenuCipherData,
|
||||
) {
|
||||
if (cipher.login) {
|
||||
fillCipherElement.setAttribute(
|
||||
"aria-description",
|
||||
`${this.getTranslation("username")}, ${cipher.login.username}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipher.card) {
|
||||
const cardParts = cipher.card.split(", *");
|
||||
if (cardParts.length === 1) {
|
||||
const cardDigits = cardParts[0].startsWith("*") ? cardParts[0].substring(1) : cardParts[0];
|
||||
fillCipherElement.setAttribute(
|
||||
"aria-description",
|
||||
`${this.getTranslation("cardNumberEndsWith")} ${cardDigits}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cardBrand = cardParts[0];
|
||||
const cardDigits = cardParts[1];
|
||||
fillCipherElement.setAttribute(
|
||||
"aria-description",
|
||||
`${cardBrand}, ${this.getTranslation("cardNumberEndsWith")} ${cardDigits}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event for the fill cipher button.
|
||||
* Sends a message to the parent window to fill the selected cipher.
|
||||
@@ -412,7 +486,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
dummyImageElement.src = url.href;
|
||||
dummyImageElement.addEventListener("error", () => {
|
||||
cipherIcon.style.backgroundImage = "";
|
||||
cipherIcon.classList.add("cipher-icon", "bwi", cipher.icon.icon);
|
||||
const iconClasses = cipher.icon.icon.split(" ");
|
||||
cipherIcon.classList.add("cipher-icon", "bwi", ...iconClasses);
|
||||
});
|
||||
dummyImageElement.remove();
|
||||
|
||||
@@ -423,7 +498,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
}
|
||||
|
||||
if (cipher.icon?.icon) {
|
||||
cipherIcon.classList.add("cipher-icon", "bwi", cipher.icon.icon);
|
||||
const iconClasses = cipher.icon.icon.split(" ");
|
||||
cipherIcon.classList.add("cipher-icon", "bwi", ...iconClasses);
|
||||
return cipherIcon;
|
||||
}
|
||||
|
||||
@@ -432,21 +508,21 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the details for a given cipher. Includes the cipher name and username login.
|
||||
* Builds the details for a given cipher. Includes the cipher name and subtitle.
|
||||
*
|
||||
* @param cipher - The cipher to build the details for.
|
||||
*/
|
||||
private buildCipherDetailsElement(cipher: InlineMenuCipherData) {
|
||||
const cipherNameElement = this.buildCipherNameElement(cipher);
|
||||
const cipherUserLoginElement = this.buildCipherUserLoginElement(cipher);
|
||||
const cipherSubtitleElement = this.buildCipherSubtitleElement(cipher);
|
||||
|
||||
const cipherDetailsElement = globalThis.document.createElement("span");
|
||||
cipherDetailsElement.classList.add("cipher-details");
|
||||
if (cipherNameElement) {
|
||||
cipherDetailsElement.appendChild(cipherNameElement);
|
||||
}
|
||||
if (cipherUserLoginElement) {
|
||||
cipherDetailsElement.appendChild(cipherUserLoginElement);
|
||||
if (cipherSubtitleElement) {
|
||||
cipherDetailsElement.appendChild(cipherSubtitleElement);
|
||||
}
|
||||
|
||||
return cipherDetailsElement;
|
||||
@@ -471,21 +547,22 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the username login element for a given cipher.
|
||||
* Builds the subtitle element for a given cipher.
|
||||
*
|
||||
* @param cipher - The cipher to build the username login element for.
|
||||
*/
|
||||
private buildCipherUserLoginElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
|
||||
if (!cipher.login?.username) {
|
||||
private buildCipherSubtitleElement(cipher: InlineMenuCipherData): HTMLSpanElement | null {
|
||||
const subTitleText = cipher.login?.username || cipher.card;
|
||||
if (!subTitleText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cipherUserLoginElement = globalThis.document.createElement("span");
|
||||
cipherUserLoginElement.classList.add("cipher-user-login");
|
||||
cipherUserLoginElement.textContent = cipher.login.username;
|
||||
cipherUserLoginElement.setAttribute("title", cipher.login.username);
|
||||
const cipherSubtitleElement = globalThis.document.createElement("span");
|
||||
cipherSubtitleElement.classList.add("cipher-subtitle");
|
||||
cipherSubtitleElement.textContent = subTitleText;
|
||||
cipherSubtitleElement.setAttribute("title", subTitleText);
|
||||
|
||||
return cipherUserLoginElement;
|
||||
return cipherSubtitleElement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -263,7 +263,7 @@ body {
|
||||
}
|
||||
|
||||
.cipher-name,
|
||||
.cipher-user-login {
|
||||
.cipher-subtitle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
@@ -283,7 +283,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.cipher-user-login {
|
||||
.cipher-subtitle {
|
||||
font-size: 1.4rem;
|
||||
|
||||
@include themify($themes) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
[key: string]: CallableFunction;
|
||||
openAutofillInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
addNewVaultItemFromOverlay: () => void;
|
||||
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
blurMostRecentlyFocusedField: () => void;
|
||||
unsetMostRecentlyFocusedField: () => void;
|
||||
checkIsMostRecentlyFocusedFieldWithinViewport: () => Promise<boolean>;
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
export type AutofillKeywordsMap = WeakMap<
|
||||
AutofillField,
|
||||
{
|
||||
keywordsSet: Set<string>;
|
||||
stringValue: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface InlineMenuFieldQualificationService {
|
||||
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
||||
isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
|
||||
isFieldForCardholderName(field: AutofillField): boolean;
|
||||
isFieldForCardNumber(field: AutofillField): boolean;
|
||||
isFieldForCardExpirationDate(field: AutofillField): boolean;
|
||||
isFieldForCardExpirationMonth(field: AutofillField): boolean;
|
||||
isFieldForCardExpirationYear(field: AutofillField): boolean;
|
||||
isFieldForCardCvv(field: AutofillField): boolean;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AutofillOverlayVisibility, EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import AutofillInit from "../content/autofill-init";
|
||||
import {
|
||||
@@ -14,7 +15,7 @@ import AutofillForm from "../models/autofill-form";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { createAutofillFieldMock } from "../spec/autofill-mocks";
|
||||
import { flushPromises, postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
import { ElementWithOpId, FormFieldElement } from "../types";
|
||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
|
||||
import { AutoFillConstants } from "./autofill-constants";
|
||||
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
||||
@@ -147,7 +148,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
opid: "password-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 2,
|
||||
autocompleteType: "current-password",
|
||||
autoCompleteType: "current-password",
|
||||
type: "password",
|
||||
});
|
||||
pageDetailsMock = mock<AutofillPageDetails>({
|
||||
@@ -180,7 +181,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores fields that do not appear as a login field", async () => {
|
||||
it("ignores fields that do not appear as a login or card field", async () => {
|
||||
autofillFieldData.htmlName = "another-type-of-field";
|
||||
autofillFieldData.htmlID = "another-type-of-field";
|
||||
autofillFieldData.placeholder = "another-type-of-field";
|
||||
@@ -196,7 +197,10 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
it("skips setup on fields that have been previously set up", async () => {
|
||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
||||
autofillOverlayContentService["formFieldElements"].set(
|
||||
autofillFieldElement,
|
||||
autofillFieldData,
|
||||
);
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
autofillFieldElement,
|
||||
@@ -414,6 +418,17 @@ describe("AutofillOverlayContentService", () => {
|
||||
expect(autofillOverlayContentService["storeModifiedFormElement"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips storing the element if it is not present in the set of qualified autofill fields", () => {
|
||||
const randomElement = document.createElement(
|
||||
"input",
|
||||
) as ElementWithOpId<FillableFormFieldElement>;
|
||||
jest.spyOn(autofillOverlayContentService as any, "storeUserFilledLoginField");
|
||||
|
||||
autofillOverlayContentService["storeModifiedFormElement"](randomElement);
|
||||
|
||||
expect(autofillOverlayContentService["storeUserFilledLoginField"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets the field as the most recently focused form field element", async () => {
|
||||
autofillOverlayContentService["mostRecentlyFocusedField"] =
|
||||
mock<ElementWithOpId<FormFieldElement>>();
|
||||
@@ -551,6 +566,155 @@ describe("AutofillOverlayContentService", () => {
|
||||
|
||||
expect(autofillOverlayContentService["openInlineMenu"]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("input changes on a field filled by a card cipher", () => {
|
||||
let inputFieldElement: ElementWithOpId<FillableFormFieldElement>;
|
||||
let inputFieldData: AutofillField;
|
||||
let selectFieldElement: ElementWithOpId<FillableFormFieldElement>;
|
||||
let selectFieldData: AutofillField;
|
||||
|
||||
beforeEach(() => {
|
||||
inputFieldElement = document.createElement(
|
||||
"input",
|
||||
) as ElementWithOpId<FillableFormFieldElement>;
|
||||
inputFieldData = createAutofillFieldMock({
|
||||
opid: "input-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 3,
|
||||
autoCompleteType: "cc-number",
|
||||
type: "text",
|
||||
filledByCipherType: CipherType.Card,
|
||||
viewable: true,
|
||||
});
|
||||
selectFieldElement = document.createElement(
|
||||
"select",
|
||||
) as ElementWithOpId<FillableFormFieldElement>;
|
||||
selectFieldData = createAutofillFieldMock({
|
||||
opid: "select-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 4,
|
||||
autoCompleteType: "cc-type",
|
||||
type: "select",
|
||||
filledByCipherType: CipherType.Card,
|
||||
viewable: true,
|
||||
});
|
||||
pageDetailsMock.fields = [inputFieldData, selectFieldData];
|
||||
});
|
||||
|
||||
it("only stores the element if the form field is a select element", async () => {
|
||||
jest.spyOn(autofillOverlayContentService as any, "storeModifiedFormElement");
|
||||
jest.spyOn(autofillOverlayContentService as any, "hideInlineMenuListOnFilledField");
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
selectFieldElement,
|
||||
selectFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
selectFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["storeModifiedFormElement"]).toHaveBeenCalledWith(
|
||||
selectFieldElement,
|
||||
);
|
||||
expect(
|
||||
autofillOverlayContentService["hideInlineMenuListOnFilledField"],
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stores cardholder name fields", async () => {
|
||||
inputFieldData.autoCompleteType = "cc-name";
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardholderName).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
|
||||
it("stores card number fields", async () => {
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardNumber).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
|
||||
it("stores card expiration month fields", async () => {
|
||||
inputFieldData.autoCompleteType = "cc-exp-month";
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardExpirationMonth).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
|
||||
it("stores card expiration year fields", async () => {
|
||||
inputFieldData.autoCompleteType = "cc-exp-year";
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardExpirationYear).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
|
||||
it("stores card expiration date fields", async () => {
|
||||
inputFieldData.autoCompleteType = "cc-exp";
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardExpirationDate).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
|
||||
it("stores card cvv fields", async () => {
|
||||
inputFieldData.autoCompleteType = "cc-csc";
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
inputFieldElement,
|
||||
inputFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
inputFieldElement.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"].cardCvv).toEqual(
|
||||
inputFieldElement,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("form field click event listener", () => {
|
||||
@@ -627,6 +791,24 @@ describe("AutofillOverlayContentService", () => {
|
||||
expect(updateMostRecentlyFocusedFieldSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes the inline menu if the focused element is a select element", async () => {
|
||||
const selectFieldElement = document.createElement(
|
||||
"select",
|
||||
) as ElementWithOpId<HTMLSelectElement>;
|
||||
autofillFieldData.type = "select";
|
||||
autofillFieldData.autoCompleteType = "cc-type";
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
selectFieldElement,
|
||||
autofillFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
selectFieldElement.dispatchEvent(new Event("focus"));
|
||||
await flushPromises();
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("closeAutofillInlineMenu");
|
||||
});
|
||||
|
||||
it("updates the most recently focused field", async () => {
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
autofillFieldElement,
|
||||
@@ -801,6 +983,85 @@ describe("AutofillOverlayContentService", () => {
|
||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setting up the form field listeners on card fields", () => {
|
||||
const inputCardFieldData = createAutofillFieldMock({
|
||||
opid: "card-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 3,
|
||||
autoCompleteType: "cc-number",
|
||||
type: "text",
|
||||
});
|
||||
const selectCardFieldData = createAutofillFieldMock({
|
||||
opid: "card-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 3,
|
||||
autoCompleteType: "cc-type",
|
||||
type: "select-one",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pageDetailsMock.fields = [inputCardFieldData, selectCardFieldData];
|
||||
});
|
||||
|
||||
it("sets up the input card field listeners", async () => {
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
autofillFieldElement,
|
||||
inputCardFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.BLUR,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.KEYUP,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.INPUT,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.CLICK,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(autofillFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.FOCUS,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(autofillFieldElement.removeEventListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets up the input and focus listeners on a select card field", async () => {
|
||||
const selectCardFieldElement = document.createElement(
|
||||
"select",
|
||||
) as ElementWithOpId<HTMLSelectElement>;
|
||||
selectCardFieldElement.opid = "op-2";
|
||||
jest.spyOn(selectCardFieldElement, "addEventListener");
|
||||
|
||||
await autofillOverlayContentService.setupInlineMenu(
|
||||
selectCardFieldElement,
|
||||
selectCardFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
|
||||
expect(selectCardFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.FOCUS,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(selectCardFieldElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.INPUT,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(selectCardFieldElement.addEventListener).not.toHaveBeenCalledWith(
|
||||
EVENTS.KEYUP,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("skips triggering the form field focused handler if the document is not focused", async () => {
|
||||
@@ -1065,10 +1326,14 @@ describe("AutofillOverlayContentService", () => {
|
||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
|
||||
sendMockExtensionMessage({
|
||||
command: "addNewVaultItemFromOverlay",
|
||||
addNewCipherType: CipherType.Login,
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||
addNewCipherType: CipherType.Login,
|
||||
login: {
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -1101,10 +1366,14 @@ describe("AutofillOverlayContentService", () => {
|
||||
password: passwordField,
|
||||
};
|
||||
|
||||
sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" });
|
||||
sendMockExtensionMessage({
|
||||
command: "addNewVaultItemFromOverlay",
|
||||
addNewCipherType: CipherType.Login,
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||
addNewCipherType: CipherType.Login,
|
||||
login: {
|
||||
username: "test-username",
|
||||
password: "test-password",
|
||||
@@ -1113,6 +1382,30 @@ describe("AutofillOverlayContentService", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a message that facilitates adding a card cipher vault item", async () => {
|
||||
jest
|
||||
.spyOn(autofillOverlayContentService as any, "isInlineMenuListVisible")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "addNewVaultItemFromOverlay",
|
||||
addNewCipherType: CipherType.Card,
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayAddNewVaultItem", {
|
||||
addNewCipherType: CipherType.Card,
|
||||
card: {
|
||||
cardholderName: "",
|
||||
cvv: "",
|
||||
expirationDate: "",
|
||||
expirationMonth: "",
|
||||
expirationYear: "",
|
||||
number: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsetMostRecentlyFocusedField message handler", () => {
|
||||
@@ -1138,6 +1431,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
autofillOverlayContentService["focusedFieldData"] = {
|
||||
focusedFieldStyles: { paddingRight: "10", paddingLeft: "10" },
|
||||
focusedFieldRects: { width: 10, height: 10, top: 10, left: 10 },
|
||||
filledByCipherType: CipherType.Login,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1587,7 +1881,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
it("skips setup when no form fields exist on the current frame", async () => {
|
||||
autofillOverlayContentService["formFieldElements"] = new Set();
|
||||
autofillOverlayContentService["formFieldElements"] = new Map();
|
||||
|
||||
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||
await flushPromises();
|
||||
@@ -1604,7 +1898,10 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => {
|
||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
||||
autofillOverlayContentService["formFieldElements"].set(
|
||||
autofillFieldElement,
|
||||
createAutofillFieldMock(),
|
||||
);
|
||||
|
||||
sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||
await flushPromises();
|
||||
@@ -1621,7 +1918,10 @@ describe("AutofillOverlayContentService", () => {
|
||||
|
||||
describe("triggering the sub frame listener", () => {
|
||||
beforeEach(async () => {
|
||||
autofillOverlayContentService["formFieldElements"].add(autofillFieldElement);
|
||||
autofillOverlayContentService["formFieldElements"].set(
|
||||
autofillFieldElement,
|
||||
createAutofillFieldMock(),
|
||||
);
|
||||
await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" });
|
||||
});
|
||||
|
||||
@@ -1672,7 +1972,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
opid: "password-field",
|
||||
form: "validFormId",
|
||||
elementNumber: 2,
|
||||
autocompleteType: "current-password",
|
||||
autoCompleteType: "current-password",
|
||||
type: "password",
|
||||
});
|
||||
pageDetailsMock = mock<AutofillPageDetails>({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AutofillOverlayVisibility,
|
||||
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import {
|
||||
FocusedFieldData,
|
||||
@@ -24,6 +25,7 @@ import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
import {
|
||||
elementIsFillableFormField,
|
||||
elementIsSelectElement,
|
||||
getAttributeBoolean,
|
||||
sendExtensionMessage,
|
||||
throttle,
|
||||
@@ -43,7 +45,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
inlineMenuVisibility: number;
|
||||
private readonly findTabs = tabbable;
|
||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
|
||||
private formFieldElements: Map<ElementWithOpId<FormFieldElement>, AutofillField> = new Map();
|
||||
private hiddenFormFieldElements: WeakMap<ElementWithOpId<FormFieldElement>, AutofillField> =
|
||||
new WeakMap();
|
||||
private ignoredFieldTypes: Set<string> = new Set(AutoFillConstants.ExcludedInlineMenuTypes);
|
||||
@@ -57,7 +59,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
||||
private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
|
||||
openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message),
|
||||
addNewVaultItemFromOverlay: () => this.addNewVaultItem(),
|
||||
addNewVaultItemFromOverlay: ({ message }) => this.addNewVaultItem(message),
|
||||
blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(),
|
||||
unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(),
|
||||
checkIsMostRecentlyFocusedFieldWithinViewport: () =>
|
||||
@@ -122,7 +124,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setupInlineMenuOnQualifiedField(formFieldElement);
|
||||
await this.setupInlineMenuOnQualifiedField(formFieldElement, autofillFieldData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,19 +196,38 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
* Formats any found user filled fields for a login cipher and sends a message
|
||||
* to the background script to add a new cipher.
|
||||
*/
|
||||
async addNewVaultItem() {
|
||||
async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) {
|
||||
if (!(await this.isInlineMenuListVisible())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const login = {
|
||||
username: this.userFilledFields["username"]?.value || "",
|
||||
password: this.userFilledFields["password"]?.value || "",
|
||||
uri: globalThis.document.URL,
|
||||
hostname: globalThis.document.location.hostname,
|
||||
};
|
||||
const command = "autofillOverlayAddNewVaultItem";
|
||||
|
||||
void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
|
||||
if (addNewCipherType === CipherType.Login) {
|
||||
const login = {
|
||||
username: this.userFilledFields["username"]?.value || "",
|
||||
password: this.userFilledFields["password"]?.value || "",
|
||||
uri: globalThis.document.URL,
|
||||
hostname: globalThis.document.location.hostname,
|
||||
};
|
||||
|
||||
void this.sendExtensionMessage(command, { addNewCipherType, login });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (addNewCipherType === CipherType.Card) {
|
||||
const card = {
|
||||
cardholderName: this.userFilledFields["cardholderName"]?.value || "",
|
||||
number: this.userFilledFields["cardNumber"]?.value || "",
|
||||
expirationMonth: this.userFilledFields["cardExpirationMonth"]?.value || "",
|
||||
expirationYear: this.userFilledFields["cardExpirationYear"]?.value || "",
|
||||
expirationDate: this.userFilledFields["cardExpirationDate"]?.value || "",
|
||||
cvv: this.userFilledFields["cardCvv"]?.value || "",
|
||||
};
|
||||
|
||||
void this.sendExtensionMessage(command, { addNewCipherType, card });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,20 +274,25 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
private setupFormFieldElementEventListeners(formFieldElement: ElementWithOpId<FormFieldElement>) {
|
||||
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||
|
||||
formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||
formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||
formFieldElement.addEventListener(
|
||||
EVENTS.INPUT,
|
||||
this.handleFormFieldInputEvent(formFieldElement),
|
||||
);
|
||||
formFieldElement.addEventListener(
|
||||
EVENTS.CLICK,
|
||||
this.handleFormFieldClickEvent(formFieldElement),
|
||||
);
|
||||
formFieldElement.addEventListener(
|
||||
EVENTS.FOCUS,
|
||||
this.handleFormFieldFocusEvent(formFieldElement),
|
||||
);
|
||||
|
||||
if (elementIsSelectElement(formFieldElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||
formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||
formFieldElement.addEventListener(
|
||||
EVENTS.CLICK,
|
||||
this.handleFormFieldClickEvent(formFieldElement),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,6 +426,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
}
|
||||
|
||||
this.storeModifiedFormElement(formFieldElement);
|
||||
if (elementIsSelectElement(formFieldElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.hideInlineMenuListOnFilledField(formFieldElement)) {
|
||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||
@@ -425,6 +454,26 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
void this.updateMostRecentlyFocusedField(formFieldElement);
|
||||
}
|
||||
|
||||
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
||||
if (!autofillFieldData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autofillFieldData.filledByCipherType === CipherType.Login) {
|
||||
this.storeUserFilledLoginField(formFieldElement);
|
||||
}
|
||||
|
||||
if (autofillFieldData.filledByCipherType === CipherType.Card) {
|
||||
this.storeUserFilledCardField(formFieldElement, autofillFieldData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles storing the user field login field to be used when adding a new vault item.
|
||||
*
|
||||
* @param formFieldElement - The form field element that triggered the input event.
|
||||
*/
|
||||
private storeUserFilledLoginField(formFieldElement: ElementWithOpId<FillableFormFieldElement>) {
|
||||
if (formFieldElement.type === "password") {
|
||||
this.userFilledFields.password = formFieldElement;
|
||||
return;
|
||||
@@ -433,6 +482,46 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
this.userFilledFields.username = formFieldElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles storing the user field card field to be used when adding a new vault item.
|
||||
*
|
||||
* @param formFieldElement - The form field element that triggered the input event.
|
||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||
*/
|
||||
private storeUserFilledCardField(
|
||||
formFieldElement: ElementWithOpId<FillableFormFieldElement>,
|
||||
autofillFieldData: AutofillField,
|
||||
) {
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardholderName(autofillFieldData)) {
|
||||
this.userFilledFields.cardholderName = formFieldElement;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardNumber(autofillFieldData)) {
|
||||
this.userFilledFields.cardNumber = formFieldElement;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationMonth(autofillFieldData)) {
|
||||
this.userFilledFields.cardExpirationMonth = formFieldElement;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationYear(autofillFieldData)) {
|
||||
this.userFilledFields.cardExpirationYear = formFieldElement;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardExpirationDate(autofillFieldData)) {
|
||||
this.userFilledFields.cardExpirationDate = formFieldElement;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inlineMenuFieldQualificationService.isFieldForCardCvv(autofillFieldData)) {
|
||||
this.userFilledFields.cardCvv = formFieldElement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up and memoizes the form field click event handler.
|
||||
*
|
||||
@@ -483,16 +572,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
return;
|
||||
}
|
||||
|
||||
if (elementIsSelectElement(formFieldElement)) {
|
||||
await this.sendExtensionMessage("closeAutofillInlineMenu");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", {
|
||||
isFieldCurrentlyFocused: true,
|
||||
});
|
||||
const initiallyFocusedField = this.mostRecentlyFocusedField;
|
||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
||||
|
||||
const hideInlineMenuListOnFilledField = await this.hideInlineMenuListOnFilledField(
|
||||
formFieldElement as FillableFormFieldElement,
|
||||
);
|
||||
if (
|
||||
this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick ||
|
||||
(initiallyFocusedField !== this.mostRecentlyFocusedField &&
|
||||
(await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)))
|
||||
(initiallyFocusedField !== this.mostRecentlyFocusedField && hideInlineMenuListOnFilledField)
|
||||
) {
|
||||
await this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||
overlayElement: AutofillOverlayElement.List,
|
||||
@@ -500,7 +596,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
});
|
||||
}
|
||||
|
||||
if (await this.hideInlineMenuListOnFilledField(formFieldElement as FillableFormFieldElement)) {
|
||||
if (hideInlineMenuListOnFilledField) {
|
||||
this.updateInlineMenuButtonPosition();
|
||||
return;
|
||||
}
|
||||
@@ -568,9 +664,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement);
|
||||
const { width, height, top, left } =
|
||||
await this.getMostRecentlyFocusedFieldRects(formFieldElement);
|
||||
const autofillFieldData = this.formFieldElements.get(formFieldElement);
|
||||
this.focusedFieldData = {
|
||||
focusedFieldStyles: { paddingRight, paddingLeft },
|
||||
focusedFieldRects: { width, height, top, left },
|
||||
filledByCipherType: autofillFieldData?.filledByCipherType,
|
||||
};
|
||||
|
||||
await this.sendExtensionMessage("updateFocusedFieldData", {
|
||||
@@ -648,10 +746,24 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.inlineMenuFieldQualificationService.isFieldForLoginForm(
|
||||
autofillFieldData,
|
||||
pageDetails,
|
||||
);
|
||||
if (
|
||||
this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails)
|
||||
) {
|
||||
autofillFieldData.filledByCipherType = CipherType.Login;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.inlineMenuFieldQualificationService.isFieldForCreditCardForm(
|
||||
autofillFieldData,
|
||||
pageDetails,
|
||||
)
|
||||
) {
|
||||
autofillFieldData.filledByCipherType = CipherType.Card;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,7 +826,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled");
|
||||
autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled");
|
||||
autofillFieldData.viewable = true;
|
||||
void this.setupInlineMenuOnQualifiedField(formFieldElement);
|
||||
void this.setupInlineMenuOnQualifiedField(formFieldElement, autofillFieldData);
|
||||
}
|
||||
|
||||
this.removeHiddenFieldFallbackListener(formFieldElement);
|
||||
@@ -724,11 +836,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
* Sets up the inline menu on a qualified form field element.
|
||||
*
|
||||
* @param formFieldElement - The form field element to set up the inline menu on.
|
||||
* @param autofillFieldData - Autofill field data captured from the form field element.
|
||||
*/
|
||||
private async setupInlineMenuOnQualifiedField(
|
||||
formFieldElement: ElementWithOpId<FormFieldElement>,
|
||||
autofillFieldData: AutofillField,
|
||||
) {
|
||||
this.formFieldElements.add(formFieldElement);
|
||||
this.formFieldElements.set(formFieldElement, autofillFieldData);
|
||||
|
||||
if (!this.mostRecentlyFocusedField) {
|
||||
await this.updateMostRecentlyFocusedField(formFieldElement);
|
||||
@@ -1198,7 +1312,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
destroy() {
|
||||
this.clearFocusInlineMenuListTimeout();
|
||||
this.clearCloseInlineMenuOnRedirectTimeout();
|
||||
this.formFieldElements.forEach((formFieldElement) => {
|
||||
this.formFieldElements.forEach((_autofillField, formFieldElement) => {
|
||||
this.removeCachedFormFieldEventListeners(formFieldElement);
|
||||
formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent);
|
||||
formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent);
|
||||
|
||||
@@ -713,4 +713,155 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFieldForCreditCardForm", () => {
|
||||
describe("an invalid credit card field", () => {
|
||||
it("has reference to a `new field` keyword", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "new credit card",
|
||||
});
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
describe("does not have a parent form", () => {
|
||||
it("has no credit card number fields in the page details", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
});
|
||||
const secondField = mock<AutofillField>({
|
||||
placeholder: "card cvv",
|
||||
autoCompleteType: "cc-csc",
|
||||
});
|
||||
pageDetails.forms = {};
|
||||
pageDetails.fields = [field, secondField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("has no credit card cvv fields in the page details", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
});
|
||||
const secondField = mock<AutofillField>({
|
||||
placeholder: "card number",
|
||||
autoCompleteType: "cc-number",
|
||||
});
|
||||
pageDetails.forms = {};
|
||||
pageDetails.fields = [field, secondField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("has a parent form", () => {
|
||||
let form: MockProxy<AutofillForm>;
|
||||
|
||||
beforeEach(() => {
|
||||
form = mock<AutofillForm>({ opid: "validFormId" });
|
||||
pageDetails.forms = {
|
||||
validFormId: form,
|
||||
};
|
||||
});
|
||||
|
||||
it("does not have a credit card number field within the same form", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
form: "validFormId",
|
||||
});
|
||||
const cardCvvField = mock<AutofillField>({
|
||||
placeholder: "card cvv",
|
||||
autoCompleteType: "cc-csc",
|
||||
form: "validFormId",
|
||||
});
|
||||
pageDetails.fields = [field, cardCvvField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not contain a cvv field within the same form", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
form: "validFormId",
|
||||
});
|
||||
const cardNumberField = mock<AutofillField>({
|
||||
placeholder: "card number",
|
||||
autoCompleteType: "cc-number",
|
||||
form: "validFormId",
|
||||
});
|
||||
|
||||
pageDetails.fields = [field, cardNumberField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("a valid credit card field", () => {
|
||||
describe("does not have a parent form", () => {
|
||||
it("is structured on a page with a single credit card number field and a single cvv field", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
});
|
||||
const cardNumberField = mock<AutofillField>({
|
||||
placeholder: "card number",
|
||||
autoCompleteType: "cc-number",
|
||||
});
|
||||
const cardCvvField = mock<AutofillField>({
|
||||
placeholder: "card cvv",
|
||||
autoCompleteType: "cc-csc",
|
||||
});
|
||||
pageDetails.forms = {};
|
||||
pageDetails.fields = [field, cardNumberField, cardCvvField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("has a parent form", () => {
|
||||
let form: MockProxy<AutofillForm>;
|
||||
|
||||
beforeEach(() => {
|
||||
form = mock<AutofillForm>({ opid: "validFormId" });
|
||||
pageDetails.forms = {
|
||||
validFormId: form,
|
||||
};
|
||||
});
|
||||
|
||||
it("has a credit card number field and cvv field structured within the same form", () => {
|
||||
const field = mock<AutofillField>({
|
||||
placeholder: "name",
|
||||
form: "validFormId",
|
||||
});
|
||||
const cardNumberField = mock<AutofillField>({
|
||||
placeholder: "card number",
|
||||
autoCompleteType: "cc-number",
|
||||
form: "validFormId",
|
||||
});
|
||||
const cardCvvField = mock<AutofillField>({
|
||||
placeholder: "card cvv",
|
||||
autoCompleteType: "cc-csc",
|
||||
form: "validFormId",
|
||||
});
|
||||
pageDetails.fields = [field, cardNumberField, cardCvvField];
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,11 @@ import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { sendExtensionMessage } from "../utils";
|
||||
|
||||
import { InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface } from "./abstractions/inline-menu-field-qualifications.service";
|
||||
import { AutoFillConstants } from "./autofill-constants";
|
||||
import {
|
||||
AutofillKeywordsMap,
|
||||
InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface,
|
||||
} from "./abstractions/inline-menu-field-qualifications.service";
|
||||
import { AutoFillConstants, CreditCardAutoFillConstants } from "./autofill-constants";
|
||||
|
||||
export class InlineMenuFieldQualificationService
|
||||
implements InlineMenuFieldQualificationServiceInterface
|
||||
@@ -14,9 +17,13 @@ export class InlineMenuFieldQualificationService
|
||||
private usernameAutocompleteValues = new Set(["username", "email"]);
|
||||
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
||||
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
||||
private currentPasswordAutocompleteValues = new Set(["current-password"]);
|
||||
private newPasswordAutoCompleteValues = new Set(["new-password"]);
|
||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||
private currentPasswordAutocompleteValue = "current-password";
|
||||
private newPasswordAutoCompleteValue = "new-password";
|
||||
private passwordAutoCompleteValues = new Set([
|
||||
this.currentPasswordAutocompleteValue,
|
||||
this.newPasswordAutoCompleteValue,
|
||||
]);
|
||||
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
|
||||
private autocompleteDisabledValues = new Set(["off", "false"]);
|
||||
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
|
||||
private accountCreationFieldKeywords = new Set([
|
||||
@@ -26,6 +33,36 @@ export class InlineMenuFieldQualificationService
|
||||
"confirm",
|
||||
...this.newFieldKeywords,
|
||||
]);
|
||||
private creditCardFieldKeywords = new Set([
|
||||
...CreditCardAutoFillConstants.CardHolderFieldNames,
|
||||
...CreditCardAutoFillConstants.CardNumberFieldNames,
|
||||
...CreditCardAutoFillConstants.CardExpiryFieldNames,
|
||||
...CreditCardAutoFillConstants.ExpiryMonthFieldNames,
|
||||
...CreditCardAutoFillConstants.ExpiryYearFieldNames,
|
||||
...CreditCardAutoFillConstants.CVVFieldNames,
|
||||
...CreditCardAutoFillConstants.CardBrandFieldNames,
|
||||
]);
|
||||
private creditCardNameAutocompleteValues = new Set([
|
||||
"cc-name",
|
||||
"cc-given-name,",
|
||||
"cc-additional-name",
|
||||
"cc-family-name",
|
||||
]);
|
||||
private creditCardExpirationDateAutocompleteValue = "cc-exp";
|
||||
private creditCardExpirationMonthAutocompleteValue = "cc-exp-month";
|
||||
private creditCardExpirationYearAutocompleteValue = "cc-exp-year";
|
||||
private creditCardCvvAutocompleteValue = "cc-csc";
|
||||
private creditCardNumberAutocompleteValue = "cc-number";
|
||||
private creditCardTypeAutocompleteValue = "cc-type";
|
||||
private creditCardAutocompleteValues = new Set([
|
||||
...this.creditCardNameAutocompleteValues,
|
||||
this.creditCardExpirationDateAutocompleteValue,
|
||||
this.creditCardExpirationMonthAutocompleteValue,
|
||||
this.creditCardExpirationYearAutocompleteValue,
|
||||
this.creditCardNumberAutocompleteValue,
|
||||
this.creditCardCvvAutocompleteValue,
|
||||
this.creditCardTypeAutocompleteValue,
|
||||
]);
|
||||
private inlineMenuFieldQualificationFlagSet = false;
|
||||
|
||||
constructor() {
|
||||
@@ -59,6 +96,72 @@ export class InlineMenuFieldQualificationService
|
||||
return this.isUsernameFieldForLoginForm(field, pageDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card form.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
* @param pageDetails - The details of the page that the field is on.
|
||||
*/
|
||||
isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||
// If the field contains any of the standardized autocomplete attribute values
|
||||
// for credit card fields, we should assume that the field is part of a credit card form.
|
||||
if (this.fieldContainsAutocompleteValues(field, this.creditCardAutocompleteValues)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the field contains any keywords indicating this is for a "new" or "changed" credit card
|
||||
// field, we should assume that the field is not going to be autofilled.
|
||||
if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
|
||||
// If the field does not have a parent form
|
||||
if (!parentForm) {
|
||||
// If a credit card number field is not present on the page or there are multiple credit
|
||||
// card number fields, this field is not part of a credit card form.
|
||||
const numberFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardNumber);
|
||||
if (numberFieldsInPageDetails.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a credit card CVV field is not present on the page or there are multiple credit card
|
||||
// CVV fields, this field is not part of a credit card form.
|
||||
const cvvFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardCvv);
|
||||
if (cvvFieldsInPageDetails.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
|
||||
);
|
||||
}
|
||||
|
||||
// If the field has a parent form, check the fields from that form exclusively
|
||||
const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form);
|
||||
|
||||
// If a credit card number field is not present on the page or there are multiple credit
|
||||
// card number fields, this field is not part of a credit card form.
|
||||
const numberFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardNumber);
|
||||
if (numberFieldsInPageDetails.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a credit card CVV field is not present on the page or there are multiple credit card
|
||||
// CVV fields, this field is not part of a credit card form.
|
||||
const cvvFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardCvv);
|
||||
if (cvvFieldsInPageDetails.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided field as a password field for a login form.
|
||||
*
|
||||
@@ -71,12 +174,7 @@ export class InlineMenuFieldQualificationService
|
||||
): boolean {
|
||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.currentPasswordAutocompleteValues,
|
||||
)
|
||||
) {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,10 +208,7 @@ export class InlineMenuFieldQualificationService
|
||||
// provided field is for a login form. This will only be the case if the field does not
|
||||
// explicitly have its autocomplete attribute set to "off" or "false".
|
||||
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
}
|
||||
|
||||
// If the field has a form parent and there are multiple visible password fields
|
||||
@@ -135,10 +230,7 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If the field has a form parent and no username field exists and the field has an
|
||||
// autocomplete attribute set to "off" or "false", this is not a password field
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,9 +245,7 @@ export class InlineMenuFieldQualificationService
|
||||
): boolean {
|
||||
// If the provided field is set with an autocomplete of "username", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a username field.
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues)
|
||||
) {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValues)) {
|
||||
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
|
||||
return newPasswordFieldsInPageDetails.length === 0;
|
||||
}
|
||||
@@ -198,10 +288,7 @@ export class InlineMenuFieldQualificationService
|
||||
// If the page does not contain any password fields, it might be part of a multistep login form.
|
||||
// That will only be the case if the field does not explicitly have its autocomplete attribute
|
||||
// set to "off" or "false".
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
}
|
||||
|
||||
// If the field is structured within a form, but no password fields are present in the form,
|
||||
@@ -209,12 +296,7 @@ export class InlineMenuFieldQualificationService
|
||||
if (passwordFieldsInPageDetails.length === 0) {
|
||||
// If the field's autocomplete is set to a disabled value, we should assume that the field is
|
||||
// not part of a login form.
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
)
|
||||
) {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -243,12 +325,111 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If no visible password fields are found, this field might be part of a multipart form.
|
||||
// Check for an invalid autocompleteType to determine if the field is part of a login form.
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card name field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardholderName = (field: AutofillField): boolean => {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardHolderFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card number field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardNumber = (field: AutofillField): boolean => {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardNumberFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card expiration date field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardExpirationDate = (field: AutofillField): boolean => {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardExpiryFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card expiration month field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardExpirationMonth = (field: AutofillField): boolean => {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationMonthAutocompleteValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryMonthFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card expiration year field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardExpirationYear = (field: AutofillField): boolean => {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryYearFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a field for a credit card CVV field.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForCardCvv = (field: AutofillField): boolean => {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) &&
|
||||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the provided field as a username field.
|
||||
*
|
||||
@@ -272,10 +453,7 @@ export class InlineMenuFieldQualificationService
|
||||
*/
|
||||
private isCurrentPasswordField = (field: AutofillField): boolean => {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.newPasswordAutoCompleteValues,
|
||||
) ||
|
||||
this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) ||
|
||||
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
|
||||
) {
|
||||
return false;
|
||||
@@ -290,12 +468,7 @@ export class InlineMenuFieldQualificationService
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
private isNewPasswordField = (field: AutofillField): boolean => {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.currentPasswordAutocompleteValues,
|
||||
)
|
||||
) {
|
||||
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -432,60 +605,77 @@ export class InlineMenuFieldQualificationService
|
||||
*
|
||||
* @param autofillFieldData - The field data to search for keywords
|
||||
* @param keywords - The keywords to search for
|
||||
* @param fuzzyMatchKeywords - Indicates if the keywords should be matched in a fuzzy manner
|
||||
*/
|
||||
private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) {
|
||||
const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData);
|
||||
return keywords.some((keyword) => searchedString.includes(keyword));
|
||||
private keywordsFoundInFieldData(
|
||||
autofillFieldData: AutofillField,
|
||||
keywords: string[],
|
||||
fuzzyMatchKeywords = true,
|
||||
) {
|
||||
const searchedValues = this.getAutofillFieldDataKeywords(autofillFieldData, fuzzyMatchKeywords);
|
||||
if (typeof searchedValues === "string") {
|
||||
return keywords.some((keyword) => searchedValues.indexOf(keyword) > -1);
|
||||
}
|
||||
|
||||
return keywords.some((keyword) => searchedValues.has(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the keywords from the provided autofill field data.
|
||||
*
|
||||
* @param autofillFieldData - The field data to search for keywords
|
||||
* @param returnStringValue - Indicates if the method should return a string value
|
||||
*/
|
||||
private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) {
|
||||
if (this.autofillFieldKeywordsMap.has(autofillFieldData)) {
|
||||
return this.autofillFieldKeywordsMap.get(autofillFieldData);
|
||||
private getAutofillFieldDataKeywords(
|
||||
autofillFieldData: AutofillField,
|
||||
returnStringValue: boolean,
|
||||
) {
|
||||
if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) {
|
||||
const keywords = [
|
||||
autofillFieldData.htmlID,
|
||||
autofillFieldData.htmlName,
|
||||
autofillFieldData.htmlClass,
|
||||
autofillFieldData.type,
|
||||
autofillFieldData.title,
|
||||
autofillFieldData.placeholder,
|
||||
autofillFieldData.autoCompleteType,
|
||||
autofillFieldData["label-data"],
|
||||
autofillFieldData["label-aria"],
|
||||
autofillFieldData["label-left"],
|
||||
autofillFieldData["label-right"],
|
||||
autofillFieldData["label-tag"],
|
||||
autofillFieldData["label-top"],
|
||||
];
|
||||
const keywordsSet = new Set<string>(keywords);
|
||||
const stringValue = keywords.join(",").toLowerCase();
|
||||
this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue });
|
||||
}
|
||||
|
||||
const keywordValues = [
|
||||
autofillFieldData.htmlID,
|
||||
autofillFieldData.htmlName,
|
||||
autofillFieldData.htmlClass,
|
||||
autofillFieldData.type,
|
||||
autofillFieldData.title,
|
||||
autofillFieldData.placeholder,
|
||||
autofillFieldData.autoCompleteType,
|
||||
autofillFieldData["label-data"],
|
||||
autofillFieldData["label-aria"],
|
||||
autofillFieldData["label-left"],
|
||||
autofillFieldData["label-right"],
|
||||
autofillFieldData["label-tag"],
|
||||
autofillFieldData["label-top"],
|
||||
]
|
||||
.join(",")
|
||||
.toLowerCase();
|
||||
this.autofillFieldKeywordsMap.set(autofillFieldData, keywordValues);
|
||||
|
||||
return keywordValues;
|
||||
const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData);
|
||||
return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates the provided field data into space-separated values and checks if any
|
||||
* of the values are present in the provided set of autocomplete values.
|
||||
*
|
||||
* @param fieldAutocompleteValue - The field autocomplete value to validate
|
||||
* @param autofillFieldData - The field autocomplete value to validate
|
||||
* @param compareValues - The set of autocomplete values to check against
|
||||
*/
|
||||
private fieldContainsAutocompleteValues(
|
||||
fieldAutocompleteValue: string,
|
||||
compareValues: Set<string>,
|
||||
autofillFieldData: AutofillField,
|
||||
compareValues: string | Set<string>,
|
||||
) {
|
||||
if (!fieldAutocompleteValue) {
|
||||
const fieldAutocompleteValue = autofillFieldData.autoCompleteType;
|
||||
if (!fieldAutocompleteValue || typeof fieldAutocompleteValue !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const autocompleteValueParts = fieldAutocompleteValue.split(" ");
|
||||
if (typeof compareValues === "string") {
|
||||
return autocompleteValueParts.indexOf(compareValues) > -1;
|
||||
}
|
||||
|
||||
for (let index = 0; index < autocompleteValueParts.length; index++) {
|
||||
if (compareValues.has(autocompleteValueParts[index])) {
|
||||
return true;
|
||||
|
||||
@@ -208,6 +208,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
||||
theme: ThemeType.Light,
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
portKey: "portKey",
|
||||
filledByCipherType: CipherType.Login,
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
icon: {
|
||||
@@ -254,6 +255,7 @@ export function createFocusedFieldDataMock(customFields = {}) {
|
||||
paddingRight: "6px",
|
||||
paddingLeft: "6px",
|
||||
},
|
||||
filledByCipherType: CipherType.Login,
|
||||
tabId: 1,
|
||||
frameId: 2,
|
||||
...customFields,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<header
|
||||
class="tw-p-4 tw-border-0 tw-border-solid tw-border-b tw-border-secondary-300 tw-bg-background"
|
||||
class="tw-p-4 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
||||
[ngClass]="{
|
||||
'tw-bg-background-alt tw-border-transparent':
|
||||
this.background === 'alt' && !pageContentScrolled(),
|
||||
'tw-bg-background tw-border-secondary-300':
|
||||
(this.background === 'alt' && pageContentScrolled()) || this.background === 'default'
|
||||
}"
|
||||
>
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
@@ -11,7 +17,10 @@
|
||||
[ariaLabel]="'back' | i18n"
|
||||
[bitAction]="backAction"
|
||||
></button>
|
||||
<h1 bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">{{ pageTitle }}</h1>
|
||||
<h1 *ngIf="pageTitle" bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, Input, Signal, inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PopupPageComponent } from "./popup-page.component";
|
||||
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
templateUrl: "popup-header.component.html",
|
||||
@@ -17,6 +19,12 @@ import {
|
||||
imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule],
|
||||
})
|
||||
export class PopupHeaderComponent {
|
||||
protected pageContentScrolled: Signal<boolean> = inject(PopupPageComponent).isScrolled;
|
||||
|
||||
/** Background color */
|
||||
@Input()
|
||||
background: "default" | "alt" = "default";
|
||||
|
||||
/** Display the back button, which uses Location.back() to go back one page in history */
|
||||
@Input()
|
||||
get showBackButton() {
|
||||
|
||||
@@ -74,6 +74,9 @@ Basic usage example:
|
||||
- `showBackButton`: optional, defaults to `false`
|
||||
- Toggles the back button to appear. The back button uses `Location.back()` to navigate back one
|
||||
page in history.
|
||||
- `background`: optional
|
||||
- `"default"` uses a white background
|
||||
- `"alt"` uses a transparent background
|
||||
|
||||
**Slots**
|
||||
|
||||
@@ -92,6 +95,12 @@ Usage example:
|
||||
</popup-header>
|
||||
```
|
||||
|
||||
### Transparent header
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.TransparentHeader} />
|
||||
</Canvas>
|
||||
|
||||
Common interactive elements to insert into the `end` slot are:
|
||||
|
||||
- `app-current-account`: shows current account and switcher
|
||||
|
||||
@@ -303,6 +303,7 @@ export default {
|
||||
MockSettingsPageComponent,
|
||||
MockVaultPagePoppedComponent,
|
||||
NoItemsModule,
|
||||
VaultComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -311,6 +312,7 @@ export default {
|
||||
return new I18nMockService({
|
||||
back: "Back",
|
||||
loading: "Loading",
|
||||
search: "Search",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -421,3 +423,20 @@ export const Loading: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const TransparentHeader: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<extension-container>
|
||||
<popup-page>
|
||||
<popup-header slot="header" background="alt"
|
||||
><span class="tw-italic tw-text-main">🤠 Custom Content</span></popup-header
|
||||
>
|
||||
|
||||
<vault-placeholder></vault-placeholder>
|
||||
</popup-page>
|
||||
</extension-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<ng-content select="[slot=header]"></ng-content>
|
||||
<main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt">
|
||||
<div #nonScrollable [ngClass]="{ 'tw-invisible': loading }">
|
||||
<div
|
||||
#nonScrollable
|
||||
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
||||
[ngClass]="{
|
||||
'tw-invisible': loading || nonScrollable.childElementCount === 0,
|
||||
'tw-border-secondary-300': scrolled(),
|
||||
'tw-border-transparent': !scrolled()
|
||||
}"
|
||||
>
|
||||
<ng-content select="[slot=above-scroll-area]"></ng-content>
|
||||
</div>
|
||||
<div
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full"
|
||||
(scroll)="handleScroll($event)"
|
||||
[ngClass]="{ 'tw-invisible': loading }"
|
||||
>
|
||||
<!-- Only shown when the `slot=above-scroll-area` is populated -->
|
||||
<!-- The first div will "stick" and show a bottom border when content is scrolled -->
|
||||
<!-- The second div is displayed on top of the first to hide it until the content is scrolled -->
|
||||
<ng-container *ngIf="nonScrollable.children.length">
|
||||
<div class="tw-sticky tw-min-h-[1px] tw-bg-secondary-300 tw-top-0"></div>
|
||||
<div class="tw-relative tw-z-10 tw-min-h-[2px] tw-bg-background-alt -tw-mt-[2px]"></div>
|
||||
</ng-container>
|
||||
|
||||
<div
|
||||
class="tw-max-w-screen-sm tw-mx-auto tw-p-3 tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, inject } from "@angular/core";
|
||||
import { Component, Input, inject, signal } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -17,6 +17,13 @@ export class PopupPageComponent {
|
||||
|
||||
@Input() loading = false;
|
||||
|
||||
protected scrolled = signal(false);
|
||||
isScrolled = this.scrolled.asReadonly();
|
||||
|
||||
/** Accessible loading label for the spinner. Defaults to "loading" */
|
||||
@Input() loadingText?: string = this.i18nService.t("loading");
|
||||
|
||||
handleScroll(event: Event) {
|
||||
this.scrolled.set((event.currentTarget as HTMLElement).scrollTop !== 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
RegistrationStartComponent,
|
||||
RegistrationStartSecondaryComponent,
|
||||
RegistrationStartSecondaryComponentData,
|
||||
SetPasswordJitComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@@ -409,6 +410,15 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||
component: SetPasswordJitComponent,
|
||||
data: {
|
||||
pageTitle: "joinOrganization",
|
||||
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="special">!@#$%^&*</label>
|
||||
<label for="special">!@#$%^&*</label>
|
||||
<input
|
||||
id="special"
|
||||
type="checkbox"
|
||||
|
||||
@@ -141,6 +141,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
if (
|
||||
params.uri &&
|
||||
this.cipher.login.uris[0] &&
|
||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||
) {
|
||||
this.cipher.login.uris[0].uri = params.uri;
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"form-data": "4.0.0",
|
||||
"https-proxy-agent": "7.0.2",
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "23.0.1",
|
||||
"jsdom": "24.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.15.0",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
|
||||
567
apps/desktop/desktop_native/Cargo.lock
generated
567
apps/desktop/desktop_native/Cargo.lock
generated
@@ -59,6 +59,156 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
@@ -119,6 +269,25 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -156,6 +325,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -185,6 +360,15 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
@@ -219,6 +403,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -314,9 +504,11 @@ dependencies = [
|
||||
"security-framework-sys",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"typenum",
|
||||
"widestring",
|
||||
"windows",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -355,6 +547,33 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@@ -377,6 +596,27 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
@@ -427,6 +667,19 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
@@ -438,6 +691,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
@@ -451,8 +710,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -600,6 +862,18 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.9"
|
||||
@@ -729,6 +1003,15 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -810,10 +1093,23 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -830,7 +1126,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -948,6 +1244,16 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.0"
|
||||
@@ -958,6 +1264,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
@@ -1003,12 +1315,38 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.4.0",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@@ -1214,6 +1552,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
@@ -1223,6 +1572,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
@@ -1234,6 +1594,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -1249,6 +1618,12 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
@@ -1327,8 +1702,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1376,6 +1764,37 @@ dependencies = [
|
||||
"winnow 0.6.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree_magic_mini"
|
||||
version = "3.1.5"
|
||||
@@ -1396,6 +1815,17 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
@@ -1511,6 +1941,22 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.8"
|
||||
@@ -1520,6 +1966,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
@@ -1730,7 +2182,7 @@ dependencies = [
|
||||
"derive-new",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"nix 0.28.0",
|
||||
"os_pipe",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
@@ -1757,3 +2209,112 @@ name = "x11rb-protocol"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix 0.29.0",
|
||||
"ordered-stream",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "4.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -22,6 +22,7 @@ retry = "=2.0.0"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
thiserror = "=1.0.61"
|
||||
tokio = { version = "1.38.0", features = ["io-util", "sync", "macros"] }
|
||||
typenum = "=1.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@@ -49,3 +50,4 @@ security-framework-sys = "=2.11.0"
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gio = "=0.19.5"
|
||||
libsecret = "=0.5.0"
|
||||
zbus = "4.3.1"
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod password;
|
||||
pub mod powermonitor;
|
||||
|
||||
51
apps/desktop/desktop_native/core/src/powermonitor/linux.rs
Normal file
51
apps/desktop/desktop_native/core/src/powermonitor/linux.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use zbus::{Connection, MatchRule, export::futures_util::TryStreamExt};
|
||||
struct ScreenLock {
|
||||
interface: Cow<'static, str>,
|
||||
path: Cow<'static, str>,
|
||||
}
|
||||
|
||||
const SCREEN_LOCK_MONITORS: [ScreenLock; 2] = [
|
||||
ScreenLock {
|
||||
interface: Cow::Borrowed("org.gnome.ScreenSaver"),
|
||||
path: Cow::Borrowed("/org/gnome/ScreenSaver"),
|
||||
},
|
||||
ScreenLock {
|
||||
interface: Cow::Borrowed("org.freedesktop.ScreenSaver"),
|
||||
path: Cow::Borrowed("/org/freedesktop/ScreenSaver"),
|
||||
},
|
||||
];
|
||||
|
||||
pub async fn on_lock(tx: tokio::sync::mpsc::Sender<()>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let connection = Connection::session().await?;
|
||||
|
||||
let proxy = zbus::fdo::DBusProxy::new(&connection).await?;
|
||||
for monitor in SCREEN_LOCK_MONITORS.iter() {
|
||||
let match_rule = MatchRule::builder()
|
||||
.msg_type(zbus::MessageType::Signal)
|
||||
.interface(monitor.interface.clone())?
|
||||
.member("ActiveChanged")?
|
||||
.build();
|
||||
proxy.add_match_rule(match_rule).await?;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(Some(_)) = zbus::MessageStream::from(&connection).try_next().await {
|
||||
tx.send(()).await.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_lock_monitor_available() -> bool {
|
||||
let connection = Connection::session().await.unwrap();
|
||||
for monitor in SCREEN_LOCK_MONITORS {
|
||||
let res = connection.call_method(Some(monitor.interface.clone()), monitor.path.clone(), Some(monitor.interface.clone()), "GetActive", &()).await;
|
||||
if res.is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
5
apps/desktop/desktop_native/core/src/powermonitor/mod.rs
Normal file
5
apps/desktop/desktop_native/core/src/powermonitor/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "unimplemented.rs")]
|
||||
mod powermonitor;
|
||||
pub use powermonitor::*;
|
||||
@@ -0,0 +1,7 @@
|
||||
pub async fn on_lock(_: tokio::sync::mpsc::Sender<()>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub async fn is_lock_monitor_available() -> bool {
|
||||
return false;
|
||||
}
|
||||
4
apps/desktop/desktop_native/napi/index.d.ts
vendored
4
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -41,3 +41,7 @@ export namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
export namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -206,8 +206,9 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { passwords, biometrics, clipboards } = nativeBinding
|
||||
const { passwords, biometrics, clipboards, powermonitors } = nativeBinding
|
||||
|
||||
module.exports.passwords = passwords
|
||||
module.exports.biometrics = biometrics
|
||||
module.exports.clipboards = clipboards
|
||||
module.exports.powermonitors = powermonitors
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
#[napi]
|
||||
pub mod passwords {
|
||||
/// Fetch the stored password from the keychain.
|
||||
@@ -142,3 +141,26 @@ pub mod clipboards {
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod powermonitors {
|
||||
use napi::{threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode}, tokio};
|
||||
|
||||
#[napi]
|
||||
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
|
||||
desktop_core::powermonitor::on_lock(tx).await.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn is_lock_monitor_available() -> napi::Result<bool> {
|
||||
Ok(desktop_core::powermonitor::is_lock_monitor_available().await)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"**/node_modules/argon2/package.json",
|
||||
"**/node_modules/argon2/build/Release/argon2.node"
|
||||
],
|
||||
"electronVersion": "31.2.1",
|
||||
"electronVersion": "31.3.0",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
|
||||
@@ -46,7 +46,7 @@ export class SettingsComponent implements OnInit {
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
|
||||
showMinToTray = false;
|
||||
vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
vaultTimeoutOptions: VaultTimeoutOption[] = [];
|
||||
localeOptions: any[];
|
||||
themeOptions: any[];
|
||||
clearClipboardOptions: any[];
|
||||
@@ -161,29 +161,6 @@ export class SettingsComponent implements OnInit {
|
||||
// DuckDuckGo browser is only for macos initially
|
||||
this.showDuckDuckGoIntegrationOption = isMac;
|
||||
|
||||
this.vaultTimeoutOptions = [
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
|
||||
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
|
||||
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
|
||||
{ name: this.i18nService.t("oneHour"), value: 60 },
|
||||
{ name: this.i18nService.t("fourHours"), value: 240 },
|
||||
{ name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle },
|
||||
{ name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep },
|
||||
];
|
||||
|
||||
if (this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("onLocked"),
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
}
|
||||
|
||||
this.vaultTimeoutOptions = this.vaultTimeoutOptions.concat([
|
||||
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
|
||||
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
|
||||
]);
|
||||
|
||||
const localeOptions: any[] = [];
|
||||
this.i18nService.supportedTranslationLocales.forEach((locale) => {
|
||||
let name = locale;
|
||||
@@ -215,6 +192,8 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions();
|
||||
|
||||
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.isWindows = (await this.platformUtilsService.getDevice()) === DeviceType.WindowsDesktop;
|
||||
@@ -718,6 +697,33 @@ export class SettingsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
|
||||
let vaultTimeoutOptions: VaultTimeoutOption[] = [
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
|
||||
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
|
||||
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
|
||||
{ name: this.i18nService.t("oneHour"), value: 60 },
|
||||
{ name: this.i18nService.t("fourHours"), value: 240 },
|
||||
{ name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle },
|
||||
{ name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep },
|
||||
];
|
||||
|
||||
if (await ipc.platform.powermonitor.isLockMonitorAvailable()) {
|
||||
vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("onLocked"),
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
}
|
||||
|
||||
vaultTimeoutOptions = vaultTimeoutOptions.concat([
|
||||
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
|
||||
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
|
||||
]);
|
||||
|
||||
return vaultTimeoutOptions;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
RegistrationStartComponent,
|
||||
RegistrationStartSecondaryComponent,
|
||||
RegistrationStartSecondaryComponentData,
|
||||
SetPasswordJitComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@@ -149,6 +150,15 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||
component: SetPasswordJitComponent,
|
||||
data: {
|
||||
pageTitle: "joinOrganization",
|
||||
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
import {
|
||||
DefaultSetPasswordJitService,
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export class DesktopSetPasswordJitService
|
||||
extends DefaultSetPasswordJitService
|
||||
implements SetPasswordJitService
|
||||
{
|
||||
messagingService = inject(MessagingService);
|
||||
|
||||
override async setPassword(credentials: SetPasswordCredentials) {
|
||||
await super.setPassword(credentials);
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
}
|
||||
}
|
||||
@@ -18,16 +18,30 @@ import {
|
||||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { SetPasswordJitService } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import {
|
||||
KdfConfigService,
|
||||
KdfConfigService as KdfConfigServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import {
|
||||
CryptoService,
|
||||
CryptoService as CryptoServiceAbstraction,
|
||||
} from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -56,7 +70,6 @@ import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vau
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
||||
@@ -77,6 +90,7 @@ import { NativeMessagingService } from "../../services/native-messaging.service"
|
||||
import { SearchBarService } from "../layout/search/search-bar.service";
|
||||
|
||||
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
||||
import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
|
||||
import { InitService } from "./init.service";
|
||||
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
||||
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
||||
@@ -254,6 +268,20 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: CLIENT_TYPE,
|
||||
useValue: ClientType.Desktop,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: DesktopSetPasswordJitService,
|
||||
deps: [
|
||||
ApiService,
|
||||
CryptoService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="special">!@#$%^&*</label>
|
||||
<label for="special">!@#$%^&*</label>
|
||||
<input
|
||||
id="special"
|
||||
type="checkbox"
|
||||
|
||||
@@ -551,6 +551,12 @@
|
||||
"masterPassHintLabel": {
|
||||
"message": "Master password hint"
|
||||
},
|
||||
"joinOrganization": {
|
||||
"message": "Join organization"
|
||||
},
|
||||
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||
"message": "Finish joining this organization by setting a master password."
|
||||
},
|
||||
"settings": {
|
||||
"message": "Settings"
|
||||
},
|
||||
@@ -2093,6 +2099,9 @@
|
||||
"vaultTimeoutTooLarge": {
|
||||
"message": "Your vault timeout exceeds the restrictions set by your organization."
|
||||
},
|
||||
"inviteAccepted": {
|
||||
"message": "Invitation accepted"
|
||||
},
|
||||
"resetPasswordPolicyAutoEnroll": {
|
||||
"message": "Automatic enrollment"
|
||||
},
|
||||
|
||||
@@ -184,7 +184,7 @@ export class Main {
|
||||
});
|
||||
});
|
||||
|
||||
this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
|
||||
this.powerMonitorMain = new PowerMonitorMain(this.messagingService, this.logService);
|
||||
this.menuMain = new MenuMain(
|
||||
this.i18nService,
|
||||
this.messagingService,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { powerMonitor } from "electron";
|
||||
import { ipcMain, powerMonitor } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { powermonitors } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { isSnapStore } from "../utils";
|
||||
|
||||
@@ -11,7 +13,10 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds
|
||||
export class PowerMonitorMain {
|
||||
private idle = false;
|
||||
|
||||
constructor(private messagingService: MessageSender) {}
|
||||
constructor(
|
||||
private messagingService: MessageSender,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
// ref: https://github.com/electron/electron/issues/13767
|
||||
@@ -27,7 +32,22 @@ export class PowerMonitorMain {
|
||||
powerMonitor.on("lock-screen", () => {
|
||||
this.messagingService.send("systemLocked");
|
||||
});
|
||||
} else {
|
||||
powermonitors
|
||||
.onLock(() => {
|
||||
this.messagingService.send("systemLocked");
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("Error setting up lock monitor", { error });
|
||||
});
|
||||
}
|
||||
ipcMain.handle("powermonitor.isLockMonitorAvailable", async (_event: any, _message: any) => {
|
||||
if (process.platform !== "linux") {
|
||||
return true;
|
||||
} else {
|
||||
return await powermonitors.isLockMonitorAvailable();
|
||||
}
|
||||
});
|
||||
|
||||
// System idle
|
||||
global.setInterval(() => {
|
||||
|
||||
@@ -59,6 +59,11 @@ const clipboard = {
|
||||
write: (message: ClipboardWriteMessage) => ipcRenderer.invoke("clipboard.write", message),
|
||||
};
|
||||
|
||||
const powermonitor = {
|
||||
isLockMonitorAvailable: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("powermonitor.isLockMonitorAvailable"),
|
||||
};
|
||||
|
||||
const nativeMessaging = {
|
||||
sendReply: (message: EncryptedMessageResponse | UnencryptedMessageResponse) => {
|
||||
ipcRenderer.send("nativeMessagingReply", message);
|
||||
@@ -148,6 +153,7 @@ export default {
|
||||
passwords,
|
||||
biometric,
|
||||
clipboard,
|
||||
powermonitor,
|
||||
nativeMessaging,
|
||||
crypto,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,6 @@
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="requireSpecial" id="requireSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
|
||||
@@ -145,6 +145,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./web-set-password-jit.service";
|
||||
@@ -0,0 +1,27 @@
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
import {
|
||||
DefaultSetPasswordJitService,
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
|
||||
import { RouterService } from "../../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
export class WebSetPasswordJitService
|
||||
extends DefaultSetPasswordJitService
|
||||
implements SetPasswordJitService
|
||||
{
|
||||
routerService = inject(RouterService);
|
||||
acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);
|
||||
|
||||
override async setPassword(credentials: SetPasswordCredentials) {
|
||||
await super.setPassword(credentials);
|
||||
|
||||
// SSO JIT accepts org invites when setting their MP, meaning
|
||||
// we can clear the deep linked url for accepting it.
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,12 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
|
||||
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -43,6 +46,7 @@ export class TwoFactorAuthenticatorComponent
|
||||
@Output() onChangeStatus = new EventEmitter<boolean>();
|
||||
type = TwoFactorProviderType.Authenticator;
|
||||
key: string;
|
||||
private userVerificationToken: string;
|
||||
|
||||
override componentName = "app-two-factor-authenticator";
|
||||
qrScriptError = false;
|
||||
@@ -63,6 +67,7 @@ export class TwoFactorAuthenticatorComponent
|
||||
logService: LogService,
|
||||
private accountService: AccountService,
|
||||
dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -112,16 +117,46 @@ export class TwoFactorAuthenticatorComponent
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
|
||||
request.token = this.formGroup.value.token;
|
||||
request.key = this.key;
|
||||
request.userVerificationToken = this.userVerificationToken;
|
||||
|
||||
const response = await this.apiService.putTwoFactorAuthenticator(request);
|
||||
await this.processResponse(response);
|
||||
this.onUpdated.emit(true);
|
||||
}
|
||||
|
||||
protected override async disableMethod() {
|
||||
const twoFactorAuthenticatorTokenFeatureFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AuthenticatorTwoFactorToken,
|
||||
);
|
||||
if (twoFactorAuthenticatorTokenFeatureFlag === false) {
|
||||
return super.disableMethod();
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "disable" },
|
||||
content: { key: "twoStepDisableDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = await this.buildRequestModel(DisableTwoFactorAuthenticatorRequest);
|
||||
request.type = this.type;
|
||||
request.key = this.key;
|
||||
request.userVerificationToken = this.userVerificationToken;
|
||||
await this.apiService.deleteTwoFactorAuthenticator(request);
|
||||
this.enabled = false;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled"));
|
||||
this.onUpdated.emit(false);
|
||||
}
|
||||
|
||||
private async processResponse(response: TwoFactorAuthenticatorResponse) {
|
||||
this.formGroup.get("token").setValue(null);
|
||||
this.enabled = response.enabled;
|
||||
this.key = response.key;
|
||||
this.userVerificationToken = response.userVerificationToken;
|
||||
|
||||
await this.waitForQRiousToLoadOrError().catch((error) => {
|
||||
this.logService.error(error);
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<ng-template body>
|
||||
<tr *ngFor="let i of subscription.items">
|
||||
<td bitCell>
|
||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||
{{ i.amount | currency: "$" }}
|
||||
</td>
|
||||
<td bitCell>{{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}</td>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<tr bitRow *ngFor="let i of subscriptionLineItems">
|
||||
<td bitCell [ngClass]="{ 'tw-pl-20': i.addonSubscriptionItem }">
|
||||
<span *ngIf="!i.addonSubscriptionItem">{{ i.productName | i18n }} -</span>
|
||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||
{{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @
|
||||
{{ i.amount | currency: "$" }}
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
|
||||
@@ -17,11 +17,20 @@ import {
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||
import { RegistrationFinishService as RegistrationFinishServiceAbstraction } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
SetPasswordJitService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -48,7 +57,7 @@ import {
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import { WebRegistrationFinishService } from "../auth";
|
||||
import { WebSetPasswordJitService, WebRegistrationFinishService } from "../auth";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
@@ -184,6 +193,20 @@ const safeProviders: SafeProvider[] = [
|
||||
PolicyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: WebSetPasswordJitService,
|
||||
deps: [
|
||||
ApiService,
|
||||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -15,21 +15,45 @@
|
||||
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0"
|
||||
>
|
||||
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
|
||||
<a
|
||||
*ngFor="let more of moreProducts"
|
||||
[href]="more.marketingRoute"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
|
||||
<div>
|
||||
{{ more.otherProductOverrides?.name ?? more.name }}
|
||||
<div *ngIf="more.otherProductOverrides?.supportingText" class="tw-text-xs tw-font-normal">
|
||||
{{ more.otherProductOverrides.supportingText }}
|
||||
<ng-container *ngFor="let more of moreProducts">
|
||||
<!-- <a> for when the marketing route is external -->
|
||||
<a
|
||||
*ngIf="more.marketingRoute.external"
|
||||
[href]="more.marketingRoute.route"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
|
||||
<div>
|
||||
{{ more.otherProductOverrides?.name ?? more.name }}
|
||||
<div
|
||||
*ngIf="more.otherProductOverrides?.supportingText"
|
||||
class="tw-text-xs tw-font-normal"
|
||||
>
|
||||
{{ more.otherProductOverrides.supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
|
||||
<a
|
||||
*ngIf="!more.marketingRoute.external"
|
||||
[routerLink]="more.marketingRoute.route"
|
||||
rel="noreferrer"
|
||||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
|
||||
<div>
|
||||
{{ more.otherProductOverrides?.name ?? more.name }}
|
||||
<div
|
||||
*ngIf="more.otherProductOverrides?.supportingText"
|
||||
class="tw-text-xs tw-font-normal"
|
||||
>
|
||||
{{ more.otherProductOverrides.supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</section>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
isActive: false,
|
||||
name: "Other Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -100,7 +103,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
isActive: false,
|
||||
name: "Other Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
otherProductOverrides: { name: "Alternate name" },
|
||||
},
|
||||
],
|
||||
@@ -117,7 +123,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
isActive: false,
|
||||
name: "Other Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
otherProductOverrides: { name: "Alternate name", supportingText: "Supporting Text" },
|
||||
},
|
||||
],
|
||||
@@ -134,9 +143,27 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
mockProducts$.next({
|
||||
bento: [],
|
||||
other: [
|
||||
{ name: "AA Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" },
|
||||
{ name: "Test Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" },
|
||||
{ name: "Organizations", icon: "bwi-lock", marketingRoute: "https://www.example.com/" },
|
||||
{
|
||||
name: "AA Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Organizations",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: { route: "https://www.example.com/", external: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -157,7 +184,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
{
|
||||
name: "Organizations",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
marketingRoute: {
|
||||
route: "https://www.example.com/",
|
||||
external: true,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -34,17 +34,30 @@
|
||||
class="tw-mt-4 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-muted tw-p-2 tw-pb-0"
|
||||
>
|
||||
<span class="tw-mb-1 tw-text-xs tw-text-muted">{{ "moreFromBitwarden" | i18n }}</span>
|
||||
<a
|
||||
*ngFor="let product of products.other"
|
||||
bitLink
|
||||
[href]="product.marketingRoute"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span class="tw-flex tw-items-center tw-font-normal">
|
||||
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
|
||||
</span>
|
||||
</a>
|
||||
<span *ngFor="let product of products.other">
|
||||
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
|
||||
<a
|
||||
*ngIf="!product.marketingRoute.external"
|
||||
bitLink
|
||||
[routerLink]="product.marketingRoute.route"
|
||||
>
|
||||
<span class="tw-flex tw-items-center tw-font-normal">
|
||||
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
|
||||
</span>
|
||||
</a>
|
||||
<!-- <a> for when the marketing route is external -->
|
||||
<a
|
||||
*ngIf="product.marketingRoute.external"
|
||||
bitLink
|
||||
[href]="product.marketingRoute.route"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span class="tw-flex tw-items-center tw-font-normal">
|
||||
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</bit-menu>
|
||||
|
||||
@@ -30,7 +30,13 @@ export type ProductSwitcherItem = {
|
||||
/**
|
||||
* Route for items in the `otherProducts$` section
|
||||
*/
|
||||
marketingRoute?: string | any[];
|
||||
marketingRoute?: {
|
||||
route: string | any[];
|
||||
external: boolean;
|
||||
};
|
||||
/**
|
||||
* Route definition for external/internal routes for items in the `otherProducts$` section
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used to apply css styles to show when a button is selected
|
||||
@@ -136,7 +142,10 @@ export class ProductSwitcherService {
|
||||
name: "Password Manager",
|
||||
icon: "bwi-lock",
|
||||
appRoute: "/vault",
|
||||
marketingRoute: "https://bitwarden.com/products/personal/",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/personal/",
|
||||
external: true,
|
||||
},
|
||||
isActive:
|
||||
!this.router.url.includes("/sm/") &&
|
||||
!this.router.url.includes("/organizations/") &&
|
||||
@@ -146,7 +155,10 @@ export class ProductSwitcherService {
|
||||
name: "Secrets Manager",
|
||||
icon: "bwi-cli",
|
||||
appRoute: ["/sm", smOrg?.id],
|
||||
marketingRoute: "https://bitwarden.com/products/secrets-manager/",
|
||||
marketingRoute: {
|
||||
route: "/sm-landing",
|
||||
external: false,
|
||||
},
|
||||
isActive: this.router.url.includes("/sm/"),
|
||||
otherProductOverrides: {
|
||||
supportingText: this.i18n.transform("secureYourInfrastructure"),
|
||||
@@ -156,7 +168,10 @@ export class ProductSwitcherService {
|
||||
name: "Admin Console",
|
||||
icon: "bwi-business",
|
||||
appRoute: ["/organizations", acOrg?.id],
|
||||
marketingRoute: "https://bitwarden.com/products/business/",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
},
|
||||
isActive: this.router.url.includes("/organizations/"),
|
||||
},
|
||||
provider: {
|
||||
@@ -168,7 +183,10 @@ export class ProductSwitcherService {
|
||||
orgs: {
|
||||
name: "Organizations",
|
||||
icon: "bwi-business",
|
||||
marketingRoute: "https://bitwarden.com/products/business/",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
},
|
||||
otherProductOverrides: {
|
||||
name: "Share your passwords",
|
||||
supportingText: this.i18n.transform("protectYourFamilyOrBusiness"),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
RegistrationStartComponent,
|
||||
RegistrationStartSecondaryComponent,
|
||||
RegistrationStartSecondaryComponentData,
|
||||
SetPasswordJitComponent,
|
||||
LockIcon,
|
||||
RegistrationLinkExpiredComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -59,6 +60,8 @@ import { EnvironmentSelectorComponent } from "./components/environment-selector/
|
||||
import { DataProperties } from "./core";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";
|
||||
import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm-landing.component";
|
||||
import { DomainRulesComponent } from "./settings/domain-rules.component";
|
||||
import { PreferencesComponent } from "./settings/preferences.component";
|
||||
import { GeneratorComponent } from "./tools/generator.component";
|
||||
@@ -206,6 +209,15 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||
component: SetPasswordJitComponent,
|
||||
data: {
|
||||
pageTitle: "joinOrganization",
|
||||
pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "signup-link-expired",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||
@@ -405,6 +417,16 @@ const routes: Routes = [
|
||||
component: SendComponent,
|
||||
data: { titleId: "send" } satisfies DataProperties,
|
||||
},
|
||||
{
|
||||
path: "sm-landing",
|
||||
component: SMLandingComponent,
|
||||
data: { titleId: "moreProductsFromBitwarden" },
|
||||
},
|
||||
{
|
||||
path: "request-sm-access",
|
||||
component: RequestSMAccessComponent,
|
||||
data: { titleId: "requestAccessToSecretsManager" },
|
||||
},
|
||||
{
|
||||
path: "create-organization",
|
||||
component: CreateOrganizationComponent,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Guid } from "@bitwarden/common/src/types/guid";
|
||||
|
||||
export class RequestSMAccessRequest {
|
||||
OrganizationId: Guid;
|
||||
EmailContent: string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<bit-container>
|
||||
<form [formGroup]="requestAccessForm" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-9">
|
||||
<p bitTypography="body1">{{ "youNeedApprovalFromYourAdminToTrySecretsManager" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "addANote" | i18n }}</bit-label>
|
||||
<textarea
|
||||
rows="20"
|
||||
id="request_access_textarea"
|
||||
bitInput
|
||||
formControlName="requestAccessEmailContents"
|
||||
></textarea>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "organization" | i18n }}</bit-label>
|
||||
<bit-select formControlName="selectedOrganization">
|
||||
<bit-option
|
||||
*ngFor="let org of organizations"
|
||||
[value]="org"
|
||||
[label]="org.name"
|
||||
required
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex tw-gap-x-4 tw-mt-4">
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
{{ "sendRequest" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" [routerLink]="'/sm-landing'">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { OssModule } from "../../oss.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request";
|
||||
|
||||
import { SmLandingApiService } from "./sm-landing-api.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-request-sm-access",
|
||||
standalone: true,
|
||||
templateUrl: "request-sm-access.component.html",
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, OssModule],
|
||||
})
|
||||
export class RequestSMAccessComponent implements OnInit {
|
||||
requestAccessForm = new FormGroup({
|
||||
requestAccessEmailContents: new FormControl(
|
||||
this.i18nService.t("requestAccessSMDefaultEmailContent"),
|
||||
[Validators.required],
|
||||
),
|
||||
selectedOrganization: new FormControl<Organization>(null, [Validators.required]),
|
||||
});
|
||||
organizations: Organization[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private smLandingApiService: SmLandingApiService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.organizations = (await this.organizationService.getAll())
|
||||
.filter((e) => e.enabled)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (this.organizations === null || this.organizations.length < 1) {
|
||||
await this.navigateToCreateOrganizationPage();
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.requestAccessForm.markAllAsTouched();
|
||||
if (this.requestAccessForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.requestAccessForm.value;
|
||||
const request = new RequestSMAccessRequest();
|
||||
request.OrganizationId = formValue.selectedOrganization.id as Guid;
|
||||
request.EmailContent = formValue.requestAccessEmailContents;
|
||||
|
||||
await this.smLandingApiService.requestSMAccessFromAdmins(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("smAccessRequestEmailSent"),
|
||||
});
|
||||
await this.router.navigate(["/"]);
|
||||
};
|
||||
|
||||
async navigateToCreateOrganizationPage() {
|
||||
await this.router.navigate(["/create-organization"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class SmLandingApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async requestSMAccessFromAdmins(request: RequestSMAccessRequest): Promise<void> {
|
||||
await this.apiService.send("POST", "/request-access/request-sm-access", request, true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<app-header></app-header>
|
||||
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<img [src]="imageSrc" class="tw-max-w-full" alt="Bitwarden" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="tw-col-span-6 tw-mx-4">
|
||||
<h1 bitTypography="h1">{{ "bitwardenSecretsManager" | i18n }}</h1>
|
||||
<bit-container *ngIf="this.showSecretsManagerInformation">
|
||||
<p bitTypography="body1">
|
||||
{{ "developmentDevOpsAndITTeamsChooseBWSecret" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-outside">
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "centralizeSecretsManagement" | i18n }}</b>
|
||||
{{ "centralizeSecretsManagementDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "preventSecretLeaks" | i18n }}</b> {{ "preventSecretLeaksDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "enhanceDeveloperProductivity" | i18n }}</b>
|
||||
{{ "enhanceDeveloperProductivityDescription" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
<b>{{ "strengthenBusinessSecurity" | i18n }}</b>
|
||||
{{ "strengthenBusinessSecurityDescription" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</bit-container>
|
||||
<bit-container *ngIf="this.showGiveMembersAccessInstructions">
|
||||
<p bitTypography="body1">
|
||||
{{ "giveMembersAccess" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-outside">
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
{{ "openYourOrganizations" | i18n }} <b>{{ "members" | i18n }}</b>
|
||||
{{ "viewAndSelectTheMembers" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1" class="tw-mb-2">
|
||||
{{ "usingTheMenuSelect" | i18n }} <b>{{ "activateSecretsManager" | i18n }}</b>
|
||||
{{ "toGrantAccessToSelectedMembers" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</bit-container>
|
||||
<button type="button" bitButton buttonType="primary" [routerLink]="tryItNowUrl">
|
||||
{{ "tryItNow" | i18n }}
|
||||
</button>
|
||||
<a bitLink linkType="primary" [href]="learnMoreUrl" target="_blank" class="tw-m-5">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { NoItemsModule, SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-sm-landing",
|
||||
standalone: true,
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
|
||||
templateUrl: "sm-landing.component.html",
|
||||
})
|
||||
export class SMLandingComponent {
|
||||
tryItNowUrl: string;
|
||||
learnMoreUrl: string = "https://bitwarden.com/help/secrets-manager-overview/";
|
||||
imageSrc: string = "../images/sm.webp";
|
||||
showSecretsManagerInformation: boolean = true;
|
||||
showGiveMembersAccessInstructions: boolean = false;
|
||||
|
||||
constructor(private organizationService: OrganizationService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled);
|
||||
|
||||
if (enabledOrganizations.length > 0) {
|
||||
this.handleEnabledOrganizations(enabledOrganizations);
|
||||
} else {
|
||||
// Person is not part of any orgs they need to be in an organization in order to use SM
|
||||
this.tryItNowUrl = "/create-organization";
|
||||
}
|
||||
}
|
||||
|
||||
private handleEnabledOrganizations(enabledOrganizations: Organization[]) {
|
||||
// People get to this page because SM (Secrets Manager) isn't enabled for them (or the Organization they are a part of)
|
||||
// 1 - SM is enabled for the Organization but not that user
|
||||
//1a - person is Admin+ (Admin or higher) and just needs instructions on how to enable it for themselves
|
||||
//1b - person is beneath admin status and needs to request SM access from Administrators/Owners
|
||||
// 2 - SM is not enabled for the organization yet
|
||||
//2a - person is Owner/Provider - Direct them to the subscription/billing page
|
||||
//2b - person is Admin - Direct them to request access page where an email is sent to owner/admins
|
||||
//2c - person is user - Direct them to request access page where an email is sent to owner/admins
|
||||
|
||||
// We use useSecretsManager because we want to get the first org the person is a part of where SM is enabled but they don't have access enabled yet
|
||||
const adminPlusNeedsInstructionsToEnableSM = enabledOrganizations.find(
|
||||
(o) => o.isAdmin && o.useSecretsManager,
|
||||
);
|
||||
const ownerNeedsToEnableSM = enabledOrganizations.find(
|
||||
(o) => o.isOwner && !o.useSecretsManager,
|
||||
);
|
||||
|
||||
// 1a If Organization has SM Enabled, but this logged in person does not have it enabled, but they are admin+ then give them instructions to enable.
|
||||
if (adminPlusNeedsInstructionsToEnableSM != undefined) {
|
||||
this.showHowToEnableSMForMembers(adminPlusNeedsInstructionsToEnableSM.id);
|
||||
}
|
||||
// 2a Owners can enable SM in the subscription area of Admin Console.
|
||||
else if (ownerNeedsToEnableSM != undefined) {
|
||||
this.tryItNowUrl = `/organizations/${ownerNeedsToEnableSM.id}/billing/subscription`;
|
||||
}
|
||||
// 1b and 2b 2c, they must be lower than an Owner, and they need access, or want their org to have access to SM.
|
||||
else {
|
||||
this.tryItNowUrl = "/request-sm-access";
|
||||
}
|
||||
}
|
||||
|
||||
private showHowToEnableSMForMembers(orgId: string) {
|
||||
this.showGiveMembersAccessInstructions = true;
|
||||
this.showSecretsManagerInformation = false;
|
||||
this.learnMoreUrl =
|
||||
"https://bitwarden.com/help/secrets-manager-quick-start/#give-members-access";
|
||||
this.imageSrc = "../images/sm-give-access.png";
|
||||
this.tryItNowUrl = `/organizations/${orgId}/members`;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@
|
||||
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
||||
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
||||
/>
|
||||
<label for="special" class="form-check-label">!@#$%^&*</label>
|
||||
<label for="special" class="form-check-label">!@#$%^&*</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
|
||||
BIN
apps/web/src/images/sm-give-access.png
Normal file
BIN
apps/web/src/images/sm-give-access.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 524 KiB |
BIN
apps/web/src/images/sm.webp
Normal file
BIN
apps/web/src/images/sm.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -3471,6 +3471,9 @@
|
||||
"joinOrganizationDesc": {
|
||||
"message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account."
|
||||
},
|
||||
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||
"message": "Finish joining this organization by setting a master password."
|
||||
},
|
||||
"inviteAccepted": {
|
||||
"message": "Invitation accepted"
|
||||
},
|
||||
@@ -4810,6 +4813,75 @@
|
||||
"message": "or",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more, see how it works, **or** try it now.'"
|
||||
},
|
||||
"developmentDevOpsAndITTeamsChooseBWSecret": {
|
||||
"message": "Development, DevOps, and IT teams choose Bitwarden Secrets Manager to securely manage and deploy their infrastructure and machine secrets."
|
||||
},
|
||||
"centralizeSecretsManagement": {
|
||||
"message": "Centralize secrets management."
|
||||
},
|
||||
"centralizeSecretsManagementDescription": {
|
||||
"message": "Securely store and manage secrets in one location to prevent secret sprawl across your organization."
|
||||
},
|
||||
"preventSecretLeaks": {
|
||||
"message": "Prevent secret leaks."
|
||||
},
|
||||
"preventSecretLeaksDescription": {
|
||||
"message": "Protect secrets with end-to-end encryption. No more hard coding secrets or sharing through .env files."
|
||||
},
|
||||
"enhanceDeveloperProductivity": {
|
||||
"message": "Enhance developer productivity."
|
||||
},
|
||||
"enhanceDeveloperProductivityDescription": {
|
||||
"message": "Programmatically retrieve and deploy secrets at runtime so developers can focus on what matters most, like improving code quality."
|
||||
},
|
||||
"strengthenBusinessSecurity": {
|
||||
"message": "Strengthen business security."
|
||||
},
|
||||
"strengthenBusinessSecurityDescription": {
|
||||
"message": "Maintain tight control over machine and human access to secrets with SSO integrations, event logs, and access rotation."
|
||||
},
|
||||
"tryItNow": {
|
||||
"message": "Try it now"
|
||||
},
|
||||
"sendRequest": {
|
||||
"message": "Send request"
|
||||
},
|
||||
"addANote": {
|
||||
"message": "Add a note"
|
||||
},
|
||||
"bitwardenSecretsManager": {
|
||||
"message": "Bitwarden Secrets Manager"
|
||||
},
|
||||
"moreProductsFromBitwarden": {
|
||||
"message": "More products from Bitwarden"
|
||||
},
|
||||
"requestAccessToSecretsManager": {
|
||||
"message": "Request access to Secrets Manager"
|
||||
},
|
||||
"youNeedApprovalFromYourAdminToTrySecretsManager": {
|
||||
"message": "You need approval from your administrator to try Secrets Manager."
|
||||
},
|
||||
"smAccessRequestEmailSent" : {
|
||||
"message": "Access request for secrets manager email sent to admins."
|
||||
},
|
||||
"requestAccessSMDefaultEmailContent": {
|
||||
"message": "Hi,\n\nI am requesting a subscription to Bitwarden Secrets Manager for our team. Your support would mean a great deal!\n\nBitwarden Secrets Manager is an end-to-end encrypted secrets management solution for securely storing, sharing, and deploying machine credentials like API keys, database passwords, and authentication certificates.\n\nSecrets Manager will help us to:\n\n- Improve security\n- Streamline operations\n- Prevent costly secret leaks\n\nTo request a free trial for our team, please reach out to Bitwarden.\n\nThank you for your help!"
|
||||
},
|
||||
"giveMembersAccess": {
|
||||
"message": "Give members access:"
|
||||
},
|
||||
"viewAndSelectTheMembers" : {
|
||||
"message" :"view and select the members you want to give access to Secrets Manager."
|
||||
},
|
||||
"openYourOrganizations": {
|
||||
"message": "Open your organization's"
|
||||
},
|
||||
"usingTheMenuSelect": {
|
||||
"message": "Using the menu, select"
|
||||
},
|
||||
"toGrantAccessToSelectedMembers": {
|
||||
"message": "to grant access to selected members."
|
||||
},
|
||||
"sendVaultCardTryItNow": {
|
||||
"message": "try it now",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more, see how it works, or **try it now**.'"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{
|
||||
i.cadence.toLowerCase()
|
||||
}}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }}
|
||||
@
|
||||
@
|
||||
{{
|
||||
getFormattedCost(
|
||||
i.cost,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -319,6 +320,14 @@ export class SsoComponent {
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
const emailVerification = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.EmailVerification,
|
||||
);
|
||||
|
||||
if (emailVerification) {
|
||||
this.changePasswordRoute = "set-password-jit";
|
||||
}
|
||||
|
||||
await this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginChangePasswordNavigate,
|
||||
[this.changePasswordRoute],
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
SetPasswordJitService,
|
||||
DefaultSetPasswordJitService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
DefaultRegistrationFinishService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -1265,6 +1267,20 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: StripeService,
|
||||
deps: [LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RegisterRouteService,
|
||||
useClass: RegisterRouteService,
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
// icons
|
||||
export * from "./icons";
|
||||
|
||||
// anon layout
|
||||
export * from "./anon-layout/anon-layout.component";
|
||||
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||
|
||||
// fingerprint dialog
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
export * from "./input-password/input-password.component";
|
||||
|
||||
// password callout
|
||||
export * from "./password-callout/password-callout.component";
|
||||
export * from "./vault-timeout-input/vault-timeout-input.component";
|
||||
|
||||
// input password
|
||||
export * from "./input-password/input-password.component";
|
||||
export * from "./input-password/password-input-result";
|
||||
|
||||
// set password (JIT user)
|
||||
export * from "./set-password-jit/set-password-jit.component";
|
||||
export * from "./set-password-jit/set-password-jit.service.abstraction";
|
||||
export * from "./set-password-jit/default-set-password-jit.service";
|
||||
|
||||
// user verification
|
||||
export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
@@ -25,6 +38,3 @@ export * from "./registration/registration-start/registration-start-secondary.co
|
||||
export * from "./registration/registration-env-selector/registration-env-selector.component";
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// input password
|
||||
export * from "./input-password/password-input-result";
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<bit-hint>
|
||||
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||
{{ "masterPassImportant" | i18n }}
|
||||
{{ minPasswordMsg }}.
|
||||
{{ minPasswordLengthMsg }}.
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -12,6 +12,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
@@ -48,17 +49,16 @@ import { PasswordInputResult } from "./password-input-result";
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class InputPasswordComponent implements OnInit {
|
||||
export class InputPasswordComponent {
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
|
||||
@Input({ required: true }) email: string;
|
||||
@Input() protected buttonText: string;
|
||||
@Input() buttonText: string;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
@Input() loading: boolean = false;
|
||||
|
||||
private minHintLength = 0;
|
||||
protected maxHintLength = 50;
|
||||
|
||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||
protected minPasswordMsg = "";
|
||||
protected passwordStrengthScore: PasswordStrengthScore;
|
||||
@@ -103,17 +103,14 @@ export class InputPasswordComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
this.masterPasswordPolicyOptions.minLength > 0
|
||||
) {
|
||||
this.minPasswordMsg = this.i18nService.t(
|
||||
"characterMinimum",
|
||||
this.masterPasswordPolicyOptions.minLength,
|
||||
);
|
||||
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
|
||||
} else {
|
||||
this.minPasswordMsg = this.i18nService.t("characterMinimum", this.minPasswordLength);
|
||||
return this.i18nService.t("characterMinimum", this.minPasswordLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,9 +178,16 @@ export class InputPasswordComponent implements OnInit {
|
||||
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
|
||||
|
||||
const localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
password,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
this.onPasswordFormSubmit.emit({
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
localMasterKeyHash,
|
||||
kdfConfig,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<bit-callout>
|
||||
{{ message | i18n }}
|
||||
|
||||
<ul *ngIf="policy" class="tw-mb-0">
|
||||
<ul *ngIf="policy" class="tw-mb-0 tw-ml-8 tw-ps-0">
|
||||
<li *ngIf="policy?.minComplexity > 0">
|
||||
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form [formGroup]="formGroup">
|
||||
<form [formGroup]="formGroup" *ngIf="!hideEnvSelector">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "creatingAccountOn" | i18n }}</bit-label>
|
||||
<bit-select formControlName="selectedRegion">
|
||||
|
||||
@@ -44,6 +44,7 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
|
||||
private selectedRegionFromEnv: RegionConfig | Region.SelfHosted;
|
||||
|
||||
hideEnvSelector = false;
|
||||
isDesktopOrBrowserExtension = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -59,9 +60,15 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
const clientType = platformUtilsService.getClientType();
|
||||
this.isDesktopOrBrowserExtension =
|
||||
clientType === ClientType.Desktop || clientType === ClientType.Browser;
|
||||
|
||||
this.hideEnvSelector = clientType === ClientType.Web && this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.hideEnvSelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.initSelectedRegionAndListenForEnvChanges();
|
||||
this.listenForSelectedRegionChanges();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="full-loading-spinner" *ngIf="loading">
|
||||
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router, RouterModule } from "@angular/router";
|
||||
import { Subject, from, switchMap, takeUntil, tap } from "rxjs";
|
||||
import { EMPTY, Subject, from, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
@@ -76,6 +76,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
return from(
|
||||
this.registerVerificationEmailClicked(this.email, this.emailVerificationToken),
|
||||
);
|
||||
} else {
|
||||
// org invite flow
|
||||
this.loading = false;
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { DefaultSetPasswordJitService } from "./default-set-password-jit.service";
|
||||
import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
|
||||
|
||||
describe("DefaultSetPasswordJitService", () => {
|
||||
let sut: DefaultSetPasswordJitService;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserService = mock<OrganizationUserService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
apiService,
|
||||
cryptoService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserService,
|
||||
userDecryptionOptionsService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should instantiate the DefaultSetPasswordJitService", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setPassword", () => {
|
||||
let masterKey: MasterKey;
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let protectedUserKey: [UserKey, EncString];
|
||||
let keyPair: [string, EncString];
|
||||
let keysRequest: KeysRequest;
|
||||
let organizationKeys: OrganizationKeysResponse;
|
||||
let orgPublicKey: Uint8Array;
|
||||
|
||||
let orgSsoIdentifier: string;
|
||||
let orgId: string;
|
||||
let resetPasswordAutoEnroll: boolean;
|
||||
let userId: UserId;
|
||||
let passwordInputResult: PasswordInputResult;
|
||||
let credentials: SetPasswordCredentials;
|
||||
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
let setPasswordRequest: SetPasswordRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
userKeyEncString = new EncString("userKeyEncrypted");
|
||||
protectedUserKey = [userKey, userKeyEncString];
|
||||
keyPair = ["publicKey", new EncString("privateKey")];
|
||||
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
organizationKeys = {
|
||||
privateKey: "orgPrivateKey",
|
||||
publicKey: "orgPublicKey",
|
||||
} as OrganizationKeysResponse;
|
||||
orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
orgSsoIdentifier = "orgSsoIdentifier";
|
||||
orgId = "orgId";
|
||||
resetPasswordAutoEnroll = false;
|
||||
userId = "userId" as UserId;
|
||||
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
};
|
||||
|
||||
credentials = {
|
||||
...passwordInputResult,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
};
|
||||
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.masterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.hint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
passwordInputResult.kdfConfig.iterations,
|
||||
);
|
||||
});
|
||||
|
||||
function setupSetPasswordMocks(hasUserKey = true) {
|
||||
if (!hasUserKey) {
|
||||
cryptoService.userKey$.mockReturnValue(of(null));
|
||||
cryptoService.makeUserKey.mockResolvedValue(protectedUserKey);
|
||||
} else {
|
||||
cryptoService.userKey$.mockReturnValue(of(userKey));
|
||||
cryptoService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey);
|
||||
}
|
||||
|
||||
cryptoService.makeKeyPair.mockResolvedValue(keyPair);
|
||||
|
||||
apiService.setPassword.mockResolvedValue(undefined);
|
||||
masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
|
||||
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined);
|
||||
kdfConfigService.setKdfConfig.mockResolvedValue(undefined);
|
||||
cryptoService.setUserKey.mockResolvedValue(undefined);
|
||||
|
||||
cryptoService.setPrivateKey.mockResolvedValue(undefined);
|
||||
|
||||
masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) {
|
||||
if (organizationKeysExist) {
|
||||
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
|
||||
} else {
|
||||
organizationApiService.getKeys.mockResolvedValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
cryptoService.userKey$.mockReturnValue(of(userKey));
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString);
|
||||
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
it("should set password successfully (given a user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should set password successfully (given no user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks(false);
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should handle reset password auto enroll", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
|
||||
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
|
||||
expect(organizationUserService.putOrganizationUserResetPasswordEnrollment).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks(false);
|
||||
|
||||
// Act and Assert
|
||||
await expect(sut.setPassword(credentials)).rejects.toThrow();
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserService: OrganizationUserService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
localMasterKeyHash,
|
||||
hint,
|
||||
kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
} = credentials;
|
||||
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
|
||||
const protectedUserKey = await this.makeProtectedUserKey(masterKey, userId);
|
||||
if (protectedUserKey == null) {
|
||||
throw new Error("protectedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
// Since this is an existing JIT provisioned user in a MP encryption org setting first password,
|
||||
// they will not already have a user asymmetric key pair so we must create it for them.
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
hint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType, // kdfConfig is always DEFAULT_KDF_CONFIG (see InputPasswordComponent)
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
await this.apiService.setPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(masterKey, kdfConfig, protectedUserKey, userId);
|
||||
|
||||
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(masterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async makeProtectedUserKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
): Promise<[UserKey, EncString]> {
|
||||
let protectedUserKey: [UserKey, EncString] = null;
|
||||
|
||||
const userKey = await firstValueFrom(this.cryptoService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
protectedUserKey = await this.cryptoService.makeUserKey(masterKey);
|
||||
} else {
|
||||
protectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey);
|
||||
}
|
||||
|
||||
return protectedUserKey;
|
||||
}
|
||||
|
||||
private async makeKeyPairAndRequest(
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
): Promise<[[string, EncString], KeysRequest]> {
|
||||
const keyPair = await this.cryptoService.makeKeyPair(protectedUserKey[0]);
|
||||
if (keyPair == null) {
|
||||
throw new Error("keyPair not found. Could not set password.");
|
||||
}
|
||||
const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
|
||||
return [keyPair, keysRequest];
|
||||
}
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: PBKDF2KdfConfig,
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.cryptoService.setUserKey(protectedUserKey[0], userId);
|
||||
}
|
||||
|
||||
private async handleResetPasswordAutoEnroll(
|
||||
masterKeyHash: string,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) {
|
||||
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||
|
||||
if (organizationKeys == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await firstValueFrom(this.cryptoService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||
}
|
||||
|
||||
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterKeyHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||
orgId,
|
||||
userId,
|
||||
resetRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<ng-container *ngIf="syncLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-mr-2" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!syncLoading">
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="resetPasswordAutoEnroll"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[email]="email"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,126 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import { ToastService } from "../../../../components/src/toast";
|
||||
import { InputPasswordComponent } from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-set-password-jit",
|
||||
templateUrl: "set-password-jit.component.html",
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
protected orgSsoIdentifier: string;
|
||||
protected resetPasswordAutoEnroll: boolean;
|
||||
protected submitting = false;
|
||||
protected syncLoading = true;
|
||||
protected userId: UserId;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private router: Router,
|
||||
private setPasswordJitService: SetPasswordJitService,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
await this.handleQueryParams();
|
||||
}
|
||||
|
||||
private async handleQueryParams() {
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (qParams.identifier != null) {
|
||||
try {
|
||||
this.orgSsoIdentifier = qParams.identifier;
|
||||
|
||||
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
|
||||
this.orgSsoIdentifier,
|
||||
);
|
||||
this.orgId = autoEnrollStatus.id;
|
||||
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
|
||||
this.masterPasswordPolicyOptions =
|
||||
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
...passwordInputResult,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
userId,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.setPasswordJitService.setPassword(credentials);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("inviteAccepted"),
|
||||
});
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
userId: UserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles setting a password for a "just-in-time" provisioned user.
|
||||
*
|
||||
* A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the
|
||||
* time they first click "Login with SSO". Once they click "Login with SSO" we register the account on
|
||||
* the fly ("just-in-time").
|
||||
*/
|
||||
export abstract class SetPasswordJitService {
|
||||
/**
|
||||
* Sets the password for a JIT provisioned user.
|
||||
*
|
||||
* @param credentials An object of the credentials needed to set the password for a JIT provisioned user
|
||||
* @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
|
||||
* or newKeyPair could not be created.
|
||||
*/
|
||||
setPassword: (credentials: SetPasswordCredentials) => Promise<void>;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "../auth/models/request/email.request";
|
||||
import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request";
|
||||
@@ -323,6 +324,9 @@ export abstract class ApiService {
|
||||
putTwoFactorAuthenticator: (
|
||||
request: UpdateTwoFactorAuthenticatorRequest,
|
||||
) => Promise<TwoFactorAuthenticatorResponse>;
|
||||
deleteTwoFactorAuthenticator: (
|
||||
request: DisableTwoFactorAuthenticatorRequest,
|
||||
) => Promise<TwoFactorProviderResponse>;
|
||||
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
|
||||
putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
|
||||
putTwoFactorOrganizationDuo: (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TwoFactorProviderRequest } from "./two-factor-provider.request";
|
||||
|
||||
export class DisableTwoFactorAuthenticatorRequest extends TwoFactorProviderRequest {
|
||||
key: string;
|
||||
userVerificationToken: string;
|
||||
}
|
||||
@@ -3,4 +3,5 @@ import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
export class UpdateTwoFactorAuthenticatorRequest extends SecretVerificationRequest {
|
||||
token: string;
|
||||
key: string;
|
||||
userVerificationToken: string;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { BaseResponse } from "../../../models/response/base.response";
|
||||
export class TwoFactorAuthenticatorResponse extends BaseResponse {
|
||||
enabled: boolean;
|
||||
key: string;
|
||||
userVerificationToken: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.enabled = this.getResponseProperty("Enabled");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.userVerificationToken = this.getResponseProperty("UserVerificationToken");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum FeatureFlag {
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
|
||||
DeviceTrustLogging = "pm-8285-device-trust-logging",
|
||||
AuthenticatorTwoFactorToken = "authenticator-2fa-token",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -64,6 +65,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
|
||||
[FeatureFlag.DeviceTrustLogging]: FALSE,
|
||||
[FeatureFlag.AuthenticatorTwoFactorToken]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -37,6 +37,7 @@ import { SelectionReadOnlyResponse } from "../admin-console/models/response/sele
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "../auth/models/request/email.request";
|
||||
import { DeviceRequest } from "../auth/models/request/identity-token/device.request";
|
||||
@@ -998,6 +999,13 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new TwoFactorAuthenticatorResponse(r);
|
||||
}
|
||||
|
||||
async deleteTwoFactorAuthenticator(
|
||||
request: DisableTwoFactorAuthenticatorRequest,
|
||||
): Promise<TwoFactorProviderResponse> {
|
||||
const r = await this.send("DELETE", "/two-factor/authenticator", request, true, true);
|
||||
return new TwoFactorProviderResponse(r);
|
||||
}
|
||||
|
||||
async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse> {
|
||||
const r = await this.send("PUT", "/two-factor/email", request, true, true);
|
||||
return new TwoFactorEmailResponse(r);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratedCredential, GeneratorCategory } from "../history";
|
||||
|
||||
/** Tracks the history of password generations.
|
||||
* Each user gets their own store.
|
||||
*/
|
||||
export abstract class GeneratorHistoryService {
|
||||
/** Tracks a new credential. When an item with the same `credential` value
|
||||
* is found, this method does nothing. When the total number of items exceeds
|
||||
* {@link HistoryServiceOptions.maxTotal}, then the oldest items exceeding the total
|
||||
* are deleted.
|
||||
* @param userId identifies the user storing the credential.
|
||||
* @param credential stored by the history service.
|
||||
* @param date when the credential was generated. If this is omitted, then the generator
|
||||
* uses the date the credential was added to the store instead.
|
||||
* @returns a promise that completes with the added credential. If the credential
|
||||
* wasn't added, then the promise completes with `null`.
|
||||
* @remarks this service is not suitable for use with vault items/ciphers. It models only
|
||||
* a history of an individually generated credential, while a vault item's history
|
||||
* may contain several credentials that are better modelled as atomic versions of the
|
||||
* vault item itself.
|
||||
*/
|
||||
track: (
|
||||
userId: UserId,
|
||||
credential: string,
|
||||
category: GeneratorCategory,
|
||||
date?: Date,
|
||||
) => Promise<GeneratedCredential | null>;
|
||||
|
||||
/** Removes a matching credential from the history service.
|
||||
* @param userId identifies the user taking the credential.
|
||||
* @param credential to match in the history service.
|
||||
* @returns A promise that completes with the credential read. If the credential wasn't found,
|
||||
* the promise completes with null.
|
||||
* @remarks this can be used to extract an entry when a credential is stored in the vault.
|
||||
*/
|
||||
take: (userId: UserId, credential: string) => Promise<GeneratedCredential | null>;
|
||||
|
||||
/** Deletes a user's credential history.
|
||||
* @param userId identifies the user taking the credential.
|
||||
* @returns A promise that completes when the history is cleared.
|
||||
*/
|
||||
clear: (userId: UserId) => Promise<GeneratedCredential[]>;
|
||||
|
||||
/** Lists all credentials for a user.
|
||||
* @param userId identifies the user listing the credential.
|
||||
* @remarks This field is eventually consistent with `track` and `take` operations.
|
||||
* It is not guaranteed to immediately reflect those changes.
|
||||
*/
|
||||
credentials$: (userId: UserId) => Observable<GeneratedCredential[]>;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorNavigation } from "../navigation/generator-navigation";
|
||||
import { GeneratorNavigationPolicy } from "../navigation/generator-navigation-policy";
|
||||
|
||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||
|
||||
/** Loads and stores generator navigational data
|
||||
*/
|
||||
export abstract class GeneratorNavigationService {
|
||||
/** An observable monitoring the options saved to disk.
|
||||
* The observable updates when the options are saved.
|
||||
* @param userId: Identifies the user making the request
|
||||
*/
|
||||
options$: (userId: UserId) => Observable<GeneratorNavigation>;
|
||||
|
||||
/** Gets the default options. */
|
||||
defaults$: (userId: UserId) => Observable<GeneratorNavigation>;
|
||||
|
||||
/** An observable monitoring the options used to enforce policy.
|
||||
* The observable updates when the policy changes.
|
||||
* @param userId: Identifies the user making the request
|
||||
*/
|
||||
evaluator$: (
|
||||
userId: UserId,
|
||||
) => Observable<PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>>;
|
||||
|
||||
/** Enforces the policy on the given options
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to enforce the policy on
|
||||
* @returns a new instance of the options with the policy enforced
|
||||
*/
|
||||
enforcePolicy: (userId: UserId, options: GeneratorNavigation) => Promise<GeneratorNavigation>;
|
||||
|
||||
/** Saves the navigation options to disk.
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to save
|
||||
* @returns a promise that resolves when the options are saved
|
||||
*/
|
||||
saveOptions: (userId: UserId, options: GeneratorNavigation) => Promise<void>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user