diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b6d24aa8c0..763b48ab1d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,11 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +## Desktop native module ## +apps/desktop/desktop_native @bitwarden/team-platform-dev +apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev +apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev + ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev apps/cli/src/auth @bitwarden/team-auth-dev @@ -124,10 +129,6 @@ apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation apps/web/src/app/layouts @bitwarden/team-ui-foundation -## Desktop native module ## -apps/desktop/desktop_native @bitwarden/team-platform-dev -apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev -apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev ## Key management team files ## apps/desktop/src/key-management @bitwarden/team-key-management-dev diff --git a/.github/renovate.json b/.github/renovate.json5 similarity index 58% rename from .github/renovate.json rename to .github/renovate.json5 index f1efcbaffbe..6d6fbbd2539 100644 --- a/.github/renovate.json +++ b/.github/renovate.json5 @@ -1,46 +1,46 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>bitwarden/renovate-config"], - "enabledManagers": ["cargo", "github-actions", "npm"], - "packageRules": [ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["github>bitwarden/renovate-config"], // Extends our base config for pinned dependencies + enabledManagers: ["cargo", "github-actions", "npm"], + packageRules: [ { - "groupName": "github-action minor", - "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor"] + groupName: "github-action minor", + matchManagers: ["github-actions"], + matchUpdateTypes: ["minor"], }, { - "matchManagers": ["cargo"], - "commitMessagePrefix": "[deps] Platform:" + matchManagers: ["cargo"], + commitMessagePrefix: "[deps] Platform:", }, { - "groupName": "napi", - "matchPackageNames": ["napi", "napi-build", "napi-derive"] + groupName: "napi", + matchPackageNames: ["napi", "napi-build", "napi-derive"], }, { - "matchPackageNames": ["typescript", "zone.js"], - "matchUpdateTypes": ["major", "minor"], - "description": "Determined by Angular", - "enabled": false + matchPackageNames: ["typescript", "zone.js"], + matchUpdateTypes: ["major", "minor"], + description: "Determined by Angular", + enabled: false, }, { - "matchPackageNames": ["typescript", "zone.js"], - "matchUpdateTypes": "patch" + matchPackageNames: ["typescript", "zone.js"], + matchUpdateTypes: "patch", }, { - "groupName": "jest", - "matchPackageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"], - "matchUpdateTypes": "major" + groupName: "jest", + matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"], + matchUpdateTypes: "major", }, { - "groupName": "macOS/iOS bindings", - "matchPackageNames": ["core-foundation", "security-framework", "security-framework-sys"] + groupName: "macOS/iOS bindings", + matchPackageNames: ["core-foundation", "security-framework", "security-framework-sys"], }, { - "groupName": "zbus", - "matchPackageNames": ["zbus", "zbus_polkit"] + groupName: "zbus", + matchPackageNames: ["zbus", "zbus_polkit"], }, { - "matchPackageNames": [ + matchPackageNames: [ "base64-loader", "buffer", "bufferutil", @@ -56,20 +56,20 @@ "style-loader", "ts-loader", "url", - "util" + "util", ], - "description": "Admin Console owned dependencies", - "commitMessagePrefix": "[deps] AC:", - "reviewers": ["team:team-admin-console-dev"] + description: "Admin Console owned dependencies", + commitMessagePrefix: "[deps] AC:", + reviewers: ["team:team-admin-console-dev"], }, { - "matchPackageNames": ["qrious"], - "description": "Auth owned dependencies", - "commitMessagePrefix": "[deps] Auth:", - "reviewers": ["team:team-auth-dev"] + matchPackageNames: ["qrious"], + description: "Auth owned dependencies", + commitMessagePrefix: "[deps] Auth:", + reviewers: ["team:team-auth-dev"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@angular-eslint/schematics", "angular-eslint", "eslint-config-prettier", @@ -82,14 +82,14 @@ "eslint", "husky", "lint-staged", - "typescript-eslint" + "typescript-eslint", ], - "description": "Architecture owned dependencies", - "commitMessagePrefix": "[deps] Architecture:", - "reviewers": ["team:dept-architecture"] + description: "Architecture owned dependencies", + commitMessagePrefix: "[deps] Architecture:", + reviewers: ["team:dept-architecture"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@angular-eslint/eslint-plugin-template", "@angular-eslint/eslint-plugin", "@angular-eslint/schematics", @@ -105,13 +105,13 @@ "eslint-plugin-tailwindcss", "eslint", "husky", - "lint-staged" + "lint-staged", ], - "groupName": "Linting minor-patch", - "matchUpdateTypes": ["minor", "patch"] + groupName: "Linting minor-patch", + matchUpdateTypes: ["minor", "patch"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@emotion/css", "@webcomponents/custom-elements", "concurrently", @@ -126,20 +126,20 @@ "@storybook/web-components-webpack5", "tabbable", "tldts", - "wait-on" + "wait-on", ], - "description": "Autofill owned dependencies", - "commitMessagePrefix": "[deps] Autofill:", - "reviewers": ["team:team-autofill-dev"] + description: "Autofill owned dependencies", + commitMessagePrefix: "[deps] Autofill:", + reviewers: ["team:team-autofill-dev"], }, { - "matchPackageNames": ["braintree-web-drop-in"], - "description": "Billing owned dependencies", - "commitMessagePrefix": "[deps] Billing:", - "reviewers": ["team:team-billing-dev"] + matchPackageNames: ["braintree-web-drop-in"], + description: "Billing owned dependencies", + commitMessagePrefix: "[deps] Billing:", + reviewers: ["team:team-billing-dev"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@babel/core", "@babel/preset-env", "@bitwarden/sdk-internal", @@ -167,6 +167,7 @@ "electron-updater", "html-webpack-injector", "html-webpack-plugin", + "json5", "lowdb", "node-forge", "node-ipc", @@ -179,14 +180,14 @@ "webpack", "webpack-cli", "webpack-dev-server", - "webpack-node-externals" + "webpack-node-externals", ], - "description": "Platform owned dependencies", - "commitMessagePrefix": "[deps] Platform:", - "reviewers": ["team:team-platform-dev"] + description: "Platform owned dependencies", + commitMessagePrefix: "[deps] Platform:", + reviewers: ["team:team-platform-dev"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@angular-devkit/build-angular", "@angular/animations", "@angular/cdk", @@ -208,6 +209,7 @@ "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/addon-links", + "@storybook/addon-themes", "@storybook/angular", "@storybook/manager-api", "@storybook/theming", @@ -225,27 +227,27 @@ "remark-gfm", "storybook", "tailwindcss", - "zone.js" + "zone.js", ], - "description": "UI Foundation owned dependencies", - "commitMessagePrefix": "[deps] UI Foundation:", - "reviewers": ["team:team-ui-foundation"] + description: "UI Foundation owned dependencies", + commitMessagePrefix: "[deps] UI Foundation:", + reviewers: ["team:team-ui-foundation"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@types/jest", "jest-junit", "jest-mock-extended", "jest-preset-angular", "jest-diff", - "ts-jest" + "ts-jest", ], - "description": "Secrets Manager owned dependencies", - "commitMessagePrefix": "[deps] SM:", - "reviewers": ["team:team-secrets-manager-dev"] + description: "Secrets Manager owned dependencies", + commitMessagePrefix: "[deps] SM:", + reviewers: ["team:team-secrets-manager-dev"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@microsoft/signalr-protocol-msgpack", "@microsoft/signalr", "@types/jsdom", @@ -256,14 +258,14 @@ "oidc-client-ts", "papaparse", "utf-8-validate", - "zxcvbn" + "zxcvbn", ], - "description": "Tools owned dependencies", - "commitMessagePrefix": "[deps] Tools:", - "reviewers": ["team:team-tools-dev"] + description: "Tools owned dependencies", + commitMessagePrefix: "[deps] Tools:", + reviewers: ["team:team-tools-dev"], }, { - "matchPackageNames": [ + matchPackageNames: [ "@koa/multer", "@koa/router", "@types/inquirer", @@ -289,18 +291,18 @@ "node-fetch", "open", "proper-lockfile", - "qrcode-parser" + "qrcode-parser", ], - "description": "Vault owned dependencies", - "commitMessagePrefix": "[deps] Vault:", - "reviewers": ["team:team-vault-dev"] + description: "Vault owned dependencies", + commitMessagePrefix: "[deps] Vault:", + reviewers: ["team:team-vault-dev"], }, { - "matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"], - "description": "Key Management owned dependencies", - "commitMessagePrefix": "[deps] KM:", - "reviewers": ["team:team-key-management-dev"] - } + matchPackageNames: ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"], + description: "Key Management owned dependencies", + commitMessagePrefix: "[deps] KM:", + reviewers: ["team:team-key-management-dev"], + }, ], - "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"] + ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"], } diff --git a/.storybook/main.ts b/.storybook/main.ts index d98ca06ead3..9583d1fc6f2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -29,6 +29,7 @@ const config: StorybookConfig = { getAbsolutePath("@storybook/addon-a11y"), getAbsolutePath("@storybook/addon-designs"), getAbsolutePath("@storybook/addon-interactions"), + getAbsolutePath("@storybook/addon-themes"), { // @storybook/addon-docs is part of @storybook/addon-essentials // eslint-disable-next-line storybook/no-uninstalled-addons diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 85515068b3a..6bd28cfe809 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,60 +1,30 @@ import { setCompodocJson } from "@storybook/addon-docs/angular"; +import { withThemeByClassName } from "@storybook/addon-themes"; import { componentWrapperDecorator } from "@storybook/angular"; import type { Preview } from "@storybook/angular"; import docJson from "../documentation.json"; setCompodocJson(docJson); -const decorator = componentWrapperDecorator( - (story) => { - return /*html*/ ` -
- ${story} -
+const wrapperDecorator = componentWrapperDecorator((story) => { + return /*html*/ ` +
+ ${story} +
`; - }, - ({ globals }) => { - // We need to add the theme class to the body to support body-appended content like popovers and menus - document.body.classList.remove("theme_light"); - document.body.classList.remove("theme_dark"); - - document.body.classList.add(`theme_${globals["theme"]}`); - - return { theme: `${globals["theme"]}` }; - }, -); +}); const preview: Preview = { - decorators: [decorator], - globalTypes: { - theme: { - description: "Global theme for components", - defaultValue: "light", - toolbar: { - title: "Theme", - icon: "circlehollow", - items: [ - { - title: "Light", - value: "light", - icon: "sun", - }, - { - title: "Dark", - value: "dark", - icon: "moon", - }, - ], - dynamicTitle: true, + decorators: [ + withThemeByClassName({ + themes: { + light: "theme_light", + dark: "theme_dark", }, - }, - }, + defaultTheme: "light", + }), + wrapperDecorator, + ], parameters: { controls: { matchers: { @@ -69,6 +39,9 @@ const preview: Preview = { }, }, docs: { source: { type: "dynamic", excludeDecorators: true } }, + backgrounds: { + disable: true, + }, }, tags: ["autodocs"], }; diff --git a/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts index 7ded23116ee..180a4685332 100644 --- a/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/abstractions/main-context-menu-handler.ts @@ -1,5 +1,6 @@ type InitContextMenuItems = Omit & { - checkPremiumAccess?: boolean; + requiresPremiumAccess?: boolean; + requiresUnblockedUri?: boolean; }; export { InitContextMenuItems }; diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index 038f4e85c9a..e2bf75350a2 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -21,7 +21,7 @@ export class CipherContextMenuHandler { private accountService: AccountService, ) {} - async update(url: string) { + async update(url: string, currentUriIsBlocked: boolean = false) { if (this.mainContextMenuHandler.initRunning) { return; } @@ -88,6 +88,10 @@ export class CipherContextMenuHandler { for (const cipher of ciphers) { await this.updateForCipher(cipher); } + + if (currentUriIsBlocked) { + await this.mainContextMenuHandler.removeBlockedUriMenuItems(); + } } private async updateForCipher(cipher: CipherView) { diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 79998b65205..267a832a671 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -2,7 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; +import { + AUTOFILL_CARD_ID, + AUTOFILL_ID, + AUTOFILL_IDENTITY_ID, + COPY_IDENTIFIER_ID, + COPY_PASSWORD_ID, + COPY_USERNAME_ID, + COPY_VERIFICATION_CODE_ID, + NOOP_COMMAND_SUFFIX, + SEPARATOR_ID, +} from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +25,43 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { MainContextMenuHandler } from "./main-context-menu-handler"; +/** + * Used in place of Set method `symmetricDifference`, which is only available to node version 22.0.0 or greater: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference + */ +function symmetricDifference(setA: Set, setB: Set) { + const _difference = new Set(setA); + for (const elem of setB) { + if (_difference.has(elem)) { + _difference.delete(elem); + } else { + _difference.add(elem); + } + } + return _difference; +} + +const createCipher = (data?: { + id?: CipherView["id"]; + username?: CipherView["login"]["username"]; + password?: CipherView["login"]["password"]; + totp?: CipherView["login"]["totp"]; + viewPassword?: CipherView["viewPassword"]; +}): CipherView => { + const { id, username, password, totp, viewPassword } = data || {}; + const cipherView = new CipherView( + new Cipher({ + id: id ?? "1", + type: CipherType.Login, + viewPassword: viewPassword ?? true, + } as any), + ); + cipherView.login.username = username ?? "USERNAME"; + cipherView.login.password = password ?? "PASSWORD"; + cipherView.login.totp = totp ?? "TOTP"; + return cipherView; +}; + describe("context-menu", () => { let stateService: MockProxy; let autofillSettingsService: MockProxy; @@ -59,6 +106,9 @@ describe("context-menu", () => { billingAccountProfileStateService, accountService, ); + + jest.spyOn(MainContextMenuHandler, "remove"); + autofillSettingsService.enableContextMenu$ = of(true); accountService.activeAccount$ = of({ id: "userId" as UserId, @@ -68,7 +118,10 @@ describe("context-menu", () => { }); }); - afterEach(() => jest.resetAllMocks()); + afterEach(async () => { + await MainContextMenuHandler.removeAll(); + jest.resetAllMocks(); + }); describe("init", () => { it("has menu disabled", async () => { @@ -97,27 +150,6 @@ describe("context-menu", () => { }); describe("loadOptions", () => { - const createCipher = (data?: { - id?: CipherView["id"]; - username?: CipherView["login"]["username"]; - password?: CipherView["login"]["password"]; - totp?: CipherView["login"]["totp"]; - viewPassword?: CipherView["viewPassword"]; - }): CipherView => { - const { id, username, password, totp, viewPassword } = data || {}; - const cipherView = new CipherView( - new Cipher({ - id: id ?? "1", - type: CipherType.Login, - viewPassword: viewPassword ?? true, - } as any), - ); - cipherView.login.username = username ?? "USERNAME"; - cipherView.login.password = password ?? "PASSWORD"; - cipherView.login.totp = totp ?? "TOTP"; - return cipherView; - }; - it("is not a login cipher", async () => { await sut.loadOptions("TEST_TITLE", "1", { ...createCipher(), @@ -128,33 +160,124 @@ describe("context-menu", () => { }); it("creates item for autofill", async () => { - await sut.loadOptions( - "TEST_TITLE", - "1", - createCipher({ - username: "", - totp: "", - viewPassword: false, - }), + const cipher = createCipher({ + username: "", + totp: "", + viewPassword: true, + }); + const optionId = "1"; + await sut.loadOptions("TEST_TITLE", optionId, cipher); + + expect(createSpy).toHaveBeenCalledTimes(2); + + expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(2); + + const expectedMenuItems = new Set([ + AUTOFILL_ID + `_${optionId}`, + COPY_PASSWORD_ID + `_${optionId}`, + ]); + + // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used + // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"]) + const expectedReceivedDiff = symmetricDifference( + expectedMenuItems, + MainContextMenuHandler["existingMenuItems"], ); - expect(createSpy).toHaveBeenCalledTimes(1); + expect(expectedReceivedDiff.size).toEqual(0); }); it("create entry for each cipher piece", async () => { billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); - - await sut.loadOptions("TEST_TITLE", "1", createCipher()); + const optionId = "arbitraryString"; + await sut.loadOptions("TEST_TITLE", optionId, createCipher()); expect(createSpy).toHaveBeenCalledTimes(4); + + expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4); + + const expectedMenuItems = new Set([ + AUTOFILL_ID + `_${optionId}`, + COPY_PASSWORD_ID + `_${optionId}`, + COPY_USERNAME_ID + `_${optionId}`, + COPY_VERIFICATION_CODE_ID + `_${optionId}`, + ]); + + // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used + // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"]) + const expectedReceivedDiff = symmetricDifference( + expectedMenuItems, + MainContextMenuHandler["existingMenuItems"], + ); + + expect(expectedReceivedDiff.size).toEqual(0); }); it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => { billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); - await sut.loadOptions("TEST_TITLE", "NOOP"); + const optionId = "NOOP"; + await sut.loadOptions("TEST_TITLE", optionId); expect(createSpy).toHaveBeenCalledTimes(6); + + expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(6); + + const expectedMenuItems = new Set([ + AUTOFILL_ID + `_${optionId}`, + COPY_PASSWORD_ID + `_${optionId}`, + COPY_USERNAME_ID + `_${optionId}`, + COPY_VERIFICATION_CODE_ID + `_${optionId}`, + AUTOFILL_CARD_ID + `_${optionId}`, + AUTOFILL_IDENTITY_ID + `_${optionId}`, + ]); + + // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used + // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"]) + const expectedReceivedDiff = symmetricDifference( + expectedMenuItems, + MainContextMenuHandler["existingMenuItems"], + ); + + expect(expectedReceivedDiff.size).toEqual(0); + }); + }); + + describe("removeBlockedUriMenuItems", () => { + it("removes menu items that require code injection", async () => { + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); + autofillSettingsService.enableContextMenu$ = of(true); + stateService.getIsAuthenticated.mockResolvedValue(true); + + const optionId = "1"; + await sut.loadOptions("TEST_TITLE", optionId, createCipher()); + + await sut.removeBlockedUriMenuItems(); + + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledTimes(5); + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_ID); + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_IDENTITY_ID); + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_CARD_ID); + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(SEPARATOR_ID + 2); + expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(COPY_IDENTIFIER_ID); + + expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4); + + const expectedMenuItems = new Set([ + AUTOFILL_ID + `_${optionId}`, + COPY_PASSWORD_ID + `_${optionId}`, + COPY_USERNAME_ID + `_${optionId}`, + COPY_VERIFICATION_CODE_ID + `_${optionId}`, + ]); + + // @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used + // const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"]) + const expectedReceivedDiff = symmetricDifference( + expectedMenuItems, + MainContextMenuHandler["existingMenuItems"], + ); + + expect(expectedReceivedDiff.size).toEqual(0); }); }); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index 41d88439e8f..ad9dc34e501 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -31,6 +31,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { InitContextMenuItems } from "./abstractions/main-context-menu-handler"; export class MainContextMenuHandler { + static existingMenuItems: Set = new Set(); initRunning = false; private initContextMenuItems: InitContextMenuItems[] = [ { @@ -41,6 +42,7 @@ export class MainContextMenuHandler { id: AUTOFILL_ID, parentId: ROOT_ID, title: this.i18nService.t("autoFillLogin"), + requiresUnblockedUri: true, }, { id: COPY_USERNAME_ID, @@ -56,7 +58,7 @@ export class MainContextMenuHandler { id: COPY_VERIFICATION_CODE_ID, parentId: ROOT_ID, title: this.i18nService.t("copyVerificationCode"), - checkPremiumAccess: true, + requiresPremiumAccess: true, }, { id: SEPARATOR_ID + 1, @@ -67,16 +69,19 @@ export class MainContextMenuHandler { id: AUTOFILL_IDENTITY_ID, parentId: ROOT_ID, title: this.i18nService.t("autoFillIdentity"), + requiresUnblockedUri: true, }, { id: AUTOFILL_CARD_ID, parentId: ROOT_ID, title: this.i18nService.t("autoFillCard"), + requiresUnblockedUri: true, }, { id: SEPARATOR_ID + 2, type: "separator", parentId: ROOT_ID, + requiresUnblockedUri: true, }, { id: GENERATE_PASSWORD_ID, @@ -87,6 +92,7 @@ export class MainContextMenuHandler { id: COPY_IDENTIFIER_ID, parentId: ROOT_ID, title: this.i18nService.t("copyElementIdentifier"), + requiresUnblockedUri: true, }, ]; private noCardsContextMenuItems: chrome.contextMenus.CreateProperties[] = [ @@ -175,13 +181,19 @@ export class MainContextMenuHandler { this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); - for (const options of this.initContextMenuItems) { - if (options.checkPremiumAccess && !hasPremium) { + for (const menuItem of this.initContextMenuItems) { + const { + requiresPremiumAccess, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + requiresUnblockedUri, // destructuring this out of being passed to `create` + ...otherOptions + } = menuItem; + + if (requiresPremiumAccess && !hasPremium) { continue; } - delete options.checkPremiumAccess; - await MainContextMenuHandler.create({ ...options, contexts: ["all"] }); + await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] }); } } catch (error) { this.logService.warning(error.message); @@ -202,12 +214,16 @@ export class MainContextMenuHandler { } return new Promise((resolve, reject) => { - chrome.contextMenus.create(options, () => { + const itemId = chrome.contextMenus.create(options, () => { if (chrome.runtime.lastError) { return reject(chrome.runtime.lastError); } resolve(); }); + + this.existingMenuItems.add(`${itemId}`); + + return itemId; }); }; @@ -221,12 +237,16 @@ export class MainContextMenuHandler { resolve(); }); + + this.existingMenuItems = new Set(); + + return; }); } static remove(menuItemId: string) { return new Promise((resolve, reject) => { - chrome.contextMenus.remove(menuItemId, () => { + const itemId = chrome.contextMenus.remove(menuItemId, () => { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); return; @@ -234,6 +254,10 @@ export class MainContextMenuHandler { resolve(); }); + + this.existingMenuItems.delete(`${itemId}`); + + return; }); } @@ -244,6 +268,11 @@ export class MainContextMenuHandler { const createChildItem = async (parentId: string) => { const menuItemId = `${parentId}_${optionId}`; + const itemAlreadyExists = MainContextMenuHandler.existingMenuItems.has(menuItemId); + if (itemAlreadyExists) { + return; + } + return await MainContextMenuHandler.create({ type: "normal", id: menuItemId, @@ -255,10 +284,18 @@ export class MainContextMenuHandler { if ( !cipher || - (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password)) + (cipher.type === CipherType.Login && + (!Utils.isNullOrEmpty(cipher.login?.username) || + !Utils.isNullOrEmpty(cipher.login?.password) || + !Utils.isNullOrEmpty(cipher.login?.totp))) ) { await createChildItem(AUTOFILL_ID); + } + if ( + !cipher || + (cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password)) + ) { if (cipher?.viewPassword ?? true) { await createChildItem(COPY_PASSWORD_ID); } @@ -305,10 +342,22 @@ export class MainContextMenuHandler { } } + async removeBlockedUriMenuItems() { + try { + for (const menuItem of this.initContextMenuItems) { + if (menuItem.requiresUnblockedUri && menuItem.id) { + await MainContextMenuHandler.remove(menuItem.id); + } + } + } catch (error) { + this.logService.warning(error.message); + } + } + async noCards() { try { - for (const option of this.noCardsContextMenuItems) { - await MainContextMenuHandler.create(option); + for (const menuItem of this.noCardsContextMenuItems) { + await MainContextMenuHandler.create(menuItem); } } catch (error) { this.logService.warning(error.message); @@ -317,8 +366,8 @@ export class MainContextMenuHandler { async noIdentities() { try { - for (const option of this.noIdentitiesContextMenuItems) { - await MainContextMenuHandler.create(option); + for (const menuItem of this.noIdentitiesContextMenuItems) { + await MainContextMenuHandler.create(menuItem); } } catch (error) { this.logService.warning(error.message); @@ -327,8 +376,8 @@ export class MainContextMenuHandler { async noLogins() { try { - for (const option of this.noLoginsContextMenuItems) { - await MainContextMenuHandler.create(option); + for (const menuItem of this.noLoginsContextMenuItems) { + await MainContextMenuHandler.create(menuItem); } await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID); diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index 992b034afa7..6cc56e079d4 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -10,3 +10,4 @@ export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; +export { Warning } from "./warning"; diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/icons/party-horn.ts index dc2144b524f..e807df1d86e 100644 --- a/apps/browser/src/autofill/content/components/icons/party-horn.ts +++ b/apps/browser/src/autofill/content/components/icons/party-horn.ts @@ -6,168 +6,262 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; export function PartyHorn({ theme }: { theme: Theme }) { if (theme === ThemeTypes.Dark) { return html` - + - - - - + + + + + + + + + - + + + + `; } return html` - + - - + + + + + + + - - - + + + - + + + + `; diff --git a/apps/browser/src/autofill/content/components/icons/warning.ts b/apps/browser/src/autofill/content/components/icons/warning.ts new file mode 100644 index 00000000000..9ae9aeca352 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/warning.ts @@ -0,0 +1,23 @@ +import { html } from "lit"; + +// This icon has static multi-colors for each theme +export function Warning() { + return html` + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts new file mode 100644 index 00000000000..94dbaace9aa --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts @@ -0,0 +1,41 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationConfirmationBody } from "../../notification/confirmation"; + +type Args = { + buttonText: string; + confirmationMessage: string; + handleClick: () => void; + theme: Theme; + error: string; +}; + +export default { + title: "Components/Notifications/Notification Confirmation Body", + argTypes: { + error: { control: "text" }, + buttonText: { control: "text" }, + confirmationMessage: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + error: "", + buttonText: "View", + confirmationMessage: "[item name] updated in Bitwarden.", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: Args) => NotificationConfirmationBody({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts new file mode 100644 index 00000000000..745899481dd --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts @@ -0,0 +1,54 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes } from "../constants/styles"; + +export function NotificationConfirmationMessage({ + buttonText, + confirmationMessage, + handleClick, + theme, +}: { + buttonText: string; + confirmationMessage: string; + handleClick: (e: Event) => void; + theme: Theme; +}) { + return html` + ${confirmationMessage} + ${buttonText} + `; +} + +const baseTextStyles = css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 24px; + white-space: nowrap; + font-family: "DM Sans", sans-serif; + font-size: 16px; +`; + +const notificationConfirmationMessageStyles = (theme: Theme) => css` + ${baseTextStyles} + color: ${themes[theme].text.main}; + font-weight: 400; +`; + +const notificationConfirmationButtonTextStyles = (theme: Theme) => css` + ${baseTextStyles} + color: ${themes[theme].primary[600]}; + font-weight: 700; + cursor: pointer; +`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation.ts b/apps/browser/src/autofill/content/components/notification/confirmation.ts new file mode 100644 index 00000000000..0c389f75eb6 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation.ts @@ -0,0 +1,58 @@ +import createEmotion from "@emotion/css/create-instance"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { themes } from "../constants/styles"; +import { PartyHorn, Warning } from "../icons"; + +import { NotificationConfirmationMessage } from "./confirmation-message"; + +export const componentClassPrefix = "notification-confirmation-body"; + +const { css } = createEmotion({ + key: componentClassPrefix, +}); + +export function NotificationConfirmationBody({ + buttonText, + error, + confirmationMessage, + theme = ThemeTypes.Light, +}: { + error?: string; + buttonText: string; + confirmationMessage: string; + theme: Theme; +}) { + const IconComponent = !error ? PartyHorn : Warning; + return html` +
+
${IconComponent({ theme })}
+ ${confirmationMessage && buttonText + ? NotificationConfirmationMessage({ + handleClick: () => {}, + confirmationMessage, + theme, + buttonText, + }) + : null} +
+ `; +} + +const iconContainerStyles = (error?: string) => css` + > svg { + width: ${!error ? "50px" : "40px"}; + height: fit-content; + } +`; +const notificationConfirmationBodyStyles = ({ theme }: { theme: Theme }) => css` + gap: 16px; + display: flex; + align-items: center; + justify-content: flex-start; + background-color: ${themes[theme].background.alt}; + padding: 12px; + white-space: nowrap; +`; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d2b51c7ef40..1c6d018a82c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1374,17 +1374,41 @@ export default class MainBackground { return; } - await this.mainContextMenuHandler?.init(); + const contextMenuIsEnabled = await this.mainContextMenuHandler?.init(); + if (!contextMenuIsEnabled) { + this.onUpdatedRan = this.onReplacedRan = false; + return; + } const tab = await BrowserApi.getTabFromCurrentWindow(); + if (tab) { - await this.cipherContextMenuHandler?.update(tab.url); + const currentUriIsBlocked = await firstValueFrom( + this.domainSettingsService.blockedInteractionsUris$.pipe( + map((blockedInteractionsUris) => { + if (blockedInteractionsUris && tab?.url?.length) { + const tabURL = new URL(tab.url); + const tabIsBlocked = Object.keys(blockedInteractionsUris).some((blockedHostname) => + tabURL.hostname.endsWith(blockedHostname), + ); + + if (tabIsBlocked) { + return true; + } + } + + return false; + }), + ), + ); + + await this.cipherContextMenuHandler?.update(tab.url, currentUriIsBlocked); this.onUpdatedRan = this.onReplacedRan = false; } } async updateOverlayCiphers() { - // overlayBackground null in popup only contexts + // `overlayBackground` is null in popup only contexts if (this.overlayBackground) { await this.overlayBackground.updateOverlayCiphers(); } diff --git a/apps/browser/src/platform/popup/layout/popup-back.directive.ts b/apps/browser/src/platform/popup/layout/popup-back.directive.ts new file mode 100644 index 00000000000..95f82588640 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-back.directive.ts @@ -0,0 +1,26 @@ +import { Directive, Optional } from "@angular/core"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { BitActionDirective, ButtonLikeAbstraction } from "@bitwarden/components"; + +import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; + +/** Navigate the browser popup to the previous page when the component is clicked. */ +@Directive({ + selector: "[popupBackAction]", + standalone: true, +}) +export class PopupBackBrowserDirective extends BitActionDirective { + constructor( + buttonComponent: ButtonLikeAbstraction, + private router: PopupRouterCacheService, + @Optional() validationService?: ValidationService, + @Optional() logService?: LogService, + ) { + super(buttonComponent, validationService, logService); + + // override `bitAction` input; the parent handles the rest + this.handler = () => this.router.back(); + } +} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4dca29ee914..18b26913b1d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -287,7 +287,7 @@ const routes: Routes = [ path: "cipher-password-history", component: PasswordHistoryV2Component, canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, + data: { elevation: 4 } satisfies RouteDataProperties, }, { path: "add-cipher", @@ -310,7 +310,7 @@ const routes: Routes = [ path: "attachments", component: AttachmentsV2Component, canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, + data: { elevation: 4 } satisfies RouteDataProperties, }, { path: "generator", @@ -382,7 +382,7 @@ const routes: Routes = [ path: "premium", component: PremiumV2Component, canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, + data: { elevation: 3 } satisfies RouteDataProperties, }, { path: "appearance", diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index dadd7917b99..bb975f48e5d 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -23,9 +23,7 @@ if (process.env.ENV === "production") { } function init() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); + void platformBrowserDynamic().bootstrapModule(AppModule); } init(); diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index 40c942539f6..5d313188d8f 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -16,6 +16,9 @@ + + diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts index 851509ab17f..27147b75d39 100644 --- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts @@ -7,6 +7,7 @@ import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/compo import { ExportComponent } from "@bitwarden/vault-export-ui"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; +import { PopupBackBrowserDirective } from "../../../../platform/popup/layout/popup-back.directive"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; @@ -25,6 +26,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopupFooterComponent, PopupHeaderComponent, PopOutComponent, + PopupBackBrowserDirective, ], }) export class ExportBrowserV2Component { diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 8055260db57..6b6096825a7 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -46,8 +46,7 @@ "useDefineForClassFields": false }, "angularCompilerOptions": { - "strictTemplates": true, - "preserveWhitespaces": true + "strictTemplates": true }, "include": [ "src", diff --git a/apps/desktop/src/app/main.ts b/apps/desktop/src/app/main.ts index 287d66795d2..ba964177dbc 100644 --- a/apps/desktop/src/app/main.ts +++ b/apps/desktop/src/app/main.ts @@ -12,9 +12,7 @@ if (!ipc.platform.isDev) { enableProdMode(); } -// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. -// eslint-disable-next-line @typescript-eslint/no-floating-promises -platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); +void platformBrowserDynamic().bootstrapModule(AppModule); // Disable drag and drop to prevent malicious links from executing in the context of the app document.addEventListener("dragover", (event) => event.preventDefault()); diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index aba7353c5e4..6f844a7bf51 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -28,7 +28,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -90,6 +90,7 @@ export class VaultComponent implements OnInit, OnDestroy { deleted = false; userHasPremiumAccess = false; activeFilter: VaultFilter = new VaultFilter(); + activeUserId: UserId; private modal: ModalRef = null; private componentIsDestroyed$ = new Subject(); @@ -237,12 +238,12 @@ export class VaultComponent implements OnInit, OnDestroy { }); } - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherService - .failedToDecryptCiphers$(activeUserId) + .failedToDecryptCiphers$(this.activeUserId) .pipe( - map((ciphers) => ciphers.filter((c) => !c.isDeleted)), + map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []), filter((ciphers) => ciphers.length > 0), take(1), takeUntil(this.componentIsDestroyed$), @@ -494,8 +495,10 @@ export class VaultComponent implements OnInit, OnDestroy { async savedCipher(cipher: CipherView) { this.cipherId = cipher.id; this.action = "view"; - this.go(); await this.vaultItemsComponent.refresh(); + await this.cipherService.clearCache(this.activeUserId); + await this.viewComponent.load(); + this.go(); } async deletedCipher(cipher: CipherView) { diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 0bef5a5564d..05253fc47d7 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -45,8 +45,7 @@ "useDefineForClassFields": false }, "angularCompilerOptions": { - "strictTemplates": true, - "preserveWhitespaces": true + "strictTemplates": true }, "include": ["src", "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"] } diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index d07f674e813..323e5326a1c 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -18,6 +18,8 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogService } from "@bitwarden/components"; @@ -41,6 +43,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme private organizationService: OrganizationService, billingAccountProfileStateService: BillingAccountProfileStateService, protected accountService: AccountService, + configService: ConfigService, + i18nService: I18nService, ) { super( dialogService, @@ -49,6 +53,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme policyService, billingAccountProfileStateService, accountService, + configService, + i18nService, ); } diff --git a/apps/web/src/app/admin-console/settings/create-organization.component.html b/apps/web/src/app/admin-console/settings/create-organization.component.html index 105a3e6a251..a5acc62dfa9 100644 --- a/apps/web/src/app/admin-console/settings/create-organization.component.html +++ b/apps/web/src/app/admin-console/settings/create-organization.component.html @@ -2,5 +2,9 @@

{{ "newOrganizationDesc" | i18n }}

- +
diff --git a/apps/web/src/app/admin-console/settings/create-organization.component.ts b/apps/web/src/app/admin-console/settings/create-organization.component.ts index 47cf1c61c5b..7a20826086d 100644 --- a/apps/web/src/app/admin-console/settings/create-organization.component.ts +++ b/apps/web/src/app/admin-console/settings/create-organization.component.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums"; import { OrganizationPlansComponent } from "../../billing"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -15,29 +16,34 @@ import { SharedModule } from "../../shared"; standalone: true, imports: [SharedModule, OrganizationPlansComponent, HeaderModule], }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class CreateOrganizationComponent implements OnInit { - @ViewChild(OrganizationPlansComponent, { static: true }) - orgPlansComponent: OrganizationPlansComponent; +export class CreateOrganizationComponent { + protected secretsManager = false; + protected plan: PlanType = PlanType.Free; + protected productTier: ProductTierType = ProductTierType.Free; - constructor(private route: ActivatedRoute) {} - - ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - if (qParams.plan === "families") { - this.orgPlansComponent.plan = PlanType.FamiliesAnnually; - this.orgPlansComponent.productTier = ProductTierType.Families; - } else if (qParams.plan === "teams") { - this.orgPlansComponent.plan = PlanType.TeamsAnnually; - this.orgPlansComponent.productTier = ProductTierType.Teams; - } else if (qParams.plan === "teamsStarter") { - this.orgPlansComponent.plan = PlanType.TeamsStarter; - this.orgPlansComponent.productTier = ProductTierType.TeamsStarter; - } else if (qParams.plan === "enterprise") { - this.orgPlansComponent.plan = PlanType.EnterpriseAnnually; - this.orgPlansComponent.productTier = ProductTierType.Enterprise; + constructor(private route: ActivatedRoute) { + this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { + if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) { + this.plan = PlanType.FamiliesAnnually; + this.productTier = ProductTierType.Families; + } else if (qParams.plan === "teams" || qParams.productTier == ProductTierType.Teams) { + this.plan = PlanType.TeamsAnnually; + this.productTier = ProductTierType.Teams; + } else if ( + qParams.plan === "teamsStarter" || + qParams.productTier == ProductTierType.TeamsStarter + ) { + this.plan = PlanType.TeamsStarter; + this.productTier = ProductTierType.TeamsStarter; + } else if ( + qParams.plan === "enterprise" || + qParams.productTier == ProductTierType.Enterprise + ) { + this.plan = PlanType.EnterpriseAnnually; + this.productTier = ProductTierType.Enterprise; } + + this.secretsManager = qParams.product == ProductType.SecretsManager; }); } } diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts index 209af41e311..5d5770e2325 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts @@ -74,10 +74,10 @@ describe("WebLoginComponentService", () => { expect(service).toBeTruthy(); }); - describe("getOrgPolicies", () => { + describe("getOrgPoliciesFromOrgInvite", () => { it("returns undefined if organization invite is null", async () => { acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null); - const result = await service.getOrgPolicies(); + const result = await service.getOrgPoliciesFromOrgInvite(); expect(result).toBeUndefined(); }); @@ -94,7 +94,7 @@ describe("WebLoginComponentService", () => { organizationName: "org-name", }); policyApiService.getPoliciesByToken.mockRejectedValue(error); - await service.getOrgPolicies(); + await service.getOrgPoliciesFromOrgInvite(); expect(logService.error).toHaveBeenCalledWith(error); }); @@ -130,7 +130,7 @@ describe("WebLoginComponentService", () => { of(masterPasswordPolicyOptions), ); - const result = await service.getOrgPolicies(); + const result = await service.getOrgPoliciesFromOrgInvite(); expect(result).toEqual({ policies: policies, diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index ce1bce40e39..aa0c204750f 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -48,7 +48,7 @@ export class WebLoginComponentService this.clientType = this.platformUtilsService.getClientType(); } - async getOrgPolicies(): Promise { + async getOrgPoliciesFromOrgInvite(): Promise { const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite(); if (orgInvite != null) { diff --git a/apps/web/src/app/auth/recover-two-factor.component.html b/apps/web/src/app/auth/recover-two-factor.component.html index e3641765800..dee3bec1520 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.html +++ b/apps/web/src/app/auth/recover-two-factor.component.html @@ -1,6 +1,6 @@

- {{ "recoverAccountTwoStepDesc" | i18n }} + {{ recoveryCodeMessage }} { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { @@ -56,12 +88,90 @@ export class RecoverTwoFactorComponent { request.email = this.email.trim().toLowerCase(); const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email); request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key); - await this.apiService.postTwoFactorRecover(request); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("twoStepRecoverDisabled"), - }); - await this.router.navigate(["/"]); + + try { + await this.apiService.postTwoFactorRecover(request); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("twoStepRecoverDisabled"), + }); + + if (!this.recoveryCodeLoginFeatureFlagEnabled) { + await this.router.navigate(["/"]); + return; + } + + // Handle login after recovery if the feature flag is enabled + await this.handleRecoveryLogin(request); + } catch (e) { + const errorMessage = this.extractErrorMessage(e); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: errorMessage, + }); + } }; + + /** + * Handles the login process after a successful account recovery. + */ + private async handleRecoveryLogin(request: TwoFactorRecoveryRequest) { + // Build two-factor request to pass into PasswordLoginCredentials request using the 2FA recovery code and RecoveryCode type + const twoFactorRequest: TokenTwoFactorRequest = { + provider: TwoFactorProviderType.RecoveryCode, + token: request.recoveryCode, + remember: false, + }; + + const credentials = new PasswordLoginCredentials( + request.email, + this.masterPassword, + "", + twoFactorRequest, + ); + + try { + const authResult = await this.loginStrategyService.logIn(credentials); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("youHaveBeenLoggedIn"), + }); + await this.loginSuccessHandlerService.run(authResult.userId); + await this.router.navigate(["/settings/security/two-factor"]); + } catch (error) { + // If login errors, redirect to login page per product. Don't show error + this.logService.error("Error logging in automatically: ", (error as Error).message); + await this.router.navigate(["/login"], { queryParams: { email: request.email } }); + } + } + + /** + * Extracts an error message from the error object. + */ + private extractErrorMessage(error: unknown): string { + let errorMessage: string = this.i18nService.t("unexpectedError"); + if (error && typeof error === "object" && "validationErrors" in error) { + const validationErrors = error.validationErrors; + if (validationErrors && typeof validationErrors === "object") { + errorMessage = Object.keys(validationErrors) + .map((key) => { + const messages = (validationErrors as Record)[key]; + return Array.isArray(messages) ? messages.join(" ") : messages; + }) + .join(" "); + } + } else if ( + error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ) { + errorMessage = error.message; + } + return errorMessage; + } } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html index b7cd6954fd6..985584e386d 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html @@ -26,7 +26,7 @@

-

{{ "twoStepLoginRecoveryWarning" | i18n }}

+

{{ recoveryCodeWarningMessage }}

diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 4530692ebee..a76505930d4 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -29,6 +29,9 @@ import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.s import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogService } from "@bitwarden/components"; @@ -52,6 +55,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { organization: Organization; providers: any[] = []; canAccessPremium$: Observable; + recoveryCodeWarningMessage: string; showPolicyWarning = false; loading = true; modal: ModalRef; @@ -70,6 +74,8 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, protected accountService: AccountService, + protected configService: ConfigService, + protected i18nService: I18nService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -79,6 +85,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } async ngOnInit() { + const recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.RecoveryCodeLogin, + ); + this.recoveryCodeWarningMessage = recoveryCodeLoginFeatureFlagEnabled + ? this.i18nService.t("yourSingleUseRecoveryCode") + : this.i18nService.t("twoStepLoginRecoveryWarning"); + for (const key in TwoFactorProviders) { // eslint-disable-next-line if (!TwoFactorProviders.hasOwnProperty(key)) { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 071d1f75161..f9f5b763a4e 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -110,6 +110,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this._plan = plan; this.formGroup?.controls?.plan?.setValue(plan); } + @Input() enableSecretsManagerByDefault: boolean; private _plan = PlanType.Free; @Input() providerId?: string; @@ -269,6 +270,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { .subscribe(() => { this.refreshSalesTax(); }); + + if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) { + this.secretsManagerSubscription.patchValue({ + enabled: true, + userSeats: 1, + additionalServiceAccounts: 0, + }); + } } ngOnDestroy() { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0f48595f09b..3c241003e7a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2183,6 +2183,9 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, + "yourSingleUseRecoveryCode": { + "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." + }, "viewRecoveryCode": { "message": "View recovery code" }, @@ -4193,6 +4196,9 @@ "recoverAccountTwoStepDesc": { "message": "If you cannot access your account through your normal two-step login methods, you can use your two-step login recovery code to turn off all two-step providers on your account." }, + "logInBelowUsingYourSingleUseRecoveryCode": { + "message": "Log in below using your single-use recovery code. This will turn off all two-step providers on your account." + }, "recoverAccountTwoStep": { "message": "Recover account two-step login" }, diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 1d1519c8b50..b202a170d26 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -11,6 +11,4 @@ if (process.env.NODE_ENV === "production") { enableProdMode(); } -// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. -// eslint-disable-next-line @typescript-eslint/no-floating-promises -platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); +void platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 68ac8c80085..d1da8ac4532 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -35,8 +35,7 @@ } }, "angularCompilerOptions": { - "strictTemplates": true, - "preserveWhitespaces": true + "strictTemplates": true }, "files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"], "include": [ diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index c0eb8080070..4ef5453478e 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -4,7 +4,7 @@
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html index 4dc4b7ffb1a..3eb7831c7f8 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html @@ -9,7 +9,7 @@
-

+

{{ "noCriticalAppsTitle" | i18n }}

@@ -28,7 +28,14 @@

{{ "criticalApplications" | i18n }}

- diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index f1fa38dd28f..42c1c62a437 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -18,7 +18,7 @@ import { 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 { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { Icons, NoItemsModule, @@ -27,10 +27,14 @@ import { ToastService, } from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; +import { SecurityTaskType } from "@bitwarden/vault"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; +import { CreateTasksRequest } from "../../vault/services/abstractions/admin-task.abstraction"; +import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service"; + import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ @@ -38,7 +42,7 @@ import { RiskInsightsTabType } from "./risk-insights.component"; selector: "tools-critical-applications", templateUrl: "./critical-applications.component.html", imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], - providers: [], + providers: [DefaultAdminTaskService], }) export class CriticalApplicationsComponent implements OnInit { protected dataSource = new TableDataSource(); @@ -50,6 +54,7 @@ export class CriticalApplicationsComponent implements OnInit { protected applicationSummary = {} as ApplicationHealthReportSummary; noItemsIcon = Icons.Security; isNotificationsFeatureEnabled: boolean = false; + enableRequestPasswordChange = false; async ngOnInit() { this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag( @@ -75,6 +80,7 @@ export class CriticalApplicationsComponent implements OnInit { if (applications) { this.dataSource.data = applications; this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; } }); } @@ -109,6 +115,33 @@ export class CriticalApplicationsComponent implements OnInit { this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname); }; + async requestPasswordChange() { + const apps = this.dataSource.data; + const cipherIds = apps + .filter((_) => _.atRiskPasswordCount > 0) + .flatMap((app) => app.atRiskMemberDetails.map((member) => member.cipherId)); + const distinctCipherIds = Array.from(new Set(cipherIds)); + const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({ + cipherId: cipherId as CipherId, + type: SecurityTaskType.UpdateAtRiskCredential, + })); + + try { + await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks); + this.toastService.showToast({ + message: this.i18nService.t("notifiedMembers"), + variant: "success", + title: this.i18nService.t("success"), + }); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } + } + constructor( protected activatedRoute: ActivatedRoute, protected router: Router, @@ -118,6 +151,7 @@ export class CriticalApplicationsComponent implements OnInit { protected reportService: RiskInsightsReportService, protected i18nService: I18nService, private configService: ConfigService, + private adminTaskService: DefaultAdminTaskService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html index d6f945bfb92..4e77838229e 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -1,6 +1,6 @@
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 12082e888b0..397e2a630de 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,6 +1,8 @@ -
{{ "accessIntelligence" | i18n }}
+
+ {{ "accessIntelligence" | i18n }} +

{{ "riskInsights" | i18n }}

{{ "reviewAtRiskPasswords" | i18n }} @@ -9,7 +11,7 @@ class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center" > {{ diff --git a/bitwarden_license/bit-web/src/main.ts b/bitwarden_license/bit-web/src/main.ts index 1d1519c8b50..b202a170d26 100644 --- a/bitwarden_license/bit-web/src/main.ts +++ b/bitwarden_license/bit-web/src/main.ts @@ -11,6 +11,4 @@ if (process.env.NODE_ENV === "production") { enableProdMode(); } -// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. -// eslint-disable-next-line @typescript-eslint/no-floating-promises -platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); +void platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 923f667e680..c309aa9624a 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -827,9 +827,9 @@ export class AddEditComponent implements OnInit, OnDestroy { private async generateSshKey(showNotification: boolean = true) { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); - this.cipher.sshKey.privateKey = sshKey.private_key; - this.cipher.sshKey.publicKey = sshKey.public_key; - this.cipher.sshKey.keyFingerprint = sshKey.key_fingerprint; + this.cipher.sshKey.privateKey = sshKey.privateKey; + this.cipher.sshKey.publicKey = sshKey.publicKey; + this.cipher.sshKey.keyFingerprint = sshKey.fingerprint; if (showNotification) { this.toastService.showToast({ diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 637596256b0..92a231ab8db 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -11,7 +11,7 @@ import { OnInit, Output, } from "@angular/core"; -import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { filter, firstValueFrom, map, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -80,8 +80,6 @@ export class ViewComponent implements OnDestroy, OnInit { private previousCipherId: string; private passwordReprompted = false; - private destroyed$ = new Subject(); - get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); const creationDate = this.datePipe.transform( @@ -144,18 +142,14 @@ export class ViewComponent implements OnDestroy, OnInit { async load() { this.cleanUp(); - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); // Grab individual cipher from `cipherViews$` for the most up-to-date information - this.cipherService - .cipherViews$(activeUserId) - .pipe( + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.cipher = await firstValueFrom( + this.cipherService.cipherViews$(activeUserId).pipe( map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)), filter((cipher) => !!cipher), - takeUntil(this.destroyed$), - ) - .subscribe((cipher) => { - this.cipher = cipher; - }); + ), + ); this.canAccessPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), @@ -528,7 +522,6 @@ export class ViewComponent implements OnDestroy, OnInit { this.showCardNumber = false; this.showCardCode = false; this.passwordReprompted = false; - this.destroyed$.next(); if (this.totpInterval) { clearInterval(this.totpInterval); } diff --git a/libs/auth/src/angular/login/default-login-component.service.spec.ts b/libs/auth/src/angular/login/default-login-component.service.spec.ts index 05b24da56cc..446ab44b4ee 100644 --- a/libs/auth/src/angular/login/default-login-component.service.spec.ts +++ b/libs/auth/src/angular/login/default-login-component.service.spec.ts @@ -56,13 +56,6 @@ describe("DefaultLoginComponentService", () => { expect(service).toBeTruthy(); }); - describe("getOrgPolicies", () => { - it("returns null", async () => { - const result = await service.getOrgPolicies(); - expect(result).toBeNull(); - }); - }); - describe("isLoginWithPasskeySupported", () => { it("returns true when clientType is Web", () => { service["clientType"] = ClientType.Web; diff --git a/libs/auth/src/angular/login/default-login-component.service.ts b/libs/auth/src/angular/login/default-login-component.service.ts index 84a7d923d12..41b761ce1d9 100644 --- a/libs/auth/src/angular/login/default-login-component.service.ts +++ b/libs/auth/src/angular/login/default-login-component.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; -import { LoginComponentService, PasswordPolicies } from "@bitwarden/auth/angular"; +import { LoginComponentService } from "@bitwarden/auth/angular"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -23,10 +23,6 @@ export class DefaultLoginComponentService implements LoginComponentService { protected ssoLoginService: SsoLoginServiceAbstraction, ) {} - async getOrgPolicies(): Promise { - return null; - } - isLoginWithPasskeySupported(): boolean { return this.clientType === ClientType.Web; } diff --git a/libs/auth/src/angular/login/login-component.service.ts b/libs/auth/src/angular/login/login-component.service.ts index 8ca857cef59..1147c5d8644 100644 --- a/libs/auth/src/angular/login/login-component.service.ts +++ b/libs/auth/src/angular/login/login-component.service.ts @@ -23,7 +23,7 @@ export abstract class LoginComponentService { * Gets the organization policies if there is an organization invite. * - Used by: Web */ - getOrgPolicies: () => Promise; + getOrgPoliciesFromOrgInvite?: () => Promise; /** * Indicates whether login with passkey is supported on the given client diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 66fe2503508..f31e02fdb1f 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -12,6 +12,7 @@ import { PasswordLoginCredentials, } from "@bitwarden/auth/common"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; @@ -30,6 +31,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, ButtonModule, @@ -43,7 +45,7 @@ import { import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; import { VaultIcon, WaveIcon } from "../icons"; -import { LoginComponentService } from "./login-component.service"; +import { LoginComponentService, PasswordPolicies } from "./login-component.service"; const BroadcasterSubscriptionId = "LoginComponent"; @@ -72,7 +74,6 @@ export class LoginComponent implements OnInit, OnDestroy { @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined; private destroy$ = new Subject(); - private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined; readonly Icons = { WaveIcon, VaultIcon }; clientType: ClientType; @@ -97,11 +98,6 @@ export class LoginComponent implements OnInit, OnDestroy { return this.formGroup.controls.email; } - // Web properties - enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined; - policies: Policy[] | undefined; - showResetPasswordAutoEnrollWarning = false; - // Desktop properties deferFocus: boolean | null = null; @@ -281,18 +277,39 @@ export class LoginComponent implements OnInit, OnDestroy { return; } + // User logged in successfully so execute side effects await this.loginSuccessHandlerService.run(authResult.userId); + this.loginEmailService.clearValues(); + // Determine where to send the user next if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { - this.loginEmailService.clearValues(); await this.router.navigate(["update-temp-password"]); return; } - // If none of the above cases are true, proceed with login... - await this.evaluatePassword(); + // TODO: PM-18269 - evaluate if we can combine this with the + // password evaluation done in the password login strategy. + // If there's an existing org invite, use it to get the org's password policies + // so we can evaluate the MP against the org policies + if (this.loginComponentService.getOrgPoliciesFromOrgInvite) { + const orgPolicies: PasswordPolicies | null = + await this.loginComponentService.getOrgPoliciesFromOrgInvite(); - this.loginEmailService.clearValues(); + if (orgPolicies) { + // Since we have retrieved the policies, we can go ahead and set them into state for future use + // e.g., the update-password page currently only references state for policy data and + // doesn't fallback to pulling them from the server like it should if they are null. + await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies); + + const isPasswordChangeRequired = await this.isPasswordChangeRequiredByOrgPolicy( + orgPolicies.enforcedPasswordPolicyOptions, + ); + if (isPasswordChangeRequired) { + await this.router.navigate(["update-password"]); + return; + } + } + } if (this.clientType === ClientType.Browser) { await this.router.navigate(["/tabs/vault"]); @@ -310,54 +327,51 @@ export class LoginComponent implements OnInit, OnDestroy { await this.loginComponentService.launchSsoBrowserWindow(email, clientId); } - protected async evaluatePassword(): Promise { + /** + * Checks if the master password meets the enforced policy requirements + * and if the user is required to change their password. + */ + private async isPasswordChangeRequiredByOrgPolicy( + enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions, + ): Promise { try { - // If we do not have any saved policies, attempt to load them from the service - if (this.enforcedMasterPasswordOptions == undefined) { - this.enforcedMasterPasswordOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(), - ); + if (enforcedPasswordPolicyOptions == undefined) { + return false; } - if (this.requirePasswordChange()) { - await this.router.navigate(["update-password"]); - return; + // Note: we deliberately do not check enforcedPasswordPolicyOptions.enforceOnLogin + // as existing users who are logging in after getting an org invite should + // always be forced to set a password that meets the org's policy. + // Org Invite -> Registration also works this way for new BW users as well. + + const masterPassword = this.formGroup.controls.masterPassword.value; + + // Return false if masterPassword is null/undefined since this is only evaluated after successful login + if (!masterPassword) { + return false; } + + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + this.formGroup.value.email ?? undefined, + )?.score; + + return !this.policyService.evaluateMasterPassword( + passwordStrength, + masterPassword, + enforcedPasswordPolicyOptions, + ); } catch (e) { // Do not prevent unlock if there is an error evaluating policies this.logService.error(e); + return false; } } - /** - * Checks if the master password meets the enforced policy requirements - * If not, returns false - */ - private requirePasswordChange(): boolean { - if ( - this.enforcedMasterPasswordOptions == undefined || - !this.enforcedMasterPasswordOptions.enforceOnLogin - ) { - return false; - } - - const masterPassword = this.formGroup.controls.masterPassword.value; - - // Return false if masterPassword is null/undefined since this is only evaluated after successful login - if (!masterPassword) { - return false; - } - - const passwordStrength = this.passwordStrengthService.getPasswordStrength( - masterPassword, - this.formGroup.value.email ?? undefined, - )?.score; - - return !this.policyService.evaluateMasterPassword( - passwordStrength, - masterPassword, - this.enforcedMasterPasswordOptions, - ); + private async setPoliciesIntoState(userId: UserId, policies: Policy[]): Promise { + const policiesData: { [id: string]: PolicyData } = {}; + policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p))); + await this.policyService.replace(policiesData, userId); } protected async startAuthRequestLogin(): Promise { @@ -528,12 +542,6 @@ export class LoginComponent implements OnInit, OnDestroy { } private async defaultOnInit(): Promise { - // If there's an existing org invite, use it to get the password policies - const orgPolicies = await this.loginComponentService.getOrgPolicies(); - - this.policies = orgPolicies?.policies; - this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false; - let paramEmailIsSet = false; const params = await firstValueFrom(this.activatedRoute.queryParams); diff --git a/libs/common/src/auth/enums/two-factor-provider-type.ts b/libs/common/src/auth/enums/two-factor-provider-type.ts index a1708032016..b3308b6c12f 100644 --- a/libs/common/src/auth/enums/two-factor-provider-type.ts +++ b/libs/common/src/auth/enums/two-factor-provider-type.ts @@ -7,4 +7,5 @@ export enum TwoFactorProviderType { Remember = 5, OrganizationDuo = 6, WebAuthn = 7, + RecoveryCode = 8, } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5137fda329f..75346c0edb8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -50,6 +50,7 @@ export enum FeatureFlag { AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner", NewDeviceVerification = "new-device-verification", PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", + RecoveryCodeLogin = "pm-17128-recovery-code-login", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AccountDeprovisioningBanner]: FALSE, [FeatureFlag.NewDeviceVerification]: FALSE, [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, + [FeatureFlag.RecoveryCodeLogin]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index 192034254b9..5aa79946653 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -65,7 +65,6 @@ export default class Domain { key: SymmetricCryptoKey = null, objectContext: string = "No Domain Context", ): Promise { - const promises = []; const self: any = this; for (const prop in map) { @@ -74,27 +73,15 @@ export default class Domain { continue; } - (function (theProp) { - const p = Promise.resolve() - .then(() => { - const mapProp = map[theProp] || theProp; - if (self[mapProp]) { - return self[mapProp].decrypt( - orgId, - key, - `Property: ${prop}; ObjectContext: ${objectContext}`, - ); - } - return null; - }) - .then((val: any) => { - (viewModel as any)[theProp] = val; - }); - promises.push(p); - })(prop); + const mapProp = map[prop] || prop; + if (self[mapProp]) { + (viewModel as any)[prop] = await self[mapProp].decrypt( + orgId, + key, + `Property: ${prop}; ObjectContext: ${objectContext}`, + ); + } } - - await Promise.all(promises); return viewModel; } @@ -121,22 +108,20 @@ export default class Domain { _: Constructor = this.constructor as Constructor, objectContext: string = "No Domain Context", ): Promise> { - const promises = []; + const decryptedObjects = []; for (const prop of encryptedProperties) { const value = (this as any)[prop] as EncString; - promises.push( - this.decryptProperty( - prop, - value, - key, - encryptService, - `Property: ${prop.toString()}; ObjectContext: ${objectContext}`, - ), + const decrypted = await this.decryptProperty( + prop, + value, + key, + encryptService, + `Property: ${prop.toString()}; ObjectContext: ${objectContext}`, ); + decryptedObjects.push(decrypted); } - const decryptedObjects = await Promise.all(promises); const decryptedObject = decryptedObjects.reduce( (acc, obj) => { return { ...acc, ...obj }; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index d82f4585e65..21538b87788 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -12,7 +12,10 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../data/cipher.data"; import { LocalData } from "../data/local.data"; +import { AttachmentView } from "../view/attachment.view"; import { CipherView } from "../view/cipher.view"; +import { FieldView } from "../view/field.view"; +import { PasswordHistoryView } from "../view/password-history.view"; import { Attachment } from "./attachment"; import { Card } from "./card"; @@ -136,6 +139,7 @@ export class Cipher extends Domain implements Decryptable { if (this.key != null) { const encryptService = Utils.getContainerService().getEncryptService(); + const keyBytes = await encryptService.decryptToBytes( this.key, encKey, @@ -198,44 +202,28 @@ export class Cipher extends Domain implements Decryptable { } if (this.attachments != null && this.attachments.length > 0) { - const attachments: any[] = []; - await this.attachments.reduce((promise, attachment) => { - return promise - .then(() => { - return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); - }) - .then((decAttachment) => { - attachments.push(decAttachment); - }); - }, Promise.resolve()); + const attachments: AttachmentView[] = []; + for (const attachment of this.attachments) { + attachments.push( + await attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey), + ); + } model.attachments = attachments; } if (this.fields != null && this.fields.length > 0) { - const fields: any[] = []; - await this.fields.reduce((promise, field) => { - return promise - .then(() => { - return field.decrypt(this.organizationId, encKey); - }) - .then((decField) => { - fields.push(decField); - }); - }, Promise.resolve()); + const fields: FieldView[] = []; + for (const field of this.fields) { + fields.push(await field.decrypt(this.organizationId, encKey)); + } model.fields = fields; } if (this.passwordHistory != null && this.passwordHistory.length > 0) { - const passwordHistory: any[] = []; - await this.passwordHistory.reduce((promise, ph) => { - return promise - .then(() => { - return ph.decrypt(this.organizationId, encKey); - }) - .then((decPh) => { - passwordHistory.push(decPh); - }); - }, Promise.resolve()); + const passwordHistory: PasswordHistoryView[] = []; + for (const ph of this.passwordHistory) { + passwordHistory.push(await ph.decrypt(this.organizationId, encKey)); + } model.passwordHistory = passwordHistory; } diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index e9be66da7d4..a4c73b699cf 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -45,7 +45,6 @@ export type ChipSelectOption = Option & { multi: true, }, ], - preserveWhitespaces: false, }) export class ChipSelectComponent implements ControlValueAccessor, AfterViewInit { @ViewChild(MenuComponent) menu: MenuComponent; diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index e48758ca59a..4fc94e41854 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -22,7 +22,6 @@ enum CharacterType { } }`, - preserveWhitespaces: false, standalone: true, }) export class ColorPasswordComponent { diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts index 54b4c89f4ce..a524c9a7a1a 100644 --- a/libs/components/src/drawer/drawer.stories.ts +++ b/libs/components/src/drawer/drawer.stories.ts @@ -9,10 +9,7 @@ import { ButtonModule } from "../button"; import { CalloutModule } from "../callout"; import { LayoutComponent } from "../layout"; import { mockLayoutI18n } from "../layout/mocks"; -import { - disableBothThemeDecorator, - positionFixedWrapperDecorator, -} from "../stories/storybook-decorators"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { TypographyModule } from "../typography"; import { I18nMockService } from "../utils"; @@ -30,7 +27,6 @@ export default { }, decorators: [ positionFixedWrapperDecorator(), - disableBothThemeDecorator, moduleMetadata({ imports: [ RouterTestingModule, diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 7788f4986bf..319b60e6435 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,4 +1,4 @@ -export { ButtonType } from "./shared/button-like.abstraction"; +export { ButtonType, ButtonLikeAbstraction } from "./shared/button-like.abstraction"; export * from "./a11y"; export * from "./async-actions"; export * from "./avatar"; diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 62bdee26740..37244f37c8d 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -29,7 +29,6 @@ import { SideNavService } from "./side-nav.service"; ], standalone: true, imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe], - preserveWhitespaces: false, }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { @ContentChildren(NavBaseComponent, { diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 62b93984384..af3b082d1c6 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; -import { disableBothThemeDecorator, positionFixedWrapperDecorator } from "../storybook-decorators"; +import { positionFixedWrapperDecorator } from "../storybook-decorators"; import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component"; import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; @@ -31,7 +31,6 @@ export default { component: LayoutComponent, decorators: [ positionFixedWrapperDecorator(), - disableBothThemeDecorator, moduleMetadata({ imports: [ KitchenSinkSharedModule, diff --git a/libs/components/src/stories/storybook-decorators.ts b/libs/components/src/stories/storybook-decorators.ts index ec0df264c7e..d1146a7cd96 100644 --- a/libs/components/src/stories/storybook-decorators.ts +++ b/libs/components/src/stories/storybook-decorators.ts @@ -17,15 +17,3 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin ${wrapper ? wrapper(story) : story}
`, ); - -export const disableBothThemeDecorator = componentWrapperDecorator( - (story) => story, - ({ globals }) => { - /** - * avoid a bug with the way that we render the same component twice in the same iframe and how - * that interacts with the router-outlet - */ - const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; - return { theme: themeOverride }; - }, -); diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index c93e96150ad..75124ceb4b3 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -23,7 +23,6 @@ import { ToastComponent } from "./toast.component"; transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), ]), ], - preserveWhitespaces: false, standalone: true, imports: [ToastComponent], }) diff --git a/libs/components/src/toggle-group/toggle-group.component.ts b/libs/components/src/toggle-group/toggle-group.component.ts index 5033a27ed6d..057a594654a 100644 --- a/libs/components/src/toggle-group/toggle-group.component.ts +++ b/libs/components/src/toggle-group/toggle-group.component.ts @@ -12,7 +12,6 @@ let nextId = 0; @Component({ selector: "bit-toggle-group", templateUrl: "./toggle-group.component.html", - preserveWhitespaces: false, standalone: true, }) export class ToggleGroupComponent { diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index 7bd62056763..bb48b7e103e 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -19,7 +19,6 @@ let nextId = 0; @Component({ selector: "bit-toggle", templateUrl: "./toggle.component.html", - preserveWhitespaces: false, standalone: true, imports: [NgClass], }) diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0a5a66337ac..90d424d1285 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -194,6 +194,9 @@ --tw-ring-offset-color: #002b36; } +/** Used by CDK a11y services */ +@import "@angular/cdk/a11y-prebuilt.css"; + @import "./popover/popover.component.css"; @import "./search/search.component.css"; diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 1a4f9374d0e..a9d63eb17d4 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -368,20 +368,20 @@ export class DefaultKeyService implements KeyServiceAbstraction { await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => { const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {}; - orgs.forEach((org) => { + for (const org of orgs) { encOrgKeyData[org.id] = { type: "organization", key: org.key, }; - }); + } - providerOrgs.forEach((org) => { + for (const org of providerOrgs) { encOrgKeyData[org.id] = { type: "provider", providerId: org.providerId, key: org.key, }; - }); + } return encOrgKeyData; }); } diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 934c35f8060..c992ecd78cf 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -42,7 +42,6 @@ import { EventType } from "@bitwarden/common/enums"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, @@ -56,7 +55,8 @@ import { SelectModule, ToastService, } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; +import { GeneratorServicesModule } from "@bitwarden/generator-components"; +import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { EncryptedExportType } from "../enums/encrypted-export-type.enum"; @@ -81,6 +81,7 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; ExportScopeCalloutComponent, UserVerificationDialogComponent, PasswordStrengthV2Component, + GeneratorServicesModule, ], }) export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { @@ -175,14 +176,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { private destroy$ = new Subject(); private onlyManagedCollections = true; + private onGenerate$ = new Subject(); constructor( protected i18nService: I18nService, protected toastService: ToastService, protected exportService: VaultExportServiceAbstraction, protected eventCollectionService: EventCollectionService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - private platformUtilsService: PlatformUtilsService, + protected generatorService: CredentialGeneratorService, private policyService: PolicyService, private logService: LogService, private formBuilder: UntypedFormBuilder, @@ -218,6 +219,17 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + // Wire up the password generation for the password-protected export + this.generatorService + .generate$(Generators.password, { on$: this.onGenerate$ }) + .pipe(takeUntil(this.destroy$)) + .subscribe((generated) => { + this.exportForm.patchValue({ + filePassword: generated.credential, + confirmFilePassword: generated.credential, + }); + }); + if (this.organizationId) { this.organizations$ = this.organizationService .memberOrganizations$(userId) @@ -302,10 +314,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } generatePassword = async () => { - const [options] = await this.passwordGenerationService.getOptions(); - const generatedPassword = await this.passwordGenerationService.generatePassword(options); - this.exportForm.get("filePassword").setValue(generatedPassword); - this.exportForm.get("confirmFilePassword").setValue(generatedPassword); + this.onGenerate$.next({ source: "export" }); }; submit = async () => { diff --git a/libs/tools/export/vault-export/vault-export-ui/tsconfig.json b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json index 1732817986e..6f2a0242dac 100644 --- a/libs/tools/export/vault-export/vault-export-ui/tsconfig.json +++ b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json @@ -9,6 +9,7 @@ "@bitwarden/common/*": ["../../../../common/src/*"], "@bitwarden/components": ["../../../../components/src"], "@bitwarden/generator-core": ["../../../../tools/generator/core/src"], + "@bitwarden/generator-components": ["../../../../tools/generator/components/src"], "@bitwarden/generator-history": ["../../../../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../../../tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../../../tools/generator/extensions/navigation/src"], diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 134897c9356..773ddd4ad66 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -104,9 +104,9 @@ export class SshKeySectionComponent implements OnInit { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); this.sshKeyForm.setValue({ - privateKey: sshKey.private_key, - publicKey: sshKey.public_key, - keyFingerprint: sshKey.key_fingerprint, + privateKey: sshKey.privateKey, + publicKey: sshKey.publicKey, + keyFingerprint: sshKey.fingerprint, }); } } diff --git a/package-lock.json b/package-lock.json index ed037d0ff5e..46bf3d23026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.38", + "@bitwarden/sdk-internal": "0.2.0-main.105", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -91,6 +91,7 @@ "@storybook/addon-essentials": "8.5.2", "@storybook/addon-interactions": "8.5.2", "@storybook/addon-links": "8.5.2", + "@storybook/addon-themes": "^8.5.2", "@storybook/angular": "8.5.2", "@storybook/manager-api": "8.5.2", "@storybook/theming": "8.5.2", @@ -153,6 +154,7 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", + "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", @@ -4469,9 +4471,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.38", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz", - "integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==", + "version": "0.2.0-main.105", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.105.tgz", + "integrity": "sha512-MaQFJbuKTCbN9oZC/+opYVeegaNNJpiUv9/zx+gu8KxWmX0hyEkNPtHKxBjDt3kLLz69CudDtUxEgqOfcDsYAw==", "license": "GPL-3.0" }, "node_modules/@bitwarden/vault": { @@ -8712,6 +8714,22 @@ "storybook": "^8.5.2" } }, + "node_modules/@storybook/addon-themes": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-themes/-/addon-themes-8.5.2.tgz", + "integrity": "sha512-MTJkPwXqLK2Co186EUw2wr+1CpVRMbuWsOmQvhMHeU704kQtSYKkhu/xmaExuDYMupn5xiKG0p8Pt5Ck3fEObQ==", + "dev": true, + "dependencies": { + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.2" + } + }, "node_modules/@storybook/addon-toolbars": { "version": "8.5.2", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.5.2.tgz", @@ -22127,7 +22145,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", "bin": { "json5": "lib/cli.js" }, diff --git a/package.json b/package.json index e691aac09b5..831e99089a4 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@storybook/addon-essentials": "8.5.2", "@storybook/addon-interactions": "8.5.2", "@storybook/addon-links": "8.5.2", + "@storybook/addon-themes": "8.5.2", "@storybook/angular": "8.5.2", "@storybook/manager-api": "8.5.2", "@storybook/theming": "8.5.2", @@ -114,6 +115,7 @@ "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", + "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", @@ -154,7 +156,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.38", + "@bitwarden/sdk-internal": "0.2.0-main.105", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", diff --git a/scripts/dep-ownership.ts b/scripts/dep-ownership.ts index e574a3e9e96..f0bcb1f7dd8 100644 --- a/scripts/dep-ownership.ts +++ b/scripts/dep-ownership.ts @@ -5,8 +5,10 @@ import fs from "fs"; import path from "path"; -const renovateConfig = JSON.parse( - fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json"), "utf8"), +import JSON5 from "json5"; + +const renovateConfig = JSON5.parse( + fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json5"), "utf8"), ); const packagesWithOwners = renovateConfig.packageRules