From e13eca57dd64d12e80b6f3b842bed780bac03a7f Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:12:04 -0700 Subject: [PATCH 01/64] fix copy and margin (#10683) --- apps/browser/src/_locales/en/messages.json | 2 +- .../src/billing/popup/settings/premium-v2.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 1e8fd03ade3..4aa9fc2b529 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1113,7 +1113,7 @@ "message": "Thank you for supporting Bitwarden." }, "premiumFeatures": { - "message": "Upgrade to premium and receive:" + "message": "Upgrade to Premium and receive:" }, "premiumPrice": { "message": "All for just $PRICE$ /year!", diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.html b/apps/browser/src/billing/popup/settings/premium-v2.component.html index 7b781eafdb4..f578de8ae7a 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.html +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.html @@ -10,7 +10,7 @@
-
    +
    • {{ "ppremiumSignUpStorage" | i18n }}
    • From bfc49c4e4143f9344028c7920fa7fec0dcf31ac8 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:13:58 -0700 Subject: [PATCH 02/64] display toast when billing sync copy button is clicked (#10661) --- .../billing/organizations/billing-sync-api-key.component.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index e1f5431c45b..8b5ef867cca 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -30,6 +30,8 @@ bitIconButton="bwi-clone" bitSuffix type="button" + showToast + [valueLabel]="'billingSyncKey' | i18n" [appCopyClick]="clientSecret" [appA11yTitle]="'copyValue' | i18n" > From 9152c3203fd56dd74e442e8bb68e384d0d22a732 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 26 Aug 2024 18:15:09 +0200 Subject: [PATCH 03/64] [BEEEP] [PM-10117] Migrate index.html (#10286) Migrate the index.html to use tailwind instead of bootstrap * Extracted some more global styles to override the bootstrap styles for layout frontend. We should revisit this when we remove bootstrap. * Removed Angular specific logo for anon pages. Now uses the same css class as loading page to prevent duplicated assets. --- apps/web/src/404.html | 6 +-- apps/web/src/images/logo-white.svg | 6 +++ apps/web/src/index.html | 20 +++++----- apps/web/src/scss/tailwind.css | 40 ++++++++++++++++++- .../anon-layout/anon-layout.component.html | 2 +- 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/images/logo-white.svg diff --git a/apps/web/src/404.html b/apps/web/src/404.html index 0c11f6680dc..817bfe30985 100644 --- a/apps/web/src/404.html +++ b/apps/web/src/404.html @@ -1,5 +1,5 @@ - + @@ -16,9 +16,9 @@ -
      - Bitwarden + Bitwarden +

      Sorry, this page isn't available.

      diff --git a/apps/web/src/images/logo-white.svg b/apps/web/src/images/logo-white.svg new file mode 100644 index 00000000000..d9ffdd8e339 --- /dev/null +++ b/apps/web/src/images/logo-white.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/apps/web/src/index.html b/apps/web/src/index.html index c3a2c03ed97..ce1a955b88c 100644 --- a/apps/web/src/index.html +++ b/apps/web/src/index.html @@ -5,7 +5,7 @@ - Bitwarden Web Vault + Bitwarden Web vault @@ -15,16 +15,14 @@ -

      -
      - -

      - -

      +
      + Bitwarden +
      +
      diff --git a/apps/web/src/scss/tailwind.css b/apps/web/src/scss/tailwind.css index 9c64be63080..1ac7b154011 100644 --- a/apps/web/src/scss/tailwind.css +++ b/apps/web/src/scss/tailwind.css @@ -5,9 +5,14 @@ @import "../../../../libs/components/src/tw-theme.css"; /* - * Duplicated styling from Angular components. + * Web specific global styling. * - * For use in non Angular pages like the 404 and connectors. + * Be mindful of what is added here. Generally use Tailwind classes directly in Angular components. + * + * Some valid scenarios for adding styles here: + * + * - Duplicated styling for CL components used in non Angular pages like connectors and 404. + * - Shared styles like Logo. */ @layer components { .tw-h1 { @@ -24,4 +29,35 @@ @apply tw-bg-transparent tw-border-text-muted hover:tw-bg-text-muted hover:tw-border-text-muted hover:!tw-text-contrast disabled:tw-bg-transparent disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60 disabled:tw-cursor-not-allowed; @apply tw-text-muted !important; } + + /** + * Loading page + */ + body.layout_frontend { + /* We apply the background color here since body classes are dynamically added and removed */ + @apply tw-bg-background-alt !important; + + /* Spinner requires fixed height and width to appear centered */ + .spinner-container { + @apply tw-fixed tw-inset-2/4 -tw-translate-x-1/2 -tw-translate-y-1/2; + + height: 42px; + width: 42px; + } + } + + /** + * Logo, used both in loading and on "frontend" pages. + */ + img.new-logo-themed { + @apply tw-block; + + width: 128px; + } + .theme_light img.new-logo-themed { + content: url("../images/logo.svg"); + } + .theme_dark img.new-logo-themed { + content: url("../images/logo-white.svg"); + } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index bd3de51c461..082edf40630 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -2,7 +2,7 @@ class="tw-flex tw-min-h-screen tw-w-full tw-mx-auto tw-flex-col tw-gap-7 tw-bg-background-alt tw-px-8 tw-pb-4 tw-text-main" [ngClass]="{ 'tw-pt-0': decreaseTopPadding, 'tw-pt-8': !decreaseTopPadding }" > - +
      From c3e1b0964396674940f1430dbb81ae0c9adab3f8 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:09:38 -0400 Subject: [PATCH 04/64] PM-9906 - Web Preferences Component - Fix issue where having a vault timeout policy would prevent users from updating their preferences. (#10554) --- apps/web/src/app/settings/preferences.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index e1ba6389abf..96de2585532 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -167,7 +167,10 @@ export class PreferencesComponent implements OnInit, OnDestroy { ); return; } - const values = this.form.value; + + // must get raw value b/c the vault timeout action is disabled when a policy is applied + // which removes the timeout action property and value from the normal form.value. + const values = this.form.getRawValue(); const activeAcct = await firstValueFrom(this.accountService.activeAccount$); From da6b3535da32413d3cb361a3bc8527ed5c914789 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:33:14 -0400 Subject: [PATCH 05/64] BRE-277 - Fix CLI NPM publish job (#10729) --- .github/workflows/publish-cli.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 09b6b53e584..8a9495d66f7 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -183,8 +183,11 @@ jobs: keyvault: "bitwarden-ci" secrets: "npm-api-key" - - name: Download artifacts - run: wget https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip + - name: Download and set up artifact + run: | + mkdir -p build + wget https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip + unzip bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip -d build - name: Setup NPM run: | From 2005d99056ee471a4a0a48d81ac9f7a033a71f9c Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 26 Aug 2024 14:11:31 -0400 Subject: [PATCH 06/64] PM-11118 adjust hover cursor styles for TOTP buttons (#10730) --- .../login-credentials/login-credentials-view.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 10b0a1a07c3..de9825d76e5 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -92,7 +92,7 @@ *ngIf="!(isPremium$ | async)" bitBadge variant="success" - class="tw-ml-2" + class="tw-ml-2 tw-cursor-pointer" (click)="getPremium()" slot="end" > @@ -115,6 +115,7 @@ bitSuffix type="button" (sendCopyCode)="setTotpCopyCode($event)" + class="tw-cursor-default" > From c56736e5e205a8f5228f8032cc8b27ba54fd6b35 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:14:14 -0400 Subject: [PATCH 07/64] Bumped client version(s) (#10732) --- apps/cli/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 00c810e0cc4..d6ed912e4d8 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.0", + "version": "2024.8.1", "keywords": [ "bitwarden", "password", diff --git a/package-lock.json b/package-lock.json index eed0eda0958..22ffa0a3950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -196,7 +196,7 @@ }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.8.0", + "version": "2024.8.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", From c18a7cb3e82f910426c1185401a885bfa2c0f4d8 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:23:33 -0400 Subject: [PATCH 08/64] [PM-8748] Ensure Reasonable ConfigService Emission (#10452) * Add Slow Emission Guard * Remove Jest Timer Call * Do Slow Emission Guard For All Environments * Update Comment --- .../services/config/config.service.spec.ts | 66 +++++++++++++++++-- .../services/config/default-config.service.ts | 45 ++++++++----- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index d7e33473d01..d03103f255c 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -3,8 +3,8 @@ * @jest-environment ../../libs/shared/test.environment.ts */ -import { mock } from "jest-mock-extended"; -import { Subject, firstValueFrom, of } from "rxjs"; +import { matches, mock } from "jest-mock-extended"; +import { BehaviorSubject, Subject, bufferCount, firstValueFrom, of } from "rxjs"; import { FakeGlobalState, @@ -35,6 +35,7 @@ import { RETRIEVAL_INTERVAL, GLOBAL_SERVER_CONFIGURATIONS, USER_SERVER_CONFIG, + SLOW_EMISSION_GUARD, } from "./default-config.service"; describe("ConfigService", () => { @@ -65,12 +66,14 @@ describe("ConfigService", () => { describe.each([null, userId])("active user: %s", (activeUserId) => { let sut: DefaultConfigService; + const environmentSubject = new BehaviorSubject(environmentFactory(activeApiUrl)); + beforeAll(async () => { await accountService.switchAccount(activeUserId); }); beforeEach(() => { - environmentService.environment$ = of(environmentFactory(activeApiUrl)); + environmentService.environment$ = environmentSubject; sut = new DefaultConfigService( configApiService, environmentService, @@ -129,7 +132,8 @@ describe("ConfigService", () => { await firstValueFrom(sut.serverConfig$); expect(logService.error).toHaveBeenCalledWith( - `Unable to fetch ServerConfig from ${activeApiUrl}: Unable to fetch`, + `Unable to fetch ServerConfig from ${activeApiUrl}`, + matches((e) => e.message === "Unable to fetch"), ); }); }); @@ -138,6 +142,10 @@ describe("ConfigService", () => { const response = serverConfigResponseFactory(); const newConfig = new ServerConfig(new ServerConfigData(response)); + beforeEach(() => { + configApiService.get.mockResolvedValue(response); + }); + it("should be a new config", async () => { expect(newConfig).not.toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); }); @@ -149,8 +157,6 @@ describe("ConfigService", () => { }); it("returns the updated config", async () => { - configApiService.get.mockResolvedValue(response); - const actual = await firstValueFrom(sut.serverConfig$); // This is the time the response is converted to a config @@ -270,6 +276,51 @@ describe("ConfigService", () => { }); }); }); + + describe("slow configuration", () => { + const environmentSubject = new BehaviorSubject(null); + + let sut: DefaultConfigService = null; + + beforeEach(async () => { + const config = serverConfigFactory("existing-data", tooOld); + environmentService.environment$ = environmentSubject; + + globalState.stateSubject.next({ [apiUrl(0)]: config }); + userState.stateSubject.next([userId, config]); + + configApiService.get.mockImplementation(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(serverConfigResponseFactory("slow-response")); + }, SLOW_EMISSION_GUARD + 20); + }); + }); + + sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + authService, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("emits old configuration when the http call takes a long time", async () => { + environmentSubject.next(environmentFactory(apiUrl(0))); + + const configs = await firstValueFrom(sut.serverConfig$.pipe(bufferCount(2))); + + await jest.runOnlyPendingTimersAsync(); + + expect(configs[0].gitHash).toBe("existing-data"); + expect(configs[1].gitHash).toBe("slow-response"); + }); + }); }); function apiUrl(count: number) { @@ -305,8 +356,9 @@ function serverConfigResponseFactory(hash?: string) { }); } -function environmentFactory(apiUrl: string) { +function environmentFactory(apiUrl: string, isCloud: boolean = true) { return { getApiUrl: () => apiUrl, + isCloud: () => isCloud, } as Environment; } diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 16878a72832..74dd5055d4b 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -24,7 +24,7 @@ import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigService } from "../../abstractions/config/config.service"; import { ServerConfig } from "../../abstractions/config/server-config"; -import { EnvironmentService, Region } from "../../abstractions/environment.service"; +import { Environment, EnvironmentService, Region } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; import { devFlagEnabled, devFlagValue } from "../../misc/flags"; import { ServerConfigData } from "../../models/data/server-config.data"; @@ -34,6 +34,8 @@ export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs") ? (devFlagValue("configRetrievalIntervalMs") as number) : 3_600_000; // 1 hour +export const SLOW_EMISSION_GUARD = 800; + export type ApiUrl = string; export const USER_SERVER_CONFIG = new UserKeyDefinition(CONFIG_DISK, "serverConfig", { @@ -64,29 +66,32 @@ export class DefaultConfigService implements ConfigService { private stateProvider: StateProvider, private authService: AuthService, ) { - const apiUrl$ = this.environmentService.environment$.pipe( - map((environment) => environment.getApiUrl()), - ); const userId$ = this.stateProvider.activeUserId$; const authStatus$ = userId$.pipe( switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))), ); - this.serverConfig$ = combineLatest([userId$, apiUrl$, authStatus$]).pipe( - switchMap(([userId, apiUrl, authStatus]) => { + this.serverConfig$ = combineLatest([ + userId$, + this.environmentService.environment$, + authStatus$, + ]).pipe( + switchMap(([userId, environment, authStatus]) => { if (userId == null || authStatus !== AuthenticationStatus.Unlocked) { - return this.globalConfigFor$(apiUrl).pipe( - map((config) => [config, null, apiUrl] as const), + return this.globalConfigFor$(environment.getApiUrl()).pipe( + map((config) => [config, null, environment] as const), ); } - return this.userConfigFor$(userId).pipe(map((config) => [config, userId, apiUrl] as const)); + return this.userConfigFor$(userId).pipe( + map((config) => [config, userId, environment] as const), + ); }), tap(async (rec) => { - const [existingConfig, userId, apiUrl] = rec; + const [existingConfig, userId, environment] = rec; // Grab new config if older retrieval interval if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { - await this.renewConfig(existingConfig, userId, apiUrl); + await this.renewConfig(existingConfig, userId, environment); } }), switchMap(([existingConfig]) => { @@ -149,10 +154,20 @@ export class DefaultConfigService implements ConfigService { private async renewConfig( existingConfig: ServerConfig, userId: UserId, - apiUrl: string, + environment: Environment, ): Promise { try { + // Feature flags often have a big impact on user experience, lets ensure we return some value + // somewhat quickly even though it may not be accurate, we won't cancel the HTTP request + // though so that hopefully it can have finished and hydrated a more accurate value. + const handle = setTimeout(() => { + this.logService.info( + "Self-host environment did not respond in time, emitting previous config.", + ); + this.failedFetchFallbackSubject.next(existingConfig); + }, SLOW_EMISSION_GUARD); const response = await this.configApiService.get(userId); + clearTimeout(handle); const newConfig = new ServerConfig(new ServerConfigData(response)); // Update the environment region @@ -167,7 +182,7 @@ export class DefaultConfigService implements ConfigService { if (userId == null) { // update global state with new pulled config await this.stateProvider.getGlobal(GLOBAL_SERVER_CONFIGURATIONS).update((configs) => { - return { ...configs, [apiUrl]: newConfig }; + return { ...configs, [environment.getApiUrl()]: newConfig }; }); } else { // update state with new pulled config @@ -175,9 +190,7 @@ export class DefaultConfigService implements ConfigService { } } catch (e) { // mutate error to be handled by catchError - this.logService.error( - `Unable to fetch ServerConfig from ${apiUrl}: ${(e as Error)?.message}`, - ); + this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e); // Emit the existing config this.failedFetchFallbackSubject.next(existingConfig); } From 9e093f88af42703eff33bcde4fc2f18cba4ff2f5 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 26 Aug 2024 12:03:47 -0700 Subject: [PATCH 09/64] [PM-10247] Browser Refresh - Fix save credential banner (#10520) * [PM-10247] Prioritize initialValues when initiating the CipherForm child forms * [PM-10247] Fetch the addEditCipherInfo when opening the cipher form in Browser and override any initialValues if present * [PM-10247] Fix item details section tests * [PM-10247] Add login details section test * [PM-10247] Add autofill options tests * [PM-10247] Undo webpack config change * [PM-10247] Fix failing tests * [PM-10247] Add additional tests for addEditCipherInfo --- .../add-edit/add-edit-v2.component.spec.ts | 85 +++++++++++++++- .../add-edit/add-edit-v2.component.ts | 97 +++++++++++++++++-- .../cipher-form-config.service.ts | 4 +- .../autofill-options.component.spec.ts | 41 ++++++++ .../autofill-options.component.ts | 14 +++ .../components/identity/identity.component.ts | 2 +- .../item-details-section.component.spec.ts | 38 ++++++++ .../item-details-section.component.ts | 11 ++- .../login-details-section.component.spec.ts | 23 +++++ .../login-details-section.component.ts | 12 ++- 10 files changed, 306 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index 95288f6b411..77a50ea35d9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,14 +1,22 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault"; +import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; +import { + CipherFormConfig, + CipherFormConfigService, + CipherFormMode, + OptionalInitialValues, +} from "@bitwarden/vault"; import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; @@ -25,6 +33,8 @@ jest.mock("qrcode-parser", () => {}); describe("AddEditV2Component", () => { let component: AddEditV2Component; let fixture: ComponentFixture; + let addEditCipherInfo$: BehaviorSubject; + let cipherServiceMock: MockProxy; const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => @@ -41,6 +51,10 @@ describe("AddEditV2Component", () => { navigate.mockClear(); back.mockClear(); + addEditCipherInfo$ = new BehaviorSubject(null); + cipherServiceMock = mock(); + cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + await TestBed.configureTestingModule({ imports: [AddEditV2Component], providers: [ @@ -51,6 +65,7 @@ describe("AddEditV2Component", () => { { provide: Router, useValue: { navigate } }, { provide: ActivatedRoute, useValue: { queryParams: queryParams$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: CipherService, useValue: cipherServiceMock }, ], }) .overrideProvider(CipherFormConfigService, { @@ -107,6 +122,72 @@ describe("AddEditV2Component", () => { }); }); + describe("addEditCipherInfo initialization", () => { + it("populates config.initialValues with `addEditCipherInfo` values", fakeAsync(() => { + const addEditCipherInfo = { + cipher: { + name: "test", + folderId: "folder1", + organizationId: "org1", + type: CipherType.Login, + login: { + password: "password", + username: "username", + uris: [{ uri: "https://example.com" }], + }, + }, + collectionIds: ["col1", "col2"], + } as AddEditCipherInfo; + addEditCipherInfo$.next(addEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(component.config.initialValues).toEqual({ + name: "test", + folderId: "folder1", + organizationId: "org1", + password: "password", + username: "username", + loginUri: "https://example.com", + collectionIds: ["col1", "col2"], + } as OptionalInitialValues); + })); + + it("populates config.initialValues.username when `addEditCipherInfo` is an Identity", fakeAsync(() => { + addEditCipherInfo$.next({ + cipher: { type: CipherType.Identity, identity: { username: "identity-username" } }, + } as AddEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(component.config.initialValues.username).toBe("identity-username"); + })); + + it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { + addEditCipherInfo$.next({ + cipher: { name: "AddEditCipherName" }, + } as AddEditCipherInfo); + queryParams$.next({ + name: "QueryParamName", + }); + + tick(); + + expect(component.config.initialValues.name).toBe("AddEditCipherName"); + })); + + it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { + addEditCipherInfo$.next({ cipher: { name: "test" } } as AddEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(cipherServiceMock.setAddEditCipherInfo).toHaveBeenCalledTimes(1); + })); + }); + describe("onCipherSaved", () => { it("disables warning when in popout", async () => { jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValueOnce(true); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index b830ae75048..b1e95afb535 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -8,8 +8,10 @@ import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; import { CipherFormConfig, @@ -18,6 +20,7 @@ import { CipherFormMode, CipherFormModule, DefaultCipherFormConfigService, + OptionalInitialValues, TotpCaptureService, } from "@bitwarden/vault"; @@ -156,6 +159,7 @@ export class AddEditV2Component implements OnInit { private popupCloseWarningService: PopupCloseWarningService, private popupRouterCacheService: PopupRouterCacheService, private router: Router, + private cipherService: CipherService, ) { this.subscribeToParams(); } @@ -255,7 +259,21 @@ export class AddEditV2Component implements OnInit { config.mode = "partial-edit"; } - this.setInitialValuesFromParams(params, config); + config.initialValues = this.setInitialValuesFromParams(params); + + // The browser notification bar and overlay use addEditCipherInfo$ to pass modified cipher details to the form + // Attempt to fetch them here and overwrite the initialValues if present + const cachedCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$); + + if (cachedCipherInfo != null) { + // Cached cipher info has priority over queryParams + config.initialValues = { + ...config.initialValues, + ...mapAddEditCipherInfoToInitialValues(cachedCipherInfo), + }; + // Be sure to clear the "cached" cipher info, so it doesn't get used again + await this.cipherService.setAddEditCipherInfo(null); + } return config; }), @@ -266,26 +284,27 @@ export class AddEditV2Component implements OnInit { }); } - setInitialValuesFromParams(params: QueryParams, config: CipherFormConfig) { - config.initialValues = {}; + setInitialValuesFromParams(params: QueryParams) { + const initialValues = {} as OptionalInitialValues; if (params.folderId) { - config.initialValues.folderId = params.folderId; + initialValues.folderId = params.folderId; } if (params.organizationId) { - config.initialValues.organizationId = params.organizationId; + initialValues.organizationId = params.organizationId; } if (params.collectionId) { - config.initialValues.collectionIds = [params.collectionId]; + initialValues.collectionIds = [params.collectionId]; } if (params.uri) { - config.initialValues.loginUri = params.uri; + initialValues.loginUri = params.uri; } if (params.username) { - config.initialValues.username = params.username; + initialValues.username = params.username; } if (params.name) { - config.initialValues.name = params.name; + initialValues.name = params.name; } + return initialValues; } setHeader(mode: CipherFormMode, type: CipherType) { @@ -303,3 +322,63 @@ export class AddEditV2Component implements OnInit { } } } + +/** + * Helper to map the old AddEditCipherInfo to the new OptionalInitialValues type used by the CipherForm + * @param cipherInfo + */ +const mapAddEditCipherInfoToInitialValues = ( + cipherInfo: AddEditCipherInfo | null, +): OptionalInitialValues => { + const initialValues: OptionalInitialValues = {}; + + if (cipherInfo == null) { + return initialValues; + } + + if (cipherInfo.collectionIds != null) { + initialValues.collectionIds = cipherInfo.collectionIds as CollectionId[]; + } + + if (cipherInfo.cipher == null) { + return initialValues; + } + + const cipher = cipherInfo.cipher; + + if (cipher.folderId != null) { + initialValues.folderId = cipher.folderId; + } + + if (cipher.organizationId != null) { + initialValues.organizationId = cipher.organizationId as OrganizationId; + } + + if (cipher.name != null) { + initialValues.name = cipher.name; + } + + if (cipher.type === CipherType.Login) { + const login = cipher.login; + + if (login != null) { + if (login.uris != null && login.uris.length > 0) { + initialValues.loginUri = login.uris[0].uri; + } + + if (login.username != null) { + initialValues.username = login.username; + } + + if (login.password != null) { + initialValues.password = login.password; + } + } + } + + if (cipher.type === CipherType.Identity && cipher.identity?.username != null) { + initialValues.username = cipher.identity.username; + } + + return initialValues; +}; diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 837c9d11ede..25c0e411243 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -23,6 +23,7 @@ export type OptionalInitialValues = { collectionIds?: CollectionId[]; loginUri?: string; username?: string; + password?: string; name?: string; }; @@ -58,7 +59,8 @@ type BaseCipherFormConfig = { originalCipher?: Cipher; /** - * Optional initial values for the form when creating a new cipher. Useful when creating a cipher in a filtered view. + * Optional initial values for the form when opening the cipher form. + * Useful when creating a new cipher in a filtered view or modifying a cipher with values from another source (e.g. the notification bar in Browser) */ initialValues?: OptionalInitialValues; diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index 601380f98a1..e4dba11525b 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -128,6 +128,47 @@ describe("AutofillOptionsComponent", () => { expect(component.autofillOptionsForm.value.autofillOnPageLoad).toEqual(null); }); + it("initializes 'autoFillOptionsForm' with initialValues when editing an existing cipher", () => { + cipherFormContainer.config.initialValues = { loginUri: "https://new-website.com" }; + const existingLogin = new LoginUriView(); + existingLogin.uri = "https://example.com"; + existingLogin.match = UriMatchStrategy.Exact; + + (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); + cipherFormContainer.originalCipherView.login = { + autofillOnPageLoad: true, + uris: [existingLogin], + } as LoginView; + + fixture.detectChanges(); + + expect(component.autofillOptionsForm.value.uris).toEqual([ + { uri: "https://example.com", matchDetection: UriMatchStrategy.Exact }, + { uri: "https://new-website.com", matchDetection: null }, + ]); + expect(component.autofillOptionsForm.value.autofillOnPageLoad).toEqual(true); + }); + + it("initializes 'autoFillOptionsForm' with initialValues without duplicating an existing URI", () => { + cipherFormContainer.config.initialValues = { loginUri: "https://example.com" }; + const existingLogin = new LoginUriView(); + existingLogin.uri = "https://example.com"; + existingLogin.match = UriMatchStrategy.Exact; + + (cipherFormContainer.originalCipherView as CipherView) = new CipherView(); + cipherFormContainer.originalCipherView.login = { + autofillOnPageLoad: true, + uris: [existingLogin], + } as LoginView; + + fixture.detectChanges(); + + expect(component.autofillOptionsForm.value.uris).toEqual([ + { uri: "https://example.com", matchDetection: UriMatchStrategy.Exact }, + ]); + expect(component.autofillOptionsForm.value.autofillOnPageLoad).toEqual(true); + }); + it("initializes 'autoFillOptionsForm' with an empty URI when creating a new cipher", () => { cipherFormContainer.config.initialValues = null; diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index 80de50c4421..eb5767b534f 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -143,6 +143,20 @@ export class AutofillOptionsComponent implements OnInit { this.autofillOptionsForm.patchValue({ autofillOnPageLoad: existingLogin.autofillOnPageLoad, }); + + if (this.cipherFormContainer.config.initialValues?.loginUri) { + // Avoid adding the same uri again if it already exists + if ( + existingLogin.uris?.findIndex( + (uri) => uri.uri === this.cipherFormContainer.config.initialValues.loginUri, + ) === -1 + ) { + this.addUri({ + uri: this.cipherFormContainer.config.initialValues.loginUri, + matchDetection: null, + }); + } + } } private initNewCipher() { diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index ae712b915b3..d8f938f4ae7 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -127,7 +127,7 @@ export class IdentitySectionComponent implements OnInit { firstName: identity.firstName, middleName: identity.middleName, lastName: identity.lastName, - username: identity.username, + username: this.cipherFormContainer.config.initialValues?.username ?? identity.username, company: identity.company, ssn: identity.ssn, passportNumber: identity.passportNumber, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index a0a1b4e83f7..d7678aa596a 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -5,6 +5,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -104,6 +105,43 @@ describe("ItemDetailsSectionComponent", () => { expect(updatedCipher.favorite).toBe(true); })); + it("should prioritize initialValues when editing an existing cipher ", fakeAsync(async () => { + component.config.allowPersonalOwnership = true; + component.config.organizations = [{ id: "org1" } as Organization]; + component.config.collections = [ + { id: "col1", name: "Collection 1", organizationId: "org1" } as CollectionView, + { id: "col2", name: "Collection 2", organizationId: "org1" } as CollectionView, + ]; + component.originalCipherView = { + name: "cipher1", + organizationId: "org1", + folderId: "folder1", + collectionIds: ["col1"], + favorite: true, + } as CipherView; + + component.config.initialValues = { + name: "new-name", + folderId: "new-folder", + organizationId: "bad-org" as OrganizationId, // Should not be set in edit mode + collectionIds: ["col2" as CollectionId], + }; + + await component.ngOnInit(); + tick(); + + expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); + const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; + + const updatedCipher = patchFn(new CipherView()); + + expect(updatedCipher.name).toBe("new-name"); + expect(updatedCipher.organizationId).toBe("org1"); + expect(updatedCipher.folderId).toBe("new-folder"); + expect(updatedCipher.collectionIds).toEqual(["col2"]); + expect(updatedCipher.favorite).toBe(true); + })); + it("should disable organizationId control if ownership change is not allowed", async () => { component.config.allowPersonalOwnership = false; component.config.organizations = [{ id: "org1" } as Organization]; diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 99ecd84cd29..b0716218b59 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -190,9 +190,9 @@ export class ItemDetailsSectionComponent implements OnInit { private async initFromExistingCipher() { this.itemDetailsForm.setValue({ - name: this.originalCipherView.name, - organizationId: this.originalCipherView.organizationId, - folderId: this.originalCipherView.folderId, + name: this.initialValues?.name ?? this.originalCipherView.name, + organizationId: this.originalCipherView.organizationId, // We do not allow changing ownership of an existing cipher. + folderId: this.initialValues?.folderId ?? this.originalCipherView.folderId, collectionIds: [], favorite: this.originalCipherView.favorite, }); @@ -208,7 +208,10 @@ export class ItemDetailsSectionComponent implements OnInit { } } - await this.updateCollectionOptions(this.originalCipherView.collectionIds as CollectionId[]); + await this.updateCollectionOptions( + this.initialValues?.collectionIds ?? + (this.originalCipherView.collectionIds as CollectionId[]), + ); if (this.partialEdit) { this.itemDetailsForm.disable(); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 06f325d0534..f50f8598b94 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -125,6 +125,29 @@ describe("LoginDetailsSectionComponent", () => { }); }); + it("initializes 'loginDetailsForm' with initialValues that override any original cipher view values", async () => { + (cipherFormContainer.originalCipherView as CipherView) = { + viewPassword: true, + login: { + password: "original-password", + username: "original-username", + totp: "original-totp", + } as LoginView, + } as CipherView; + cipherFormContainer.config.initialValues = { + username: "new-username", + password: "new-password", + }; + + await component.ngOnInit(); + + expect(component.loginDetailsForm.value).toEqual({ + username: "new-username", + password: "new-password", + totp: "original-totp", + }); + }); + describe("viewHiddenFields", () => { beforeEach(() => { (cipherFormContainer.originalCipherView as CipherView) = { diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 020c2d18bd8..0186b0820c3 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -95,6 +95,10 @@ export class LoginDetailsSectionComponent implements OnInit { return true; } + get initialValues() { + return this.cipherFormContainer.config.initialValues; + } + constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, @@ -139,8 +143,8 @@ export class LoginDetailsSectionComponent implements OnInit { private initFromExistingCipher(existingLogin: LoginView) { this.loginDetailsForm.patchValue({ - username: existingLogin.username, - password: existingLogin.password, + username: this.initialValues?.username ?? existingLogin.username, + password: this.initialValues?.password ?? existingLogin.password, totp: existingLogin.totp, }); @@ -154,8 +158,8 @@ export class LoginDetailsSectionComponent implements OnInit { private async initNewCipher() { this.loginDetailsForm.patchValue({ - username: this.cipherFormContainer.config.initialValues?.username || "", - password: "", + username: this.initialValues?.username || "", + password: this.initialValues?.password || "", }); } From fbf9c5abfadd2f57179ce9ee84f3c37aef716192 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 26 Aug 2024 12:05:22 -0700 Subject: [PATCH 10/64] [PM-10993] Browser Refresh - Fix duplicate password generation emissions in Firefox (#10704) * [PM-10993] Avoid saving the generation options to state when toggling the password type * [PM-10993] Fix tests --- .../cipher-form-generator.component.spec.ts | 19 ++++--- .../cipher-form-generator.component.ts | 52 ++++++++++++++----- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts index 5b65c6da24d..85ace2f0ac0 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts @@ -126,15 +126,22 @@ describe("CipherFormGeneratorComponent", () => { expect(fixture.nativeElement.querySelector("bit-toggle-group")).toBeTruthy(); }); - it("should save password options when the password type is updated", async () => { - mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("generated-password"); + it("should update the generated value when the password type is updated", fakeAsync(async () => { + mockLegacyPasswordGenerationService.generatePassword + .mockResolvedValueOnce("first-password") + .mockResolvedValueOnce("second-password"); + + component.ngOnChanges(); + tick(); + + expect(component["generatedValue"]).toBe("first-password"); await component["updatePasswordType"]("passphrase"); + tick(); - expect(mockLegacyPasswordGenerationService.saveOptions).toHaveBeenCalledWith({ - type: "passphrase", - }); - }); + expect(component["generatedValue"]).toBe("second-password"); + expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(2); + })); it("should update the password history when a new password is generated", fakeAsync(() => { mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("new-password"); diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index 2d24194d290..7d93ca20d94 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -1,7 +1,18 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, EventEmitter, Input, OnChanges, Output } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { firstValueFrom, map, startWith, Subject, Subscription, switchMap, tap } from "rxjs"; +import { + combineLatest, + map, + merge, + shareReplay, + startWith, + Subject, + Subscription, + switchMap, + take, + tap, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -61,18 +72,13 @@ export class CipherFormGeneratorComponent implements OnChanges { protected regenerateButtonTitle: string; protected regenerate$ = new Subject(); + protected passwordTypeSubject$ = new Subject(); /** * The currently generated value displayed to the user. * @protected */ protected generatedValue: string = ""; - /** - * The current password generation options. - * @private - */ - private passwordOptions$ = this.legacyPasswordGenerationService.getOptions$(); - /** * The current username generation options. * @private @@ -80,10 +86,30 @@ export class CipherFormGeneratorComponent implements OnChanges { private usernameOptions$ = this.legacyUsernameGenerationService.getOptions$(); /** - * The current password type specified by the password generation options. + * The current password type selected in the UI. Starts with the saved value from the service. * @protected */ - protected passwordType$ = this.passwordOptions$.pipe(map(([options]) => options.type)); + protected passwordType$ = merge( + this.legacyPasswordGenerationService.getOptions$().pipe( + take(1), + map(([options]) => options.type), + ), + this.passwordTypeSubject$, + ).pipe(shareReplay({ bufferSize: 1, refCount: false })); + + /** + * The current password generation options. + * @private + */ + private passwordOptions$ = combineLatest([ + this.legacyPasswordGenerationService.getOptions$(), + this.passwordType$, + ]).pipe( + map(([[options], type]) => { + options.type = type; + return options; + }), + ); /** * Tracks the regenerate$ subscription @@ -121,7 +147,7 @@ export class CipherFormGeneratorComponent implements OnChanges { .pipe( startWith(null), switchMap(() => this.passwordOptions$), - switchMap(([options]) => this.legacyPasswordGenerationService.generatePassword(options)), + switchMap((options) => this.legacyPasswordGenerationService.generatePassword(options)), tap(async (password) => { await this.legacyPasswordGenerationService.addHistory(password); }), @@ -148,12 +174,10 @@ export class CipherFormGeneratorComponent implements OnChanges { } /** - * Switch the password generation type and save the options (generating a new password automatically). + * Switch the password generation type. * @param value The new password generation type. */ protected updatePasswordType = async (value: GeneratorType) => { - const [currentOptions] = await firstValueFrom(this.passwordOptions$); - currentOptions.type = value; - await this.legacyPasswordGenerationService.saveOptions(currentOptions); + this.passwordTypeSubject$.next(value); }; } From 2165452947a217589715fda6d6eb39dd1c6c92c9 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:28:13 -0400 Subject: [PATCH 11/64] Bumped client version(s) (#10733) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 07fb9deb26f..db743b509bf 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.8.0", + "version": "2024.8.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 84d17eb2345..b13a98e7a46 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.0", + "version": "2024.8.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c6de26b8192..c3420e1c6a5 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.0", + "version": "2024.8.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index d6ed912e4d8..9a0b5df3468 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.1", + "version": "2024.8.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0056673a5a0..1ca7de0e135 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.1", + "version": "2024.8.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index ba7b14a054c..e7496714416 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.8.1", + "version": "2024.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.8.1", + "version": "2024.8.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 2987d5ec746..577683e0a86 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.1", + "version": "2024.8.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 35ef8056ee5..8d4b130f72b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.8.0", + "version": "2024.8.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 22ffa0a3950..6eb7fcacb8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,11 +192,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.8.0" + "version": "2024.8.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.8.1", + "version": "2024.8.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.8.1", + "version": "2024.8.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -246,7 +246,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.8.0" + "version": "2024.8.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From e242d7d2d5ef166c16006f0f036d453b34a4b331 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 26 Aug 2024 15:28:37 -0400 Subject: [PATCH 12/64] PM-11126 limit pw toggle and copy to viewPassword true (#10725) --- .../login-credentials/login-credentials-view.component.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index de9825d76e5..48a89eb307d 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -37,6 +37,7 @@ data-testid="login-password" />
      `, }) export class AppComponent implements OnInit, OnDestroy { + private viewCacheService = inject(PopupViewCacheService); + private lastActivity: Date; private activeUserId: UserId; private recordActivitySubject = new Subject(); @@ -64,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy { async ngOnInit() { initPopupClosedListener(); + await this.viewCacheService.init(); // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7c5e49e741b..6830809374a 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,6 +1,7 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Subject, merge, of } from "rxjs"; +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -102,6 +103,7 @@ import { OffscreenDocumentService } from "../../platform/offscreen-document/abst import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; +import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; @@ -305,6 +307,11 @@ const safeProviders: SafeProvider[] = [ provide: AutofillServiceAbstraction, useExisting: AutofillService, }), + safeProvider({ + provide: ViewCacheService, + useExisting: PopupViewCacheService, + deps: [], + }), safeProvider({ provide: AutofillService, deps: [ diff --git a/libs/angular/src/platform/abstractions/view-cache.service.ts b/libs/angular/src/platform/abstractions/view-cache.service.ts new file mode 100644 index 00000000000..0ee09afb812 --- /dev/null +++ b/libs/angular/src/platform/abstractions/view-cache.service.ts @@ -0,0 +1,83 @@ +import { Injector, WritableSignal } from "@angular/core"; +import type { FormGroup } from "@angular/forms"; +import type { Jsonify, JsonValue } from "type-fest"; + +type Deserializer = { + /** + * A function to use to safely convert your type from json to your expected type. + * + * @param jsonValue The JSON object representation of your state. + * @returns The fully typed version of your state. + */ + readonly deserializer?: (jsonValue: Jsonify) => T; +}; + +type BaseCacheOptions = { + /** A unique key for saving the cached value to state */ + key: string; + + /** An optional injector. Required if the method is called outside of an injection context. */ + injector?: Injector; +} & (T extends JsonValue ? Deserializer : Required>); + +export type SignalCacheOptions = BaseCacheOptions & { + /** The initial value for the signal. */ + initialValue: T; +}; + +/** Extract the value type from a FormGroup */ +type FormValue = TFormGroup["value"]; + +export type FormCacheOptions = BaseCacheOptions< + FormValue +> & { + control: TFormGroup; +}; + +/** + * Cache for temporary component state + * + * #### Implementations + * - browser extension popup: used to persist UI between popup open and close + * - all other clients: noop + */ +export abstract class ViewCacheService { + /** + * Create a signal from a previously cached value. Whenever the signal is updated, the new value is saved to the cache. + * + * Non browser extension implementations are noop and return a normal signal. + * + * @returns the created signal + * + * @example + * ```ts + * const mySignal = this.viewCacheService.signal({ + * key: "popup-search-text" + * initialValue: "" + * }); + * ``` + */ + abstract signal(options: SignalCacheOptions): WritableSignal; + + /** + * - Initialize a form from a cached value + * - Save form value to cache when it changes + * - The form is marked dirty if the restored value is not `undefined`. + * + * Non browser extension implementations are noop and return the original form group. + * + * @example + * ```ts + * this.loginDetailsForm = this.viewCacheService.formGroup({ + * key: "vault-login-details-form", + * control: this.formBuilder.group({ + * username: [""], + * email: [""], + * }) + * }); + * ``` + **/ + abstract formGroup( + options: FormCacheOptions, + ): TFormGroup; +} diff --git a/libs/angular/src/platform/services/noop-view-cache.service.ts b/libs/angular/src/platform/services/noop-view-cache.service.ts new file mode 100644 index 00000000000..9953e80b3b0 --- /dev/null +++ b/libs/angular/src/platform/services/noop-view-cache.service.ts @@ -0,0 +1,33 @@ +import { Injectable, signal, WritableSignal } from "@angular/core"; +import type { FormGroup } from "@angular/forms"; + +import { + FormCacheOptions, + SignalCacheOptions, + ViewCacheService, +} from "../abstractions/view-cache.service"; + +/** + * The functionality of the {@link ViewCacheService} is only needed in the browser extension popup, + * yet is provided to all clients to make sharing components easier. + * + * Non-extension clients use this noop implementation. + * */ +@Injectable({ + providedIn: "root", +}) +export class NoopViewCacheService implements ViewCacheService { + /** + * Return a normal signal. + */ + signal(options: SignalCacheOptions): WritableSignal { + return signal(options.initialValue); + } + + /** + * Return the original form group. + **/ + formGroup(options: FormCacheOptions): TFormGroup { + return options.control; + } +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0997fb68635..851e02c8e04 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -268,8 +268,10 @@ import { } from "@bitwarden/vault-export-core"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; +import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; +import { NoopViewCacheService } from "../platform/services/noop-view-cache.service"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction"; import { safeProvider, SafeProvider } from "../platform/utils/safe-provider"; @@ -1290,6 +1292,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultRegistrationFinishService, deps: [CryptoServiceAbstraction, AccountApiServiceAbstraction], }), + safeProvider({ + provide: ViewCacheService, + useExisting: NoopViewCacheService, + deps: [], + }), ]; @NgModule({ From f9b66db1a99551dc2baababc1a3909799cdc2a56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:45:28 -0400 Subject: [PATCH 14/64] [deps] DevOps: Update gh minor (#10577) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/auto-branch-updater.yml | 2 +- .github/workflows/build-browser.yml | 38 +++---- .github/workflows/build-cli.yml | 34 +++--- .github/workflows/build-desktop.yml | 116 ++++++++++----------- .github/workflows/build-web.yml | 16 +-- .github/workflows/chromatic.yml | 6 +- .github/workflows/crowdin-pull.yml | 2 +- .github/workflows/lint.yml | 4 +- .github/workflows/publish-cli.yml | 6 +- .github/workflows/publish-desktop.yml | 4 +- .github/workflows/publish-web.yml | 4 +- .github/workflows/release-browser.yml | 4 +- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-desktop-beta.yml | 86 +++++++-------- .github/workflows/release-desktop.yml | 2 +- .github/workflows/release-web.yml | 2 +- .github/workflows/scan.yml | 10 +- .github/workflows/test.yml | 8 +- .github/workflows/version-bump.yml | 4 +- 19 files changed, 175 insertions(+), 175 deletions(-) diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index 90376c99560..e2f181680d9 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -29,7 +29,7 @@ jobs: run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index a8660bad182..22c1680a41d 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: gen_vars @@ -73,7 +73,7 @@ jobs: working-directory: apps/browser steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Testing locales - extName length run: | @@ -111,10 +111,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -173,63 +173,63 @@ jobs: working-directory: browser-source/apps/browser - name: Upload Opera artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: dist-opera-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-opera.zip if-no-files-found: error - name: Upload Opera MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-opera-mv3.zip if-no-files-found: error - name: Upload Chrome MV3 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip if-no-files-found: error - name: Upload Firefox artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: dist-firefox-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-firefox.zip if-no-files-found: error - name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-firefox-mv3.zip if-no-files-found: error - name: Upload Edge artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: dist-edge-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-edge.zip if-no-files-found: error - name: Upload Edge MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-edge-mv3.zip if-no-files-found: error - name: Upload browser source - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -237,7 +237,7 @@ jobs: - name: Upload coverage artifact if: false - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: coverage-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip @@ -254,10 +254,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -352,7 +352,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip @@ -367,7 +367,7 @@ jobs: - build-safari steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -423,7 +423,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1f1b9936bf6..e5c2b5e519f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: retrieve-package-version @@ -84,7 +84,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Unix Vars run: | @@ -93,7 +93,7 @@ jobs: awk '{print tolower($0)}')" >> $GITHUB_ENV - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -130,14 +130,14 @@ jobs: matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - name: Upload unix zip asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload unix checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -162,7 +162,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Windows builder run: | @@ -171,7 +171,7 @@ jobs: choco install nasm --no-progress - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -269,14 +269,14 @@ jobs: -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt - name: Upload windows zip asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload windows checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -284,7 +284,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -292,7 +292,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/build @@ -309,7 +309,7 @@ jobs: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Print environment run: | @@ -319,7 +319,7 @@ jobs: echo "BW Package Version: $_PACKAGE_VERSION" - name: Get bw linux cli - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap @@ -332,7 +332,7 @@ jobs: ls -alth - name: Build snap - uses: snapcore/action-build@2096990827aa966f773676c8a53793c723b6b40f # v1.2.0 + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 with: path: apps/cli/dist/snap @@ -361,14 +361,14 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload snap checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -405,7 +405,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index c933ea304c6..54560cf15dd 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Verify run: | @@ -67,7 +67,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: retrieve-version @@ -140,10 +140,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -169,7 +169,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: | @@ -193,42 +193,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -249,10 +249,10 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -298,7 +298,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -351,91 +351,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -455,10 +455,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -481,14 +481,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -581,7 +581,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -619,10 +619,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -645,14 +645,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -745,7 +745,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -761,7 +761,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -792,28 +792,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -836,10 +836,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -862,14 +862,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -962,7 +962,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -978,7 +978,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1009,7 +1009,7 @@ jobs: run: npm run pack:mac:mas - name: Upload .pkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1044,10 +1044,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -1065,14 +1065,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1165,7 +1165,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -1181,7 +1181,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1215,7 +1215,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip @@ -1233,7 +1233,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -1294,7 +1294,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 46e65e8924b..6e0f6a3eb89 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get GitHub sha as version id: version @@ -91,10 +91,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -130,7 +130,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip @@ -157,7 +157,7 @@ jobs: _VERSION: ${{ needs.setup.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Check Branch to Publish env: @@ -194,7 +194,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Download ${{ matrix.artifact_name }} artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip path: apps/web @@ -255,7 +255,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -345,7 +345,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 6a02c2d1245..c8dd3e77838 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 @@ -38,13 +38,13 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ steps.retrieve-node-version.outputs.node_version }} - name: Cache NPM id: npm-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: "~/.npm" key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 1f5df5a66c0..527dedb5a86 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -23,7 +23,7 @@ jobs: crowdin_project_id: "308189" steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2d881a4c304..bb495a5a26d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Lint filenames (no capital characters) run: | @@ -48,7 +48,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 8a9495d66f7..6581e260900 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -92,7 +92,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -129,7 +129,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -169,7 +169,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index c03697fc802..d12072c7e6d 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -184,7 +184,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -228,7 +228,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} steps: - name: Checkout Repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Print Environment run: | diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index b7ea8498593..4409da93560 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -27,7 +27,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -67,7 +67,7 @@ jobs: echo "Github Release Option: $_RELEASE_OPTION" - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 ########## ACR ########## - name: Login to Azure - PROD Subscription diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 3feaff8cede..2811b23af9b 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -27,7 +27,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -56,7 +56,7 @@ jobs: needs: setup steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Testing locales - extName length run: | diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index ddcdb4e904f..cd450b2cd79 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -27,7 +27,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 74db61563e1..3f8bc45d51d 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -24,7 +24,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check run: | @@ -125,12 +125,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -159,42 +159,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release-channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml @@ -215,12 +215,12 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -300,91 +300,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release-channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml @@ -404,12 +404,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -427,14 +427,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -538,12 +538,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -561,14 +561,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -708,28 +708,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: ${{ needs.setup.outputs.release-channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml @@ -751,12 +751,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -774,14 +774,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -916,7 +916,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -958,7 +958,7 @@ jobs: aws-electron-bucket-name" - name: Download all artifacts - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: apps/desktop/artifacts @@ -1011,7 +1011,7 @@ jobs: - release steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup git config run: | diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 2fe7cb2b7a4..5b75460ef92 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -27,7 +27,7 @@ jobs: release-channel: ${{ steps.release-channel.outputs.channel }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 596341459cd..982e3867585 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -24,7 +24,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 212795d3a2b..d90e009bf36 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -27,12 +27,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 # v2.0.23 + uses: checkmarx/ast-github-action@1fe318de2993222574e6249750ba9000a4e2a6cd # 2.0.33 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -47,7 +47,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: cx_result.sarif @@ -61,13 +61,13 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1 + uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52928e9a040..5b4cd52ac8e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Node Version id: retrieve-node-version @@ -51,7 +51,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -86,7 +86,7 @@ jobs: fail-on-error: true - name: Upload coverage to codecov.io - uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -121,7 +121,7 @@ jobs: sudo apt-get install -y gnome-keyring dbus-x11 - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Build working-directory: ./apps/desktop/desktop_native diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 4bf502da21c..fc30996e850 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -58,7 +58,7 @@ jobs: fi - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: main @@ -526,7 +526,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: main From 866a624e44ccf33e42e9bffdbf24b5e28004bf03 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:05:43 -0400 Subject: [PATCH 15/64] Fix NPM build artifact (#10734) --- .github/workflows/build-cli.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e5c2b5e519f..fd864cf99a5 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -289,13 +289,16 @@ jobs: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error + + - name: Zip NPM Build Artifact + run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath .\bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip - path: apps/cli/build + path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip if-no-files-found: error snap: From 9459cda304c906188879c24625446fa10e436711 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 26 Aug 2024 17:44:08 -0700 Subject: [PATCH 16/64] Pm-10953/add-user-context-to-sync-replaces (#10627) * Require userId for setting masterKeyEncryptedUserKey * Replace folders for specified user * Require userId for collection replace * Cipher Replace requires userId * Require UserId to update equivalent domains * Require userId for policy replace * sync state updates between fake state for better testing * Revert to public observable tests Since they now sync, we can test single-user updates impacting active user observables * Do not init fake states through sync Do not sync initial null values, that might wipe out already existing data. * Require userId for Send replace * Include userId for organization replace * Require userId for billing sync data * Require user Id for key connector sync data * Allow decode of token by userId * Require userId for synced key connector updates * Add userId to policy setting during organization invite accept * Fix cli * Handle null userId --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> --- apps/cli/src/auth/commands/login.command.ts | 6 +- apps/cli/src/auth/commands/unlock.command.ts | 1 + apps/cli/src/base-program.ts | 20 +- .../convert-to-key-connector.command.ts | 4 +- apps/cli/src/program.ts | 4 +- .../web/src/app/auth/login/login.component.ts | 5 +- .../src/auth/components/login.component.ts | 5 +- .../auth-request-login.strategy.spec.ts | 10 +- .../auth-request-login.strategy.ts | 2 +- .../common/login-strategies/login.strategy.ts | 6 +- .../user-api-login.strategy.spec.ts | 5 +- .../user-api-login.strategy.ts | 2 +- libs/common/spec/fake-state-provider.ts | 159 ++++++--- libs/common/spec/fake-state.ts | 60 +++- .../policy/policy.service.abstraction.ts | 2 +- .../services/policy/policy.service.spec.ts | 12 +- .../services/policy/policy.service.ts | 4 +- .../abstractions/key-connector.service.ts | 14 +- .../src/auth/abstractions/token.service.ts | 8 +- .../services/key-connector.service.spec.ts | 8 +- .../auth/services/key-connector.service.ts | 34 +- .../src/auth/services/token.service.spec.ts | 319 ++++++++---------- .../common/src/auth/services/token.service.ts | 14 +- .../services/domain-settings.service.ts | 7 +- .../billing-account-profile-state.service.ts | 3 + ...ling-account-profile-state.service.spec.ts | 15 +- .../billing-account-profile-state.service.ts | 6 +- .../platform/abstractions/crypto.service.ts | 2 +- .../biometric-state.service.spec.ts | 2 +- .../platform/services/crypto.service.spec.ts | 22 +- .../src/platform/services/crypto.service.ts | 2 +- .../default-state.provider.spec.ts | 2 +- .../src/platform/sync/default-sync.service.ts | 53 +-- .../send-state.provider.abstraction.ts | 10 +- .../send/services/send-state.provider.spec.ts | 4 +- .../send/services/send-state.provider.ts | 13 +- .../send/services/send.service.abstraction.ts | 2 +- .../tools/send/services/send.service.spec.ts | 240 ++++++++----- .../src/tools/send/services/send.service.ts | 26 +- .../src/vault/abstractions/cipher.service.ts | 2 +- .../vault/abstractions/collection.service.ts | 4 +- .../folder/folder.service.abstraction.ts | 2 +- .../src/vault/services/cipher.service.ts | 17 +- .../src/vault/services/collection.service.ts | 6 +- .../services/folder/folder.service.spec.ts | 2 +- .../vault/services/folder/folder.service.ts | 4 +- 46 files changed, 666 insertions(+), 484 deletions(-) diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3b67f955406..9a69bcc3c0a 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -342,7 +342,7 @@ export class LoginCommand { } } - return await this.handleSuccessResponse(); + return await this.handleSuccessResponse(response); } catch (e) { return Response.error(e); } @@ -353,8 +353,8 @@ export class LoginCommand { process.env.BW_SESSION = Utils.fromBufferToB64(key); } - private async handleSuccessResponse(): Promise { - const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(); + private async handleSuccessResponse(response: AuthResult): Promise { + const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector(response.userId); if ( (this.options.sso != null || this.options.apikey != null) && diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index f4486ff9667..bebaa946040 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -73,6 +73,7 @@ export class UnlockCommand { if (await this.keyConnectorService.getConvertAccountRequired()) { const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand( + userId, this.keyConnectorService, this.environmentService, this.syncService, diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index f308bdc2deb..e4340b68e22 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -116,20 +116,30 @@ export abstract class BaseProgram { } } + /** + * Exist if no user is authenticated + * @returns the userId of the active account + */ protected async exitIfNotAuthed() { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); - if (!authed) { - this.processResponse(Response.error("You are not logged in."), true); + const fail = () => this.processResponse(Response.error("You are not logged in."), true); + const userId = (await firstValueFrom(this.serviceContainer.accountService.activeAccount$))?.id; + if (!userId) { + fail(); } + const authed = await this.serviceContainer.stateService.getIsAuthenticated({ userId }); + if (!authed) { + fail(); + } + return userId; } protected async exitIfLocked() { - await this.exitIfNotAuthed(); + const userId = await this.exitIfNotAuthed(); if (await this.serviceContainer.cryptoService.hasUserKey()) { return; } else if (process.env.BW_NOINTERACTION !== "true") { // must unlock - if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { + if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector(userId)) { const response = Response.error( "Your vault is locked. You must unlock your vault using your session key.\n" + "If you do not have your session key, you can get a new one by logging out and logging in again.", diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/commands/convert-to-key-connector.command.ts index 654606dc062..0dbdbb43250 100644 --- a/apps/cli/src/commands/convert-to-key-connector.command.ts +++ b/apps/cli/src/commands/convert-to-key-connector.command.ts @@ -7,6 +7,7 @@ import { EnvironmentService, Region, } from "@bitwarden/common/platform/abstractions/environment.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Response } from "../models/response"; @@ -14,6 +15,7 @@ import { MessageResponse } from "../models/response/message.response"; export class ConvertToKeyConnectorCommand { constructor( + private readonly userId: UserId, private keyConnectorService: KeyConnectorService, private environmentService: EnvironmentService, private syncService: SyncService, @@ -68,7 +70,7 @@ export class ConvertToKeyConnectorCommand { } await this.keyConnectorService.removeConvertAccountRequired(); - await this.keyConnectorService.setUsesKeyConnector(true); + await this.keyConnectorService.setUsesKeyConnector(true, this.userId); // Update environment URL - required for api key login const env = await firstValueFrom(this.environmentService.environment$); diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 51c4b39e988..6ecdb249315 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -206,9 +206,9 @@ export class Program extends BaseProgram { writeLn("", true); }) .action(async (cmd) => { - await this.exitIfNotAuthed(); + const userId = await this.exitIfNotAuthed(); - if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { + if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector(userId)) { const logoutCommand = new LogoutCommand( this.serviceContainer.authService, this.serviceContainer.i18nService, diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index d0a4376556a..145d7666273 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; @@ -129,7 +130,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } } - async goAfterLogIn() { + async goAfterLogIn(userId: UserId) { const masterPassword = this.formGroup.value.masterPassword; // Check master password against policy @@ -150,7 +151,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ) { const policiesData: { [id: string]: PolicyData } = {}; this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p))); - await this.policyService.replace(policiesData); + await this.policyService.replace(policiesData, userId); await this.router.navigate(["update-password"]); return; } diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 057d67b1527..40880b514aa 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -39,7 +40,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, showPassword = false; formPromise: Promise; onSuccessfulLogin: () => Promise; - onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginNavigate: (userId: UserId) => Promise; onSuccessfulLoginTwoFactorNavigate: () => Promise; onSuccessfulLoginForceResetNavigate: () => Promise; showLoginWithDevice: boolean; @@ -185,7 +186,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, if (this.onSuccessfulLoginNavigate != null) { // 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 - this.onSuccessfulLoginNavigate(); + this.onSuccessfulLoginNavigate(response.userId); } else { this.loginEmailService.clearValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 9e9efa12bab..b112e5aa2ab 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -158,7 +158,10 @@ describe("AuthRequestLoginStrategy", () => { decMasterKeyHash, mockUserId, ); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + tokenResponse.key, + mockUserId, + ); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); @@ -183,7 +186,10 @@ describe("AuthRequestLoginStrategy", () => { expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + tokenResponse.key, + mockUserId, + ); expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 9998abb30d3..ae0024d2181 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -99,7 +99,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { const authRequestCredentials = this.cache.value.authRequestCredentials; // User now may or may not have a master password // but set the master key encrypted user key if it exists regardless - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); + await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); if (authRequestCredentials.decryptedUserKey) { await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey, userId); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 2065f898be6..ff6bf07af7e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -222,7 +222,11 @@ export abstract class LoginStrategy { ), ); - await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); + await this.billingAccountProfileStateService.setHasPremium( + accountInformation.premium, + false, + userId, + ); return userId; } diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 6b9cddd99c5..16614497964 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -172,7 +172,10 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + tokenResponse.key, + userId, + ); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 1faac3f6c75..3b112c79a0f 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -64,7 +64,7 @@ export class UserApiLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); + await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); if (response.apiUseKeyConnector) { const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index cd868931f20..666487ecf09 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -32,7 +32,7 @@ export class FakeGlobalStateProvider implements GlobalStateProvider { states: Map> = new Map(); get(keyDefinition: KeyDefinition): GlobalState { this.mock.get(keyDefinition); - const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`; + const cacheKey = this.cacheKey(keyDefinition); let result = this.states.get(cacheKey); if (result == null) { @@ -53,94 +53,143 @@ export class FakeGlobalStateProvider implements GlobalStateProvider { return result as GlobalState; } + private cacheKey(keyDefinition: KeyDefinition) { + return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`; + } + getFake(keyDefinition: KeyDefinition): FakeGlobalState { return this.get(keyDefinition) as FakeGlobalState; } - mockFor(keyDefinitionKey: string, initialValue?: T): FakeGlobalState { - if (!this.establishedMocks.has(keyDefinitionKey)) { - this.establishedMocks.set(keyDefinitionKey, new FakeGlobalState(initialValue)); + mockFor(keyDefinition: KeyDefinition, initialValue?: T): FakeGlobalState { + const cacheKey = this.cacheKey(keyDefinition); + if (!this.states.has(cacheKey)) { + this.states.set(cacheKey, new FakeGlobalState(initialValue)); } - return this.establishedMocks.get(keyDefinitionKey) as FakeGlobalState; + return this.states.get(cacheKey) as FakeGlobalState; } } export class FakeSingleUserStateProvider implements SingleUserStateProvider { mock = mock(); - establishedMocks: Map> = new Map(); states: Map> = new Map(); + + constructor( + readonly updateSyncCallback?: ( + key: UserKeyDefinition, + userId: UserId, + newValue: unknown, + ) => Promise, + ) {} + get(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { this.mock.get(userId, userKeyDefinition); - const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}_${userId}`; + const cacheKey = this.cacheKey(userId, userKeyDefinition); let result = this.states.get(cacheKey); if (result == null) { - let fake: FakeSingleUserState; - // Look for established mock - if (this.establishedMocks.has(userKeyDefinition.key)) { - fake = this.establishedMocks.get(userKeyDefinition.key) as FakeSingleUserState; - } else { - fake = new FakeSingleUserState(userId); - } - fake.keyDefinition = userKeyDefinition; - result = fake; + result = this.buildFakeState(userId, userKeyDefinition); this.states.set(cacheKey, result); } return result as SingleUserState; } - getFake(userId: UserId, userKeyDefinition: UserKeyDefinition): FakeSingleUserState { + getFake( + userId: UserId, + userKeyDefinition: UserKeyDefinition, + { allowInit }: { allowInit: boolean } = { allowInit: true }, + ): FakeSingleUserState { + if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) { + return null; + } + return this.get(userId, userKeyDefinition) as FakeSingleUserState; } - mockFor(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState { - if (!this.establishedMocks.has(keyDefinitionKey)) { - this.establishedMocks.set(keyDefinitionKey, new FakeSingleUserState(userId, initialValue)); + mockFor( + userId: UserId, + userKeyDefinition: UserKeyDefinition, + initialValue?: T, + ): FakeSingleUserState { + const cacheKey = this.cacheKey(userId, userKeyDefinition); + if (!this.states.has(cacheKey)) { + this.states.set(cacheKey, this.buildFakeState(userId, userKeyDefinition, initialValue)); } - return this.establishedMocks.get(keyDefinitionKey) as FakeSingleUserState; + return this.states.get(cacheKey) as FakeSingleUserState; + } + + private buildFakeState( + userId: UserId, + userKeyDefinition: UserKeyDefinition, + initialValue?: T, + ) { + const state = new FakeSingleUserState(userId, initialValue, async (...args) => { + await this.updateSyncCallback?.(userKeyDefinition, ...args); + }); + state.keyDefinition = userKeyDefinition; + return state; + } + + private cacheKey(userId: UserId, userKeyDefinition: UserKeyDefinition) { + return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`; } } export class FakeActiveUserStateProvider implements ActiveUserStateProvider { activeUserId$: Observable; - establishedMocks: Map> = new Map(); - states: Map> = new Map(); - constructor(public accountService: FakeAccountService) { + constructor( + public accountService: FakeAccountService, + readonly updateSyncCallback?: ( + key: UserKeyDefinition, + userId: UserId, + newValue: unknown, + ) => Promise, + ) { this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id)); } get(userKeyDefinition: UserKeyDefinition): ActiveUserState { - const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`; + const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition); let result = this.states.get(cacheKey); if (result == null) { - // Look for established mock - if (this.establishedMocks.has(userKeyDefinition.key)) { - result = this.establishedMocks.get(userKeyDefinition.key); - } else { - result = new FakeActiveUserState(this.accountService); - } - result.keyDefinition = userKeyDefinition; + result = this.buildFakeState(userKeyDefinition); this.states.set(cacheKey, result); } return result as ActiveUserState; } - getFake(userKeyDefinition: UserKeyDefinition): FakeActiveUserState { + getFake( + userKeyDefinition: UserKeyDefinition, + { allowInit }: { allowInit: boolean } = { allowInit: true }, + ): FakeActiveUserState { + if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) { + return null; + } return this.get(userKeyDefinition) as FakeActiveUserState; } - mockFor(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState { - if (!this.establishedMocks.has(keyDefinitionKey)) { - this.establishedMocks.set( - keyDefinitionKey, - new FakeActiveUserState(this.accountService, initialValue), - ); + mockFor(userKeyDefinition: UserKeyDefinition, initialValue?: T): FakeActiveUserState { + const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition); + if (!this.states.has(cacheKey)) { + this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue)); } - return this.establishedMocks.get(keyDefinitionKey) as FakeActiveUserState; + return this.states.get(cacheKey) as FakeActiveUserState; } + + private buildFakeState(userKeyDefinition: UserKeyDefinition, initialValue?: T) { + const state = new FakeActiveUserState(this.accountService, initialValue, async (...args) => { + await this.updateSyncCallback?.(userKeyDefinition, ...args); + }); + state.keyDefinition = userKeyDefinition; + return state; + } +} + +function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition) { + return `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`; } export class FakeStateProvider implements StateProvider { @@ -207,9 +256,35 @@ export class FakeStateProvider implements StateProvider { constructor(public accountService: FakeAccountService) {} + private distributeSingleUserUpdate( + key: UserKeyDefinition, + userId: UserId, + newState: unknown, + ) { + if (this.activeUser.accountService.activeUserId === userId) { + const state = this.activeUser.getFake(key, { allowInit: false }); + state?.nextState(newState, { syncValue: false }); + } + } + + private distributeActiveUserUpdate( + key: UserKeyDefinition, + userId: UserId, + newState: unknown, + ) { + this.singleUser + .getFake(userId, key, { allowInit: false }) + ?.nextState(newState, { syncValue: false }); + } + global: FakeGlobalStateProvider = new FakeGlobalStateProvider(); - singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider(); - activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService); + singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider( + this.distributeSingleUserUpdate.bind(this), + ); + activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider( + this.accountService, + this.distributeActiveUserUpdate.bind(this), + ); derived: FakeDerivedStateProvider = new FakeDerivedStateProvider(); activeUserId$: Observable = this.activeUser.activeUserId$; } diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts index 0f2a09d9c1b..2400e470d42 100644 --- a/libs/common/spec/fake-state.ts +++ b/libs/common/spec/fake-state.ts @@ -1,4 +1,4 @@ -import { Observable, ReplaySubject, concatMap, firstValueFrom, map, timeout } from "rxjs"; +import { Observable, ReplaySubject, concatMap, filter, firstValueFrom, map, timeout } from "rxjs"; import { DerivedState, @@ -41,6 +41,10 @@ export class FakeGlobalState implements GlobalState { this.stateSubject.next(initialValue ?? null); } + nextState(state: T) { + this.stateSubject.next(state); + } + async update( configureState: (state: T, dependency: TCombine) => T, options?: StateUpdateOptions, @@ -89,7 +93,10 @@ export class FakeGlobalState implements GlobalState { export class FakeSingleUserState implements SingleUserState { // eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup - stateSubject = new ReplaySubject>(1); + stateSubject = new ReplaySubject<{ + syncValue: boolean; + combinedState: CombinedState; + }>(1); state$: Observable; combinedState$: Observable>; @@ -97,15 +104,28 @@ export class FakeSingleUserState implements SingleUserState { constructor( readonly userId: UserId, initialValue?: T, + updateSyncCallback?: (userId: UserId, newValue: T) => Promise, ) { - this.stateSubject.next([userId, initialValue ?? null]); + // Inform the state provider of updates to keep active user states in sync + this.stateSubject + .pipe( + filter((next) => next.syncValue), + concatMap(async ({ combinedState }) => { + await updateSyncCallback?.(...combinedState); + }), + ) + .subscribe(); + this.nextState(initialValue ?? null, { syncValue: initialValue != null }); - this.combinedState$ = this.stateSubject.asObservable(); + this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState)); this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state)); } - nextState(state: T) { - this.stateSubject.next([this.userId, state]); + nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) { + this.stateSubject.next({ + syncValue, + combinedState: [this.userId, state], + }); } async update( @@ -122,7 +142,7 @@ export class FakeSingleUserState implements SingleUserState { return current; } const newState = configureState(current, combinedDependencies); - this.stateSubject.next([this.userId, newState]); + this.nextState(newState); this.nextMock(newState); return newState; } @@ -146,7 +166,10 @@ export class FakeActiveUserState implements ActiveUserState { [activeMarker]: true; // eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup - stateSubject = new ReplaySubject>(1); + stateSubject = new ReplaySubject<{ + syncValue: boolean; + combinedState: CombinedState; + }>(1); state$: Observable; combinedState$: Observable>; @@ -154,10 +177,18 @@ export class FakeActiveUserState implements ActiveUserState { constructor( private accountService: FakeAccountService, initialValue?: T, + updateSyncCallback?: (userId: UserId, newValue: T) => Promise, ) { - this.stateSubject.next([accountService.activeUserId, initialValue ?? null]); + // Inform the state provider of updates to keep single user states in sync + this.stateSubject.pipe( + filter((next) => next.syncValue), + concatMap(async ({ combinedState }) => { + await updateSyncCallback?.(...combinedState); + }), + ); + this.nextState(initialValue ?? null, { syncValue: initialValue != null }); - this.combinedState$ = this.stateSubject.asObservable(); + this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState)); this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state)); } @@ -165,8 +196,11 @@ export class FakeActiveUserState implements ActiveUserState { return this.accountService.activeUserId; } - nextState(state: T) { - this.stateSubject.next([this.userId, state]); + nextState(state: T, { syncValue }: { syncValue: boolean } = { syncValue: true }) { + this.stateSubject.next({ + syncValue, + combinedState: [this.userId, state], + }); } async update( @@ -183,7 +217,7 @@ export class FakeActiveUserState implements ActiveUserState { return [this.userId, current]; } const newState = configureState(current, combinedDependencies); - this.stateSubject.next([this.userId, newState]); + this.nextState(newState); this.nextMock([this.userId, newState]); return [this.userId, newState]; } diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index 21669f78ad2..1067c242346 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -77,5 +77,5 @@ export abstract class PolicyService { export abstract class InternalPolicyService extends PolicyService { upsert: (policy: PolicyData) => Promise; - replace: (policies: { [id: string]: PolicyData }) => Promise; + replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index 88264d1c3b7..d9802db9e38 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -20,6 +20,7 @@ import { POLICIES, PolicyService } from "../../../admin-console/services/policy/ import { PolicyId, UserId } from "../../../types/guid"; describe("PolicyService", () => { + const userId = "userId" as UserId; let stateProvider: FakeStateProvider; let organizationService: MockProxy; let activeUserState: FakeActiveUserState>; @@ -27,7 +28,7 @@ describe("PolicyService", () => { let policyService: PolicyService; beforeEach(() => { - const accountService = mockAccountServiceWith("userId" as UserId); + const accountService = mockAccountServiceWith(userId); stateProvider = new FakeStateProvider(accountService); organizationService = mock(); @@ -95,9 +96,12 @@ describe("PolicyService", () => { ]), ); - await policyService.replace({ - "2": policyData("2", "test-organization", PolicyType.DisableSend, true), - }); + await policyService.replace( + { + "2": policyData("2", "test-organization", PolicyType.DisableSend, true), + }, + userId, + ); expect(await firstValueFrom(policyService.policies$)).toEqual([ { diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 2287ef9b4f4..f52d061ad9c 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -219,8 +219,8 @@ export class PolicyService implements InternalPolicyServiceAbstraction { }); } - async replace(policies: { [id: string]: PolicyData }): Promise { - await this.activeUserPolicyState.update(() => policies); + async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise { + await this.stateProvider.setUserState(POLICIES, policies, userId); } /** diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts index b1b6727cd1b..26335ced489 100644 --- a/libs/common/src/auth/abstractions/key-connector.service.ts +++ b/libs/common/src/auth/abstractions/key-connector.service.ts @@ -4,17 +4,17 @@ import { IdentityTokenResponse } from "../models/response/identity-token.respons export abstract class KeyConnectorService { setMasterKeyFromUrl: (url: string, userId: UserId) => Promise; - getManagingOrganization: () => Promise; - getUsesKeyConnector: () => Promise; - migrateUser: () => Promise; - userNeedsMigration: () => Promise; + getManagingOrganization: (userId?: UserId) => Promise; + getUsesKeyConnector: (userId: UserId) => Promise; + migrateUser: (userId?: UserId) => Promise; + userNeedsMigration: (userId: UserId) => Promise; convertNewSsoUserToKeyConnector: ( tokenResponse: IdentityTokenResponse, orgId: string, userId: UserId, ) => Promise; - setUsesKeyConnector: (enabled: boolean) => Promise; - setConvertAccountRequired: (status: boolean) => Promise; + setUsesKeyConnector: (enabled: boolean, userId: UserId) => Promise; + setConvertAccountRequired: (status: boolean, userId?: UserId) => Promise; getConvertAccountRequired: () => Promise; - removeConvertAccountRequired: () => Promise; + removeConvertAccountRequired: (userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index c86b5f1ee39..9239a0db543 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -148,10 +148,11 @@ export abstract class TokenService { /** * Decodes the access token. - * @param token The access token to decode. + * @param tokenOrUserId The access token to decode or the user id to retrieve the access token for, and then decode. + * If null, the currently active user's token is used. * @returns A promise that resolves with the decoded access token. */ - decodeAccessToken: (token?: string) => Promise; + decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise; /** * Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration @@ -212,9 +213,10 @@ export abstract class TokenService { /** * Gets whether or not the user authenticated via an external mechanism. + * @param userId The optional user id to check for external authN status; if not provided, the active user is used. * @returns A promise that resolves with a boolean representing the user's external authN status. */ - getIsExternal: () => Promise; + getIsExternal: (userId: UserId) => Promise; /** Gets the active or passed in user's security stamp */ getSecurityStamp: (userId?: UserId) => Promise; diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 0fc0267a533..5d1aff45f60 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -78,9 +78,9 @@ describe("KeyConnectorService", () => { const newValue = true; - await keyConnectorService.setUsesKeyConnector(newValue); + await keyConnectorService.setUsesKeyConnector(newValue, mockUserId); - expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue); + expect(await keyConnectorService.getUsesKeyConnector(mockUserId)).toBe(newValue); }); }); @@ -185,7 +185,7 @@ describe("KeyConnectorService", () => { const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(false); - const result = await keyConnectorService.userNeedsMigration(); + const result = await keyConnectorService.userNeedsMigration(mockUserId); expect(result).toBe(true); }); @@ -197,7 +197,7 @@ describe("KeyConnectorService", () => { const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(true); - const result = await keyConnectorService.userNeedsMigration(); + const result = await keyConnectorService.userNeedsMigration(mockUserId); expect(result).toBe(false); }); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index 8f204e557ed..ad9b7081cdf 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -69,25 +69,25 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { ); } - async setUsesKeyConnector(usesKeyConnector: boolean) { - await this.usesKeyConnectorState.update(() => usesKeyConnector); + async setUsesKeyConnector(usesKeyConnector: boolean, userId: UserId) { + await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector); } - getUsesKeyConnector(): Promise { - return firstValueFrom(this.usesKeyConnectorState.state$); + getUsesKeyConnector(userId: UserId): Promise { + return firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId)); } - async userNeedsMigration() { - const loggedInUsingSso = await this.tokenService.getIsExternal(); - const requiredByOrganization = (await this.getManagingOrganization()) != null; - const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector()); + async userNeedsMigration(userId: UserId) { + const loggedInUsingSso = await this.tokenService.getIsExternal(userId); + const requiredByOrganization = (await this.getManagingOrganization(userId)) != null; + const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector(userId)); return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector; } - async migrateUser() { - const organization = await this.getManagingOrganization(); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + async migrateUser(userId?: UserId) { + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + const organization = await this.getManagingOrganization(userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); @@ -115,8 +115,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } } - async getManagingOrganization(): Promise { - const orgs = await this.organizationService.getAll(); + async getManagingOrganization(userId?: UserId): Promise { + const orgs = await this.organizationService.getAll(userId); return orgs.find( (o) => o.keyConnectorEnabled && @@ -178,16 +178,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.apiService.postSetKeyConnectorKey(setPasswordRequest); } - async setConvertAccountRequired(status: boolean) { - await this.convertAccountToKeyConnectorState.update(() => status); + async setConvertAccountRequired(status: boolean, userId?: UserId) { + await this.stateProvider.setUserState(CONVERT_ACCOUNT_TO_KEY_CONNECTOR, status, userId); } getConvertAccountRequired(): Promise { return firstValueFrom(this.convertAccountToKeyConnectorState.state$); } - async removeConvertAccountRequired() { - await this.setConvertAccountRequired(null); + async removeConvertAccountRequired(userId?: UserId) { + await this.setConvertAccountRequired(null, userId); } private handleKeyConnectorError(e: any) { diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 4be945de5f8..f8882e1b118 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -126,7 +126,7 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Act const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); @@ -139,11 +139,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Act const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); @@ -156,7 +156,7 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]); + .nextState("encryptedAccessToken"); secureStorageService.get.mockResolvedValue(accessTokenKeyB64); @@ -282,7 +282,7 @@ describe("TokenService", () => { // For testing purposes, let's assume that the access token is already in memory singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); keyGenerationService.createKey.mockResolvedValue(accessTokenKey); @@ -411,9 +411,7 @@ describe("TokenService", () => { it("returns null when no access token is found in memory, disk, or secure storage", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getAccessToken(); @@ -429,18 +427,16 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // set disk to undefined singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Need to have global active id set to the user id if (!userId) { - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); } // Act @@ -459,17 +455,15 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Need to have global active id set to the user id if (!userId) { - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); } // Act @@ -498,20 +492,18 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]); + .nextState("encryptedAccessToken"); secureStorageService.get.mockResolvedValue(accessTokenKeyB64); encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken"); // Need to have global active id set to the user id if (!userId) { - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); } // Act @@ -534,17 +526,15 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Need to have global active id set to the user id if (!userId) { - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); } // No access token key set @@ -564,11 +554,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + .nextState(encryptedAccessToken); // No access token key set @@ -596,11 +586,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + .nextState(encryptedAccessToken); // Mock linux secure storage error const secureStorageError = "Secure storage error"; @@ -655,17 +645,15 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Need to have global active id set to the user id if (!userId) { - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); } // Act @@ -688,8 +676,32 @@ describe("TokenService", () => { }); describe("decodeAccessToken", () => { + it("retrieves the requested user's token when the passed in parameter is a Guid", async () => { + // Arrange + tokenService.getAccessToken = jest.fn().mockResolvedValue(accessTokenJwt); + + // Act + const result = await tokenService.decodeAccessToken(userIdFromAccessToken); + + // Assert + expect(result).toEqual(accessTokenDecoded); + expect(tokenService.getAccessToken).toHaveBeenCalledWith(userIdFromAccessToken); + }); + + it("decodes the given token when a string is passed in that is not a Guid", async () => { + // Arrange + tokenService.getAccessToken = jest.fn(); + + // Act + const result = await tokenService.decodeAccessToken(accessTokenJwt); + + // Assert + expect(result).toEqual(accessTokenDecoded); + expect(tokenService.getAccessToken).not.toHaveBeenCalled(); + }); + it("throws an error when no access token is provided or retrievable from state", async () => { - // Access + // Arrange tokenService.getAccessToken = jest.fn().mockResolvedValue(null); // Act @@ -1194,7 +1206,7 @@ describe("TokenService", () => { // Act // note: don't await here because we want to test the error - const result = tokenService.getIsExternal(); + const result = tokenService.getIsExternal(null); // Assert await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); @@ -1210,7 +1222,7 @@ describe("TokenService", () => { .mockResolvedValue(accessTokenDecodedWithoutExternalAmr); // Act - const result = await tokenService.getIsExternal(); + const result = await tokenService.getIsExternal(null); // Assert expect(result).toEqual(false); @@ -1227,11 +1239,22 @@ describe("TokenService", () => { .mockResolvedValue(accessTokenDecodedWithExternalAmr); // Act - const result = await tokenService.getIsExternal(); + const result = await tokenService.getIsExternal(null); // Assert expect(result).toEqual(true); }); + + it("passes the requested userId to decode", async () => { + // Arrange + tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); + + // Act + await tokenService.getIsExternal(userIdFromAccessToken); + + // Assert + expect(tokenService.decodeAccessToken).toHaveBeenCalledWith(userIdFromAccessToken); + }); }); }); }); @@ -1326,11 +1349,11 @@ describe("TokenService", () => { // For testing purposes, let's assume that the token is already in disk and memory singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // We immediately call to get the refresh token from secure storage after setting it to ensure it was set. secureStorageService.get.mockResolvedValue(refreshToken); @@ -1423,11 +1446,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + .nextState(accessTokenJwt); // Mock linux secure storage error const secureStorageError = "Secure storage error"; @@ -1480,11 +1503,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + .nextState(encryptedAccessToken); secureStorageService.get.mockResolvedValue(accessTokenKeyB64); encryptService.decryptToUtf8.mockRejectedValue(new Error("Decryption error")); @@ -1520,9 +1543,7 @@ describe("TokenService", () => { it("returns null when no refresh token is found in memory, disk, or secure storage", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await (tokenService as any).getRefreshToken(); @@ -1535,16 +1556,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getRefreshToken(); @@ -1557,11 +1576,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); @@ -1575,16 +1594,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getRefreshToken(); @@ -1596,11 +1613,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); @@ -1619,18 +1636,16 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); secureStorageService.get.mockResolvedValue(refreshToken); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getRefreshToken(); @@ -1643,11 +1658,11 @@ describe("TokenService", () => { singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); secureStorageService.get.mockResolvedValue(refreshToken); @@ -1661,11 +1676,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); @@ -1681,16 +1696,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getRefreshToken(); @@ -1719,11 +1732,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); secureStorageService.get.mockResolvedValue(null); @@ -1743,11 +1756,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); const secureStorageSvcMockErrorMsg = "Secure storage retrieval error"; @@ -1792,11 +1805,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, refreshToken]); + .nextState(refreshToken); // Act await (tokenService as any).clearRefreshToken(userIdFromAccessToken); @@ -1833,9 +1846,7 @@ describe("TokenService", () => { it("should throw an error if the vault timeout is missing", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null); @@ -1847,9 +1858,7 @@ describe("TokenService", () => { it("should throw an error if the vault timeout action is missing", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = tokenService.setClientId(clientId, null, VaultTimeoutStringType.Never); @@ -1861,9 +1870,7 @@ describe("TokenService", () => { describe("Memory storage tests", () => { it("sets the client id in memory when there is an active user in global state", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await tokenService.setClientId(clientId, memoryVaultTimeoutAction, memoryVaultTimeout); @@ -1895,9 +1902,7 @@ describe("TokenService", () => { describe("Disk storage tests", () => { it("sets the client id in disk when there is an active user in global state", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await tokenService.setClientId(clientId, diskVaultTimeoutAction, diskVaultTimeout); @@ -1935,9 +1940,7 @@ describe("TokenService", () => { it("returns null when no client id is found in memory or disk", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientId(); @@ -1950,17 +1953,15 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // set disk to undefined singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientId(); @@ -1973,12 +1974,12 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // set disk to undefined singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Act const result = await tokenService.getClientId(userIdFromAccessToken); @@ -1992,16 +1993,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientId(); @@ -2013,11 +2012,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // Act const result = await tokenService.getClientId(userIdFromAccessToken); @@ -2040,11 +2039,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // Act await (tokenService as any).clearClientId(userIdFromAccessToken); @@ -2062,16 +2061,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK) - .stateSubject.next([userIdFromAccessToken, clientId]); + .nextState(clientId); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await (tokenService as any).clearClientId(); @@ -2106,9 +2103,7 @@ describe("TokenService", () => { it("should throw an error if the vault timeout is missing", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null); @@ -2120,9 +2115,7 @@ describe("TokenService", () => { it("should throw an error if the vault timeout action is missing", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = tokenService.setClientSecret( @@ -2138,9 +2131,7 @@ describe("TokenService", () => { describe("Memory storage tests", () => { it("sets the client secret in memory when there is an active user in global state", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await tokenService.setClientSecret( @@ -2176,9 +2167,7 @@ describe("TokenService", () => { describe("Disk storage tests", () => { it("sets the client secret on disk when there is an active user in global state", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await tokenService.setClientSecret( @@ -2222,9 +2211,7 @@ describe("TokenService", () => { it("returns null when no client secret is found in memory or disk", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientSecret(); @@ -2237,17 +2224,15 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // set disk to undefined singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientSecret(); @@ -2260,12 +2245,12 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // set disk to undefined singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); // Act const result = await tokenService.getClientSecret(userIdFromAccessToken); @@ -2279,16 +2264,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act const result = await tokenService.getClientSecret(); @@ -2300,11 +2283,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + .nextState(undefined); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // Act const result = await tokenService.getClientSecret(userIdFromAccessToken); @@ -2327,11 +2310,11 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // Act await (tokenService as any).clearClientSecret(userIdFromAccessToken); @@ -2351,16 +2334,14 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK) - .stateSubject.next([userIdFromAccessToken, clientSecret]); + .nextState(clientSecret); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await (tokenService as any).clearClientSecret(); @@ -2634,7 +2615,7 @@ describe("TokenService", () => { // Arrange const userId = "userId" as UserId; - globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).stateSubject.next(userId); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userId); tokenService.clearAccessToken = jest.fn(); (tokenService as any).clearRefreshToken = jest.fn(); @@ -2693,7 +2674,7 @@ describe("TokenService", () => { globalStateProvider .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) - .stateSubject.next(initialTwoFactorTokenRecord); + .nextState(initialTwoFactorTokenRecord); // Act await tokenService.setTwoFactorToken(email, twoFactorToken); @@ -2716,7 +2697,7 @@ describe("TokenService", () => { globalStateProvider .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) - .stateSubject.next(initialTwoFactorTokenRecord); + .nextState(initialTwoFactorTokenRecord); // Act const result = await tokenService.getTwoFactorToken(email); @@ -2734,7 +2715,7 @@ describe("TokenService", () => { globalStateProvider .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) - .stateSubject.next(initialTwoFactorTokenRecord); + .nextState(initialTwoFactorTokenRecord); // Act const result = await tokenService.getTwoFactorToken(email); @@ -2745,9 +2726,7 @@ describe("TokenService", () => { it("returns null when there is no two factor token record", async () => { // Arrange - globalStateProvider - .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) - .stateSubject.next(null); + globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextState(null); // Act const result = await tokenService.getTwoFactorToken("testUser"); @@ -2768,7 +2747,7 @@ describe("TokenService", () => { globalStateProvider .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) - .stateSubject.next(initialTwoFactorTokenRecord); + .nextState(initialTwoFactorTokenRecord); // Act await tokenService.clearTwoFactorToken(email); @@ -2808,9 +2787,7 @@ describe("TokenService", () => { it("sets the security stamp in memory when there is an active user in global state", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); // Act await tokenService.setSecurityStamp(mockSecurityStamp); @@ -2843,13 +2820,11 @@ describe("TokenService", () => { it("returns the security stamp from memory when no user id is specified (uses global active user)", async () => { // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).nextState(userIdFromAccessToken); singleUserStateProvider .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + .nextState(mockSecurityStamp); // Act const result = await tokenService.getSecurityStamp(); @@ -2862,7 +2837,7 @@ describe("TokenService", () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + .nextState(mockSecurityStamp); // Act const result = await tokenService.getSecurityStamp(userIdFromAccessToken); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index ef7f23cb05a..c2150bc5c52 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -9,6 +9,7 @@ import { KeyGenerationService } from "../../platform/abstractions/key-generation import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; +import { Utils } from "../../platform/misc/utils"; import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; import { StorageOptions } from "../../platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; @@ -875,8 +876,13 @@ export class TokenService implements TokenServiceAbstraction { // jwthelper methods // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - async decodeAccessToken(token?: string): Promise { - token = token ?? (await this.getAccessToken()); + async decodeAccessToken(tokenOrUserId?: string | UserId): Promise { + let token = tokenOrUserId as string; + if (Utils.isGuid(tokenOrUserId)) { + token = await this.getAccessToken(tokenOrUserId as UserId); + } else { + token ??= await this.getAccessToken(); + } if (token == null) { throw new Error("Access token not found."); @@ -1012,10 +1018,10 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss; } - async getIsExternal(): Promise { + async getIsExternal(userId: UserId): Promise { let decoded: DecodedAccessToken; try { - decoded = await this.decodeAccessToken(); + decoded = await this.decodeAccessToken(userId); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index 4b36e8d2bfc..7f2e8c31508 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -15,6 +15,7 @@ import { StateProvider, UserKeyDefinition, } from "../../platform/state"; +import { UserId } from "../../types/guid"; const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", { deserializer: (value: boolean) => value ?? true, @@ -44,7 +45,7 @@ export abstract class DomainSettingsService { neverDomains$: Observable; setNeverDomains: (newValue: NeverDomains) => Promise; equivalentDomains$: Observable; - setEquivalentDomains: (newValue: EquivalentDomains) => Promise; + setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise; defaultUriMatchStrategy$: Observable; setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise; getUrlEquivalentDomains: (url: string) => Observable>; @@ -87,8 +88,8 @@ export class DefaultDomainSettingsService implements DomainSettingsService { await this.neverDomainsState.update(() => newValue); } - async setEquivalentDomains(newValue: EquivalentDomains): Promise { - await this.equivalentDomainsState.update(() => newValue); + async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise { + await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue); } async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise { diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts index e07dec3cf90..080c61e9ffb 100644 --- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts @@ -1,5 +1,7 @@ import { Observable } from "rxjs"; +import { UserId } from "../../../types/guid"; + export type BillingAccountProfile = { hasPremiumPersonally: boolean; hasPremiumFromAnyOrganization: boolean; @@ -32,5 +34,6 @@ export abstract class BillingAccountProfileStateService { abstract setHasPremium( hasPremiumPersonally: boolean, hasPremiumFromAnyOrganization: boolean, + userId: UserId, ): Promise; } diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 7f0f218a239..7e0dee0eedf 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs"; import { FakeAccountService, mockAccountServiceWith, - FakeActiveUserState, FakeStateProvider, FakeSingleUserState, } from "../../../../spec"; @@ -18,7 +17,6 @@ import { describe("BillingAccountProfileStateService", () => { let stateProvider: FakeStateProvider; let sut: DefaultBillingAccountProfileStateService; - let billingAccountProfileState: FakeActiveUserState; let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; @@ -30,10 +28,6 @@ describe("BillingAccountProfileStateService", () => { sut = new DefaultBillingAccountProfileStateService(stateProvider); - billingAccountProfileState = stateProvider.activeUser.getFake( - BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, - ); - userBillingAccountProfileState = stateProvider.singleUser.getFake( userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, @@ -133,12 +127,11 @@ describe("BillingAccountProfileStateService", () => { describe("setHasPremium", () => { it("should update the active users state when called", async () => { - await sut.setHasPremium(true, false); + await sut.setHasPremium(true, false, userId); - expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([ - userId, - { hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false }, - ]); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index cf05df2f22b..7d256da9714 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -6,6 +6,7 @@ import { StateProvider, UserKeyDefinition, } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; import { BillingAccountProfile, BillingAccountProfileStateService, @@ -27,7 +28,7 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP hasPremiumPersonally$: Observable; hasPremiumFromAnySource$: Observable; - constructor(stateProvider: StateProvider) { + constructor(private readonly stateProvider: StateProvider) { this.billingAccountProfileState = stateProvider.getActive( BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); @@ -62,8 +63,9 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP async setHasPremium( hasPremiumPersonally: boolean, hasPremiumFromAnyOrganization: boolean, + userId: UserId, ): Promise { - await this.billingAccountProfileState.update((billingAccountProfile) => { + await this.stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).update((_) => { return { hasPremiumPersonally: hasPremiumPersonally, hasPremiumFromAnyOrganization: hasPremiumFromAnyOrganization, diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index b9499c8fd59..ed18204e9e0 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -143,7 +143,7 @@ export abstract class CryptoService { * @param userKeyMasterKey The master key encrypted user key to set * @param userId The desired user */ - abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; + abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId: string): Promise; /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user diff --git a/libs/common/src/platform/biometrics/biometric-state.service.spec.ts b/libs/common/src/platform/biometrics/biometric-state.service.spec.ts index 097428e16af..56e9cb164f5 100644 --- a/libs/common/src/platform/biometrics/biometric-state.service.spec.ts +++ b/libs/common/src/platform/biometrics/biometric-state.service.spec.ts @@ -119,7 +119,7 @@ describe("BiometricStateService", () => { describe("getRequirePasswordOnStart", () => { it("returns the requirePasswordOnStart state value", async () => { - stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START.key, true); + stateProvider.singleUser.mockFor(userId, REQUIRE_PASSWORD_ON_START, true); expect(await sut.getRequirePasswordOnStart(userId)).toBe(true); }); diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 2386ad13711..dfa244ff2ab 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -365,9 +365,9 @@ describe("cryptoService", () => { const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); const fakeMasterKey = makeMasterKey ? makeSymmetricCryptoKey(64) : null; masterPasswordService.masterKeySubject.next(fakeMasterKey); - userKeyState.stateSubject.next([mockUserId, null]); + userKeyState.nextState(null); const fakeUserKey = makeUserKey ? makeSymmetricCryptoKey(64) : null; - userKeyState.stateSubject.next([mockUserId, fakeUserKey]); + userKeyState.nextState(fakeUserKey); return [fakeUserKey, fakeMasterKey]; } @@ -384,10 +384,7 @@ describe("cryptoService", () => { const fakeEncryptedUserPrivateKey = makeEncString("1"); - userEncryptedPrivateKeyState.stateSubject.next([ - mockUserId, - fakeEncryptedUserPrivateKey.encryptedString, - ]); + userEncryptedPrivateKeyState.nextState(fakeEncryptedUserPrivateKey.encryptedString); // Decryption of the user private key const fakeDecryptedUserPrivateKey = makeStaticByteArray(10, 1); @@ -423,7 +420,7 @@ describe("cryptoService", () => { mockUserId, USER_ENCRYPTED_PRIVATE_KEY, ); - encryptedUserPrivateKeyState.stateSubject.next([mockUserId, null]); + encryptedUserPrivateKeyState.nextState(null); const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); expect(userPrivateKey).toBeFalsy(); @@ -463,7 +460,7 @@ describe("cryptoService", () => { function updateKeys(keys: Partial = {}) { if ("userKey" in keys) { const userKeyState = stateProvider.singleUser.getFake(mockUserId, USER_KEY); - userKeyState.stateSubject.next([mockUserId, keys.userKey]); + userKeyState.nextState(keys.userKey); } if ("encryptedPrivateKey" in keys) { @@ -471,10 +468,7 @@ describe("cryptoService", () => { mockUserId, USER_ENCRYPTED_PRIVATE_KEY, ); - userEncryptedPrivateKey.stateSubject.next([ - mockUserId, - keys.encryptedPrivateKey.encryptedString, - ]); + userEncryptedPrivateKey.nextState(keys.encryptedPrivateKey.encryptedString); } if ("orgKeys" in keys) { @@ -482,7 +476,7 @@ describe("cryptoService", () => { mockUserId, USER_ENCRYPTED_ORGANIZATION_KEYS, ); - orgKeysState.stateSubject.next([mockUserId, keys.orgKeys]); + orgKeysState.nextState(keys.orgKeys); } if ("providerKeys" in keys) { @@ -490,7 +484,7 @@ describe("cryptoService", () => { mockUserId, USER_ENCRYPTED_PROVIDER_KEYS, ); - providerKeysState.stateSubject.next([mockUserId, keys.providerKeys]); + providerKeysState.nextState(keys.providerKeys); } encryptService.decryptToBytes.mockImplementation((encryptedPrivateKey, userKey) => { diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 6d99f920825..61830513136 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -225,7 +225,7 @@ export class CryptoService implements CryptoServiceAbstraction { } } - async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise { + async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId: UserId): Promise { userId ??= await firstValueFrom(this.stateProvider.activeUserId$); await this.masterPasswordService.setMasterKeyEncryptedUserKey( new EncString(userKeyMasterKey), diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index 5b8b2d1bfeb..b3190bd532e 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -143,7 +143,7 @@ describe("DefaultStateProvider", () => { it("should not emit any values until a truthy user id is supplied", async () => { accountService.activeAccountSubject.next(null); const state = singleUserStateProvider.getFake(userId, keyDefinition); - state.stateSubject.next([userId, "value"]); + state.nextState("value"); const emissions = trackEmissions(sut.getUserState$(keyDefinition)); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index e48ab0618c3..322687ce6a6 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -124,12 +124,12 @@ export class DefaultSyncService extends CoreSyncService { const response = await this.apiService.getSync(); await this.syncProfile(response.profile); - await this.syncFolders(response.folders); - await this.syncCollections(response.collections); - await this.syncCiphers(response.ciphers); - await this.syncSends(response.sends); - await this.syncSettings(response.domains); - await this.syncPolicies(response.policies); + await this.syncFolders(response.folders, response.profile.id); + await this.syncCollections(response.collections, response.profile.id); + await this.syncCiphers(response.ciphers, response.profile.id); + await this.syncSends(response.sends, response.profile.id); + await this.syncSettings(response.domains, response.profile.id); + await this.syncPolicies(response.policies, response.profile.id); await this.setLastSync(now, userId); return this.syncCompleted(true); @@ -190,8 +190,9 @@ export class DefaultSyncService extends CoreSyncService { await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, response.premiumFromOrganization, + response.id, ); - await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); + await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector, response.id); await this.setForceSetPasswordReasonIfNeeded(response); @@ -200,17 +201,17 @@ export class DefaultSyncService extends CoreSyncService { providers[p.id] = new ProviderData(p); }); - await this.providerService.save(providers); + await this.providerService.save(providers, response.id); - await this.syncProfileOrganizations(response); + await this.syncProfileOrganizations(response, response.id); - if (await this.keyConnectorService.userNeedsMigration()) { - await this.keyConnectorService.setConvertAccountRequired(true); + if (await this.keyConnectorService.userNeedsMigration(response.id)) { + await this.keyConnectorService.setConvertAccountRequired(true, response.id); this.messageSender.send("convertAccountToKeyConnector"); } else { // 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 - this.keyConnectorService.removeConvertAccountRequired(); + this.keyConnectorService.removeConvertAccountRequired(response.id); } } @@ -261,7 +262,7 @@ export class DefaultSyncService extends CoreSyncService { } } - private async syncProfileOrganizations(response: ProfileResponse) { + private async syncProfileOrganizations(response: ProfileResponse, userId: UserId) { const organizations: { [id: string]: OrganizationData } = {}; response.organizations.forEach((o) => { organizations[o.id] = new OrganizationData(o, { @@ -281,42 +282,42 @@ export class DefaultSyncService extends CoreSyncService { } }); - await this.organizationService.replace(organizations); + await this.organizationService.replace(organizations, userId); } - private async syncFolders(response: FolderResponse[]) { + private async syncFolders(response: FolderResponse[], userId: UserId) { const folders: { [id: string]: FolderData } = {}; response.forEach((f) => { folders[f.id] = new FolderData(f); }); - return await this.folderService.replace(folders); + return await this.folderService.replace(folders, userId); } - private async syncCollections(response: CollectionDetailsResponse[]) { + private async syncCollections(response: CollectionDetailsResponse[], userId: UserId) { const collections: { [id: string]: CollectionData } = {}; response.forEach((c) => { collections[c.id] = new CollectionData(c); }); - return await this.collectionService.replace(collections); + return await this.collectionService.replace(collections, userId); } - private async syncCiphers(response: CipherResponse[]) { + private async syncCiphers(response: CipherResponse[], userId: UserId) { const ciphers: { [id: string]: CipherData } = {}; response.forEach((c) => { ciphers[c.id] = new CipherData(c); }); - return await this.cipherService.replace(ciphers); + return await this.cipherService.replace(ciphers, userId); } - private async syncSends(response: SendResponse[]) { + private async syncSends(response: SendResponse[], userId: UserId) { const sends: { [id: string]: SendData } = {}; response.forEach((s) => { sends[s.id] = new SendData(s); }); - return await this.sendService.replace(sends); + return await this.sendService.replace(sends, userId); } - private async syncSettings(response: DomainsResponse) { + private async syncSettings(response: DomainsResponse, userId: UserId) { let eqDomains: string[][] = []; if (response != null && response.equivalentDomains != null) { eqDomains = eqDomains.concat(response.equivalentDomains); @@ -330,16 +331,16 @@ export class DefaultSyncService extends CoreSyncService { }); } - return this.domainSettingsService.setEquivalentDomains(eqDomains); + return this.domainSettingsService.setEquivalentDomains(eqDomains, userId); } - private async syncPolicies(response: PolicyResponse[]) { + private async syncPolicies(response: PolicyResponse[], userId: UserId) { const policies: { [id: string]: PolicyData } = {}; if (response != null) { response.forEach((p) => { policies[p.id] = new PolicyData(p); }); } - return await this.policyService.replace(policies); + return await this.policyService.replace(policies, userId); } } diff --git a/libs/common/src/tools/send/services/send-state.provider.abstraction.ts b/libs/common/src/tools/send/services/send-state.provider.abstraction.ts index 7a35506b56c..c16d06fb929 100644 --- a/libs/common/src/tools/send/services/send-state.provider.abstraction.ts +++ b/libs/common/src/tools/send/services/send-state.provider.abstraction.ts @@ -1,15 +1,19 @@ import { Observable } from "rxjs"; +import type { Simplify } from "type-fest"; +import { CombinedState } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; import { SendData } from "../models/data/send.data"; import { SendView } from "../models/view/send.view"; +type EncryptedSendState = Simplify>>; export abstract class SendStateProvider { - encryptedState$: Observable>; + encryptedState$: Observable; decryptedState$: Observable; - getEncryptedSends: () => Promise<{ [id: string]: SendData }>; + getEncryptedSends: () => Promise; - setEncryptedSends: (value: { [id: string]: SendData }) => Promise; + setEncryptedSends: (value: { [id: string]: SendData }, userId: UserId) => Promise; getDecryptedSends: () => Promise; diff --git a/libs/common/src/tools/send/services/send-state.provider.spec.ts b/libs/common/src/tools/send/services/send-state.provider.spec.ts index 069e0d80697..abca614d11b 100644 --- a/libs/common/src/tools/send/services/send-state.provider.spec.ts +++ b/libs/common/src/tools/send/services/send-state.provider.spec.ts @@ -27,11 +27,11 @@ describe("Send State Provider", () => { describe("Encrypted Sends", () => { it("should return SendData", async () => { const sendData = { "1": testSendData("1", "Test Send Data") }; - await sendStateProvider.setEncryptedSends(sendData); + await sendStateProvider.setEncryptedSends(sendData, mockUserId); await awaitAsync(); const actual = await sendStateProvider.getEncryptedSends(); - expect(actual).toStrictEqual(sendData); + expect(actual).toStrictEqual([mockUserId, sendData]); }); }); diff --git a/libs/common/src/tools/send/services/send-state.provider.ts b/libs/common/src/tools/send/services/send-state.provider.ts index 1e9397b7a9d..66989a70543 100644 --- a/libs/common/src/tools/send/services/send-state.provider.ts +++ b/libs/common/src/tools/send/services/send-state.provider.ts @@ -1,6 +1,7 @@ import { Observable, firstValueFrom } from "rxjs"; -import { ActiveUserState, StateProvider } from "../../../platform/state"; +import { ActiveUserState, CombinedState, StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; import { SendData } from "../models/data/send.data"; import { SendView } from "../models/view/send.view"; @@ -10,7 +11,7 @@ import { SendStateProvider as SendStateProviderAbstraction } from "./send-state. /** State provider for sends */ export class SendStateProvider implements SendStateProviderAbstraction { /** Observable for the encrypted sends for an active user */ - encryptedState$: Observable>; + encryptedState$: Observable>>; /** Observable with the decrypted sends for an active user */ decryptedState$: Observable; @@ -19,20 +20,20 @@ export class SendStateProvider implements SendStateProviderAbstraction { constructor(protected stateProvider: StateProvider) { this.activeUserEncryptedState = this.stateProvider.getActive(SEND_USER_ENCRYPTED); - this.encryptedState$ = this.activeUserEncryptedState.state$; + this.encryptedState$ = this.activeUserEncryptedState.combinedState$; this.activeUserDecryptedState = this.stateProvider.getActive(SEND_USER_DECRYPTED); this.decryptedState$ = this.activeUserDecryptedState.state$; } /** Gets the encrypted sends from state for an active user */ - async getEncryptedSends(): Promise<{ [id: string]: SendData }> { + async getEncryptedSends(): Promise> { return await firstValueFrom(this.encryptedState$); } /** Sets the encrypted send state for an active user */ - async setEncryptedSends(value: { [id: string]: SendData }): Promise { - await this.activeUserEncryptedState.update(() => value); + async setEncryptedSends(value: { [id: string]: SendData }, userId: UserId): Promise { + await this.stateProvider.getUser(userId, SEND_USER_ENCRYPTED).update(() => value); } /** Gets the decrypted sends from state for the active user */ diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index 6033c9c6cb4..4fa927942c1 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -55,6 +55,6 @@ export abstract class SendService implements UserKeyRotationDataProvider Promise; - replace: (sends: { [id: string]: SendData }) => Promise; + replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise; delete: (id: string | string[]) => Promise; } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 5d04127192f..5743eff481b 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -110,9 +110,12 @@ describe("SendService", () => { const result = await firstValueFrom(singleSendObservable); expect(result).toEqual(testSend("1", "Test Send")); - await sendService.replace({ - "1": testSendData("1", "Test Send Updated"), - }); + await sendService.replace( + { + "1": testSendData("1", "Test Send Updated"), + }, + mockUserId, + ); const result2 = await firstValueFrom(singleSendObservable); expect(result2).toEqual(testSend("1", "Test Send Updated")); @@ -127,10 +130,13 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -138,10 +144,13 @@ describe("SendService", () => { it("reports a change when notes changes on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -152,10 +161,13 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -163,10 +175,13 @@ describe("SendService", () => { it("reports a change when Text changes on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -177,10 +192,13 @@ describe("SendService", () => { changed = false; sendDataObject.text.text = "new text"; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -188,10 +206,13 @@ describe("SendService", () => { it("reports a change when Text is set as null on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -202,10 +223,13 @@ describe("SendService", () => { changed = false; sendDataObject.text = null; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -215,10 +239,13 @@ describe("SendService", () => { type: SendType.File, file: new SendFileData(new SendFileApi({ FileName: "name of file" })), }) as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" })); let changed = false; @@ -229,10 +256,13 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(false); }); @@ -240,10 +270,13 @@ describe("SendService", () => { it("reports a change when key changes on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -254,10 +287,13 @@ describe("SendService", () => { changed = false; sendDataObject.key = "newKey"; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -265,10 +301,13 @@ describe("SendService", () => { it("reports a change when revisionDate changes on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -279,10 +318,13 @@ describe("SendService", () => { changed = false; sendDataObject.revisionDate = "2025-04-05"; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -290,10 +332,13 @@ describe("SendService", () => { it("reports a change when a property is set as null on a new send", async () => { const sendDataObject = createSendData() as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -304,10 +349,13 @@ describe("SendService", () => { changed = false; sendDataObject.name = null; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -317,10 +365,13 @@ describe("SendService", () => { text: new SendTextData(new SendTextApi({ Text: null })), }) as SendData; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); let changed = false; sendService.get$("1").subscribe(() => { @@ -330,23 +381,29 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(false); sendDataObject.text.text = "Asdf"; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); - it("do not reports a change when nothing changes on the observed send", async () => { + it("do not report a change when nothing changes on the observed send", async () => { let changed = false; sendService.get$("1").subscribe(() => { changed = true; @@ -357,10 +414,13 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "1": sendDataObject, - "2": testSendData("3", "Test Send 3"), - }); + await sendService.replace( + { + "1": sendDataObject, + "2": testSendData("3", "Test Send 3"), + }, + mockUserId, + ); expect(changed).toEqual(false); }); @@ -373,9 +433,12 @@ describe("SendService", () => { //it is immediately called when subscribed, we need to reset the value changed = false; - await sendService.replace({ - "2": testSendData("2", "Test Send 2"), - }); + await sendService.replace( + { + "2": testSendData("2", "Test Send 2"), + }, + mockUserId, + ); expect(changed).toEqual(true); }); @@ -426,7 +489,7 @@ describe("SendService", () => { }); it("returns empty array if there are no sends", async () => { - await sendService.replace(null); + await sendService.replace(null, mockUserId); await awaitAsync(); @@ -461,16 +524,11 @@ describe("SendService", () => { }); it("replace", async () => { - await sendService.replace({ "2": testSendData("2", "test 2") }); + await sendService.replace({ "2": testSendData("2", "test 2") }, mockUserId); expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]); }); - it("clear", async () => { - await sendService.clear(); - await awaitAsync(); - expect(await firstValueFrom(sendService.sends$)).toEqual([]); - }); describe("Delete", () => { it("Sends count should decrease after delete", async () => { const sendsBeforeDelete = await firstValueFrom(sendService.sends$); @@ -488,7 +546,7 @@ describe("SendService", () => { }); it("Deleting on an empty sends array should not throw", async () => { - sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null); + stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED).nextState(null); await expect(sendService.delete("2")).resolves.not.toThrow(); }); diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 7048cf5a371..63c07e862ff 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -28,10 +28,10 @@ export class SendService implements InternalSendServiceAbstraction { readonly sendKeyPurpose = "send"; sends$ = this.stateProvider.encryptedState$.pipe( - map((record) => Object.values(record || {}).map((data) => new Send(data))), + map(([, record]) => Object.values(record || {}).map((data) => new Send(data))), ); sendViews$ = this.stateProvider.encryptedState$.pipe( - concatMap((record) => + concatMap(([, record]) => this.decryptSends(Object.values(record || {}).map((data) => new Send(data))), ), ); @@ -167,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction { } async getFromState(id: string): Promise { - const sends = await this.stateProvider.getEncryptedSends(); + const [, sends] = await this.stateProvider.getEncryptedSends(); // eslint-disable-next-line if (sends == null || !sends.hasOwnProperty(id)) { return null; @@ -177,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction { } async getAll(): Promise { - const sends = await this.stateProvider.getEncryptedSends(); + const [, sends] = await this.stateProvider.getEncryptedSends(); const response: Send[] = []; for (const id in sends) { // eslint-disable-next-line @@ -214,7 +214,8 @@ export class SendService implements InternalSendServiceAbstraction { } async upsert(send: SendData | SendData[]): Promise { - let sends = await this.stateProvider.getEncryptedSends(); + const [userId, currentSends] = await this.stateProvider.getEncryptedSends(); + let sends = currentSends; if (sends == null) { sends = {}; } @@ -227,16 +228,11 @@ export class SendService implements InternalSendServiceAbstraction { }); } - await this.replace(sends); - } - - async clear(userId?: string): Promise { - await this.stateProvider.setDecryptedSends(null); - await this.stateProvider.setEncryptedSends(null); + await this.replace(sends, userId); } async delete(id: string | string[]): Promise { - const sends = await this.stateProvider.getEncryptedSends(); + const [userId, sends] = await this.stateProvider.getEncryptedSends(); if (sends == null) { return; } @@ -252,11 +248,11 @@ export class SendService implements InternalSendServiceAbstraction { }); } - await this.replace(sends); + await this.replace(sends, userId); } - async replace(sends: { [id: string]: SendData }): Promise { - await this.stateProvider.setEncryptedSends(sends); + async replace(sends: { [id: string]: SendData }, userId: UserId): Promise { + await this.stateProvider.setEncryptedSends(sends, userId); } async getRotatedData( diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index c95ae27f612..061bd5cedb5 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -133,7 +133,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise>; - replace: (ciphers: { [id: string]: CipherData }) => Promise; + replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise; clear: (userId?: string) => Promise; moveManyWithServer: (ids: string[], folderId: string) => Promise; delete: (id: string | string[]) => Promise; diff --git a/libs/common/src/vault/abstractions/collection.service.ts b/libs/common/src/vault/abstractions/collection.service.ts index 0c206139633..084aa3a8084 100644 --- a/libs/common/src/vault/abstractions/collection.service.ts +++ b/libs/common/src/vault/abstractions/collection.service.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { CollectionId } from "../../types/guid"; +import { CollectionId, UserId } from "../../types/guid"; import { CollectionData } from "../models/data/collection.data"; import { Collection } from "../models/domain/collection"; import { TreeNode } from "../models/domain/tree-node"; @@ -22,7 +22,7 @@ export abstract class CollectionService { getAllNested: (collections?: CollectionView[]) => Promise[]>; getNested: (id: string) => Promise>; upsert: (collection: CollectionData | CollectionData[]) => Promise; - replace: (collections: { [id: string]: CollectionData }) => Promise; + replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise; clear: (userId?: string) => Promise; delete: (id: string | string[]) => Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index 71b8089fa6f..3480a8aca03 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -45,7 +45,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; - replace: (folders: { [id: string]: FolderData }) => Promise; + replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise; clear: (userId?: string) => Promise; delete: (id: string | string[]) => Promise; } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 92676aea97b..cb72d413c8f 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -913,8 +913,8 @@ export class CipherService implements CipherServiceAbstraction { }); } - async replace(ciphers: { [id: string]: CipherData }): Promise { - await this.updateEncryptedCipherState(() => ciphers); + async replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise { + await this.updateEncryptedCipherState(() => ciphers, userId); } /** @@ -924,15 +924,18 @@ export class CipherService implements CipherServiceAbstraction { */ private async updateEncryptedCipherState( update: (current: Record) => Record, + userId: UserId = null, ): Promise> { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); + userId ||= await firstValueFrom(this.stateProvider.activeUserId$); // Store that we should wait for an update to return any ciphers await this.ciphersExpectingUpdate.forceValue(true); await this.clearDecryptedCiphersState(userId); - const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { - const result = update(current ?? {}); - return result; - }); + const updatedCiphers = await this.stateProvider + .getUser(userId, ENCRYPTED_CIPHERS) + .update((current) => { + const result = update(current ?? {}); + return result; + }); return updatedCiphers; } diff --git a/libs/common/src/vault/services/collection.service.ts b/libs/common/src/vault/services/collection.service.ts index 47063aa29dc..e9ad09a4831 100644 --- a/libs/common/src/vault/services/collection.service.ts +++ b/libs/common/src/vault/services/collection.service.ts @@ -184,8 +184,10 @@ export class CollectionService implements CollectionServiceAbstraction { }); } - async replace(collections: Record): Promise { - await this.encryptedCollectionDataState.update(() => collections); + async replace(collections: Record, userId: UserId): Promise { + await this.stateProvider + .getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY) + .update(() => collections); } async clear(userId?: UserId): Promise { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 6f181cf882e..c27ea7646b0 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -120,7 +120,7 @@ describe("Folder Service", () => { }); it("replace", async () => { - await folderService.replace({ "2": folderData("2", "test 2") }); + await folderService.replace({ "2": folderData("2", "test 2") }, mockUserId); expect(await firstValueFrom(folderService.folders$)).toEqual([ { diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 7de7222edca..0c17d7178b2 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -111,12 +111,12 @@ export class FolderService implements InternalFolderServiceAbstraction { }); } - async replace(folders: { [id: string]: FolderData }): Promise { + async replace(folders: { [id: string]: FolderData }, userId: UserId): Promise { if (!folders) { return; } - await this.encryptedFoldersState.update(() => { + await this.stateProvider.getUser(userId, FOLDER_ENCRYPTED_FOLDERS).update(() => { const newFolders: Record = { ...folders }; return newFolders; }); From 3c9b3ea2ccd22c3418458602b0c4b6a3f413c2be Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 27 Aug 2024 08:25:20 +0200 Subject: [PATCH 17/64] [PM-6296] Fix biometrics error prompt when biometrics are temporarily unavailable in browser extension (v2) (#10374) * Create unavailable message for biometrics when in clamshell mode * Move browser biometrics * Inject nativemessagingbackground instead of using constructor * Fix linting * Fix build on browser --- apps/browser/src/_locales/en/messages.json | 6 +++ apps/browser/src/auth/popup/lock.component.ts | 24 ++++++++++-- .../settings/account-security.component.ts | 6 ++- .../browser/src/background/main.background.ts | 18 +++------ .../background/nativeMessaging.background.ts | 8 +++- .../src/background/runtime.background.ts | 7 +++- apps/browser/src/models/biometricErrors.ts | 7 +++- .../background-browser-biometrics.service.ts | 36 ++++++++++++++++++ .../services/browser-biometrics.service.ts | 19 ++++++++++ .../services/browser-crypto.service.ts | 4 +- .../services/foreground-browser-biometrics.ts | 34 +++++++++++++++++ .../background-platform-utils.service.ts | 3 +- .../browser-platform-utils.service.spec.ts | 2 +- .../browser-platform-utils.service.ts | 25 ------------ .../foreground-platform-utils.service.ts | 3 +- .../src/popup/services/services.module.ts | 22 ++++++----- .../services/cli-platform-utils.service.ts | 20 ---------- .../src/app/accounts/settings.component.ts | 11 +++--- .../src/app/services/services.module.ts | 7 ++++ apps/desktop/src/auth/lock.component.spec.ts | 8 ++++ apps/desktop/src/auth/lock.component.ts | 5 ++- apps/desktop/src/main.ts | 4 +- .../main/biometric/biometric.darwin.main.ts | 2 +- .../main/biometric/biometric.noop.main.ts | 2 +- .../main/biometric/biometric.unix.main.ts | 2 +- .../main/biometric/biometric.windows.main.ts | 2 +- .../main/biometric/biometrics.service.spec.ts | 2 +- .../main/biometric/biometrics.service.ts | 19 ++++++---- ...ction.ts => desktop.biometrics.service.ts} | 13 ++++--- .../src/platform/main/biometric/index.ts | 2 +- .../desktop-credential-storage-listener.ts | 12 +++--- .../services/electron-biometrics.service.ts | 38 +++++++++++++++++++ .../electron-platform-utils.service.ts | 24 ------------ .../src/services/native-messaging.service.ts | 25 ++++++++++-- .../app/core/web-platform-utils.service.ts | 14 ------- .../src/auth/components/lock.component.ts | 11 +++++- .../abstractions/platform-utils.service.ts | 20 ---------- .../platform/biometrics/biometric.service.ts | 37 ++++++++++++++++++ 38 files changed, 326 insertions(+), 178 deletions(-) create mode 100644 apps/browser/src/platform/services/background-browser-biometrics.service.ts create mode 100644 apps/browser/src/platform/services/browser-biometrics.service.ts create mode 100644 apps/browser/src/platform/services/foreground-browser-biometrics.ts rename apps/desktop/src/platform/main/biometric/{biometrics.service.abstraction.ts => desktop.biometrics.service.ts} (84%) create mode 100644 apps/desktop/src/platform/services/electron-biometrics.service.ts create mode 100644 libs/common/src/platform/biometrics/biometric.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4aa9fc2b529..cd2cc912930 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2083,6 +2083,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 2819d6a21fe..a6da98fe996 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -24,6 +24,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -67,6 +68,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { pinService: PinServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, @@ -129,22 +132,35 @@ export class LockComponent extends BaseLockComponent implements OnInit { this.isInitialLockScreen && (await this.authService.getAuthStatus()) === AuthenticationStatus.Locked ) { - await this.unlockBiometric(); + await this.unlockBiometric(true); } }, 100); } - override async unlockBiometric(): Promise { + override async unlockBiometric(automaticPrompt: boolean = false): Promise { if (!this.biometricLock) { return; } - this.pendingBiometric = true; this.biometricError = null; let success; try { - success = await super.unlockBiometric(); + const available = await super.isBiometricUnlockAvailable(); + if (!available) { + if (!automaticPrompt) { + await this.dialogService.openSimpleDialog({ + type: "warning", + title: { key: "biometricsNotAvailableTitle" }, + content: { key: "biometricsNotAvailableDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + }); + } + } else { + this.pendingBiometric = true; + success = await super.unlockBiometric(); + } } catch (e) { const error = BiometricErrors[e?.message as BiometricErrorTypes]; diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 076c03801aa..7bced79a0a8 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -33,6 +33,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { VaultTimeout, VaultTimeoutOption, @@ -94,6 +95,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); } @@ -165,7 +167,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); this.form.controls.vaultTimeout.valueChanges @@ -405,7 +407,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const biometricsPromise = async () => { try { - const result = await this.platformUtilsService.authenticateBiometric(); + const result = await this.biometricsService.authenticateBiometric(); // prevent duplicate dialog biometricsResponseReceived = true; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 40e9c8551b9..3944f2d8afc 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -97,6 +97,7 @@ import { BiometricStateService, DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation @@ -228,6 +229,7 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; +import { BackgroundBrowserBiometricsService } from "../platform/services/background-browser-biometrics.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -343,6 +345,7 @@ export default class MainBackground { organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; biometricStateService: BiometricStateService; + biometricsService: BiometricsService; stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; @@ -429,7 +432,6 @@ export default class MainBackground { this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), self, this.offscreenDocumentService, ); @@ -611,6 +613,8 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground); + this.kdfConfigService = new KdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -637,6 +641,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.biometricsService, this.kdfConfigService, ); @@ -1508,17 +1513,6 @@ export default class MainBackground { } } - async biometricUnlock(): Promise { - if (this.nativeMessagingBackground == null) { - return false; - } - - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); - const response = await responsePromise; - return response.response === "unlocked"; - } - private async fullSync(override = false) { const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const lastSync = await this.syncService.getLastSync(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 777af9538b0..613fe777efb 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -285,7 +285,9 @@ export class NativeMessagingBackground { switch (message.command) { case "biometricUnlock": { if ( - ["not enabled", "not supported", "not unlocked", "canceled"].includes(message.response) + ["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes( + message.response, + ) ) { this.rejecter(message.response); return; @@ -352,6 +354,10 @@ export class NativeMessagingBackground { } break; } + case "biometricUnlockAvailable": { + this.resolver(message); + break; + } default: this.logService.error("NativeMessage, got unknown command: " + message.command); break; diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index b667936581f..44e395659b3 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -68,6 +68,7 @@ export default class RuntimeBackground { ) => { const messagesWithResponse = [ "biometricUnlock", + "biometricUnlockAvailable", "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", ]; @@ -179,7 +180,11 @@ export default class RuntimeBackground { } break; case "biometricUnlock": { - const result = await this.main.biometricUnlock(); + const result = await this.main.biometricsService.authenticateBiometric(); + return result; + } + case "biometricUnlockAvailable": { + const result = await this.main.biometricsService.isBiometricUnlockAvailable(); return result; } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { diff --git a/apps/browser/src/models/biometricErrors.ts b/apps/browser/src/models/biometricErrors.ts index 570c776f563..42d9c679d34 100644 --- a/apps/browser/src/models/biometricErrors.ts +++ b/apps/browser/src/models/biometricErrors.ts @@ -11,7 +11,8 @@ export type BiometricErrorTypes = | "not unlocked" | "invalidateEncryption" | "userkey wrong" - | "wrongUserId"; + | "wrongUserId" + | "not available"; export const BiometricErrors: Record = { startDesktop: { @@ -46,4 +47,8 @@ export const BiometricErrors: Record = { title: "biometricsWrongUserTitle", description: "biometricsWrongUserDesc", }, + "not available": { + title: "biometricsNotAvailableTitle", + description: "biometricsNotAvailableDesc", + }, }; diff --git a/apps/browser/src/platform/services/background-browser-biometrics.service.ts b/apps/browser/src/platform/services/background-browser-biometrics.service.ts new file mode 100644 index 00000000000..41ae15972cd --- /dev/null +++ b/apps/browser/src/platform/services/background-browser-biometrics.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@angular/core"; + +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +@Injectable() +export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { + constructor(private nativeMessagingBackground: NativeMessagingBackground) { + super(); + } + + async authenticateBiometric(): Promise { + const responsePromise = this.nativeMessagingBackground.getResponse(); + await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + const response = await responsePromise; + return response.response === "unlocked"; + } + + async isBiometricUnlockAvailable(): Promise { + const responsePromise = this.nativeMessagingBackground.getResponse(); + await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" }); + const response = await responsePromise; + return response.response === "available"; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} diff --git a/apps/browser/src/platform/services/browser-biometrics.service.ts b/apps/browser/src/platform/services/browser-biometrics.service.ts new file mode 100644 index 00000000000..84734fb4927 --- /dev/null +++ b/apps/browser/src/platform/services/browser-biometrics.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +import { BrowserApi } from "../browser/browser-api"; + +@Injectable() +export abstract class BrowserBiometricsService extends BiometricsService { + async supportsBiometric() { + const platformInfo = await BrowserApi.getPlatformInfo(); + if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { + return true; + } + return false; + } + + abstract authenticateBiometric(): Promise; + abstract isBiometricUnlockAvailable(): Promise; +} diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 1242d520213..1d61fb4c8ed 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -11,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; @@ -31,6 +32,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) { super( @@ -68,7 +70,7 @@ export class BrowserCryptoService extends CryptoService { userId?: UserId, ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { - const biometricsResult = await this.platformUtilService.authenticateBiometric(); + const biometricsResult = await this.biometricsService.authenticateBiometric(); if (!biometricsResult) { return null; diff --git a/apps/browser/src/platform/services/foreground-browser-biometrics.ts b/apps/browser/src/platform/services/foreground-browser-biometrics.ts new file mode 100644 index 00000000000..ee55de20108 --- /dev/null +++ b/apps/browser/src/platform/services/foreground-browser-biometrics.ts @@ -0,0 +1,34 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +export class ForegroundBrowserBiometricsService extends BrowserBiometricsService { + async authenticateBiometric(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + } + + async isBiometricUnlockAvailable(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlockAvailable"); + return response.result && response.result === true; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index ec26d6aa29b..da6a8faf3e8 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -8,11 +8,10 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private messagingService: MessagingService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index c86c9158019..762380071b7 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -16,7 +16,7 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardSpy, null, win, offscreenDocumentService); + super(clipboardSpy, win, offscreenDocumentService); } showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 8428a74d430..b47488bdd7d 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -15,7 +15,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic constructor( private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private biometricCallback: () => Promise, private globalContext: Window | ServiceWorkerGlobalScope, private offscreenDocumentService: OffscreenDocumentService, ) {} @@ -276,30 +275,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return await BrowserClipboardService.read(windowContext); } - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { - return true; - } - return false; - } - - async biometricsNeedsSetup(): Promise { - return false; - } - - async biometricsSupportsAutoSetup(): Promise { - return false; - } - - async biometricsSetup(): Promise { - return; - } - - authenticateBiometric() { - return this.biometricCallback(); - } - supportsSecureStorage(): boolean { return false; } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index f775f049e78..5b4b7288d19 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -8,11 +8,10 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6830809374a..0349d1a694a 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -63,6 +63,7 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -109,6 +110,7 @@ import { BrowserCryptoService } from "../../platform/services/browser-crypto.ser import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; +import { ForegroundBrowserBiometricsService } from "../../platform/services/foreground-browser-biometrics"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; @@ -217,6 +219,7 @@ const safeProviders: SafeProvider[] = [ accountService: AccountServiceAbstraction, stateProvider: StateProvider, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { const cryptoService = new BrowserCryptoService( @@ -231,6 +234,7 @@ const safeProviders: SafeProvider[] = [ accountService, stateProvider, biometricStateService, + biometricsService, kdfConfigService, ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); @@ -248,6 +252,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + BiometricsService, KdfConfigService, ], }), @@ -272,22 +277,19 @@ const safeProviders: SafeProvider[] = [ (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, - async () => { - const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; - error: string; - }>("biometricUnlock"); - if (!response.result) { - throw response.error; - } - return response.result; - }, window, offscreenDocumentService, ); }, deps: [ToastService, OffscreenDocumentService], }), + safeProvider({ + provide: BiometricsService, + useFactory: () => { + return new ForegroundBrowserBiometricsService(); + }, + deps: [], + }), safeProvider({ provide: SyncService, useFactory: getBgService("syncService"), diff --git a/apps/cli/src/platform/services/cli-platform-utils.service.ts b/apps/cli/src/platform/services/cli-platform-utils.service.ts index 2a39510fda8..24bceec389c 100644 --- a/apps/cli/src/platform/services/cli-platform-utils.service.ts +++ b/apps/cli/src/platform/services/cli-platform-utils.service.ts @@ -131,26 +131,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { throw new Error("Not implemented."); } - supportsBiometric(): Promise { - return Promise.resolve(false); - } - - authenticateBiometric(): Promise { - return Promise.resolve(false); - } - - biometricsNeedsSetup(): Promise { - return Promise.resolve(false); - } - - biometricsSupportsAutoSetup(): Promise { - return Promise.resolve(false); - } - - biometricsSetup(): Promise { - return Promise.resolve(); - } - supportsSecureStorage(): boolean { return false; } diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index d85447398a4..ebcae98a88a 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -20,6 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -133,6 +134,7 @@ export class SettingsComponent implements OnInit, OnDestroy { private userVerificationService: UserVerificationServiceAbstraction, private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, private pinService: PinServiceAbstraction, private logService: LogService, @@ -287,7 +289,7 @@ export class SettingsComponent implements OnInit, OnDestroy { // Non-form values this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.previousVaultTimeout = this.form.value.vaultTimeout; this.refreshTimeoutSettings$ @@ -466,13 +468,12 @@ export class SettingsComponent implements OnInit, OnDestroy { return; } - const needsSetup = await this.platformUtilsService.biometricsNeedsSetup(); - const supportsBiometricAutoSetup = - await this.platformUtilsService.biometricsSupportsAutoSetup(); + const needsSetup = await this.biometricsService.biometricsNeedsSetup(); + const supportsBiometricAutoSetup = await this.biometricsService.biometricsSupportsAutoSetup(); if (needsSetup) { if (supportsBiometricAutoSetup) { - await this.platformUtilsService.biometricsSetup(); + await this.biometricsService.biometricsSetup(); } else { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "biometricsManualSetupTitle" }, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 85bfbc09f63..be110be138b 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -56,6 +56,7 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/platf import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -72,6 +73,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { ElectronBiometricsService } from "../../platform/services/electron-biometrics.service"; import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; import { @@ -104,6 +106,11 @@ const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); */ const safeProviders: SafeProvider[] = [ safeProvider(InitService), + safeProvider({ + provide: BiometricsService, + useClass: ElectronBiometricsService, + deps: [], + }), safeProvider(NativeMessagingService), safeProvider(SearchBarService), safeProvider(DialogService), diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c46b791b1b6..c5b5b7acf00 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -28,6 +28,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService as AbstractBiometricService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -35,6 +36,8 @@ import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { BiometricsService } from "src/platform/main/biometric"; + import { LockComponent } from "./lock.component"; // ipc mock global @@ -53,6 +56,7 @@ describe("LockComponent", () => { let fixture: ComponentFixture; let stateServiceMock: MockProxy; let biometricStateService: MockProxy; + let biometricsService: MockProxy; let messagingServiceMock: MockProxy; let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; @@ -163,6 +167,10 @@ describe("LockComponent", () => { provide: BiometricStateService, useValue: biometricStateService, }, + { + provide: AbstractBiometricService, + useValue: biometricsService, + }, { provide: AccountService, useValue: accountService, diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index e6c2f7d11d6..55cc79e0a68 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -25,6 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro userVerificationService: UserVerificationService, pinService: PinServiceAbstraction, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, authService: AuthService, kdfConfigService: KdfConfigService, @@ -93,6 +95,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, @@ -139,7 +142,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro // start background listener until destroyed on interval this.timerId = setInterval(async () => { - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.biometricReady = await this.canUseBiometric(); }, 1000); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b77cc722691..86d07440a73 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -32,7 +32,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main"; import { TrayMain } from "./main/tray.main"; import { UpdaterMain } from "./main/updater.main"; import { WindowMain } from "./main/window.main"; -import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main/biometric/index"; +import { BiometricsService, DesktopBiometricsService } from "./platform/main/biometric/index"; import { ClipboardMain } from "./platform/main/clipboard.main"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; @@ -64,7 +64,7 @@ export class Main { menuMain: MenuMain; powerMonitorMain: PowerMonitorMain; trayMain: TrayMain; - biometricsService: BiometricsServiceAbstraction; + biometricsService: DesktopBiometricsService; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; desktopAutofillSettingsService: DesktopAutofillSettingsService; diff --git a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts index 838968f1909..0f26cc78fbf 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts @@ -3,7 +3,7 @@ import { systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { passwords } from "@bitwarden/desktop-napi"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class BiometricDarwinMain implements OsBiometricService { constructor(private i18nservice: I18nService) {} diff --git a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts index 3dfba76432b..57a86942e8c 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts @@ -1,4 +1,4 @@ -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class NoopBiometricsService implements OsBiometricService { constructor() {} diff --git a/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts index e2428d9d129..c748276a6ef 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts @@ -7,7 +7,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi"; import { WindowMain } from "../../../main/window.main"; import { isFlatpak, isLinux, isSnapStore } from "../../../utils"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; const polkitPolicy = ` { return { diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.ts index 686007c7b5f..e432939c877 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.ts @@ -6,9 +6,9 @@ import { UserId } from "@bitwarden/common/types/guid"; import { WindowMain } from "../../../main/window.main"; -import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction"; +import { DesktopBiometricsService, OsBiometricService } from "./desktop.biometrics.service"; -export class BiometricsService implements BiometricsServiceAbstraction { +export class BiometricsService extends DesktopBiometricsService { private platformSpecificService: OsBiometricService; private clientKeyHalves = new Map(); @@ -20,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { private platform: NodeJS.Platform, private biometricStateService: BiometricStateService, ) { + super(); this.loadPlatformSpecificService(this.platform); } @@ -63,19 +64,19 @@ export class BiometricsService implements BiometricsServiceAbstraction { this.platformSpecificService = new NoopBiometricsService(); } - async osSupportsBiometric() { + async supportsBiometric() { return await this.platformSpecificService.osSupportsBiometric(); } - async osBiometricsNeedsSetup() { + async biometricsNeedsSetup() { return await this.platformSpecificService.osBiometricsNeedsSetup(); } - async osBiometricsCanAutoSetup() { + async biometricsSupportsAutoSetup() { return await this.platformSpecificService.osBiometricsCanAutoSetup(); } - async osBiometricsSetup() { + async biometricsSetup() { await this.platformSpecificService.osBiometricsSetup(); } @@ -91,7 +92,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); const clientKeyHalfB64 = this.getClientKeyHalf(service, key); const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; - return clientKeyHalfSatisfied && (await this.osSupportsBiometric()); + return clientKeyHalfSatisfied && (await this.supportsBiometric()); } async authenticateBiometric(): Promise { @@ -110,6 +111,10 @@ export class BiometricsService implements BiometricsServiceAbstraction { return result; } + async isBiometricUnlockAvailable(): Promise { + return await this.platformSpecificService.osSupportsBiometric(); + } + async getBiometricKey(service: string, storageKey: string): Promise { return await this.interruptProcessReload(async () => { await this.enforceClientKeyHalf(service, storageKey); diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts similarity index 84% rename from apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts rename to apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts index 22766b7a312..c8e3a59612a 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts @@ -1,8 +1,10 @@ -export abstract class BiometricsServiceAbstraction { - abstract osSupportsBiometric(): Promise; - abstract osBiometricsNeedsSetup: () => Promise; - abstract osBiometricsCanAutoSetup: () => Promise; - abstract osBiometricsSetup: () => Promise; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service extends the base biometrics service to provide desktop specific functions, + * specifically for the main process. + */ +export abstract class DesktopBiometricsService extends BiometricsService { abstract canAuthBiometric({ service, key, @@ -12,7 +14,6 @@ export abstract class BiometricsServiceAbstraction { key: string; userId: string; }): Promise; - abstract authenticateBiometric(): Promise; abstract getBiometricKey(service: string, key: string): Promise; abstract setBiometricKey(service: string, key: string, value: string): Promise; abstract setEncryptionKeyHalf({ diff --git a/apps/desktop/src/platform/main/biometric/index.ts b/apps/desktop/src/platform/main/biometric/index.ts index f5a594d966f..ad7725d718a 100644 --- a/apps/desktop/src/platform/main/biometric/index.ts +++ b/apps/desktop/src/platform/main/biometric/index.ts @@ -1,2 +1,2 @@ -export * from "./biometrics.service.abstraction"; +export * from "./desktop.biometrics.service"; export * from "./biometrics.service"; diff --git a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts index 2f423e75fcf..5f278b23a0a 100644 --- a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts +++ b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts @@ -6,14 +6,14 @@ import { passwords } from "@bitwarden/desktop-napi"; import { BiometricMessage, BiometricAction } from "../../types/biometric-message"; -import { BiometricsServiceAbstraction } from "./biometric/index"; +import { DesktopBiometricsService } from "./biometric/index"; const AuthRequiredSuffix = "_biometric"; export class DesktopCredentialStorageListener { constructor( private serviceName: string, - private biometricService: BiometricsServiceAbstraction, + private biometricService: DesktopBiometricsService, private logService: ConsoleLogService, ) {} @@ -77,16 +77,16 @@ export class DesktopCredentialStorageListener { }); break; case BiometricAction.OsSupported: - val = await this.biometricService.osSupportsBiometric(); + val = await this.biometricService.supportsBiometric(); break; case BiometricAction.NeedsSetup: - val = await this.biometricService.osBiometricsNeedsSetup(); + val = await this.biometricService.biometricsNeedsSetup(); break; case BiometricAction.Setup: - await this.biometricService.osBiometricsSetup(); + await this.biometricService.biometricsSetup(); break; case BiometricAction.CanAutoSetup: - val = await this.biometricService.osBiometricsCanAutoSetup(); + val = await this.biometricService.biometricsSupportsAutoSetup(); break; default: } diff --git a/apps/desktop/src/platform/services/electron-biometrics.service.ts b/apps/desktop/src/platform/services/electron-biometrics.service.ts new file mode 100644 index 00000000000..8e1b1f8a5d6 --- /dev/null +++ b/apps/desktop/src/platform/services/electron-biometrics.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service implement the base biometrics service to provide desktop specific functions, + * specifically for the renderer process by passing messages to the main process. + */ +@Injectable() +export class ElectronBiometricsService extends BiometricsService { + async supportsBiometric(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + async isBiometricUnlockAvailable(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + /** This method is used to authenticate the user presence _only_. + * It should not be used in the process to retrieve + * biometric keys, which has a separate authentication mechanism. + * For biometric keys, invoke "keytar" with a biometric key suffix */ + async authenticateBiometric(): Promise { + return await ipc.platform.biometric.authenticate(); + } + + async biometricsNeedsSetup(): Promise { + return await ipc.platform.biometric.biometricsNeedsSetup(); + } + + async biometricsSupportsAutoSetup(): Promise { + return await ipc.platform.biometric.biometricsCanAutoSetup(); + } + + async biometricsSetup(): Promise { + return await ipc.platform.biometric.biometricsSetup(); + } +} diff --git a/apps/desktop/src/platform/services/electron-platform-utils.service.ts b/apps/desktop/src/platform/services/electron-platform-utils.service.ts index 30753f09f12..2808b74f097 100644 --- a/apps/desktop/src/platform/services/electron-platform-utils.service.ts +++ b/apps/desktop/src/platform/services/electron-platform-utils.service.ts @@ -131,30 +131,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return ipc.platform.clipboard.read(); } - async supportsBiometric(): Promise { - return await ipc.platform.biometric.osSupported(); - } - - async biometricsNeedsSetup(): Promise { - return await ipc.platform.biometric.biometricsNeedsSetup(); - } - - async biometricsSupportsAutoSetup(): Promise { - return await ipc.platform.biometric.biometricsCanAutoSetup(); - } - - async biometricsSetup(): Promise { - return await ipc.platform.biometric.biometricsSetup(); - } - - /** This method is used to authenticate the user presence _only_. - * It should not be used in the process to retrieve - * biometric keys, which has a separate authentication mechanism. - * For biometric keys, invoke "keytar" with a biometric key suffix */ - async authenticateBiometric(): Promise { - return await ipc.platform.biometric.authenticate(); - } - supportsSecureStorage(): boolean { return ELECTRON_SUPPORTS_SECURE_STORAGE; } diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 4137c4e680f..f106d137b76 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -8,8 +8,8 @@ import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/c import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -33,11 +33,11 @@ export class NativeMessagingService { constructor( private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, - private platformUtilService: PlatformUtilsService, private logService: LogService, private messagingService: MessagingService, private desktopSettingService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, private accountService: AccountService, @@ -133,7 +133,14 @@ export class NativeMessagingService { switch (message.command) { case "biometricUnlock": { - if (!(await this.platformUtilService.supportsBiometric())) { + const isTemporarilyDisabled = + (await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) && + !(await this.biometricsService.supportsBiometric()); + if (isTemporarilyDisabled) { + return this.send({ command: "biometricUnlock", response: "not available" }, appId); + } + + if (!(await this.biometricsService.supportsBiometric())) { return this.send({ command: "biometricUnlock", response: "not supported" }, appId); } @@ -198,8 +205,18 @@ export class NativeMessagingService { break; } + case "biometricUnlockAvailable": { + const isAvailable = await this.biometricsService.supportsBiometric(); + return this.send( + { + command: "biometricUnlockAvailable", + response: isAvailable ? "available" : "not available", + }, + appId, + ); + } default: - this.logService.error("NativeMessage, got unknown command."); + this.logService.error("NativeMessage, got unknown command: " + message.command); break; } } diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts index dceaaf51d15..dbd0ef593d6 100644 --- a/apps/web/src/app/core/web-platform-utils.service.ts +++ b/apps/web/src/app/core/web-platform-utils.service.ts @@ -186,20 +186,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { throw new Error("Cannot read from clipboard on web."); } - supportsBiometric() { - return Promise.resolve(false); - } - - authenticateBiometric() { - return Promise.resolve(false); - } - - biometricsNeedsSetup: () => Promise; - biometricsSupportsAutoSetup(): Promise { - throw new Error("Method not implemented."); - } - biometricsSetup: () => Promise; - supportsSecureStorage() { return false; } diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 50eded416b2..400dcfd8891 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -30,6 +30,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; @@ -84,6 +85,7 @@ export class LockComponent implements OnInit, OnDestroy { protected userVerificationService: UserVerificationService, protected pinService: PinServiceAbstraction, protected biometricStateService: BiometricStateService, + protected biometricsService: BiometricsService, protected accountService: AccountService, protected authService: AuthService, protected kdfConfigService: KdfConfigService, @@ -146,6 +148,13 @@ export class LockComponent implements OnInit, OnDestroy { return !!userKey; } + async isBiometricUnlockAvailable(): Promise { + if (!(await this.biometricsService.supportsBiometric())) { + return false; + } + return this.biometricsService.isBiometricUnlockAvailable(); + } + togglePassword() { this.showPassword = !this.showPassword; const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword"); @@ -327,7 +336,7 @@ export class LockComponent implements OnInit, OnDestroy { this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword(); - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.biometricLock = (await this.vaultTimeoutSettingsService.isBiometricLockSet()) && ((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) || diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index 9725435afa4..fa0fc8f2501 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -43,26 +43,6 @@ export abstract class PlatformUtilsService { abstract isSelfHost(): boolean; abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; abstract readFromClipboard(): Promise; - abstract supportsBiometric(): Promise; - /** - * Determine whether biometrics support requires going through a setup process. - * This is currently only needed on Linux. - * - * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) - */ - abstract biometricsNeedsSetup: () => Promise; - /** - * Determine whether biometrics support can be automatically setup, or requires user interaction. - * Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak. - * - * @returns true if biometrics support can be automatically setup, false if it requires user interaction. - */ - abstract biometricsSupportsAutoSetup(): Promise; - /** - * Start automatic biometric setup, which places the required configuration files / changes the required settings. - */ - abstract biometricsSetup: () => Promise; - abstract authenticateBiometric(): Promise; abstract supportsSecureStorage(): boolean; abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/biometrics/biometric.service.ts b/libs/common/src/platform/biometrics/biometric.service.ts new file mode 100644 index 00000000000..ae65dcd1765 --- /dev/null +++ b/libs/common/src/platform/biometrics/biometric.service.ts @@ -0,0 +1,37 @@ +/** + * The biometrics service is used to provide access to the status of and access to biometric functionality on the platforms. + */ +export abstract class BiometricsService { + /** + * Check if the platform supports biometric authentication. + */ + abstract supportsBiometric(): Promise; + + /** + * Checks whether biometric unlock is currently available at the moment (e.g. if the laptop lid is shut, biometric unlock may not be available) + */ + abstract isBiometricUnlockAvailable(): Promise; + + /** + * Performs biometric authentication + */ + abstract authenticateBiometric(): Promise; + /** + * Determine whether biometrics support requires going through a setup process. + * This is currently only needed on Linux. + * + * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) + */ + abstract biometricsNeedsSetup(): Promise; + /** + * Determine whether biometrics support can be automatically setup, or requires user interaction. + * Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak. + * + * @returns true if biometrics support can be automatically setup, false if it requires user interaction. + */ + abstract biometricsSupportsAutoSetup(): Promise; + /** + * Start automatic biometric setup, which places the required configuration files / changes the required settings. + */ + abstract biometricsSetup(): Promise; +} From cd1bbef5e5412286c61b2087fb769de6330073cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:23:24 +0000 Subject: [PATCH 18/64] [PM-11167][deps] Tools: Update electron to v32 (#10630) * [deps] Tools: Update electron to v32 * Bump version in electron-builder.json --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith --- apps/desktop/electron-builder.json | 2 +- package-lock.json | 9 +++++---- package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index aded8cc3914..b6572587faa 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -25,7 +25,7 @@ "**/node_modules/argon2/package.json", "**/node_modules/argon2/build/Release/argon2.node" ], - "electronVersion": "31.4.0", + "electronVersion": "32.0.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/package-lock.json b/package-lock.json index 6eb7fcacb8d..775981c3f9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,7 +129,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "31.4.0", + "electron": "32.0.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -15037,11 +15037,12 @@ } }, "node_modules/electron": { - "version": "31.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-31.4.0.tgz", - "integrity": "sha512-YTwKoAA+nrJMlI1TTHnIXLYWoQLKnhbkz0qxZcI7Hadcy0UaFMFs9xzwvH2MnrRpVJy7RKo49kVGuvSdRl8zMA==", + "version": "32.0.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-32.0.1.tgz", + "integrity": "sha512-5Hd5Jaf9niYVR2hZxoRd3gOrcxPOxQV1XPV5WaoSfT9jLJHFadhlKtuSDIk3U6rQZke+aC7GqPPAv55nWFCMsA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", diff --git a/package.json b/package.json index 99fd8a40499..3e06be06743 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "31.4.0", + "electron": "32.0.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", From 5e999f56b58bb0a9248f72aaa095ad6c93472f0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:23:31 +0200 Subject: [PATCH 19/64] [deps] Tools: Update electron-updater to v6.3.4 (#10681) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 30 ++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 775981c3f9a..941839c6f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,7 +134,7 @@ "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", - "electron-updater": "6.3.3", + "electron-updater": "6.3.4", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.1", @@ -15190,10 +15190,11 @@ "license": "ISC" }, "node_modules/electron-updater": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.3.tgz", - "integrity": "sha512-Kj1u6kfyxUyatnspvKa6qhGn82rMZfUD03WOvCGJ12PyRss/AC8kkYsN9IrJihKTlN8nRwTjZ1JM2UUXoD0KsA==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz", + "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==", "dev": true, + "license": "MIT", "dependencies": { "builder-util-runtime": "9.2.5", "fs-extra": "^10.1.0", @@ -15201,7 +15202,7 @@ "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", - "semver": "^7.3.8", + "semver": "^7.6.3", "tiny-typed-emitter": "^2.1.0" } }, @@ -15209,13 +15210,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/electron-updater/node_modules/builder-util-runtime": { "version": "9.2.5", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz", "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -15229,6 +15232,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -15243,6 +15247,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -15250,6 +15255,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/emitter-component": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", diff --git a/package.json b/package.json index 3e06be06743..ce5c3e81df2 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", - "electron-updater": "6.3.3", + "electron-updater": "6.3.4", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.1", From 80f4935171489e924d7d9c20fa198f091f1dcdff Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 27 Aug 2024 09:12:28 -0400 Subject: [PATCH 20/64] [CL-324] migrate app-callout internals to bit-callout (#9925) --- .../src/auth/popup/lock.component.html | 2 +- apps/browser/src/popup/scss/misc.scss | 96 ------------------- apps/desktop/src/scss/misc.scss | 86 ----------------- apps/web/src/scss/callouts.scss | 79 --------------- apps/web/src/scss/styles.scss | 1 - .../select-payment-method.component.html | 2 +- .../src/components/callout.component.html | 15 +-- .../src/components/callout.component.ts | 37 ++----- libs/angular/src/jslib.module.ts | 8 +- .../src/callout/callout.component.ts | 2 +- 10 files changed, 17 insertions(+), 311 deletions(-) delete mode 100644 apps/web/src/scss/callouts.scss diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index 5ea839470be..ccc743d86d4 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -89,7 +89,7 @@

      - {{ biometricError }} + {{ biometricError }}

      {{ "awaitDesktop" | i18n }}

      diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index 134bac917d3..57bd3e010c8 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -287,102 +287,6 @@ app-vault-icon, cursor: move; } -.callout { - padding: 10px; - margin: 10px; - border: 1px solid #000000; - border-left-width: 5px; - border-radius: 3px; - @include themify($themes) { - border-color: themed("calloutBorderColor"); - background-color: themed("calloutBackgroundColor"); - } - - .callout-heading { - margin-top: 0; - } - - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; - } - - &.callout-primary { - @include themify($themes) { - border-left-color: themed("primaryColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("primaryColor"); - } - } - } - - &.callout-info { - @include themify($themes) { - border-left-color: themed("infoColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("infoColor"); - } - } - } - - &.callout-danger { - @include themify($themes) { - border-left-color: themed("dangerColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("dangerColor"); - } - } - } - - &.callout-success { - @include themify($themes) { - border-left-color: themed("successColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("successColor"); - } - } - } - - &.callout-warning { - @include themify($themes) { - border-left-color: themed("warningColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("warningColor"); - } - } - } - - &.clickable { - &:hover, - &:focus, - &.active { - @include themify($themes) { - background-color: themed("boxBackgroundHoverColor"); - } - } - } - - .enforced-policy-options ul { - padding-left: 30px; - margin: 0; - } -} - input[type="password"]::-ms-reveal { display: none; } diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index ccc0af8fa4a..75a72640f2b 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -439,92 +439,6 @@ app-root > #loading, cursor: move; } -.callout { - padding: 10px; - margin-bottom: 10px; - border: 1px solid #000000; - border-left-width: 5px; - border-radius: 3px; - @include themify($themes) { - border-color: themed("calloutBorderColor"); - background-color: themed("calloutBackgroundColor"); - } - - .callout-heading { - margin-top: 0; - } - - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; - } - - &.callout-primary { - @include themify($themes) { - border-left-color: themed("primaryColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("primaryColor"); - } - } - } - - &.callout-info { - @include themify($themes) { - border-left-color: themed("infoColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("infoColor"); - } - } - } - - &.callout-danger { - @include themify($themes) { - border-left-color: themed("dangerColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("dangerColor"); - } - } - } - - &.callout-success { - @include themify($themes) { - border-left-color: themed("successColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("successColor"); - } - } - } - - &.callout-warning { - @include themify($themes) { - border-left-color: themed("warningColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("warningColor"); - } - } - } - - ul { - padding-left: 40px; - margin: 0; - } -} - .password-reprompt { text-align: left; margin-top: 15px; diff --git a/apps/web/src/scss/callouts.scss b/apps/web/src/scss/callouts.scss deleted file mode 100644 index da28d607161..00000000000 --- a/apps/web/src/scss/callouts.scss +++ /dev/null @@ -1,79 +0,0 @@ -.callout { - border-left-width: 5px !important; - border-radius: $card-inner-border-radius; - margin-bottom: $alert-margin-bottom; - padding: $alert-padding-y $alert-padding-x; - @include themify($themes) { - background-color: themed("calloutBackground"); - border: 1px solid themed("borderColor"); - color: themed("calloutColor"); - } - - .callout-heading { - margin-top: 0; - } - - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; - } - - &.callout-primary { - @include themify($themes) { - border-left-color: themed("primary"); - } - .callout-heading { - @include themify($themes) { - color: themed("primary"); - } - } - } - - &.callout-info { - @include themify($themes) { - border-left-color: themed("info"); - } - - .callout-heading { - @include themify($themes) { - color: themed("info"); - } - } - } - - &.callout-danger { - @include themify($themes) { - border-left-color: themed("danger"); - } - - .callout-heading { - @include themify($themes) { - color: themed("danger"); - } - } - } - - &.callout-success { - @include themify($themes) { - border-left-color: themed("success"); - } - - .callout-heading { - @include themify($themes) { - color: themed("success"); - } - } - } - - &.callout-warning { - @include themify($themes) { - border-left-color: themed("warning"); - } - - .callout-heading { - @include themify($themes) { - color: themed("warning"); - } - } - } -} diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss index 8fbea200a96..d17181615ca 100644 --- a/apps/web/src/scss/styles.scss +++ b/apps/web/src/scss/styles.scss @@ -45,7 +45,6 @@ @import "./base"; @import "./buttons"; -@import "./callouts"; @import "./cards"; @import "./forms"; @import "./modals"; diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html index 7add3f6d35d..f7e57acb7f8 100644 --- a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html +++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html @@ -141,7 +141,7 @@ - + {{ "makeSureEnoughCredit" | i18n }} diff --git a/libs/angular/src/components/callout.component.html b/libs/angular/src/components/callout.component.html index a049d5cb722..7e352fa0ced 100644 --- a/libs/angular/src/components/callout.component.html +++ b/libs/angular/src/components/callout.component.html @@ -1,14 +1,5 @@ -
      -

      - - {{ title }} -

      -
      + +
      {{ enforcedPolicyMessage }}
      • @@ -32,4 +23,4 @@
      -
      + diff --git a/libs/angular/src/components/callout.component.ts b/libs/angular/src/components/callout.component.ts index c595beec196..2fd0878654d 100644 --- a/libs/angular/src/components/callout.component.ts +++ b/libs/angular/src/components/callout.component.ts @@ -2,16 +2,19 @@ import { Component, Input, OnInit } from "@angular/core"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CalloutTypes } from "@bitwarden/components"; +/** + * @deprecated use the CL's `CalloutComponent` instead + */ @Component({ selector: "app-callout", templateUrl: "callout.component.html", }) -export class CalloutComponent implements OnInit { - @Input() type = "info"; +export class DeprecatedCalloutComponent implements OnInit { + @Input() type: CalloutTypes = "info"; @Input() icon: string; @Input() title: string; - @Input() clickable: boolean; @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; @Input() enforcedPolicyMessage: string; @Input() useAlertRole = false; @@ -26,34 +29,6 @@ export class CalloutComponent implements OnInit { if (this.enforcedPolicyMessage === undefined) { this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect"); } - - if (this.type === "warning" || this.type === "danger") { - if (this.type === "danger") { - this.calloutStyle = "danger"; - } - if (this.title === undefined) { - this.title = this.i18nService.t("warning"); - } - if (this.icon === undefined) { - this.icon = "bwi-exclamation-triangle"; - } - } else if (this.type === "error") { - this.calloutStyle = "danger"; - if (this.title === undefined) { - this.title = this.i18nService.t("error"); - } - if (this.icon === undefined) { - this.icon = "bwi-error"; - } - } else if (this.type === "tip") { - this.calloutStyle = "success"; - if (this.title === undefined) { - this.title = this.i18nService.t("tip"); - } - if (this.icon === undefined) { - this.icon = "bwi-lightbulb"; - } - } } getPasswordScoreAlertDisplay() { diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index da8a4dd4181..755d52c0e77 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -14,6 +14,7 @@ import { AsyncActionsModule, AutofocusDirective, ButtonModule, + CalloutModule, CheckboxModule, DialogModule, FormFieldModule, @@ -29,7 +30,7 @@ import { } from "@bitwarden/components"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; -import { CalloutComponent } from "./components/callout.component"; +import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -72,6 +73,7 @@ import { IconComponent } from "./vault/components/icon.component"; FormFieldModule, SelectModule, ButtonModule, + CalloutModule, CheckboxModule, DialogModule, TypographyModule, @@ -88,7 +90,7 @@ import { IconComponent } from "./vault/components/icon.component"; ApiActionDirective, AutofocusDirective, BoxRowDirective, - CalloutComponent, + DeprecatedCalloutComponent, CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, @@ -125,7 +127,7 @@ import { IconComponent } from "./vault/components/icon.component"; AutofocusDirective, ToastModule, BoxRowDirective, - CalloutComponent, + DeprecatedCalloutComponent, CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index 6942d4bc15a..bfeae17b359 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -type CalloutTypes = "success" | "info" | "warning" | "danger"; +export type CalloutTypes = "success" | "info" | "warning" | "danger"; const defaultIcon: Record = { success: "bwi-check", From e926aa4bc93d02725b56971323ea6aaae0f2030a Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 27 Aug 2024 10:01:04 -0400 Subject: [PATCH 21/64] fixed user state type (#10744) --- .../src/platform/services/config/config.service.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index d03103f255c..efe75f0882a 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -287,7 +287,10 @@ describe("ConfigService", () => { environmentService.environment$ = environmentSubject; globalState.stateSubject.next({ [apiUrl(0)]: config }); - userState.stateSubject.next([userId, config]); + userState.stateSubject.next({ + syncValue: true, + combinedState: [userId, config], + }); configApiService.get.mockImplementation(() => { return new Promise((resolve) => { From 3b0f27f2e9a9937bf6db55e41fea8297c6d02f83 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:42:45 -0400 Subject: [PATCH 22/64] Bumped client version(s) (#10750) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1ca7de0e135..994aba73292 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.2", + "version": "2024.8.3", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index e7496714416..ab46091790b 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 577683e0a86..df1f7f70d41 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.2", + "version": "2024.8.3", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 941839c6f41..68873cc8314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.8.2", + "version": "2024.8.3", "hasInstallScript": true, "license": "GPL-3.0" }, From 3fc1b5731cb7c43d70a870a3d5147c923a008478 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:59:30 -0400 Subject: [PATCH 23/64] Correct `dist:safari` Build (#10748) --- apps/browser/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index db743b509bf..433ddecd2ac 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -20,8 +20,8 @@ "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", - "dist:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist:safari", - "dist:safari:mv3": "npm run build:prod && gulp dist:safari", + "dist:safari": "cross-env BROWSER=safari npm run build:prod && gulp dist:safari", + "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 BROWSER=safari run build:prod && cross-env MANIFEST_VERSION=3 BROWSER=safari gulp dist:safari", "dist:safari:mas": "npm run build:prod && gulp dist:safari:mas", "dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev", "dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg", From 11eba8d77902c8b115da541cc09a0472cf74aca3 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:11:42 -0400 Subject: [PATCH 24/64] Add Null Check On `diskAccount` (#10550) --- libs/common/src/platform/services/state.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 70d2211a884..d46a5189a48 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -117,6 +117,13 @@ export class StateService< state.accounts = {}; } state.accounts[userId] = this.createAccount(); + + if (diskAccount == null) { + // Return early because we can't set the diskAccount.profile + // if diskAccount itself is null + return state; + } + state.accounts[userId].profile = diskAccount.profile; return state; }); From a7da5bb40f42312a839d034a3a9621c411f2b848 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:12:21 -0400 Subject: [PATCH 25/64] Remove Some Unneccesary Async From Init Methods (#10485) --- .../src/autofill/background/notification.background.spec.ts | 4 ++-- .../src/autofill/background/notification.background.ts | 2 +- apps/browser/src/background/commands.background.ts | 2 +- apps/browser/src/background/idle.background.ts | 2 +- apps/browser/src/background/main.background.ts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 2c9232d8b5c..f3ebe5b1cc9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -137,8 +137,8 @@ describe("NotificationBackground", () => { }); describe("notification bar extension message handlers", () => { - beforeEach(async () => { - await notificationBackground.init(); + beforeEach(() => { + notificationBackground.init(); }); it("ignores messages whose command does not match the expected handlers", () => { diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 25f45bd0659..9aac9b099a2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -94,7 +94,7 @@ export default class NotificationBackground { private accountService: AccountService, ) {} - async init() { + init() { if (chrome.runtime == null) { return; } diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index b88f6127bc4..b87f5d0fcfb 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -26,7 +26,7 @@ export default class CommandsBackground { this.isVivaldi = this.platformUtilsService.isVivaldi(); } - async init() { + init() { BrowserApi.messageListener("commands.background", (msg: any) => { if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") { this.processCommand( diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index c0cd3a86aae..a5d50e8508f 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -23,7 +23,7 @@ export default class IdleBackground { this.idle = chrome.idle || (browser != null ? browser.idle : null); } - async init() { + init() { if (!this.idle) { return; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3944f2d8afc..432d6566dda 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1238,11 +1238,11 @@ export default class MainBackground { await this.vaultTimeoutService.init(true); this.fido2Background.init(); await this.runtimeBackground.init(); - await this.notificationBackground.init(); + this.notificationBackground.init(); this.filelessImporterBackground.init(); - await this.commandsBackground.init(); + this.commandsBackground.init(); this.contextMenusBackground?.init(); - await this.idleBackground.init(); + this.idleBackground.init(); this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); await this.autoSubmitLoginBackground.init(); From e255d84121b27dee5afcb45cff44d1d1d74c39e6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:18:02 -0700 Subject: [PATCH 26/64] [PM-4473] use anon layout for send access component (#10699) * use anon layout for send access component * fix width on explainer. * don't show creator ID until send is decrypted in send access --- apps/web/src/app/oss-routing.module.ts | 24 +++- .../src/app/tools/send/access.component.html | 127 +++++++----------- .../src/app/tools/send/access.component.ts | 11 ++ .../send/send-access-explainer.component.html | 18 +++ .../send/send-access-explainer.component.ts | 17 +++ apps/web/src/locales/en/messages.json | 4 + 6 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 apps/web/src/app/tools/send/send-access-explainer.component.html create mode 100644 apps/web/src/app/tools/send/send-access-explainer.component.ts diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 32dcb695a8f..de0e8a2da93 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -68,6 +68,7 @@ import { PreferencesComponent } from "./settings/preferences.component"; import { GeneratorComponent } from "./tools/generator.component"; import { ReportsModule } from "./tools/reports"; import { AccessComponent } from "./tools/send/access.component"; +import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component"; import { SendComponent } from "./tools/send/send.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; @@ -145,11 +146,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn()], data: { titleId: "deleteAccount" } satisfies DataProperties, }, - { - path: "send/:sendId/:key", - component: AccessComponent, - data: { titleId: "Bitwarden Send" } satisfies DataProperties, - }, { path: "update-temp-password", component: UpdateTempPasswordComponent, @@ -210,6 +206,24 @@ const routes: Routes = [ }, ], }, + { + path: "send/:sendId/:key", + data: { + pageTitle: "viewSend", + showReadonlyHostname: true, + } satisfies DataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: AccessComponent, + }, + { + path: "", + outlet: "secondary", + component: SendAccessExplainerComponent, + }, + ], + }, { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/web/src/app/tools/send/access.component.html b/apps/web/src/app/tools/send/access.component.html index d1a6f4d42d2..2a7514ed854 100644 --- a/apps/web/src/app/tools/send/access.component.html +++ b/apps/web/src/app/tools/send/access.component.html @@ -1,85 +1,52 @@
      -
      - -
      -

      View Send

      -
      -
      -

      {{ "sendAccessCreatorIdentifier" | i18n: creatorIdentifier }}

      -
      - - {{ "viewSendHiddenEmailWarning" | i18n }} - {{ - "learnMore" | i18n - }}. - -
      - - - - {{ "sendAccessUnavailable" | i18n }} - - - {{ "unexpectedErrorSend" | i18n }} - -
      -

      - {{ send.name }} -

      -
      - - - - - - - - -

      - Expires: {{ expirationDate | date: "medium" }} -

      -
      + + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ + "learnMore" | i18n + }}. + + + + + {{ "sendAccessUnavailable" | i18n }} + + + {{ "unexpectedErrorSend" | i18n }} + +
      +

      + {{ send.name }} +

      +
      + + + - -
      - - {{ "loading" | i18n }} -
      -
      -
      -
      -

      - {{ "sendAccessTaglineProductDesc" | i18n }} - {{ "sendAccessTaglineLearnMore" | i18n }} - Bitwarden Send - {{ "sendAccessTaglineOr" | i18n }} - {{ - "sendAccessTaglineSignUp" | i18n - }} - {{ "sendAccessTaglineTryToday" | i18n }} + + + + +

      + Expires: {{ expirationDate | date: "medium" }}

      -
      + + +
      + + {{ "loading" | i18n }} +
      +
      diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index f553542a619..07697ee8b37 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; +import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { RegisterRouteService } from "@bitwarden/auth/common"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -70,6 +71,7 @@ export class AccessComponent implements OnInit { private i18nService: I18nService, private configService: ConfigService, private registerRouteService: RegisterRouteService, + private layoutWrapperDataService: AnonLayoutWrapperDataService, protected formBuilder: FormBuilder, ) {} @@ -151,6 +153,15 @@ export class AccessComponent implements OnInit { !this.passwordRequired && !this.loading && !this.unavailable; + + if (this.creatorIdentifier != null) { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + subtitle: this.i18nService.t("sendAccessCreatorIdentifier", this.creatorIdentifier), + translate: false, + }, + }); + } }; protected setPassword(password: string) { diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.html b/apps/web/src/app/tools/send/send-access-explainer.component.html new file mode 100644 index 00000000000..e8090cb850c --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-explainer.component.html @@ -0,0 +1,18 @@ +
      +

      + {{ "sendAccessTaglineProductDesc" | i18n }} + {{ "sendAccessTaglineLearnMore" | i18n }} + Bitwarden Send + {{ "sendAccessTaglineOr" | i18n }} + {{ + "sendAccessTaglineSignUp" | i18n + }} + {{ "sendAccessTaglineTryToday" | i18n }} +

      +
      diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access-explainer.component.ts new file mode 100644 index 00000000000..756a1068985 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-explainer.component.ts @@ -0,0 +1,17 @@ +import { Component } from "@angular/core"; + +import { RegisterRouteService } from "@bitwarden/auth/common"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-explainer", + templateUrl: "send-access-explainer.component.html", + standalone: true, + imports: [SharedModule], +}) +export class SendAccessExplainerComponent { + // TODO: remove when email verification flag is removed + registerRoute$ = this.registerRouteService.registerRoute$(); + constructor(private registerRouteService: RegisterRouteService) {} +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 40e4789923e..29e2e398254 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4983,6 +4983,10 @@ } } }, + "viewSend": { + "message": "View Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." From 9041a4cd4cd44a3f1741f45c357df4e4efca6002 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:33:58 -0700 Subject: [PATCH 27/64] [PM-6564] migrate auth toasts to CL toastService (#10665) * migrate auth toasts to CL toastService * fix component args * fix component args * fix specs * fix toastService args --- .../src/auth/popup/environment.component.ts | 4 +- apps/browser/src/auth/popup/hint.component.ts | 12 ++- apps/browser/src/auth/popup/home.component.ts | 12 +-- apps/browser/src/auth/popup/lock.component.ts | 4 +- .../popup/login-via-auth-request.component.ts | 3 + .../browser/src/auth/popup/login.component.ts | 3 + .../src/auth/popup/register.component.ts | 4 +- .../settings/account-security.component.ts | 33 ++++---- apps/browser/src/auth/popup/sso.component.ts | 3 + .../auth/popup/two-factor-auth.component.ts | 3 + .../auth/accessibility-cookie.component.ts | 32 +++---- .../src/auth/delete-account.component.ts | 12 +-- .../desktop/src/auth/environment.component.ts | 4 +- apps/desktop/src/auth/hint.component.ts | 12 ++- apps/desktop/src/auth/lock.component.spec.ts | 8 +- apps/desktop/src/auth/lock.component.ts | 4 +- .../auth/login/login-approval.component.ts | 42 +++++----- .../login/login-via-auth-request.component.ts | 3 + .../desktop/src/auth/login/login.component.ts | 13 +-- apps/desktop/src/auth/register.component.ts | 4 +- .../src/auth/set-password.component.ts | 4 +- apps/desktop/src/auth/sso.component.ts | 3 + apps/web/src/app/auth/hint.component.ts | 12 ++- .../web/src/app/auth/login/login.component.ts | 3 + .../migrate-legacy-encryption.component.ts | 14 ++-- .../src/app/auth/recover-delete.component.ts | 12 +-- .../app/auth/recover-two-factor.component.ts | 12 +-- .../register-form/register-form.component.ts | 14 ++-- .../account/change-avatar-dialog.component.ts | 15 +++- .../account/change-email.component.ts | 12 +-- .../account/deauthorize-sessions.component.ts | 12 +-- .../delete-account-dialog.component.ts | 13 +-- .../settings/account/profile.component.ts | 9 +- .../settings/change-password.component.ts | 50 ++++++----- .../emergency-access-add-edit.component.ts | 16 ++-- .../emergency-access.component.ts | 83 ++++++++++--------- .../emergency-access-takeover.component.ts | 14 ++-- .../change-kdf-confirmation.component.ts | 12 +-- .../two-factor-authenticator.component.ts | 10 ++- .../settings/two-factor-base.component.ts | 15 +++- .../auth/settings/two-factor-duo.component.ts | 4 +- .../settings/two-factor-email.component.ts | 4 +- .../settings/two-factor-webauthn.component.ts | 4 +- .../settings/two-factor-yubikey.component.ts | 3 +- .../auth/settings/verify-email.component.ts | 25 ++++-- .../create-credential-dialog.component.ts | 33 ++++---- .../delete-credential-dialog.component.ts | 19 +++-- .../user-verification-prompt.component.ts | 13 ++- apps/web/src/app/auth/sso.component.ts | 3 + .../src/app/auth/two-factor-auth.component.ts | 10 ++- .../app/auth/verify-email-token.component.ts | 14 +++- .../auth/verify-recover-delete.component.ts | 12 +-- .../bit-web/src/app/auth/sso/sso.component.ts | 10 ++- ...base-login-decryption-options.component.ts | 12 +-- .../components/captcha-protected.component.ts | 14 +++- .../components/change-password.component.ts | 43 +++++----- .../auth/components/environment.component.ts | 8 +- .../src/auth/components/hint.component.ts | 28 ++++--- .../src/auth/components/lock.component.ts | 63 +++++++------- .../login-via-auth-request.component.ts | 28 +++++-- .../src/auth/components/login.component.ts | 20 +++-- .../src/auth/components/register.component.ts | 41 +++++---- .../components/remove-password.component.ts | 31 +++++-- .../auth/components/set-password.component.ts | 16 +++- .../src/auth/components/sso.component.spec.ts | 22 +++-- .../src/auth/components/sso.component.ts | 22 ++--- .../two-factor-auth-email.component.ts | 22 ++--- .../two-factor-auth-webauthn.component.ts | 12 +-- .../two-factor-auth.component.spec.ts | 5 +- .../two-factor-auth.component.ts | 24 +++--- .../auth/components/two-factor.component.ts | 48 ++++++----- .../components/update-password.component.ts | 30 ++++--- .../update-temp-password.component.ts | 14 ++-- .../user-verification-prompt.component.ts | 8 +- .../user-verification-dialog.component.ts | 24 ++++-- 75 files changed, 782 insertions(+), 457 deletions(-) diff --git a/apps/browser/src/auth/popup/environment.component.ts b/apps/browser/src/auth/popup/environment.component.ts index ed348e563b6..b84f03b5fd7 100644 --- a/apps/browser/src/auth/popup/environment.component.ts +++ b/apps/browser/src/auth/popup/environment.component.ts @@ -5,6 +5,7 @@ import { EnvironmentComponent as BaseEnvironmentComponent } from "@bitwarden/ang import { ModalService } from "@bitwarden/angular/services/modal.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; @@ -21,8 +22,9 @@ export class EnvironmentComponent extends BaseEnvironmentComponent implements On i18nService: I18nService, private router: Router, modalService: ModalService, + toastService: ToastService, ) { - super(platformUtilsService, environmentService, i18nService, modalService); + super(platformUtilsService, environmentService, i18nService, modalService, toastService); this.showCustom = true; } diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index 214a43efb71..bc1f68f4c43 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -21,8 +22,17 @@ export class HintComponent extends BaseHintComponent { logService: LogService, private route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, + toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); super.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 43f8f3dcf4c..505931ad0f1 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -7,6 +7,7 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; @@ -36,6 +37,7 @@ export class HomeComponent implements OnInit, OnDestroy { private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, private registerRouteService: RegisterRouteService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { @@ -76,11 +78,11 @@ export class HomeComponent implements OnInit, OnDestroy { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index a6da98fe996..f5413e4bea4 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -27,7 +27,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; @@ -72,6 +72,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, + toastService: ToastService, ) { super( masterPasswordService, @@ -100,6 +101,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { authService, kdfConfigService, syncService, + toastService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index f83062e6c97..53f29badee6 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -50,6 +51,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, private location: Location, + toastService: ToastService, ) { super( router, @@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustService, authRequestService, loginStrategyService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 79a02ede85d..6e73199969a 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -22,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../platform/flags"; @@ -53,6 +54,7 @@ export class LoginComponent extends BaseLoginComponent { ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -74,6 +76,7 @@ export class LoginComponent extends BaseLoginComponent { ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index 61e007ac52a..dab1e62f850 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,7 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -39,6 +39,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -55,6 +56,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService, auditService, dialogService, + toastService, ); } } diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 7bced79a0a8..25401f06f38 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -39,7 +39,7 @@ import { VaultTimeoutOption, VaultTimeoutStringType, } from "@bitwarden/common/types/vault-timeout.type"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -95,6 +95,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private toastService: ToastService, private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); @@ -274,11 +275,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { // The minTimeoutError does not apply to browser because it supports Immediately // So only check for the policyError if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -315,11 +316,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -417,11 +418,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.form.controls.biometric.setValue(result); if (!result) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorEnableBiometricTitle"), - this.i18nService.t("errorEnableBiometricDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorEnableBiometricTitle"), + message: this.i18nService.t("errorEnableBiometricDesc"), + }); } } catch (e) { // prevent duplicate dialog diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 33284717ab5..42222c42b97 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -51,6 +52,7 @@ export class SsoComponent extends BaseSsoComponent { accountService: AccountService, private authService: AuthService, @Inject(WINDOW) private win: Window, + toastService: ToastService, ) { super( ssoLoginService, @@ -69,6 +71,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index ea0d4a48e55..27c95321100 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -32,6 +32,7 @@ import { LinkModule, TypographyModule, DialogService, + ToastService, } from "@bitwarden/components"; import { @@ -95,6 +96,7 @@ export class TwoFactorAuthComponent @Inject(WINDOW) protected win: Window, private syncService: SyncService, private messagingService: MessagingService, + toastService: ToastService, ) { super( loginStrategyService, @@ -114,6 +116,7 @@ export class TwoFactorAuthComponent accountService, formBuilder, win, + toastService, ); super.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); diff --git a/apps/desktop/src/auth/accessibility-cookie.component.ts b/apps/desktop/src/auth/accessibility-cookie.component.ts index fc72b1a9d77..697bbcc88ec 100644 --- a/apps/desktop/src/auth/accessibility-cookie.component.ts +++ b/apps/desktop/src/auth/accessibility-cookie.component.ts @@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-accessibility-cookie", @@ -25,6 +26,7 @@ export class AccessibilityCookieComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, protected ngZone: NgZone, + private toastService: ToastService, ) {} registerhCaptcha() { @@ -42,28 +44,28 @@ export class AccessibilityCookieComponent { } onCookieSavedSuccess() { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("accessibilityCookieSaved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accessibilityCookieSaved"), + }); } onCookieSavedFailure() { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("noAccessibilityCookieSaved"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("noAccessibilityCookieSaved"), + }); } async submit() { if (Utils.getHostname(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidUrl"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidUrl"), + }); return; } this.listenForCookie = true; diff --git a/apps/desktop/src/auth/delete-account.component.ts b/apps/desktop/src/auth/delete-account.component.ts index a473310d386..0cb2bdd79ee 100644 --- a/apps/desktop/src/auth/delete-account.component.ts +++ b/apps/desktop/src/auth/delete-account.component.ts @@ -13,6 +13,7 @@ import { CalloutModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; import { UserVerificationComponent } from "../app/components/user-verification.component"; @@ -41,6 +42,7 @@ export class DeleteAccountComponent { private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private accountApiService: AccountApiService, + private toastService: ToastService, ) {} static open(dialogService: DialogService): DialogRef { @@ -54,10 +56,10 @@ export class DeleteAccountComponent { submit = async () => { const verification = this.deleteForm.get("verification").value; await this.accountApiService.deleteAccount(verification); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); }; } diff --git a/apps/desktop/src/auth/environment.component.ts b/apps/desktop/src/auth/environment.component.ts index a4b137a9e51..d24e0f86abe 100644 --- a/apps/desktop/src/auth/environment.component.ts +++ b/apps/desktop/src/auth/environment.component.ts @@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-environment", @@ -16,7 +17,8 @@ export class EnvironmentComponent extends BaseEnvironmentComponent { environmentService: EnvironmentService, i18nService: I18nService, modalService: ModalService, + toastService: ToastService, ) { - super(platformUtilsService, environmentService, i18nService, modalService); + super(platformUtilsService, environmentService, i18nService, modalService, toastService); } } diff --git a/apps/desktop/src/auth/hint.component.ts b/apps/desktop/src/auth/hint.component.ts index cee1f189817..34457029257 100644 --- a/apps/desktop/src/auth/hint.component.ts +++ b/apps/desktop/src/auth/hint.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -20,7 +21,16 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, logService: LogService, loginEmailService: LoginEmailServiceAbstraction, + toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); } } diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c5b5b7acf00..d81f2a486f6 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -34,7 +34,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricsService } from "src/platform/main/biometric"; @@ -62,6 +62,7 @@ describe("LockComponent", () => { let platformUtilsServiceMock: MockProxy; let activatedRouteMock: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; + let mockToastService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -72,6 +73,7 @@ describe("LockComponent", () => { messagingServiceMock = mock(); broadcasterServiceMock = mock(); platformUtilsServiceMock = mock(); + mockToastService = mock(); activatedRouteMock = mock(); activatedRouteMock.queryParams = mock(); @@ -187,6 +189,10 @@ describe("LockComponent", () => { provide: SyncService, useValue: mock(), }, + { + provide: ToastService, + useValue: mockToastService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 55cc79e0a68..350512b0f31 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -28,7 +28,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; const BroadcasterSubscriptionId = "LockComponent"; @@ -72,6 +72,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro authService: AuthService, kdfConfigService: KdfConfigService, syncService: SyncService, + toastService: ToastService, ) { super( masterPasswordService, @@ -100,6 +101,7 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro authService, kdfConfigService, syncService, + toastService, ); } diff --git a/apps/desktop/src/auth/login/login-approval.component.ts b/apps/desktop/src/auth/login/login-approval.component.ts index 296efb50e4a..39876f2945f 100644 --- a/apps/desktop/src/auth/login/login-approval.component.ts +++ b/apps/desktop/src/auth/login/login-approval.component.ts @@ -18,6 +18,7 @@ import { ButtonModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; const RequestTimeOut = 60000 * 15; //15 Minutes @@ -54,6 +55,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { protected appIdService: AppIdService, protected cryptoService: CryptoService, private dialogRef: DialogRef, + private toastService: ToastService, ) { this.notificationId = params.notificationId; } @@ -117,11 +119,11 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { private async retrieveAuthRequestAndRespond(approve: boolean) { this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("thisRequestIsNoLongerValid"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("thisRequestIsNoLongerValid"), + }); } else { const loginResponse = await this.authRequestService.approveOrDenyAuthRequest( approve, @@ -133,21 +135,21 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { showResultToast(loginResponse: AuthRequestResponse) { if (loginResponse.requestApproved) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t( + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( "logInConfirmedForEmailOnDevice", this.email, loginResponse.requestDeviceType, ), - ); + }); } else { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice"), + }); } } @@ -186,11 +188,11 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { } else { clearInterval(this.interval); this.dialogRef.close(); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("loginRequestHasAlreadyExpired"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("loginRequestHasAlreadyExpired"), + }); } } } diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index 7a8dfcbcda7..c0a6a51b907 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -23,6 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -58,6 +59,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, private location: Location, + toastService: ToastService, ) { super( router, @@ -78,6 +80,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustService, authRequestService, loginStrategyService, + toastService, ); super.onSuccessfulLogin = () => { diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index 68b25b8b7e9..2b5910baa96 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -25,6 +25,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -74,6 +75,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -95,6 +97,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); @@ -162,11 +165,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest async continue() { await super.validateEmail(); if (!this.formGroup.controls.email.valid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } this.focusInput(); diff --git a/apps/desktop/src/auth/register.component.ts b/apps/desktop/src/auth/register.component.ts index be44c276485..e7c2cfd32b3 100644 --- a/apps/desktop/src/auth/register.component.ts +++ b/apps/desktop/src/auth/register.component.ts @@ -14,7 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "RegisterComponent"; @@ -41,6 +41,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -57,6 +58,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService, auditService, dialogService, + toastService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index f14434b277b..28f1f69a598 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -22,7 +22,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "SetPasswordComponent"; @@ -56,6 +56,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService: DialogService, kdfConfigService: KdfConfigService, encryptService: EncryptService, + toastService: ToastService, ) { super( accountService, @@ -79,6 +80,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService, kdfConfigService, encryptService, + toastService, ); } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 234ebc85cee..6821a548945 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -18,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -43,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( ssoLoginService, @@ -61,6 +63,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index 944b386e277..42744546234 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -8,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -30,8 +31,17 @@ export class HintComponent extends BaseHintComponent implements OnInit { logService: LogService, loginEmailService: LoginEmailServiceAbstraction, private formBuilder: FormBuilder, + protected toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); } ngOnInit(): void { diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 145d7666273..1422a7c1239 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -29,6 +29,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; @@ -71,6 +72,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -92,6 +94,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ssoLoginService, webAuthnLoginService, registerRouteService, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.showPasswordless = flagEnabled("showPasswordless"); diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 6695039307e..68eaae618fd 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -9,6 +9,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { SharedModule } from "../../shared"; import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module"; @@ -35,6 +36,7 @@ export class MigrateFromLegacyEncryptionComponent { private messagingService: MessagingService, private logService: LogService, private syncService: SyncService, + private toastService: ToastService, ) {} submit = async () => { @@ -59,12 +61,12 @@ export class MigrateFromLegacyEncryptionComponent { await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("keyUpdated"), - this.i18nService.t("logBackInOthersToo"), - { timeout: 15000 }, - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("keyUpdated"), + message: this.i18nService.t("logBackInOthersToo"), + timeout: 15000, + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 96afd910598..04c3eb1df25 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeleteRecoverRequest } from "@bitwarden/common/models/request/delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-delete", @@ -25,6 +26,7 @@ export class RecoverDeleteComponent { private apiService: ApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} submit = async () => { @@ -35,11 +37,11 @@ export class RecoverDeleteComponent { const request = new DeleteRecoverRequest(); request.email = this.email.value.trim().toLowerCase(); await this.apiService.postAccountRecoverDelete(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deleteRecoverEmailSent"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deleteRecoverEmailSent"), + }); await this.router.navigate(["/"]); }; diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 28296aa89d5..0774a9c777a 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -8,6 +8,7 @@ import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-two-factor", @@ -27,6 +28,7 @@ export class RecoverTwoFactorComponent { private i18nService: I18nService, private cryptoService: CryptoService, private loginStrategyService: LoginStrategyServiceAbstraction, + private toastService: ToastService, ) {} get email(): string { @@ -53,11 +55,11 @@ export class RecoverTwoFactorComponent { const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email); request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key); await this.apiService.postTwoFactorRecover(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("twoStepRecoverDisabled"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepRecoverDisabled"), + }); await this.router.navigate(["/"]); }; } diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index 693cdb4d6c0..bf4a3e8203f 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; @@ -52,6 +52,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn auditService: AuditService, dialogService: DialogService, acceptOrgInviteService: AcceptOrganizationInviteService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -68,6 +69,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn logService, auditService, dialogService, + toastService, ); super.modifyRegisterRequest = async (request: RegisterRequest) => { // Org invites are deep linked. Non-existent accounts are redirected to the register page. @@ -104,11 +106,11 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return; } diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index b555faf9a12..e20245bfa00 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -15,7 +15,7 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; type ChangeAvatarDialogData = { profile: ProfileResponse; @@ -55,6 +55,7 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private avatarService: AvatarService, private dialogRef: DialogRef, + private toastService: ToastService, ) { this.profile = data.profile; } @@ -93,9 +94,17 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) { await this.avatarService.setAvatarColor(this.currentSelection); this.dialogRef.close(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("avatarUpdated"), + }); } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } }; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index e5a3c72337b..ac493357765 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -12,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-email", @@ -39,6 +40,7 @@ export class ChangeEmailComponent implements OnInit { private stateService: StateService, private formBuilder: FormBuilder, private kdfConfigService: KdfConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -100,11 +102,11 @@ export class ChangeEmailComponent implements OnInit { try { await this.apiService.postEmail(request); this.reset(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("emailChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("emailChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index 9b9ba7eb795..dcaf38ee29e 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", @@ -23,6 +24,7 @@ export class DeauthorizeSessionsComponent { private userVerificationService: UserVerificationService, private messagingService: MessagingService, private logService: LogService, + private toastService: ToastService, ) {} async submit() { @@ -31,11 +33,11 @@ export class DeauthorizeSessionsComponent { .buildRequest(this.masterPassword) .then((request) => this.apiService.postSecurityStamp(request)); await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("sessionsDeauthorized"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("sessionsDeauthorized"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index b3dd8fbe616..c7c67416e18 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -7,7 +7,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ templateUrl: "delete-account-dialog.component.html", @@ -24,6 +24,7 @@ export class DeleteAccountDialogComponent { private formBuilder: FormBuilder, private accountApiService: AccountApiService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} submit = async () => { @@ -31,11 +32,11 @@ export class DeleteAccountDialogComponent { const verification = this.deleteForm.get("verification").value; await this.accountApiService.deleteAccount(verification); this.dialogRef.close(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); } catch (e) { if (e instanceof ErrorResponse && e.statusCode === 400) { this.invalidSecret = true; diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index a960adfe5de..8b659e579da 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -8,7 +8,7 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; @@ -33,6 +33,7 @@ export class ProfileComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private stateService: StateService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -64,6 +65,10 @@ export class ProfileComponent implements OnInit, OnDestroy { submit = async () => { const request = new UpdateProfileRequest(this.formGroup.get("name").value); await this.apiService.putProfile(request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accountUpdated"), + }); }; } diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 5279c05032a..2cc7c101d0b 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -22,7 +22,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { UserKeyRotationService } from "../key-rotation/user-key-rotation.service"; @@ -60,6 +60,7 @@ export class ChangePasswordComponent kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( i18nService, @@ -73,6 +74,7 @@ export class ChangePasswordComponent kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -141,11 +143,11 @@ export class ChangePasswordComponent this.masterPasswordHint != null && this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase() ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("hintEqualsPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("hintEqualsPassword"), + }); return; } @@ -159,11 +161,11 @@ export class ChangePasswordComponent async setupSubmitActions() { if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } @@ -194,11 +196,11 @@ export class ChangePasswordComponent const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); if (userKey == null) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("invalidMasterPassword"), + }); return; } @@ -225,14 +227,18 @@ export class ChangePasswordComponent await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("masterPasswordChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("masterPasswordChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index d99c693e73e..fa5e80c81f5 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; @@ -51,6 +51,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private logService: LogService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} async ngOnInit() { this.editMode = this.loading = this.params.emergencyAccessId != null; @@ -104,11 +105,14 @@ export class EmergencyAccessAddEditComponent implements OnInit { this.addEditForm.value.waitTime, ); } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editMode ? "editedUserId" : "invitedUsers", + this.params.name, + ), + }); this.dialogRef.close(EmergencyAccessAddEditDialogResult.Saved); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 05e65405fb7..d8cedd5bd43 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -10,7 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessStatusType } from "../../emergency-access/enums/emergency-access-status-type"; @@ -66,6 +66,7 @@ export class EmergencyAccessComponent implements OnInit { protected dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private toastService: ToastService, ) { this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } @@ -121,11 +122,11 @@ export class EmergencyAccessComponent implements OnInit { } this.actionPromise = this.emergencyAccessService.reinvite(contact.id); await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenReinvited", contact.email), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenReinvited", contact.email), + }); this.actionPromise = null; } @@ -153,11 +154,11 @@ export class EmergencyAccessComponent implements OnInit { if (result === EmergencyAccessConfirmDialogResult.Confirmed) { await this.emergencyAccessService.confirm(contact.id, contact.granteeId); updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); } return; } @@ -166,11 +167,11 @@ export class EmergencyAccessComponent implements OnInit { await this.actionPromise; updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); this.actionPromise = null; } @@ -187,11 +188,11 @@ export class EmergencyAccessComponent implements OnInit { try { await this.emergencyAccessService.delete(details.id); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), + }); if (details instanceof GranteeEmergencyAccess) { this.removeGrantee(details); @@ -221,11 +222,11 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.requestAccess(details.id); details.status = EmergencyAccessStatusType.RecoveryInitiated; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("requestSent", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("requestSent", this.userNamePipe.transform(details)), + }); } async approve(details: GranteeEmergencyAccess) { @@ -250,22 +251,22 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.approve(details.id); details.status = EmergencyAccessStatusType.RecoveryApproved; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), + }); } async reject(details: GranteeEmergencyAccess) { await this.emergencyAccessService.reject(details.id); details.status = EmergencyAccessStatusType.Confirmed; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), + }); } takeover = async (details: GrantorEmergencyAccess) => { @@ -278,11 +279,11 @@ export class EmergencyAccessComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === EmergencyAccessTakeoverResultType.Done) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), + }); } }; diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index a3d856aa697..26995c7ce09 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -15,7 +15,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EmergencyAccessService } from "../../../emergency-access"; @@ -64,6 +64,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + protected toastService: ToastService, ) { super( i18nService, @@ -77,6 +78,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -114,11 +116,11 @@ export class EmergencyAccessTakeoverComponent ); } catch (e) { this.logService.error(e); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("unexpectedError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); } this.dialogRef.close(EmergencyAccessTakeoverResultType.Done); }; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0c754e262e1..295037ce6b5 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -12,6 +12,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 { KdfType } from "@bitwarden/common/platform/enums"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-kdf-confirmation", @@ -35,6 +36,7 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, private accountService: AccountService, + private toastService: ToastService, ) { this.kdfConfig = params.kdfConfig; this.masterPassword = null; @@ -46,11 +48,11 @@ export class ChangeKdfConfirmationComponent { } this.loading = true; await this.makeKeyAndSaveAsync(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("encKeySettingsChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("encKeySettingsChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); this.loading = false; }; diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts index b4bef9f74e3..fdd595b7fcd 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -68,6 +68,7 @@ export class TwoFactorAuthenticatorComponent private accountService: AccountService, dialogService: DialogService, private configService: ConfigService, + protected toastService: ToastService, ) { super( apiService, @@ -76,6 +77,7 @@ export class TwoFactorAuthenticatorComponent logService, userVerificationService, dialogService, + toastService, ); this.qrScript = window.document.createElement("script"); this.qrScript.src = "scripts/qrious.min.js"; @@ -148,7 +150,11 @@ export class TwoFactorAuthenticatorComponent request.userVerificationToken = this.userVerificationToken; await this.apiService.deleteTwoFactorAuthenticator(request); this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } diff --git a/apps/web/src/app/auth/settings/two-factor-base.component.ts b/apps/web/src/app/auth/settings/two-factor-base.component.ts index ac8906641dd..2a6af1df98c 100644 --- a/apps/web/src/app/auth/settings/two-factor-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-base.component.ts @@ -10,7 +10,7 @@ import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export abstract class TwoFactorBaseComponent { @@ -33,6 +33,7 @@ export abstract class TwoFactorBaseComponent { protected logService: LogService, protected userVerificationService: UserVerificationService, protected dialogService: DialogService, + protected toastService: ToastService, ) {} protected auth(authResponse: AuthResponseBase) { @@ -76,7 +77,11 @@ export abstract class TwoFactorBaseComponent { } await promise; this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } catch (e) { this.logService.error(e); @@ -102,7 +107,11 @@ export abstract class TwoFactorBaseComponent { await this.apiService.putTwoFactorDisable(request); } this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index a211aa4c9ba..1a5b5917108 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -11,7 +11,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -40,6 +40,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnI dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -48,6 +49,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnI logService, userVerificationService, dialogService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.ts b/apps/web/src/app/auth/settings/two-factor-email.component.ts index 96b4bfb38d5..524b00d114f 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-email.component.ts @@ -14,7 +14,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -45,6 +45,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements O dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -53,6 +54,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements O logService, userVerificationService, dialogService, + toastService, ); } get token() { diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index 5e8ea37e930..9aeafaf2c65 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -16,7 +16,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -61,6 +61,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService: LogService, userVerificationService: UserVerificationService, dialogService: DialogService, + toastService: ToastService, ) { super( apiService, @@ -69,6 +70,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService, userVerificationService, dialogService, + toastService, ); this.auth(data); } diff --git a/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts b/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts index 83718360cab..3b601084c35 100644 --- a/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-yubikey.component.ts @@ -55,7 +55,7 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements userVerificationService: UserVerificationService, dialogService: DialogService, private formBuilder: FormBuilder, - private toastService: ToastService, + protected toastService: ToastService, ) { super( apiService, @@ -64,6 +64,7 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements logService, userVerificationService, dialogService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/verify-email.component.ts b/apps/web/src/app/auth/settings/verify-email.component.ts index e8809cd8931..6fa7a49bc08 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.ts +++ b/apps/web/src/app/auth/settings/verify-email.component.ts @@ -6,7 +6,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { AsyncActionsModule, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + BannerModule, + ButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; @Component({ standalone: true, @@ -25,22 +31,27 @@ export class VerifyEmailComponent { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private tokenService: TokenService, + private toastService: ToastService, ) {} async verifyEmail(): Promise { await this.apiService.refreshIdentityToken(); if (await this.tokenService.getEmailVerified()) { this.onVerified.emit(true); - this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emailVerified"), + }); return; } await this.apiService.postAccountVerifyEmail(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("checkInboxForVerification"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("checkInboxForVerification"), + }); } send = async () => { diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index fd72cbbb711..c0ed678d0a7 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -9,7 +9,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebauthnLoginAdminService } from "../../../core"; import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view"; @@ -60,6 +60,7 @@ export class CreateCredentialDialogComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private logService: LogService, + private toastService: ToastService, ) {} ngOnInit(): void { @@ -102,11 +103,11 @@ export class CreateCredentialDialogComponent implements OnInit { this.invalidSecret = true; } else { this.logService?.error(error); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("unexpectedError"), - error.message, - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("unexpectedError"), + message: error.message, + }); } return; } @@ -162,17 +163,17 @@ export class CreateCredentialDialogComponent implements OnInit { ); if (await firstValueFrom(this.hasPasskeys$)) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("passkeySaved", name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passkeySaved", name), + }); } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("loginWithPasskeyEnabled"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginWithPasskeyEnabled"), + }); } this.dialogRef.close(CreateCredentialDialogResult.Success); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts index 4fec10d2695..ce86874ce46 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -8,7 +8,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebauthnLoginAdminService } from "../../../core"; import { WebauthnLoginCredentialView } from "../../../core/views/webauthn-login-credential.view"; @@ -38,6 +38,7 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private logService: LogService, + private toastService: ToastService, ) {} ngOnInit(): void { @@ -55,17 +56,21 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy { this.dialogRef.disableClose = true; try { await this.webauthnService.deleteCredential(this.credential.id, this.formGroup.value.secret); - this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passkeyRemoved"), + }); } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 400) { this.invalidSecret = true; } else { this.logService?.error(error); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("unexpectedError"), - error.message, - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("unexpectedError"), + message: error.message, + }); } return false; } finally { diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts index cd4ac2db356..7947d53c992 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts @@ -9,7 +9,7 @@ import { import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; /** * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead. @@ -25,8 +25,17 @@ export class UserVerificationPromptComponent extends BaseUserVerificationPrompt formBuilder: FormBuilder, platformUtilsService: PlatformUtilsService, i18nService: I18nService, + toastService: ToastService, ) { - super(null, data, userVerificationService, formBuilder, platformUtilsService, i18nService); + super( + null, + data, + userVerificationService, + formBuilder, + platformUtilsService, + i18nService, + toastService, + ); } override close(success: boolean) { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 93a00054eb3..f55152fed3b 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -60,6 +61,7 @@ export class SsoComponent extends BaseSsoComponent implements OnInit { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( ssoLoginService, @@ -78,6 +80,7 @@ export class SsoComponent extends BaseSsoComponent implements OnInit { configService, masterPasswordService, accountService, + toastService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/two-factor-auth.component.ts b/apps/web/src/app/auth/two-factor-auth.component.ts index fbdddecce98..18660b2ca63 100644 --- a/apps/web/src/app/auth/two-factor-auth.component.ts +++ b/apps/web/src/app/auth/two-factor-auth.component.ts @@ -17,7 +17,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi 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 { LinkModule, TypographyModule, CheckboxModule, DialogService } from "@bitwarden/components"; +import { + LinkModule, + TypographyModule, + CheckboxModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; import { TwoFactorAuthEmailComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; @@ -81,6 +87,7 @@ export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent { accountService: AccountService, formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, + toastService: ToastService, ) { super( loginStrategyService, @@ -100,6 +107,7 @@ export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent { accountService, formBuilder, win, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 67ea7c9785a..d47e2c885ec 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-email-token", @@ -23,6 +24,7 @@ export class VerifyEmailTokenComponent implements OnInit { private apiService: ApiService, private logService: LogService, private stateService: StateService, + private toastService: ToastService, ) {} ngOnInit() { @@ -36,7 +38,11 @@ export class VerifyEmailTokenComponent implements OnInit { if (await this.stateService.getIsAuthenticated()) { await this.apiService.refreshIdentityToken(); } - this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emailVerified"), + }); // 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 this.router.navigate(["/"]); @@ -45,7 +51,11 @@ export class VerifyEmailTokenComponent implements OnInit { this.logService.error(e); } } - this.platformUtilsService.showToast("error", null, this.i18nService.t("emailVerifiedFailed")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("emailVerifiedFailed"), + }); // 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 this.router.navigate(["/"]); diff --git a/apps/web/src/app/auth/verify-recover-delete.component.ts b/apps/web/src/app/auth/verify-recover-delete.component.ts index b3d380fcbda..179913d7e32 100644 --- a/apps/web/src/app/auth/verify-recover-delete.component.ts +++ b/apps/web/src/app/auth/verify-recover-delete.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VerifyDeleteRecoverRequest } from "@bitwarden/common/models/request/verify-delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-recover-delete", @@ -26,6 +27,7 @@ export class VerifyRecoverDeleteComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, + private toastService: ToastService, ) {} ngOnInit() { @@ -44,11 +46,11 @@ export class VerifyRecoverDeleteComponent implements OnInit { submit = async () => { const request = new VerifyDeleteRecoverRequest(this.userId, this.token); await this.apiService.postAccountRecoverDeleteToken(request); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); await this.router.navigate(["/"]); }; } diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 4074c20d9df..e37da8c7840 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -26,9 +26,11 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ToastService } from "@bitwarden/components"; import { ssoTypeValidator } from "./sso-type.validator"; @@ -189,6 +191,8 @@ export class SsoComponent implements OnInit, OnDestroy { private i18nService: I18nService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -282,7 +286,11 @@ export class SsoComponent implements OnInit, OnDestroy { const response = await this.organizationApiService.updateSso(this.organizationId, request); this.populateForm(response); - this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("ssoSettingsSaved"), + }); }; async validateKeyConnectorUrl() { diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 0cc416a74bd..80088bf7f91 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -39,6 +39,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; enum State { NewUser, @@ -104,6 +105,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -275,11 +277,11 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); await this.apiService.postAccountKeys(keysRequest); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("accountSuccessfullyCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("accountSuccessfullyCreated"), + }); await this.passwordResetEnrollmentService.enroll(this.data.organizationId); diff --git a/libs/angular/src/auth/components/captcha-protected.component.ts b/libs/angular/src/auth/components/captcha-protected.component.ts index 0f549d24988..7186f6c3c48 100644 --- a/libs/angular/src/auth/components/captcha-protected.component.ts +++ b/libs/angular/src/auth/components/captcha-protected.component.ts @@ -6,6 +6,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ToastService } from "@bitwarden/components"; @Directive() export abstract class CaptchaProtectedComponent { @@ -17,6 +18,7 @@ export abstract class CaptchaProtectedComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected toastService: ToastService, ) {} async setupCaptcha() { @@ -31,10 +33,18 @@ export abstract class CaptchaProtectedComponent { this.captchaToken = token; }, (error: string) => { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: error, + }); }, (info: string) => { - this.platformUtilsService.showToast("info", this.i18nService.t("info"), info); + this.toastService.showToast({ + variant: "info", + title: this.i18nService.t("info"), + message: info, + }); }, ); } diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index d6c0ec92710..45ddf9095fe 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -15,7 +15,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordColorText } from "../../tools/password-strength/password-strength.component"; @@ -49,6 +49,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected kdfConfigService: KdfConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -127,27 +128,27 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { async strongPassword(): Promise { if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } if (this.masterPassword.length < this.minimumLength) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordMinimumlength", this.minimumLength), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength), + }); return false; } if (this.masterPassword !== this.masterPasswordRetype) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPassDoesntMatch"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPassDoesntMatch"), + }); return false; } @@ -161,11 +162,11 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return false; } diff --git a/libs/angular/src/auth/components/environment.component.ts b/libs/angular/src/auth/components/environment.component.ts index a58d5e5082e..25f10553308 100644 --- a/libs/angular/src/auth/components/environment.component.ts +++ b/libs/angular/src/auth/components/environment.component.ts @@ -7,6 +7,7 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { ModalService } from "../../services/modal.service"; @@ -27,6 +28,7 @@ export class EnvironmentComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, private modalService: ModalService, + private toastService: ToastService, ) { this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { if (env.getRegion() !== Region.SelfHosted) { @@ -59,7 +61,11 @@ export class EnvironmentComponent { notifications: this.notificationsUrl, }); - this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("environmentSaved"), + }); this.saved(); } diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts index 484604b6a5a..7a152efbb9f 100644 --- a/libs/angular/src/auth/components/hint.component.ts +++ b/libs/angular/src/auth/components/hint.component.ts @@ -7,6 +7,7 @@ import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/passw 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 { ToastService } from "@bitwarden/components"; @Directive() export class HintComponent implements OnInit { @@ -23,6 +24,7 @@ export class HintComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, private logService: LogService, private loginEmailService: LoginEmailServiceAbstraction, + protected toastService: ToastService, ) {} ngOnInit(): void { @@ -31,26 +33,30 @@ export class HintComponent implements OnInit { async submit() { if (this.email == null || this.email === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("emailRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("emailRequired"), + }); return; } if (this.email.indexOf("@") === -1) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmail"), + }); return; } try { this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); await this.formPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("masterPassSent")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("masterPassSent"), + }); if (this.onSuccessfulSubmit != null) { this.onSuccessfulSubmit(); } else if (this.router != null) { diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 400dcfd8891..484e1c63469 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -36,7 +36,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class LockComponent implements OnInit, OnDestroy { @@ -90,6 +90,7 @@ export class LockComponent implements OnInit, OnDestroy { protected authService: AuthService, protected kdfConfigService: KdfConfigService, protected syncService: SyncService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -167,11 +168,11 @@ export class LockComponent implements OnInit, OnDestroy { private async handlePinRequiredUnlock() { if (this.pin == null || this.pin === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("pinRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("pinRequired"), + }); return; } @@ -195,36 +196,36 @@ export class LockComponent implements OnInit, OnDestroy { // Log user out if they have entered an invalid PIN too many times if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + }); this.messagingService.send("logout"); return; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidPin"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidPin"), + }); } catch { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("unexpectedError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); } } private async handleMasterPasswordRequiredUnlock() { if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return; } await this.doUnlockWithMasterPassword(); @@ -258,11 +259,11 @@ export class LockComponent implements OnInit, OnDestroy { } if (!passwordValid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); return; } diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index f6356314f5b..452b5ceee1e 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -32,6 +32,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 { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -88,8 +89,9 @@ export class LoginViaAuthRequestComponent private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); // TODO: I don't know why this is necessary. // Why would the existence of the email depend on the navigation? @@ -105,7 +107,11 @@ export class LoginViaAuthRequestComponent .subscribe((id) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => { - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); this.logService.error("Failed to use approved auth request: " + e.message); }); }); @@ -135,7 +141,11 @@ export class LoginViaAuthRequestComponent const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (!this.email) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("userEmailMissing"), + }); // 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 this.router.navigate(["/login-initiated"]); @@ -158,7 +168,11 @@ export class LoginViaAuthRequestComponent this.email = this.loginEmailService.getEmail(); if (!this.email) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("userEmailMissing"), + }); // 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 this.router.navigate(["/login"]); @@ -402,7 +416,11 @@ export class LoginViaAuthRequestComponent // TODO: this should eventually be enforced via deleting this on the server once it is used await this.authRequestService.clearAdminAuthRequest(userId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginApproved"), + }); // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 40880b514aa..501d753a976 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -24,6 +24,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -92,8 +93,9 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, protected ssoLoginService: SsoLoginServiceAbstraction, protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, protected registerRouteService: RegisterRouteService, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); } async ngOnInit() { @@ -135,7 +137,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, //desktop, browser; This should be removed once all clients use reactive forms if (this.formGroup.invalid && showToast) { const errorText = this.getErrorToastMessage(); - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: errorText, + }); return; } @@ -327,11 +333,11 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, return false; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("encryptionKeyMigrationRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); return true; } diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 366290a79ad..60adcba4d9e 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -17,7 +17,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { @@ -97,8 +97,9 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn protected logService: LogService, protected auditService: AuditService, protected dialogService: DialogService, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); this.showTerms = !platformUtilsService.isSelfHost(); this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength); } @@ -129,11 +130,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } if (this.isInTrialFlow) { if (!this.accountCreated) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("trialAccountCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("trialAccountCreated"), + }); } const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken); if (loginResponse.captchaRequired) { @@ -141,11 +142,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn } this.createdAccount.emit(this.formGroup.value.email); } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("newAccountCreated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("newAccountCreated"), + }); // 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 this.router.navigate([this.successRoute], { queryParams: { email: email } }); @@ -210,11 +211,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn this.showErrorSummary = true; if (this.formGroup.get("acceptPolicies").hasError("required")) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("acceptPoliciesRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("acceptPoliciesRequired"), + }); return { isValid: false }; } @@ -226,7 +227,11 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn //desktop, browser if (this.formGroup.invalid && showToast) { const errorText = this.getErrorToastMessage(); - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: errorText, + }); return { isValid: false }; } diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/angular/src/auth/components/remove-password.component.ts index ad5027734c8..4b8e9cc52f0 100644 --- a/libs/angular/src/auth/components/remove-password.component.ts +++ b/libs/angular/src/auth/components/remove-password.component.ts @@ -9,7 +9,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class RemovePasswordComponent implements OnInit { @@ -30,6 +30,7 @@ export class RemovePasswordComponent implements OnInit { private keyConnectorService: KeyConnectorService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -47,17 +48,21 @@ export class RemovePasswordComponent implements OnInit { try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedMasterPassword"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedMasterPassword"), + }); await this.keyConnectorService.removeConvertAccountRequired(); // 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 this.router.navigate([""]); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); } }; @@ -76,13 +81,21 @@ export class RemovePasswordComponent implements OnInit { this.leaving = true; this.actionPromise = this.organizationApiService.leave(this.organization.id); await this.actionPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("leftOrganization"), + }); await this.keyConnectorService.removeConvertAccountRequired(); // 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 this.router.navigate([""]); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e, + }); } }; } diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index f94a31e8d20..e9662c71076 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -32,7 +32,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -74,6 +74,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements dialogService: DialogService, kdfConfigService: KdfConfigService, private encryptService: EncryptService, + protected toastService: ToastService, ) { super( i18nService, @@ -87,6 +88,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -137,7 +139,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements ) .subscribe({ error: () => { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); }, }); } @@ -237,7 +243,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements this.router.navigate([this.successRoute]); } } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 8584abeeb85..af92c7dd1d4 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -28,6 +28,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { SsoComponent } from "./sso.component"; @@ -65,7 +66,7 @@ describe("SsoComponent", () => { let mockSsoLoginService: MockProxy; let mockStateService: MockProxy; - let mockPlatformUtilsService: MockProxy; + let mockToastService: MockProxy; let mockApiService: MockProxy; let mockCryptoFunctionService: MockProxy; let mockEnvironmentService: MockProxy; @@ -75,6 +76,7 @@ describe("SsoComponent", () => { let mockConfigService: MockProxy; let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; + let mockPlatformUtilsService: MockProxy; // Mock authService.logIn params let code: string; @@ -117,7 +119,7 @@ describe("SsoComponent", () => { mockSsoLoginService = mock(); mockStateService = mock(); - mockPlatformUtilsService = mock(); + mockToastService = mock(); mockApiService = mock(); mockCryptoFunctionService = mock(); mockEnvironmentService = mock(); @@ -127,6 +129,7 @@ describe("SsoComponent", () => { mockConfigService = mock(); mockAccountService = mockAccountServiceWith(userId); mockMasterPasswordService = new FakeMasterPasswordService(); + mockPlatformUtilsService = mock(); // Mock loginStrategyService.logIn params code = "code"; @@ -196,7 +199,7 @@ describe("SsoComponent", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: StateService, useValue: mockStateService }, - { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: ToastService, useValue: mockToastService }, { provide: ApiService, useValue: mockApiService }, { provide: CryptoFunctionService, useValue: mockCryptoFunctionService }, @@ -214,6 +217,7 @@ describe("SsoComponent", () => { { provide: ConfigService, useValue: mockConfigService }, { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, ], }); @@ -594,12 +598,12 @@ describe("SsoComponent", () => { expect(mockLogService.error).toHaveBeenCalledTimes(1); expect(mockLogService.error).toHaveBeenCalledWith(error); - expect(mockPlatformUtilsService.showToast).toHaveBeenCalledTimes(1); - expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith( - "error", - null, - "ssoKeyConnectorError", - ); + expect(mockToastService.showToast).toHaveBeenCalledTimes(1); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: "ssoKeyConnectorError", + }); expect(mockRouter.navigate).not.toHaveBeenCalled(); }); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index cc105222c26..ac64ae02462 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -26,6 +26,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Directive() @@ -71,6 +72,7 @@ export class SsoComponent implements OnInit { protected configService: ConfigService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -111,11 +113,11 @@ export class SsoComponent implements OnInit { async submit(returnUri?: string, includeUserIdentifier?: boolean) { if (this.identifier == null || this.identifier === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("ssoValidationFailed"), - this.i18nService.t("ssoIdentifierRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("ssoValidationFailed"), + message: this.i18nService.t("ssoIdentifierRequired"), + }); return; } @@ -382,11 +384,11 @@ export class SsoComponent implements OnInit { // TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here if (e.message === "Key Connector error") { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("ssoKeyConnectorError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("ssoKeyConnectorError"), + }); } } diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts index 5a1231daac6..72d7e6a76c8 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.ts @@ -20,6 +20,7 @@ import { TypographyModule, FormFieldModule, AsyncActionsModule, + ToastService, } from "@bitwarden/components"; @Component({ @@ -55,6 +56,7 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected logService: LogService, protected apiService: ApiService, protected appIdService: AppIdService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { @@ -74,11 +76,11 @@ export class TwoFactorAuthEmailComponent implements OnInit { } if ((await this.loginStrategyService.getEmail()) == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("sessionTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("sessionTimeout"), + }); return; } @@ -94,11 +96,11 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + }); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts index 7f0554c1f0f..7e9f6486911 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component.ts @@ -21,6 +21,7 @@ import { TypographyModule, FormFieldModule, AsyncActionsModule, + ToastService, } from "@bitwarden/components"; @Component({ @@ -56,6 +57,7 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { protected environmentService: EnvironmentService, protected twoFactorService: TwoFactorService, protected route: ActivatedRoute, + private toastService: ToastService, ) { this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -85,11 +87,11 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { this.token.emit(token); }, (error: string) => { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("webauthnCancelOrTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("webauthnCancelOrTimeout"), + }); }, (info: string) => { if (info === "ready") { diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts index 489ca5c1781..755813a677a 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.spec.ts @@ -35,7 +35,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorAuthComponent } from "./two-factor-auth.component"; @@ -76,6 +76,7 @@ describe("TwoFactorComponent", () => { let mockMasterPasswordService: FakeMasterPasswordService; let mockAccountService: FakeAccountService; let mockDialogService: MockProxy; + let mockToastService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -113,6 +114,7 @@ describe("TwoFactorComponent", () => { mockAccountService = mockAccountServiceWith(userId); mockMasterPasswordService = new FakeMasterPasswordService(); mockDialogService = mock(); + mockToastService = mock(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -193,6 +195,7 @@ describe("TwoFactorComponent", () => { { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: AccountService, useValue: mockAccountService }, { provide: DialogService, useValue: mockDialogService }, + { provide: ToastService, useValue: mockToastService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts index 21aaf119c43..58edeed93e9 100644 --- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts +++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts @@ -34,6 +34,7 @@ import { ButtonModule, DialogService, FormFieldModule, + ToastService, } from "@bitwarden/components"; import { CaptchaProtectedComponent } from "../captcha-protected.component"; @@ -142,8 +143,9 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements private accountService: AccountService, private formBuilder: FormBuilder, @Inject(WINDOW) protected win: Window, + protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); } async ngOnInit() { @@ -184,11 +186,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements await this.setupCaptcha(); if (this.token == null || this.token === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verificationCodeRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verificationCodeRequired"), + }); return; } @@ -202,11 +204,11 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements await this.handleLoginResponse(authResult); } catch { this.logService.error("Error submitting two factor token"); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidVerificationCode"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidVerificationCode"), + }); } } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 8c849db6c63..eaff9d665fd 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -97,7 +97,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected accountService: AccountService, protected toastService: ToastService, ) { - super(environmentService, i18nService, platformUtilsService); + super(environmentService, i18nService, platformUtilsService, toastService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); } @@ -135,7 +135,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.submit(); }, (error: string) => { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), error); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: error, + }); }, (info: string) => { if (info === "ready") { @@ -201,11 +205,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI await this.setupCaptcha(); if (this.token == null || this.token === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verificationCodeRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verificationCodeRequired"), + }); return; } @@ -243,11 +247,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI return false; } - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("encryptionKeyMigrationRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); return true; } @@ -414,11 +418,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } if ((await this.loginStrategyService.getEmail()) == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("sessionTimeout"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("sessionTimeout"), + }); return; } @@ -434,11 +438,11 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail), + }); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 661e50e887a..98dd4aeb49a 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -19,7 +19,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -50,6 +50,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( i18nService, @@ -63,6 +64,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -77,11 +79,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } @@ -92,7 +94,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { try { await this.userVerificationService.verifyUser(secret); } catch (e) { - this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); return false; } @@ -120,11 +126,11 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.apiService.postPassword(request); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("masterPasswordChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("masterPasswordChanged"), + message: this.i18nService.t("logBackIn"), + }); if (this.onSuccessfulChangePassword != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 4991e2b152f..78fb9b625b9 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -24,7 +24,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; @@ -64,6 +64,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp kdfConfigService: KdfConfigService, accountService: AccountService, masterPasswordService: InternalMasterPasswordServiceAbstraction, + toastService: ToastService, ) { super( i18nService, @@ -77,6 +78,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -176,11 +178,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp } await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedMasterPassword"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedMasterPassword"), + }); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setForceSetPasswordReason( diff --git a/libs/angular/src/auth/components/user-verification-prompt.component.ts b/libs/angular/src/auth/components/user-verification-prompt.component.ts index d999042722d..4b25a7cc0fe 100644 --- a/libs/angular/src/auth/components/user-verification-prompt.component.ts +++ b/libs/angular/src/auth/components/user-verification-prompt.component.ts @@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { ModalRef } from "../../components/modal/modal.ref"; @@ -37,6 +38,7 @@ export class UserVerificationPromptComponent { private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} get secret() { @@ -56,7 +58,11 @@ export class UserVerificationPromptComponent { this.invalidSecret = false; } catch (e) { this.invalidSecret = true; - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); return; } diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index f8746b5b24e..0c3e6ab71e8 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -15,6 +15,7 @@ import { CalloutModule, DialogModule, DialogService, + ToastService, } from "@bitwarden/components"; import { ActiveClientVerificationOption } from "./active-client-verification-option.enum"; @@ -58,6 +59,7 @@ export class UserVerificationDialogComponent { private userVerificationService: UserVerificationService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} /** @@ -256,19 +258,27 @@ export class UserVerificationDialogComponent { // Only pin should ever get here, but added this check to be safe. if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("error"), - this.i18nService.t("invalidPin"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: this.i18nService.t("invalidPin"), + }); } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unexpectedError"), + }); } } } catch (e) { // Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics. this.invalidSecret = true; - this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); return; } }; From 5b4e4d8f1a19b801634beab4e801e025a34dbc62 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 27 Aug 2024 13:31:44 -0500 Subject: [PATCH 28/64] [PM-10669] Fix inconsistencies with notification bar when saving or updating login credentials (#10617) * [PM-10669] Notification bar appears inconsistently after login * [PM-10669] Notification bar appears inconsistently after login * [PM-10669] Migrating work from POC branch into feature branch * [PM-10669] Incorporating styles for select element * [PM-10669] Incorporating styles for select element * [PM-10669] Fixing notification bar lifespan const * [PM-10669] Incorporating logic that conditionally loads specific bootstrap autofill feature files * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating logic to more smoothly handle transitioning between pages within the notification bar0 * [PM-10669] Incorporating a circle checkmark icon within the success message of the notification bar * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing an issue where the notification bar can potentially load in between loading states for a tab * [PM-10669] Fixing how we handle keyup events on the submit button * [PM-10669] Fixing how we handle keyup events on the submit button * [PM-10669] Fixing jest tests within notification bar * [PM-10669] Adding a jest tests to validate behavior within AutofillInit * [PM-10669] Adding a jest tests to validate behavior within AutofillInit * [PM-11170] Addressing test coverage within CollectAutofillContentService * [PM-11170] Addressing test coverage within CollectAutofillContentService * [PM-10669] Refactoring implementation * [PM-10669] Adding documentation to the methods incorporated within the AutofillOverlayContentService * [PM-10669] Incorporating jest tests for the AutofillOverlayContentService * [PM-10669] Migrating logic associated with the DomQuerySevice away from the CollectAutofillContentService * [PM-10669] Fixing required references to DomQueryService within the implementation * [PM-10669] Holding off on re-incorporating the userTreeWalkerStrategyFlag * [PM-10669] Incorporating jest tests for DomQueryService * [PM-10669] Adding jest test to validate changes within AutofillService * [PM-10669] Adding jest tests to validate changes within AutofillOverlayContentService * [PM-10669] Adding documentation to the OverlayNotificationsBackground class * [PM-10669] Adding documentation to the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Incorporating jest tests to validate the OverlayNotificationsBackground class * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Refactoring OverlayNotificationsContentService and incorporating logic that triggers a fade out of the notification bar on success of a saved password * [PM-10669] Finalizing jest tests for OverlayNotificationsContentService * [PM-10669] Finalizing jest tests for OverlayNotificationsContentService * [PM-10669] Adding new copy for the password saved/updated event in the notification bar * [PM-10669] Fixing visual presentation of sucesss message * [PM-10669] Fixing visual presentation of sucesss message * [PM-10418] Incorporating fallback for when we cannot capture the form button effectively * [PM-10669] Incorporating fixes for form submission button not being captured * [PM-10669] Incorporating a guard to ensure that an AJAX submission captures form data after the user has entered their credentials * [PM-10669] Incorporating a field qualification rule to ensure that we capture forms that are non-viewable on load * [PM-10669] Incorporating a document readyState listener to ensure that we populate the notification bar once the document body is loaded * [PM-10669] Incorporating a match pattern for subdomains of a main domain when filtering out web requests * [PM-10669] Incorporating a match pattern for subdomains of a main domain when filtering out web requests * [PM-10669] Incorporating a redundant methodology to capture `GET` requests that trigger after a form submisson * [PM-10669] Incorporating a redundant methodology to capture `GET` requests that trigger after a form submisson * [PM-10669] Adding jest tests to validate changes within OverlayNotificationsBackground * [PM-10669] Adjusting timeout for modified login credentials to ensure user can enter data on form * [PM-10669] Refining how we handle re-capturing user credentails on before request to better handle multi-part forms * [PM-10669] Refining how we handle re-capturing user credentails on before request to better handle multi-part forms * [PM-10669] Adjusting jest tests to ensure code coverage * [PM-10669] Fixing issues with Safari * [PM-10669] Fixing an invalid qualification rule * [PM-10669] Ensuring that we capture input changes correctly when a field is going from a hidden to non-hidden state * [PM-10669] Fixing jest tests within overlay content service * [PM-10669] Fixing jest tests within overlay content service * [PM-10669] Adding a jest test to validate changes to overlay content service --- apps/browser/src/_locales/en/messages.json | 8 + .../abstractions/notification.background.ts | 7 +- .../overlay-notifications.background.ts | 52 ++ .../notification.background.spec.ts | 74 +-- .../background/notification.background.ts | 51 +- .../overlay-notifications.background.spec.ts | 548 +++++++++++++++++ .../overlay-notifications.background.ts | 557 ++++++++++++++++++ .../content/auto-submit-login.spec.ts | 14 +- .../src/autofill/content/auto-submit-login.ts | 12 +- .../autofill/content/autofill-init.spec.ts | 23 +- .../src/autofill/content/autofill-init.ts | 22 +- .../bootstrap-autofill-overlay-menu.ts | 30 + ...ootstrap-autofill-overlay-notifications.ts | 33 ++ .../content/bootstrap-autofill-overlay.ts | 10 + .../autofill/content/bootstrap-autofill.ts | 4 +- .../src/autofill/content/notification-bar.ts | 3 +- .../content/autofill-init.deprecated.ts | 3 + ...fill-overlay-content.service.deprecated.ts | 2 +- .../autofill/enums/autofill-field.enums.ts | 1 + .../abstractions/notification-bar.ts | 2 + .../src/autofill/notification/bar.html | 27 +- .../src/autofill/notification/bar.scss | 174 +++++- apps/browser/src/autofill/notification/bar.ts | 21 +- ...tofill-inline-menu-content.service.spec.ts | 7 +- .../overlay-notifications-content.service.ts | 40 ++ ...notifications-content.service.spec.ts.snap | 14 + ...rlay-notifications-content.service.spec.ts | 264 +++++++++ .../overlay-notifications-content.service.ts | 281 +++++++++ .../autofill-overlay-content.service.ts | 10 +- .../collect-autofill-content.service.ts | 10 - .../abstractions/dom-query.service.ts | 12 + ...nline-menu-field-qualifications.service.ts | 4 + .../autofill/services/autofill-constants.ts | 18 + .../autofill-overlay-content.service.spec.ts | 401 +++++++++++-- .../autofill-overlay-content.service.ts | 282 ++++++++- .../services/autofill.service.spec.ts | 51 +- .../src/autofill/services/autofill.service.ts | 83 ++- .../collect-autofill-content.service.spec.ts | 53 +- .../collect-autofill-content.service.ts | 270 ++------- .../services/dom-query.service.spec.ts | 82 +++ .../autofill/services/dom-query.service.ts | 185 ++++++ ...e-menu-field-qualification.service.spec.ts | 15 +- ...inline-menu-field-qualification.service.ts | 77 ++- .../insert-autofill-content.service.spec.ts | 4 + .../insert-autofill-content.service.ts | 11 +- .../src/autofill/spec/testing-utils.ts | 9 + apps/browser/src/autofill/utils/index.ts | 2 +- apps/browser/src/autofill/utils/svg-icons.ts | 3 + .../browser/src/background/main.background.ts | 13 +- .../src/popup/services/services.module.ts | 1 + apps/browser/test.setup.ts | 4 + apps/browser/webpack.config.js | 4 + libs/common/src/autofill/constants/index.ts | 6 + libs/common/src/enums/feature-flag.enum.ts | 2 + 54 files changed, 3412 insertions(+), 484 deletions(-) create mode 100644 apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts create mode 100644 apps/browser/src/autofill/background/overlay-notifications.background.spec.ts create mode 100644 apps/browser/src/autofill/background/overlay-notifications.background.ts create mode 100644 apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts create mode 100644 apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap create mode 100644 apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts create mode 100644 apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts create mode 100644 apps/browser/src/autofill/services/abstractions/dom-query.service.ts create mode 100644 apps/browser/src/autofill/services/dom-query.service.spec.ts create mode 100644 apps/browser/src/autofill/services/dom-query.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cd2cc912930..4f1696461ae 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3645,10 +3645,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index e01e2c5c02b..ed9d8e6d84b 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -9,6 +9,7 @@ interface NotificationQueueMessage { type: NotificationQueueMessageTypes; domain: string; tab: chrome.tabs.Tab; + launchTimestamp: number; expires: Date; wasVaultLocked: boolean; } @@ -88,10 +89,9 @@ type NotificationBackgroundExtensionMessage = { tab?: chrome.tabs.Tab; sender?: string; notificationType?: string; + fadeOutNotification?: boolean; }; -type SaveOrUpdateCipherResult = undefined | { error: string }; - type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage }; type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; @@ -100,7 +100,7 @@ type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; - bgCloseNotificationBar: ({ sender }: BackgroundSenderParam) => Promise; + bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -129,7 +129,6 @@ export { ChangePasswordMessageData, UnlockVaultMessageData, AddLoginMessageData, - SaveOrUpdateCipherResult, NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, }; diff --git a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts new file mode 100644 index 00000000000..0ec6a9ae04a --- /dev/null +++ b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts @@ -0,0 +1,52 @@ +import AutofillPageDetails from "../../models/autofill-page-details"; + +export type NotificationTypeData = { + isVaultLocked?: boolean; + theme?: string; + removeIndividualVault?: boolean; + importType?: string; + launchTimestamp?: number; +}; + +export type WebsiteOriginsWithFields = Map>; + +export type ActiveFormSubmissionRequests = Set; + +export type ModifyLoginCipherFormData = { + uri: string; + username: string; + password: string; + newPassword: string; +}; + +export type ModifyLoginCipherFormDataForTab = Map< + chrome.tabs.Tab["id"], + { uri: string; username: string; password: string; newPassword: string } +>; + +export type OverlayNotificationsExtensionMessage = { + command: string; + uri?: string; + username?: string; + password?: string; + newPassword?: string; + details?: AutofillPageDetails; +}; + +type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage }; +type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender }; +type OverlayNotificationsMessageHandlersParams = OverlayNotificationsMessageParams & + OverlayNotificationSenderParams; + +export type OverlayNotificationsExtensionMessageHandlers = { + [key: string]: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => any; + formFieldSubmitted: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => void; + collectPageDetailsResponse: ({ + message, + sender, + }: OverlayNotificationsMessageHandlersParams) => Promise; +}; + +export interface OverlayNotificationsBackground { + init(): void; +} diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index f3ebe5b1cc9..0ede9b96091 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,4 +1,4 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; @@ -48,7 +48,8 @@ describe("NotificationBackground", () => { let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); - const authService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; const policyService = mock(); const folderService = mock(); const userNotificationSettingsService = mock(); @@ -60,6 +61,9 @@ describe("NotificationBackground", () => { const accountService = mock(); beforeEach(() => { + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; notificationBackground = new NotificationBackground( autofillService, cipherService, @@ -91,6 +95,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); @@ -126,6 +131,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"]( message, @@ -222,6 +228,7 @@ describe("NotificationBackground", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( sender.tab, "closeNotificationBar", + { fadeOutNotification: false }, ); }); }); @@ -249,7 +256,6 @@ describe("NotificationBackground", () => { describe("bgAddLogin message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance; let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance; @@ -259,7 +265,6 @@ describe("NotificationBackground", () => { beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); getEnableAddedLoginPromptSpy = jest.spyOn( notificationBackground as any, "getEnableAddedLoginPrompt", @@ -281,12 +286,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -296,12 +300,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -311,13 +314,12 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -329,14 +331,13 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([]); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -348,7 +349,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -358,7 +359,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled(); @@ -371,7 +371,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), @@ -380,7 +380,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -390,13 +389,12 @@ describe("NotificationBackground", () => { it("adds the login to the queue if the user has a locked account", async () => { const login = { username: "test", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true); }); @@ -407,7 +405,7 @@ describe("NotificationBackground", () => { url: "https://example.com", } as any; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "anotherTestUsername", password: "password" } }), @@ -416,14 +414,13 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab); }); it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => { const login = { username: "tEsT", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true); getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -448,14 +445,12 @@ describe("NotificationBackground", () => { describe("bgChangedPassword message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -484,12 +479,11 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( null, "example.com", @@ -508,7 +502,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), ]); @@ -516,7 +510,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -530,7 +523,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -539,7 +532,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -553,7 +545,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -580,7 +572,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -589,7 +581,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -602,7 +593,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -658,12 +649,10 @@ describe("NotificationBackground", () => { }); describe("bgSaveCipher message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance; let openUnlockPopoutSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); openUnlockPopoutSpy = jest .spyOn(notificationBackground as any, "openUnlockPopout") @@ -677,12 +666,11 @@ describe("NotificationBackground", () => { edit: false, folder: "folder-id", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(tabSendMessageDataSpy).toHaveBeenCalledWith( sender.tab, "addToLockedVaultPendingNotifications", @@ -716,7 +704,7 @@ describe("NotificationBackground", () => { let cipherEncryptSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getDecryptedCipherByIdSpy = jest.spyOn( notificationBackground as any, "getDecryptedCipherById", @@ -1214,11 +1202,9 @@ describe("NotificationBackground", () => { }); describe("bgUnlockPopoutOpened message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let pushUnlockVaultToQueueSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushUnlockVaultToQueueSpy = jest.spyOn( notificationBackground as any, "pushUnlockVaultToQueue", @@ -1236,7 +1222,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).not.toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1246,12 +1231,11 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1261,7 +1245,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); notificationBackground["notificationQueue"] = [mock()]; sendMockExtensionMessage(message, sender); @@ -1276,7 +1260,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9aac9b099a2..683e3d8f581 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -44,6 +44,7 @@ import { NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, } from "./abstractions/notification.background"; +import { NotificationTypeData } from "./abstractions/overlay-notifications.background"; import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background"; export default class NotificationBackground { @@ -58,7 +59,8 @@ export default class NotificationBackground { private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), bgGetFolderData: () => this.getFolderData(), - bgCloseNotificationBar: ({ sender }) => this.handleCloseNotificationBarMessage(sender), + bgCloseNotificationBar: ({ message, sender }) => + this.handleCloseNotificationBarMessage(message, sender), bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), bgAddLogin: ({ message, sender }) => this.addLogin(message, sender), @@ -132,6 +134,10 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + /** * Checks the notification queue for any messages that need to be sent to the * specified tab. If no tab is specified, the current tab will be used. @@ -186,9 +192,10 @@ export default class NotificationBackground { ) { const notificationType = notificationQueueMessage.type; - const typeData: Record = { + const typeData: NotificationTypeData = { isVaultLocked: notificationQueueMessage.wasVaultLocked, theme: await firstValueFrom(this.themeStateService.selectedTheme$), + launchTimestamp: notificationQueueMessage.launchTimestamp, }; switch (notificationType) { @@ -230,11 +237,11 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async addLogin( + async addLogin( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - const authStatus = await this.authService.getAuthStatus(); + const authStatus = await this.getAuthStatus(); if (authStatus === AuthenticationStatus.LoggedOut) { return; } @@ -289,6 +296,7 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddLoginQueueMessage = { type: NotificationQueueMessageType.AddLogin, username: loginInfo.username, @@ -296,7 +304,8 @@ export default class NotificationBackground { domain: loginDomain, uri: loginInfo.url, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -310,7 +319,7 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async changedPassword( + async changedPassword( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { @@ -320,7 +329,7 @@ export default class NotificationBackground { return; } - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await this.pushChangePasswordToQueue( null, loginDomain, @@ -380,7 +389,7 @@ export default class NotificationBackground { return; } - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Locked || this.notificationQueue.length) { return; } @@ -399,7 +408,7 @@ export default class NotificationBackground { * @param importType - The type of import that is being requested */ async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { return; } @@ -419,13 +428,15 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddChangePasswordQueueMessage = { type: NotificationQueueMessageType.ChangePassword, cipherId: cipherId, newPassword: newPassword, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -434,11 +445,13 @@ export default class NotificationBackground { private async pushUnlockVaultToQueue(loginDomain: string, tab: chrome.tabs.Tab) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddUnlockVaultQueueMessage = { type: NotificationQueueMessageType.UnlockVault, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: true, }; await this.sendNotificationQueueMessage(tab, message); @@ -459,11 +472,13 @@ export default class NotificationBackground { importType?: string, ) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddRequestFilelessImportQueueMessage = { type: NotificationQueueMessageType.RequestFilelessImport, domain: loginDomain, tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: false, importType, }; @@ -484,7 +499,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await BrowserApi.tabSendMessageData(sender.tab, "addToLockedVaultPendingNotifications", { commandToRetry: { message: { @@ -736,10 +751,16 @@ export default class NotificationBackground { * Sends a message back to the sender tab which * triggers closure of the notification bar. * + * @param message - The extension message * @param sender - The contextual sender of the message */ - private async handleCloseNotificationBarMessage(sender: chrome.runtime.MessageSender) { - await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); + private async handleCloseNotificationBarMessage( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }); } /** diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts new file mode 100644 index 00000000000..d694438c00f --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -0,0 +1,548 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data"; + +import { BrowserApi } from "../../platform/browser/browser-api"; +import AutofillField from "../models/autofill-field"; +import AutofillPageDetails from "../models/autofill-page-details"; +import { + flushPromises, + sendMockExtensionMessage, + triggerTabOnRemovedEvent, + triggerTabOnUpdatedEvent, + triggerWebNavigationOnCompletedEvent, + triggerWebRequestOnBeforeRequestEvent, + triggerWebRequestOnCompletedEvent, +} from "../spec/testing-utils"; + +import NotificationBackground from "./notification.background"; +import { OverlayNotificationsBackground } from "./overlay-notifications.background"; + +describe("OverlayNotificationsBackground", () => { + let logService: MockProxy; + let configService: MockProxy; + let notificationBackground: NotificationBackground; + let getEnableChangedPasswordPromptSpy: jest.SpyInstance; + let getEnableAddedLoginPromptSpy: jest.SpyInstance; + let overlayNotificationsBackground: OverlayNotificationsBackground; + + beforeEach(async () => { + jest.useFakeTimers(); + logService = mock(); + configService = mock(); + notificationBackground = mock(); + getEnableChangedPasswordPromptSpy = jest + .spyOn(notificationBackground, "getEnableChangedPasswordPrompt") + .mockResolvedValue(true); + getEnableAddedLoginPromptSpy = jest + .spyOn(notificationBackground, "getEnableAddedLoginPrompt") + .mockResolvedValue(true); + overlayNotificationsBackground = new OverlayNotificationsBackground( + logService, + configService, + notificationBackground, + ); + configService.getFeatureFlag.mockResolvedValue(true); + await overlayNotificationsBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe("setting up the form submission listeners", () => { + let fields: MockProxy[]; + let details: MockProxy; + + beforeEach(() => { + fields = [mock(), mock(), mock()]; + details = mock({ fields }); + }); + + describe("skipping setting up the web request listeners", () => { + it("skips setting up listeners when the notification bar is disabled", async () => { + getEnableChangedPasswordPromptSpy.mockResolvedValue(false); + getEnableAddedLoginPromptSpy.mockResolvedValue(false); + + sendMockExtensionMessage({ + command: "collectPageDetailsResponse", + details, + }); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + describe("when the sender is from an excluded domain", () => { + const senderHost = "example.com"; + const senderUrl = `https://${senderHost}`; + + beforeEach(() => { + jest.spyOn(notificationBackground, "getExcludedDomains").mockResolvedValue({ + [senderHost]: null, + }); + }); + + it("skips setting up listeners when the sender is the user's vault", async () => { + const vault = "https://vault.bitwarden.com"; + const sender = mock({ origin: vault }); + jest + .spyOn(notificationBackground, "getActiveUserServerConfig") + .mockResolvedValue( + mock({ environment: mock({ vault }) }), + ); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender is an excluded domain", async () => { + const sender = mock({ origin: senderUrl }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender contains a malformed origin", async () => { + const senderOrigin = "-_-!..exampwle.com"; + const sender = mock({ origin: senderOrigin }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("skips setting up listeners when the sender tab does not contain page details fields", async () => { + const sender = mock({ tab: { id: 1 } }); + details.fields = []; + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("sets up the web request listeners", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalled(); + }); + + it("skips setting up duplicate listeners when the website origin has been previously encountered with fields", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalledTimes(1); + }); + }); + + describe("storing the modified login form data", () => { + const sender = mock({ tab: { id: 1 } }); + + it("stores the modified login cipher form data", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + + it("clears the modified login cipher form data after 5 seconds", () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + + jest.advanceTimersByTime(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION); + + expect(overlayNotificationsBackground["modifyLoginCipherFormData"].size).toBe(0); + }); + + it("attempts to store the modified login cipher form data within the onBeforeRequest listener when the data is not captured through a submit button click event", async () => { + const pageDetails = mock({ fields: [mock()] }); + const tab = mock({ id: sender.tab.id }); + jest.spyOn(BrowserApi, "getTab").mockResolvedValueOnce(tab); + const response = { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + jest.spyOn(BrowserApi, "tabSendMessage").mockResolvedValueOnce(response); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com", + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + }); + + describe("web request listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + let notificationChangedPasswordSpy: jest.SpyInstance; + let notificationAddLoginSpy: jest.SpyInstance; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + notificationChangedPasswordSpy = jest.spyOn(notificationBackground, "changedPassword"); + notificationAddLoginSpy = jest.spyOn(notificationBackground, "addLogin"); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + }); + + describe("ignored web requests", () => { + it("ignores requests from urls that do not start with a valid protocol", async () => { + sender.url = "chrome-extension://extension-id"; + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid tabId", async () => { + sender.tab = mock({ id: -1 }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid request method", async () => { + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "GET", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests that are not part of an active form submission", async () => { + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + + it("ignores requests for tabs that do not contain stored login data", async () => { + const requestId = "123345"; + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + }); + + describe("web requests that trigger notifications", () => { + const requestId = "123345"; + + beforeEach(async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + }); + + it("waits for the tab's navigation to complete using the web navigation API before initializing the notification", async () => { + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "loading", + url: sender.url, + }), + ); + }); + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: sender.tab.id, + url: sender.url, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("initializes the notification immediately when the tab's navigation is complete", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => { + sender.tab = mock({ id: 4 }); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementation((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com/redirect", + tabId: sender.tab.id, + method: "GET", + requestId, + }), + ); + await flushPromises(); + + expect(notificationChangedPasswordSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("tab listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + const requestId = "123345"; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + }); + + it("clears all associated data with a removed tab", () => { + triggerTabOnRemovedEvent(sender.tab.id, mock()); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + + it("clears all associated data with a tab that is entering a `loading` state", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "loading" }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + }); +}); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts new file mode 100644 index 00000000000..e252bdcc4af --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -0,0 +1,557 @@ +import { Subject, switchMap, timer } from "rxjs"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { BrowserApi } from "../../platform/browser/browser-api"; + +import { + ActiveFormSubmissionRequests, + ModifyLoginCipherFormData, + ModifyLoginCipherFormDataForTab, + OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface, + OverlayNotificationsExtensionMessage, + OverlayNotificationsExtensionMessageHandlers, + WebsiteOriginsWithFields, +} from "./abstractions/overlay-notifications.background"; +import NotificationBackground from "./notification.background"; + +export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface { + private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map(); + private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set(); + private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map(); + private clearLoginCipherFormDataSubject: Subject = new Subject(); + private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]); + private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = { + formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => + this.handleCollectPageDetailsResponse(message, sender), + }; + + constructor( + private logService: LogService, + private configService: ConfigService, + private notificationBackground: NotificationBackground, + ) {} + + /** + * Initialize the overlay notifications background service. + */ + async init() { + const featureFlagActive = await this.configService.getFeatureFlag( + FeatureFlag.NotificationBarAddLoginImprovements, + ); + if (!featureFlagActive) { + return; + } + + this.setupExtensionListeners(); + this.clearLoginCipherFormDataSubject + .pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION))) + .subscribe(() => this.modifyLoginCipherFormData.clear()); + } + + /** + * Handles the response from the content script with the page details. Triggers an initialization + * of the add login or change password notification if the conditions are met. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async handleCollectPageDetailsResponse( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (await this.shouldInitAddLoginOrChangePasswordNotification(message, sender)) { + this.websiteOriginsWithFields.set(sender.tab.id, this.getSenderUrlMatchPatterns(sender)); + this.setupWebRequestsListeners(); + } + } + + /** + * Determines if the add login or change password notification should be initialized. This depends + * on whether the user has enabled the notification, the sender is not from an excluded domain, the + * tab's page details contains fillable fields, and the website origin has not been previously stored. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async shouldInitAddLoginOrChangePasswordNotification( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + return ( + (await this.isAddLoginOrChangePasswordNotificationEnabled()) && + !(await this.isSenderFromExcludedDomain(sender)) && + message.details?.fields?.length > 0 && + !this.websiteOriginsWithFields.has(sender.tab.id) + ); + } + + /** + * Determines if the add login or change password notification is enabled. + * This is based on the user's settings for the notification. + */ + private async isAddLoginOrChangePasswordNotificationEnabled() { + return ( + (await this.notificationBackground.getEnableChangedPasswordPrompt()) || + (await this.notificationBackground.getEnableAddedLoginPrompt()) + ); + } + + /** + * Returns the match patterns for the sender's URL. This is used to filter out + * the web requests that are not from the sender's tab. + * + * @param sender - The sender of the message + */ + private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) { + return new Set([ + ...this.generateMatchPatterns(sender.url), + ...this.generateMatchPatterns(sender.tab.url), + ]); + } + + /** + * Generates the origin and subdomain match patterns for the URL. + * + * @param url - The URL of the tab + */ + private generateMatchPatterns(url: string): string[] { + try { + if (!url.startsWith("http")) { + url = `https://${url}`; + } + + const originMatchPattern = `${new URL(url).origin}/*`; + + const parsedUrl = new URL(url); + const splitHost = parsedUrl.hostname.split("."); + const domain = splitHost.slice(-2).join("."); + const subDomainMatchPattern = `${parsedUrl.protocol}//*.${domain}/*`; + + return [originMatchPattern, subDomainMatchPattern]; + } catch { + return []; + } + } + + /** + * Stores the login form data that was modified by the user in the content script. This data is + * used to trigger the add login or change password notification when the form is submitted. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private storeModifiedLoginFormData = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) => { + const { uri, username, password, newPassword } = message; + if (!username && !password && !newPassword) { + return; + } + + this.clearLoginCipherFormDataSubject.next(); + const formData = { uri, username, password, newPassword }; + + const existingModifyLoginData = this.modifyLoginCipherFormData.get(sender.tab.id); + if (existingModifyLoginData) { + formData.username = formData.username || existingModifyLoginData.username; + formData.password = formData.password || existingModifyLoginData.password; + formData.newPassword = formData.newPassword || existingModifyLoginData.newPassword; + } + + this.modifyLoginCipherFormData.set(sender.tab.id, formData); + }; + + /** + * Determines if the sender of the message is from an excluded domain. This is used to prevent the + * add login or change password notification from being triggered on the user's vault domain or + * other excluded domains. + * + * @param sender - The sender of the message + */ + private async isSenderFromExcludedDomain(sender: chrome.runtime.MessageSender): Promise { + try { + const senderOrigin = sender.origin; + const serverConfig = await this.notificationBackground.getActiveUserServerConfig(); + const activeUserVault = serverConfig?.environment?.vault; + if (activeUserVault === senderOrigin) { + return true; + } + + const excludedDomains = await this.notificationBackground.getExcludedDomains(); + if (!excludedDomains) { + return false; + } + + const senderDomain = new URL(senderOrigin).hostname; + return excludedDomains[senderDomain] !== undefined; + } catch { + return true; + } + } + + /** + * Removes and resets the onBeforeRequest and onCompleted listeners for web requests. This ensures + * that we are only listening for form submission requests on the tabs that have fillable form fields. + */ + private setupWebRequestsListeners() { + chrome.webRequest.onBeforeRequest.removeListener(this.handleOnBeforeRequestEvent); + chrome.webRequest.onCompleted.removeListener(this.handleOnCompletedRequestEvent); + if (this.websiteOriginsWithFields.size) { + const requestFilter: chrome.webRequest.RequestFilter = this.generateRequestFilter(); + chrome.webRequest.onBeforeRequest.addListener(this.handleOnBeforeRequestEvent, requestFilter); + chrome.webRequest.onCompleted.addListener(this.handleOnCompletedRequestEvent, requestFilter); + } + } + + /** + * Generates the request filter for the web requests. This is used to filter out the web requests + * that are not from the tabs that have fillable form fields. + */ + private generateRequestFilter(): chrome.webRequest.RequestFilter { + const websiteOrigins = Array.from(this.websiteOriginsWithFields.values()); + const urls: string[] = []; + websiteOrigins.forEach((origins) => urls.push(...origins)); + return { + urls, + types: ["main_frame", "sub_frame", "xmlhttprequest"], + }; + } + + /** + * Handles the onBeforeRequest event for web requests. This is used to ensures that the following + * onCompleted event is only triggered for form submission requests. + * + * @param details - The details of the web request + */ + private handleOnBeforeRequestEvent = (details: chrome.webRequest.WebRequestDetails) => { + if (this.isPostSubmissionFormRedirection(details)) { + this.setupNotificationInitTrigger( + details.tabId, + details.requestId, + this.modifyLoginCipherFormData.get(details.tabId), + ).catch((error) => this.logService.error(error)); + + return; + } + + if (!this.isValidFormSubmissionRequest(details)) { + return; + } + + const { requestId, tabId, frameId } = details; + this.activeFormSubmissionRequests.add(requestId); + + if (this.notificationDataIncompleteOnBeforeRequest(tabId)) { + this.getFormFieldDataFromTab(tabId, frameId).catch((error) => this.logService.error(error)); + } + }; + + /** + * Captures the modified login form data if the tab contains incomplete data. This is used as + * a redundancy to ensure that the modified login form data is captured in cases where the form + * is split into multiple parts. + * + * @param tabId - The id of the tab + */ + private notificationDataIncompleteOnBeforeRequest = (tabId: number) => { + const modifyLoginData = this.modifyLoginCipherFormData.get(tabId); + return ( + !modifyLoginData || + !this.shouldTriggerAddLoginNotification(modifyLoginData) || + !this.shouldTriggerChangePasswordNotification(modifyLoginData) + ); + }; + + /** + * Determines whether the request is happening after a form submission. This is identified by a GET + * request that is triggered after a form submission POST request from the same request id. If + * this is the case, and the modified login form data is available, the add login or change password + * notification is triggered. + * + * @param details - The details of the web request + */ + private isPostSubmissionFormRedirection = (details: chrome.webRequest.WebRequestDetails) => { + return ( + details.method?.toUpperCase() === "GET" && + this.activeFormSubmissionRequests.has(details.requestId) && + this.modifyLoginCipherFormData.has(details.tabId) + ); + }; + + /** + * Determines if the web request is a valid form submission request. A valid web request + * is a POST, PUT, or PATCH request that is not from an invalid host. + * + * @param details - The details of the web request + */ + private isValidFormSubmissionRequest = (details: chrome.webRequest.WebRequestDetails) => { + return ( + !this.requestHostIsInvalid(details) && + this.formSubmissionRequestMethods.has(details.method?.toUpperCase()) + ); + }; + + /** + * Retrieves the form field data from the tab. This is used to get the modified login form data + * in cases where the submit button is not clicked, but the form is submitted through other means. + * + * @param tabId - The senders tab id + * @param frameId - The frame where the form is located + */ + private getFormFieldDataFromTab = async (tabId: number, frameId: number) => { + const tab = await BrowserApi.getTab(tabId); + if (!tab) { + return; + } + + const response = (await BrowserApi.tabSendMessage( + tab, + { command: "getFormFieldDataForNotification" }, + { frameId }, + )) as OverlayNotificationsExtensionMessage; + if (response) { + this.storeModifiedLoginFormData(response, { tab }); + } + }; + + /** + * Handles the onCompleted event for web requests. This is used to trigger the add login or change + * password notification when a form submission request is completed. + * + * @param details - The details of the web response + */ + private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => { + if ( + this.requestHostIsInvalid(details) || + this.isInvalidStatusCode(details.statusCode) || + !this.activeFormSubmissionRequests.has(details.requestId) + ) { + return; + } + + const modifyLoginData = this.modifyLoginCipherFormData.get(details.tabId); + if (!modifyLoginData) { + return; + } + + this.setupNotificationInitTrigger(details.tabId, details.requestId, modifyLoginData).catch( + (error) => this.logService.error(error), + ); + }; + + /** + * Sets up the initialization trigger for the add login or change password notification. This is used + * to ensure that the notification is triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private setupNotificationInitTrigger = async ( + tabId: number, + requestId: string, + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const tab = await BrowserApi.getTab(tabId); + if (tab.status !== "complete") { + await this.delayNotificationInitUntilTabIsComplete(tabId, requestId, modifyLoginData); + return; + } + + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + + /** + * Delays the initialization of the add login or change password notification + * until the tab is complete. This is used to ensure that the notification is + * triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private delayNotificationInitUntilTabIsComplete = async ( + tabId: chrome.webRequest.ResourceRequest["tabId"], + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const handleWebNavigationOnCompleted = async () => { + chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted); + const tab = await BrowserApi.getTab(tabId); + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted); + }; + + /** + * Initializes the add login or change password notification based on the modified login form data + * and the tab details. This will trigger the notification to be displayed to the user. + * + * @param requestId - The details of the web response + * @param modifyLoginData - The modified login form data + * @param tab - The tab details + */ + private triggerNotificationInit = async ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + tab: chrome.tabs.Tab, + ) => { + if (this.shouldTriggerChangePasswordNotification(modifyLoginData)) { + // These notifications are temporarily setup as "messages" to the notification background. + // This will be structured differently in a future refactor. + await this.notificationBackground.changedPassword( + { + command: "bgChangedPassword", + data: { + url: modifyLoginData.uri, + currentPassword: modifyLoginData.password, + newPassword: modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + return; + } + + if (this.shouldTriggerAddLoginNotification(modifyLoginData)) { + await this.notificationBackground.addLogin( + { + command: "bgAddLogin", + login: { + url: modifyLoginData.uri, + username: modifyLoginData.username, + password: modifyLoginData.password || modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + } + }; + + /** + * Determines if the change password notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerChangePasswordNotification = ( + modifyLoginData: ModifyLoginCipherFormData, + ) => { + return modifyLoginData.newPassword && !modifyLoginData.username; + }; + + /** + * Determines if the add login notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => { + return modifyLoginData.username && (modifyLoginData.password || modifyLoginData.newPassword); + }; + + /** + * Clears the completed web request and removes the modified login form data for the tab. + * + * @param requestId - The request id of the web request + * @param tab - The tab details + */ + private clearCompletedWebRequest = ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + tab: chrome.tabs.Tab, + ) => { + this.activeFormSubmissionRequests.delete(requestId); + this.modifyLoginCipherFormData.delete(tab.id); + this.websiteOriginsWithFields.delete(tab.id); + this.setupWebRequestsListeners(); + }; + + /** + * Determines if the status code of the web response is invalid. An invalid status code is + * any status code that is not in the 200-299 range. + * + * @param statusCode - The status code of the web response + */ + private isInvalidStatusCode = (statusCode: number) => { + return statusCode < 200 || statusCode >= 300; + }; + + /** + * Determines if the host of the web request is invalid. An invalid host is any host that does not + * start with "http" or a tab id that is less than 0. + * + * @param details - The details of the web request + */ + private requestHostIsInvalid = (details: chrome.webRequest.ResourceRequest) => { + return !details.url?.startsWith("http") || details.tabId < 0; + }; + + /** + * Sets up the listeners for the extension messages and the tab events. + */ + private setupExtensionListeners() { + BrowserApi.messageListener("overlay-notifications", this.handleExtensionMessage); + chrome.tabs.onRemoved.addListener(this.handleTabRemoved); + chrome.tabs.onUpdated.addListener(this.handleTabUpdated); + } + + /** + * Handles messages that are sent to the extension background. + * + * @param message - The message from the content script + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the content script + */ + private handleExtensionMessage = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction = this.extensionMessageHandlers[message.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch((error) => this.logService.error(error)); + return true; + }; + + /** + * Handles the removal of a tab. This is used to remove the modified login form data for the tab. + * + * @param tabId - The id of the tab that was removed + */ + private handleTabRemoved = (tabId: number) => { + this.modifyLoginCipherFormData.delete(tabId); + if (this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + this.setupWebRequestsListeners(); + } + }; + + /** + * Handles the update of a tab. This is used to remove the modified + * login form data for the tab when the tab is loading. + * + * @param tabId - The id of the tab that was updated + * @param changeInfo - The change info of the tab + */ + private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (changeInfo.status === "loading" && this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + } + }; +} diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts index d8a192dbcab..98caee3d363 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -12,6 +12,16 @@ let pageDetailsMock: AutofillPageDetails; let fillScriptMock: AutofillScript; let autofillFieldElementByOpidMock: FormFieldElement; +jest.mock("../services/dom-query.service", () => { + const module = jest.requireActual("../services/dom-query.service"); + return { + DomQueryService: class extends module.DomQueryService { + deepQueryElements(element: HTMLElement, queryString: string): T[] { + return Array.from(element.querySelectorAll(queryString)) as T[]; + } + }, + }; +}); jest.mock("../services/collect-autofill-content.service", () => { const module = jest.requireActual("../services/collect-autofill-content.service"); return { @@ -20,10 +30,6 @@ jest.mock("../services/collect-autofill-content.service", () => { return pageDetailsMock; } - deepQueryElements(element: HTMLElement, queryString: string): T[] { - return Array.from(element.querySelectorAll(queryString)) as T[]; - } - getAutofillFieldElementByOpid(opid: string) { const mockedEl = autofillFieldElementByOpidMock; if (mockedEl) { diff --git a/apps/browser/src/autofill/content/auto-submit-login.ts b/apps/browser/src/autofill/content/auto-submit-login.ts index 9cc06f874e6..19ffac61bcf 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.ts @@ -4,13 +4,16 @@ import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; +import { DomQueryService } from "../services/dom-query.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from "../utils"; (function (globalContext) { + const domQueryService = new DomQueryService(); const domElementVisibilityService = new DomElementVisibilityService(); const collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, + domQueryService, ); const insertAutofillContentService = new InsertAutofillContentService( domElementVisibilityService, @@ -191,7 +194,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " element: HTMLElement, lastFieldIsPasswordInput = false, ): boolean { - const genericSubmitElement = collectAutofillContentService.deepQueryElements( + const genericSubmitElement = domQueryService.deepQueryElements( element, "[type='submit']", ); @@ -200,10 +203,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " return true; } - const buttons = collectAutofillContentService.deepQueryElements( - element, - "button", - ); + const buttons = domQueryService.deepQueryElements(element, "button"); for (let i = 0; i < buttons.length; i++) { if (isLoginButton(buttons[i])) { clickSubmitElement(buttons[i], lastFieldIsPasswordInput); @@ -274,7 +274,7 @@ import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from " */ function getAutofillFormElements(): HTMLFormElement[] { const formElements: HTMLFormElement[] = []; - collectAutofillContentService.queryAllTreeWalkerNodes( + domQueryService.queryAllTreeWalkerNodes( globalContext.document.documentElement, (node: Node) => { if (nodeIsFormElement(node)) { diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index e27e8ef73d0..ebfbda75b56 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -3,6 +3,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { flushPromises, @@ -14,6 +16,8 @@ import { AutofillExtensionMessage } from "./abstractions/autofill-init"; import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { + let domQueryService: MockProxy; + let overlayNotificationsContentService: MockProxy; let inlineMenuElements: MockProxy; let autofillOverlayContentService: MockProxy; let autofillInit: AutofillInit; @@ -27,9 +31,16 @@ describe("AutofillInit", () => { addListener: jest.fn(), }, }); + domQueryService = mock(); + overlayNotificationsContentService = mock(); inlineMenuElements = mock(); autofillOverlayContentService = mock(); - autofillInit = new AutofillInit(autofillOverlayContentService, inlineMenuElements); + autofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + overlayNotificationsContentService, + ); sendExtensionMessageSpy = jest .spyOn(autofillInit as any, "sendExtensionMessage") .mockImplementation(); @@ -171,6 +182,16 @@ describe("AutofillInit", () => { expect(inlineMenuElements.messageHandlers.messageHandler).toHaveBeenCalled(); }); + it("triggers extension message handlers from the OverlayNotificationsContentService", () => { + overlayNotificationsContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect( + overlayNotificationsContentService.messageHandlers.messageHandler, + ).toHaveBeenCalled(); + }); + describe("collectPageDetails", () => { it("sends the collected page details for autofill using a background script message", async () => { const pageDetails: AutofillPageDetails = { diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e44956e1849..c0cbac3ae67 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -2,7 +2,9 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillPageDetails from "../models/autofill-page-details"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.service"; import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; @@ -16,8 +18,6 @@ import { class AutofillInit implements AutofillInitInterface { private readonly sendExtensionMessage = sendExtensionMessage; - private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; - private readonly autofillInlineMenuContentService: AutofillInlineMenuContentService | undefined; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; @@ -32,20 +32,23 @@ class AutofillInit implements AutofillInitInterface { * AutofillInit constructor. Initializes the DomElementVisibilityService, * CollectAutofillContentService and InsertAutofillContentService classes. * + * @param domQueryService - Service used to handle DOM queries. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. - * @param inlineMenuElements - The inline menu elements, potentially undefined. + * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. + * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. */ constructor( - autofillOverlayContentService?: AutofillOverlayContentService, - inlineMenuElements?: AutofillInlineMenuContentService, + private domQueryService: DomQueryService, + private autofillOverlayContentService?: AutofillOverlayContentService, + private autofillInlineMenuContentService?: AutofillInlineMenuContentService, + private overlayNotificationsContentService?: OverlayNotificationsContentService, ) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.autofillInlineMenuContentService = inlineMenuElements; this.domElementVisibilityService = new DomElementVisibilityService( this.autofillInlineMenuContentService, ); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, + domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( @@ -204,6 +207,10 @@ class AutofillInit implements AutofillInitInterface { return this.autofillInlineMenuContentService.messageHandlers[command]; } + if (this.overlayNotificationsContentService?.messageHandlers?.[command]) { + return this.overlayNotificationsContentService.messageHandlers[command]; + } + return this.extensionMessageHandlers[command]; } @@ -217,6 +224,7 @@ class AutofillInit implements AutofillInitInterface { this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); this.autofillInlineMenuContentService?.destroy(); + this.overlayNotificationsContentService?.destroy(); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts new file mode 100644 index 00000000000..aed0f6cb940 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -0,0 +1,30 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + let inlineMenuElements: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + } + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts new file mode 100644 index 00000000000..0a810c68f56 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts @@ -0,0 +1,33 @@ +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + + let overlayNotificationsContentService: OverlayNotificationsContentService; + if (globalThis.self === globalThis.top) { + overlayNotificationsContentService = new OverlayNotificationsContentService(); + } + + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + null, + overlayNotificationsContentService, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index 22430227660..6df9397f6d8 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,5 +1,7 @@ import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -7,17 +9,25 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, inlineMenuFieldQualificationService, ); + let inlineMenuElements: AutofillInlineMenuContentService; + let overlayNotificationsContentService: OverlayNotificationsContentService; if (globalThis.self === globalThis.top) { inlineMenuElements = new AutofillInlineMenuContentService(); + overlayNotificationsContentService = new OverlayNotificationsContentService(); } + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, autofillOverlayContentService, inlineMenuElements, + overlayNotificationsContentService, ); setupAutofillInitDisconnectAction(windowContext); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill.ts b/apps/browser/src/autofill/content/bootstrap-autofill.ts index f98d4bc1d72..3de750cd671 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill.ts @@ -1,10 +1,12 @@ +import { DomQueryService } from "../services/dom-query.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - windowContext.bitwardenAutofillInit = new AutofillInit(); + const domQueryService = new DomQueryService(); + windowContext.bitwardenAutofillInit = new AutofillInit(domQueryService); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 2bcf4394fd9..5217ebbe8ed 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -6,6 +6,7 @@ import { import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; +import { NotificationTypeData } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { FormData } from "../services/abstractions/autofill.service"; import { sendExtensionMessage, setupExtensionDisconnectAction } from "../utils"; @@ -832,7 +833,7 @@ async function loadNotificationBar() { // End Form Detection and Submission Handling // Notification Bar Functions (open, close, height adjustment, etc.) - function closeExistingAndOpenBar(type: string, typeData: any) { + function closeExistingAndOpenBar(type: string, typeData: NotificationTypeData) { const notificationBarInitData: NotificationBarIframeInitData = { type, isVaultLocked: typeData.isVaultLocked, diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts index 211e3bf9251..b3ee2637b09 100644 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -2,6 +2,7 @@ import { AutofillInit } from "../../content/abstractions/autofill-init"; import AutofillPageDetails from "../../models/autofill-page-details"; import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; import DomElementVisibilityService from "../../services/dom-element-visibility.service"; +import { DomQueryService } from "../../services/dom-query.service"; import InsertAutofillContentService from "../../services/insert-autofill-content.service"; import { sendExtensionMessage } from "../../utils"; import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; @@ -40,8 +41,10 @@ class LegacyAutofillInit implements AutofillInit { constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { this.autofillOverlayContentService = autofillOverlayContentService; this.domElementVisibilityService = new DomElementVisibilityService(); + const domQueryService = new DomQueryService(); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, + domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( diff --git a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts index 6526f6993db..87af2518ddc 100644 --- a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts @@ -73,7 +73,7 @@ class LegacyAutofillOverlayContentService implements LegacyAutofillOverlayConten * Satisfy the AutofillOverlayContentService interface. */ messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers; - async setupInlineMenu( + async setupOverlayListeners( autofillFieldElement: ElementWithOpId, autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, diff --git a/apps/browser/src/autofill/enums/autofill-field.enums.ts b/apps/browser/src/autofill/enums/autofill-field.enums.ts index 4fd7c0fe88f..68408f2b671 100644 --- a/apps/browser/src/autofill/enums/autofill-field.enums.ts +++ b/apps/browser/src/autofill/enums/autofill-field.enums.ts @@ -1,5 +1,6 @@ export const AutofillFieldQualifier = { password: "password", + newPassword: "newPassword", username: "username", cardholderName: "cardholderName", cardNumber: "cardNumber", diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 268617c419e..6dfcac4abea 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -4,6 +4,8 @@ type NotificationBarIframeInitData = { theme?: string; removeIndividualVault?: boolean; importType?: string; + applyRedesign?: boolean; + launchTimestamp?: number; }; type NotificationBarWindowMessage = { diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 26d9d7086d4..6b0e76b5169 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -13,16 +13,11 @@
      -
      +
      @@ -32,8 +27,8 @@