From 117522f394249eb56d23f972890e60cc6ba4bd0e Mon Sep 17 00:00:00 2001 From: Will Martin Date: Mon, 17 Feb 2025 11:38:13 -0500 Subject: [PATCH 01/18] [PM-15613] fix incorrect route transitions (#13316) --- apps/browser/src/popup/app-routing.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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", From 6ea3e6e3143ae0f027d78c658b2f096be0601320 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 18 Feb 2025 08:36:37 -0600 Subject: [PATCH 02/18] PM-17212 Invoke notifications API (#13377) --- .../critical-applications.component.html | 9 ++++- .../critical-applications.component.ts | 38 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) 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..3135cc8ac46 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 @@ -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()) From 908160349ffbb28138ceac4463873679d126e169 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 18 Feb 2025 08:36:55 -0600 Subject: [PATCH 03/18] PM-17502 ensure usage of tailwind classes (#13408) --- .../access-intelligence/all-applications.component.html | 4 ++-- .../critical-applications.component.html | 2 +- .../risk-insights-loading.component.html | 2 +- .../tools/access-intelligence/risk-insights.component.html | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) 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 @@
-

+

{{ "noAppsInOrgTitle" | i18n: organization?.name }}

@@ -13,7 +13,7 @@ {{ "noAppsInOrgDescription" | i18n }} - {{ "learnMore" | i18n }} + {{ "learnMore" | i18n }}
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 3135cc8ac46..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 }}

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" > {{ From 5633a2504f6ab79cab69ab025fcf7ad91f20dcca Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 18 Feb 2025 16:23:57 +0100 Subject: [PATCH 04/18] Fix autofill ownership of ssh agent and windows-plugin-authenticator (#13469) --- .github/CODEOWNERS | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 From 2622422cf72ac3fe4938126965ee2df85b593cd9 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:42:16 -0500 Subject: [PATCH 05/18] Migrate Renovate config to JSON5 (#13233) * Renamed to JSON5 * Updated linting script to use new file name. * Add JSON5 dependency * Added JSON5 to renovate. * Removed JSON5 formatting * Prettier * Added comment for demonstration --------- Co-authored-by: Matt Bishop --- .github/{renovate.json => renovate.json5} | 165 +++++++++++----------- package-lock.json | 2 +- package.json | 1 + scripts/dep-ownership.ts | 6 +- 4 files changed, 89 insertions(+), 85 deletions(-) rename .github/{renovate.json => renovate.json5} (58%) 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..048d88e4f62 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", @@ -225,27 +226,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 +257,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 +290,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/package-lock.json b/package-lock.json index ed037d0ff5e..59635383625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,6 +153,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", @@ -22127,7 +22128,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..4762bad20ad 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,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", 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 From 7a7be6088a6edb8f900f84f4d80e3c1c2ea8b2c0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 18 Feb 2025 16:59:53 +0100 Subject: [PATCH 06/18] [PM-16984] Improve decryption code clarity (#12681) * Improve decrypt failure logging * Rename decryptcontext to decrypttrace * Improve docs --- .../src/platform/models/domain/domain-base.ts | 47 +++++++----------- libs/common/src/vault/models/domain/cipher.ts | 48 +++++++------------ libs/key-management/src/key.service.ts | 8 ++-- 3 files changed, 38 insertions(+), 65 deletions(-) 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/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; }); } From dc606847e4cfc4486d77b4197e25f77b63dcea07 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 18 Feb 2025 17:05:29 +0100 Subject: [PATCH 07/18] [PM-16447] Disable preserve whitespaces (#12994) Angular 6 changed the default to not preserve whitespaces. We've continued to opt into this pattern for backwards compatibility but we're experiencing issues with the new control flow syntax and would therefore like to switch and not preserve whitespace any longer. --- apps/browser/src/popup/main.ts | 4 +--- apps/browser/tsconfig.json | 3 +-- apps/desktop/src/app/main.ts | 4 +--- apps/desktop/tsconfig.json | 3 +-- apps/web/src/main.ts | 4 +--- apps/web/tsconfig.json | 3 +-- bitwarden_license/bit-web/src/main.ts | 4 +--- libs/components/src/chip-select/chip-select.component.ts | 1 - .../components/src/color-password/color-password.component.ts | 1 - libs/components/src/navigation/nav-group.component.ts | 1 - libs/components/src/toast/toastr.component.ts | 1 - libs/components/src/toggle-group/toggle-group.component.ts | 1 - libs/components/src/toggle-group/toggle.component.ts | 1 - 13 files changed, 7 insertions(+), 24 deletions(-) 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/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/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/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/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/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/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/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], }) From 30ee79d2068cfc4a095b56587ef6fc0965db4231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 18 Feb 2025 12:53:10 -0500 Subject: [PATCH 08/18] add `popupBackAction` to send item and export vault pages (#13363) --- .../popup/layout/popup-back.directive.ts | 26 +++++++++++++++++++ .../add-edit/send-add-edit.component.html | 3 +++ .../add-edit/send-add-edit.component.ts | 2 ++ .../export/export-browser-v2.component.html | 3 +++ .../export/export-browser-v2.component.ts | 2 ++ libs/components/src/index.ts | 2 +- 6 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 apps/browser/src/platform/popup/layout/popup-back.directive.ts 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/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/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"; From f798760dc5bbc3a43afa7b4e27c9a0f678fbea8e Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 18 Feb 2025 20:21:42 +0100 Subject: [PATCH 09/18] [PM-17948] Migrate export from generator legacy to generator core (#13238) * Migrate export from generator-legacy to generator-core * Remove unused platformUtilsService * Wire up password generation within ngOnInit --------- Co-authored-by: Daniel James Smith --- .../src/components/export.component.ts | 25 +++++++++++++------ .../vault-export-ui/tsconfig.json | 1 + 2 files changed, 18 insertions(+), 8 deletions(-) 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"], From a2c23aa661d9d07d298f3fa96fcbf9c586fec2aa Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 18 Feb 2025 15:27:01 -0500 Subject: [PATCH 10/18] PM-15998 - Update browser context menu options when the page domain is a blocked domain (#13378) * update main context menu handler to skip creating menu entries which do not pass blocked uri checks * refactor to remove menu entries which do not pass blocked uri checks * allow context menu autofill items without a password if they have other autofillable attributes * include ciphers without passwords in autofill context menu options and track context menu state --- .../abstractions/main-context-menu-handler.ts | 3 +- .../browser/cipher-context-menu-handler.ts | 6 +- .../browser/main-context-menu-handler.spec.ts | 193 ++++++++++++++---- .../browser/main-context-menu-handler.ts | 77 +++++-- .../browser/src/background/main.background.ts | 30 ++- 5 files changed, 255 insertions(+), 54 deletions(-) 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/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(); } From 6e4a06dab405157e8673f1c9a0925534f7b169ba Mon Sep 17 00:00:00 2001 From: Vicki League Date: Tue, 18 Feb 2025 15:29:47 -0500 Subject: [PATCH 11/18] [CL-317] Use storybook theme addon for theme switching (#13451) --- .github/renovate.json5 | 1 + .storybook/main.ts | 1 + .storybook/preview.tsx | 65 ++++++------------- libs/components/src/drawer/drawer.stories.ts | 6 +- .../kitchen-sink/kitchen-sink.stories.ts | 3 +- .../src/stories/storybook-decorators.ts | 12 ---- package-lock.json | 17 +++++ package.json | 1 + 8 files changed, 41 insertions(+), 65 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 048d88e4f62..6d6fbbd2539 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -209,6 +209,7 @@ "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/addon-links", + "@storybook/addon-themes", "@storybook/angular", "@storybook/manager-api", "@storybook/theming", 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/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/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/package-lock.json b/package-lock.json index 59635383625..744e137009c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -8713,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", diff --git a/package.json b/package.json index 4762bad20ad..e25493335bc 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", From 993c056b19334538288edad8334907a07a6331d7 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:41:42 -0800 Subject: [PATCH 12/18] [PM-18055] - sync list and item view after saving vault item (#13412) * sync list and item view after saving vault item * sync folder on save * remove unused destroy ref --- .../src/vault/app/vault/vault.component.ts | 13 ++++++++----- .../src/vault/components/view.component.ts | 19 ++++++------------- 2 files changed, 14 insertions(+), 18 deletions(-) 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/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); } From 4c09c228060fbe1b2fe78552220ee405e6ddaf22 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 18 Feb 2025 16:37:27 -0500 Subject: [PATCH 13/18] PM-18215 Create UI for confirmation notification message (#13364) * PM-18215 wip * update storybook url * optional errors and storybook args * type safety * Update apps/browser/src/autofill/content/components/icons/warning.ts Co-authored-by: Jonathan Prusik * updated svg to remove dark or light --------- Co-authored-by: Jonathan Prusik --- .../content/components/icons/index.ts | 1 + .../content/components/icons/party-horn.ts | 328 +++++++++++------- .../content/components/icons/warning.ts | 23 ++ .../notification/confirmation.lit-stories.ts | 41 +++ .../notification/confirmation-message.ts | 54 +++ .../components/notification/confirmation.ts | 58 ++++ 6 files changed, 388 insertions(+), 117 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/warning.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-message.ts create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation.ts 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; +`; From fa8ee6fa022559ebf256a808c9bf6bcc250c4a70 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:52:29 -0600 Subject: [PATCH 14/18] feat(auth): [PM-15534] log user in when submitting recovery code - Add recovery code enum and feature flag - Update recovery code text and warning messages - Log user in and redirect to two-factor settings page on valid recovery code - Run full sync and handle login errors silently - Move updated messaging behind feature flag PM-15534 --- .../settings/two-factor-setup.component.ts | 6 + .../auth/recover-two-factor.component.html | 2 +- .../app/auth/recover-two-factor.component.ts | 146 +++++++++++++++--- .../two-factor-setup.component.html | 2 +- .../two-factor/two-factor-setup.component.ts | 13 ++ apps/web/src/locales/en/messages.json | 6 + .../auth/enums/two-factor-provider-type.ts | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 + 8 files changed, 158 insertions(+), 20 deletions(-) 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/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/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/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; From 39f241db3d11ed4b8669d7ea9230dd624a9ab563 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:13:40 +0100 Subject: [PATCH 15/18] [PM-13620]Existing user email linking to create-organization (#13030) * Changes for the existing users * Remove the complicated method * add the column after the patch value * Revert removal of plan query params * Resolve the non blocking issue --- .../create-organization.component.html | 6 ++- .../settings/create-organization.component.ts | 52 +++++++++++-------- .../organization-plans.component.ts | 9 ++++ 3 files changed, 43 insertions(+), 24 deletions(-) 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/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() { From ae38e40859c43cfc8cd55725200cc74a8c9ead58 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:18:56 -0500 Subject: [PATCH 16/18] Auth/PM-17693 - Web - Existing users accepting an org invite are required to update password to meet org policy requirements (#13388) * PM-17693 - Refactor all post login logic around getting org policies from invite token and restore lost functionality. * PM-17693 - Add TODO --- .../login/web-login-component.service.spec.ts | 8 +- .../login/web-login-component.service.ts | 2 +- .../default-login-component.service.spec.ts | 7 -- .../login/default-login-component.service.ts | 6 +- .../angular/login/login-component.service.ts | 2 +- .../auth/src/angular/login/login.component.ts | 118 ++++++++++-------- 6 files changed, 70 insertions(+), 73 deletions(-) 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/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); From 9c102f056c6e55bf93aed5b15ca556b27a5b34b0 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 19 Feb 2025 15:46:43 +0100 Subject: [PATCH 17/18] chore: update sdk to main.105 (#13472) * chore: update sdk version * fix: sdk breaking changes --- libs/angular/src/vault/components/add-edit.component.ts | 6 +++--- .../components/sshkey-section/sshkey-section.component.ts | 6 +++--- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) 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/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 744e137009c..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", @@ -4471,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": { diff --git a/package.json b/package.json index e25493335bc..831e99089a4 100644 --- a/package.json +++ b/package.json @@ -156,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", From 661ee036985a4dda23fac9db437350c2b08b892a Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 19 Feb 2025 17:42:34 +0100 Subject: [PATCH 18/18] Add cdk-visually-hidden to tw-theme for Angular CDK LiveAnnouncer (#13410) Angular CKD LiveAnnouncer depends on some css logic to hide announcements from being displayed. This imports the required CSS to ensure the text is hidden from regular view. --- libs/components/src/tw-theme.css | 3 +++ 1 file changed, 3 insertions(+) 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";