From 90ca4345b3fdbab4dcb10176f90a188453d60df2 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:45:10 -0700 Subject: [PATCH 01/11] Larger min-w to anonlayout content container (#9502) * add larger min-w to content container * increase min-w --- .../anon-layout/anon-layout.component.html | 6 ++++-- .../angular/anon-layout/anon-layout.stories.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) 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 bf5edbda823..b6eeb70d5d5 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -13,9 +13,11 @@

{{ subtitle }}

-
+
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 82ca846afbf..c9054fb5e63 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -119,6 +119,24 @@ export const WithLongContent: Story = { }), }; +export const WithThinPrimaryContent: Story = { + render: (args) => ({ + props: args, + template: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + ` + +
Lorem ipsum
+ +
+
Secondary Projected Content (optional)
+ +
+
+ `, + }), +}; + export const WithIcon: Story = { render: (args) => ({ props: args, From 3154d219259e080be25c92217cc84d413d9af82c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 4 Jun 2024 18:42:04 -0400 Subject: [PATCH 02/11] [PM-8285] Resolve app id race (#9501) * Do not update appId if it is not null * Prefer linear transformations to side-effect-based changes This leaves us open to repeat emits due to updates, but distinct until changed stops those. Tracker improvements are due to passed in observables with replay causing immediate emits when `expectingEmission`s. This converts to a cold observable that only emits when the tracked observable does _after_ subscribing. * Prefer while * PR review --- libs/common/spec/observable-tracker.ts | 31 ++++--- .../platform/services/app-id.service.spec.ts | 82 +++++++++++-------- .../src/platform/services/app-id.service.ts | 22 +++-- 3 files changed, 80 insertions(+), 55 deletions(-) diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts index 16fad869c3b..9bf0475bee2 100644 --- a/libs/common/spec/observable-tracker.ts +++ b/libs/common/spec/observable-tracker.ts @@ -1,10 +1,11 @@ -import { Observable, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; +import { Observable, Subject, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; /** Test class to enable async awaiting of observable emissions */ export class ObservableTracker { private subscription: Subscription; + private emissionReceived = new Subject(); emissions: T[] = []; - constructor(private observable: Observable) { + constructor(observable: Observable) { this.emissions = this.trackEmissions(observable); } @@ -21,7 +22,7 @@ export class ObservableTracker { */ async expectEmission(msTimeout = 50): Promise { return await firstValueFrom( - this.observable.pipe( + this.emissionReceived.pipe( timeout({ first: msTimeout, with: () => throwError(() => new Error("Timeout exceeded waiting for another emission.")), @@ -34,40 +35,38 @@ export class ObservableTracker { * @param count The number of emissions to wait for */ async pauseUntilReceived(count: number, msTimeout = 50): Promise { - for (let i = 0; i < count - this.emissions.length; i++) { + while (this.emissions.length < count) { await this.expectEmission(msTimeout); } return this.emissions; } - private trackEmissions(observable: Observable): T[] { + private trackEmissions(observable: Observable): T[] { const emissions: T[] = []; this.subscription = observable.subscribe((value) => { - switch (value) { - case undefined: - case null: - emissions.push(value); - return; - default: - // process by type - break; + if (value == null) { + this.emissionReceived.next(null); + return; } switch (typeof value) { case "string": case "number": case "boolean": - emissions.push(value); + this.emissionReceived.next(value); break; case "symbol": // Cheating types to make symbols work at all - emissions.push(value.toString() as T); + this.emissionReceived.next(value as T); break; default: { - emissions.push(clone(value)); + this.emissionReceived.next(clone(value)); } } }); + this.emissionReceived.subscribe((value) => { + emissions.push(value); + }); return emissions; } } diff --git a/libs/common/src/platform/services/app-id.service.spec.ts b/libs/common/src/platform/services/app-id.service.spec.ts index 10fb153fdaa..62806204db0 100644 --- a/libs/common/src/platform/services/app-id.service.spec.ts +++ b/libs/common/src/platform/services/app-id.service.spec.ts @@ -1,20 +1,23 @@ -import { FakeGlobalStateProvider } from "../../../spec"; +import { FakeGlobalState, FakeGlobalStateProvider, ObservableTracker } from "../../../spec"; import { Utils } from "../misc/utils"; import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service"; describe("AppIdService", () => { - const globalStateProvider = new FakeGlobalStateProvider(); - const appIdState = globalStateProvider.getFake(APP_ID_KEY); - const anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY); + let globalStateProvider: FakeGlobalStateProvider; + let appIdState: FakeGlobalState; + let anonymousAppIdState: FakeGlobalState; let sut: AppIdService; beforeEach(() => { + globalStateProvider = new FakeGlobalStateProvider(); + appIdState = globalStateProvider.getFake(APP_ID_KEY); + anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY); sut = new AppIdService(globalStateProvider); }); afterEach(() => { - jest.restoreAllMocks(); + jest.resetAllMocks(); }); describe("getAppId", () => { @@ -26,19 +29,18 @@ describe("AppIdService", () => { expect(appId).toBe("existingAppId"); }); - it.each([null, undefined])( - "uses the util function to create a new id when it AppId does not exist", - async (value) => { - appIdState.stateSubject.next(value); - const spy = jest.spyOn(Utils, "newGuid"); + it("creates a new appId only once", async () => { + appIdState.stateSubject.next(null); - await sut.getAppId(); + const appIds: string[] = []; + const promises = [async () => appIds.push(await sut.getAppId())]; + promises.push(async () => appIds.push(await sut.getAppId())); + await Promise.all(promises); - expect(spy).toHaveBeenCalledTimes(1); - }, - ); + expect(appIds[0]).toBe(appIds[1]); + }); - it.each([null, undefined])("returns a new appId when it does not exist", async (value) => { + it.each([null, undefined])("returns a new appId when %s", async (value) => { appIdState.stateSubject.next(value); const appId = await sut.getAppId(); @@ -46,16 +48,23 @@ describe("AppIdService", () => { expect(appId).toMatch(Utils.guidRegex); }); - it.each([null, undefined])( - "stores the new guid when it an existing one is not found", - async (value) => { - appIdState.stateSubject.next(value); + it.each([null, undefined])("stores the new guid when %s", async (value) => { + appIdState.stateSubject.next(value); - const appId = await sut.getAppId(); + const appId = await sut.getAppId(); - expect(appIdState.nextMock).toHaveBeenCalledWith(appId); - }, - ); + expect(appIdState.nextMock).toHaveBeenCalledWith(appId); + }); + + it("emits only once when creating a new appId", async () => { + appIdState.stateSubject.next(null); + + const tracker = new ObservableTracker(sut.appId$); + const appId = await sut.getAppId(); + + expect(tracker.emissions).toEqual([appId]); + await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded"); + }); }); describe("getAnonymousAppId", () => { @@ -67,17 +76,16 @@ describe("AppIdService", () => { expect(appId).toBe("existingAppId"); }); - it.each([null, undefined])( - "uses the util function to create a new id when it AppId does not exist", - async (value) => { - anonymousAppIdState.stateSubject.next(value); - const spy = jest.spyOn(Utils, "newGuid"); + it("creates a new anonymousAppId only once", async () => { + anonymousAppIdState.stateSubject.next(null); - await sut.getAnonymousAppId(); + const appIds: string[] = []; + const promises = [async () => appIds.push(await sut.getAnonymousAppId())]; + promises.push(async () => appIds.push(await sut.getAnonymousAppId())); + await Promise.all(promises); - expect(spy).toHaveBeenCalledTimes(1); - }, - ); + expect(appIds[0]).toBe(appIds[1]); + }); it.each([null, undefined])("returns a new appId when it does not exist", async (value) => { anonymousAppIdState.stateSubject.next(value); @@ -97,5 +105,15 @@ describe("AppIdService", () => { expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId); }, ); + + it("emits only once when creating a new anonymousAppId", async () => { + anonymousAppIdState.stateSubject.next(null); + + const tracker = new ObservableTracker(sut.anonymousAppId$); + const appId = await sut.getAnonymousAppId(); + + expect(tracker.emissions).toEqual([appId]); + await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded"); + }); }); }); diff --git a/libs/common/src/platform/services/app-id.service.ts b/libs/common/src/platform/services/app-id.service.ts index 630e629749e..56e9516bce7 100644 --- a/libs/common/src/platform/services/app-id.service.ts +++ b/libs/common/src/platform/services/app-id.service.ts @@ -1,4 +1,4 @@ -import { Observable, filter, firstValueFrom, tap } from "rxjs"; +import { Observable, concatMap, distinctUntilChanged, firstValueFrom, share } from "rxjs"; import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service"; import { Utils } from "../misc/utils"; @@ -19,20 +19,28 @@ export class AppIdService implements AppIdServiceAbstraction { const appIdState = globalStateProvider.get(APP_ID_KEY); const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY); this.appId$ = appIdState.state$.pipe( - tap(async (appId) => { + concatMap(async (appId) => { if (!appId) { - await appIdState.update(() => Utils.newGuid()); + return await appIdState.update(() => Utils.newGuid(), { + shouldUpdate: (v) => v == null, + }); } + return appId; }), - filter((appId) => !!appId), + distinctUntilChanged(), + share(), ); this.anonymousAppId$ = anonymousAppIdState.state$.pipe( - tap(async (appId) => { + concatMap(async (appId) => { if (!appId) { - await anonymousAppIdState.update(() => Utils.newGuid()); + return await anonymousAppIdState.update(() => Utils.newGuid(), { + shouldUpdate: (v) => v == null, + }); } + return appId; }), - filter((appId) => !!appId), + distinctUntilChanged(), + share(), ); } From 7e86c0afd4ffeb6e0a91b4ed942f8b923904ea23 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:45:08 +0530 Subject: [PATCH 03/11] [PM-5014] Download license component migration (#8443) * Download license component migration * download license dialog component migration * download license dialog component migration --- .../download-license.component.html | 68 ++++++++-------- .../download-license.component.ts | 79 +++++++++++-------- .../organization-billing.module.ts | 4 +- ...nization-subscription-cloud.component.html | 7 -- ...ganization-subscription-cloud.component.ts | 9 ++- 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/apps/web/src/app/billing/organizations/download-license.component.html b/apps/web/src/app/billing/organizations/download-license.component.html index 0997462ce92..33a534bacf7 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.html +++ b/apps/web/src/app/billing/organizations/download-license.component.html @@ -1,39 +1,35 @@ -
-
- -

{{ "downloadLicense" | i18n }}

-
-
-
- - - - + + + {{ "downloadLicense" | i18n }} + +
+
+ + {{ "enterInstallationId" | i18n }} + + + + + +
-
-
- - -
+ + + + + + diff --git a/apps/web/src/app/billing/organizations/download-license.component.ts b/apps/web/src/app/billing/organizations/download-license.component.ts index 88a37a28aab..6b3a93548b4 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.ts +++ b/apps/web/src/app/billing/organizations/download-license.component.ts @@ -1,50 +1,61 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { DialogService } from "@bitwarden/components"; + +export enum DownloadLicenseDialogResult { + Cancelled = "cancelled", + Downloaded = "downloaded", +} +type DownloadLicenseDialogData = { + /** current organization id */ + organizationId: string; +}; @Component({ - selector: "app-download-license", templateUrl: "download-license.component.html", }) -export class DownloadLicenseComponent { - @Input() organizationId: string; - @Output() onDownloaded = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); - - installationId: string; - formPromise: Promise; - +export class DownloadLicenceDialogComponent { + licenseForm = this.formBuilder.group({ + installationId: ["", [Validators.required]], + }); constructor( + @Inject(DIALOG_DATA) protected data: DownloadLicenseDialogData, + private dialogRef: DialogRef, private fileDownloadService: FileDownloadService, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, + protected formBuilder: FormBuilder, ) {} - async submit() { - if (this.installationId == null || this.installationId === "") { + submit = async () => { + this.licenseForm.markAllAsTouched(); + const installationId = this.licenseForm.get("installationId").value; + if (installationId == null || installationId === "") { return; } - - try { - this.formPromise = this.organizationApiService.getLicense( - this.organizationId, - this.installationId, - ); - const license = await this.formPromise; - const licenseString = JSON.stringify(license, null, 2); - this.fileDownloadService.download({ - fileName: "bitwarden_organization_license.json", - blobData: licenseString, - }); - this.onDownloaded.emit(); - } catch (e) { - this.logService.error(e); - } - } - - cancel() { - this.onCanceled.emit(); + const license = await this.organizationApiService.getLicense( + this.data.organizationId, + installationId, + ); + const licenseString = JSON.stringify(license, null, 2); + this.fileDownloadService.download({ + fileName: "bitwarden_organization_license.json", + blobData: licenseString, + }); + this.dialogRef.close(DownloadLicenseDialogResult.Downloaded); + }; + /** + * Strongly typed helper to open a DownloadLicenceDialogComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(DownloadLicenceDialogComponent, config); } + cancel = () => { + this.dialogRef.close(DownloadLicenseDialogResult.Cancelled); + }; } diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 490ebafbff0..a95efe32e47 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -8,7 +8,7 @@ import { AdjustSubscription } from "./adjust-subscription.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; import { ChangePlanComponent } from "./change-plan.component"; -import { DownloadLicenseComponent } from "./download-license.component"; +import { DownloadLicenceDialogComponent } from "./download-license.component"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module"; import { OrganizationPlansComponent } from "./organization-plans.component"; @@ -32,7 +32,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; BillingSyncApiKeyComponent, BillingSyncKeyComponent, ChangePlanComponent, - DownloadLicenseComponent, + DownloadLicenceDialogComponent, OrganizationSubscriptionCloudComponent, OrganizationSubscriptionSelfhostComponent, OrgBillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 25ac3a7a155..e11cf602ad2 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -246,13 +246,6 @@ {{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
-
- -

{{ "additionalOptions" | i18n }}

diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index a0db7b5a200..b6282f1e7b1 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -29,6 +29,7 @@ import { } from "../shared/offboarding-survey.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { DownloadLicenceDialogComponent } from "./download-license.component"; import { ManageBilling } from "./icons/manage-billing.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @@ -354,8 +355,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showChangePlan = false; } - downloadLicense() { - this.showDownloadLicense = !this.showDownloadLicense; + async downloadLicense() { + DownloadLicenceDialogComponent.open(this.dialogService, { + data: { + organizationId: this.organizationId, + }, + }); } async manageBillingSync() { From cb0927ac5d9e338cf0282dbb4ea01211698f3b3a Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 5 Jun 2024 10:06:52 -0400 Subject: [PATCH 04/11] [PM-8553] browser v2 search bar defects (#9506) * update no results icon and no results scrolling * update v2 search so the term persist when the user clicks into an item and exits the item --- .../vault-search/vault-v2-search.component.ts | 35 ++++++++++++++----- .../components/vault/vault-v2.component.html | 13 +++---- .../components/vault/vault-v2.component.ts | 5 +-- .../services/vault-popup-items.service.ts | 9 ++--- libs/components/src/icon/icons/index.ts | 1 + libs/components/src/icon/icons/no-results.ts | 18 ++++++++++ 6 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 libs/components/src/icon/icons/no-results.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts index 321717285a9..e6100321607 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts @@ -1,12 +1,14 @@ import { CommonModule } from "@angular/common"; -import { Component, Output, EventEmitter } from "@angular/core"; +import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { Subject, debounceTime } from "rxjs"; +import { Subject, Subscription, debounceTime, filter } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; +import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; + const SearchTextDebounceInterval = 200; @Component({ @@ -17,19 +19,34 @@ const SearchTextDebounceInterval = 200; }) export class VaultV2SearchComponent { searchText: string; - @Output() searchTextChanged = new EventEmitter(); private searchText$ = new Subject(); - constructor() { - this.searchText$ - .pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed()) - .subscribe((data) => { - this.searchTextChanged.emit(data); - }); + constructor(private vaultPopupItemsService: VaultPopupItemsService) { + this.subscribeToLatestSearchText(); + this.subscribeToApplyFilter(); } onSearchTextChanged() { this.searchText$.next(this.searchText); } + + subscribeToLatestSearchText(): Subscription { + return this.vaultPopupItemsService.latestSearchText$ + .pipe( + takeUntilDestroyed(), + filter((data) => !!data), + ) + .subscribe((text) => { + this.searchText = text; + }); + } + + subscribeToApplyFilter(): Subscription { + return this.searchText$ + .pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed()) + .subscribe((data) => { + this.vaultPopupItemsService.applyFilter(data); + }); + } } diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index f99d3cbb303..9f38fd61fab 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -22,18 +22,15 @@

-
- - + - -
+
- + {{ "noItemsMatchSearch" | i18n }} {{ "clearFiltersOrTryAnother" | i18n }} @@ -41,7 +38,7 @@
{{ "organizationIsDeactivated" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts index 5e9487ac880..5e91f196a30 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -44,6 +44,7 @@ export class VaultV2Component implements OnInit, OnDestroy { protected vaultIcon = Icons.Vault; protected deactivatedIcon = Icons.DeactivatedOrg; + protected noResultsIcon = Icons.NoResults; constructor( private vaultPopupItemsService: VaultPopupItemsService, @@ -54,10 +55,6 @@ export class VaultV2Component implements OnInit, OnDestroy { ngOnDestroy(): void {} - handleSearchTextChange(searchText: string) { - this.vaultPopupItemsService.applyFilter(searchText); - } - addCipher() { // TODO: Add currently filtered organization to query params if available void this.router.navigate(["/add-cipher"], {}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index eacb8e013ef..ab98722d060 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -38,7 +38,8 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi }) export class VaultPopupItemsService { private _refreshCurrentTab$ = new Subject(); - private searchText$ = new BehaviorSubject(""); + private _searchText$ = new BehaviorSubject(""); + latestSearchText$: Observable = this._searchText$.asObservable(); /** * Observable that contains the list of other cipher types that should be shown @@ -105,7 +106,7 @@ export class VaultPopupItemsService { private _filteredCipherList$: Observable = combineLatest([ this._cipherList$, - this.searchText$, + this._searchText$, this.vaultPopupListFiltersService.filterFunction$, ]).pipe( map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ @@ -179,7 +180,7 @@ export class VaultPopupItemsService { * Observable that indicates whether a filter is currently applied to the ciphers. */ hasFilterApplied$ = combineLatest([ - this.searchText$, + this._searchText$, this.vaultPopupListFiltersService.filters$, ]).pipe( switchMap(([searchText, filters]) => { @@ -242,7 +243,7 @@ export class VaultPopupItemsService { } applyFilter(newSearchText: string) { - this.searchText$.next(newSearchText); + this._searchText$.next(newSearchText); } /** diff --git a/libs/components/src/icon/icons/index.ts b/libs/components/src/icon/icons/index.ts index 9de81f19913..ea583031f61 100644 --- a/libs/components/src/icon/icons/index.ts +++ b/libs/components/src/icon/icons/index.ts @@ -2,3 +2,4 @@ export * from "./deactivated-org"; export * from "./search"; export * from "./no-access"; export * from "./vault"; +export * from "./no-results"; diff --git a/libs/components/src/icon/icons/no-results.ts b/libs/components/src/icon/icons/no-results.ts new file mode 100644 index 00000000000..f68e67f88ce --- /dev/null +++ b/libs/components/src/icon/icons/no-results.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "../icon"; + +export const NoResults = svgIcon` + + + + + + + + + + + + + + +`; From 1aaa88a64d4d48aa75868980eb4c280b672057c3 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:48:12 -0400 Subject: [PATCH 05/11] [PM-7837] Move `SyncService` to Platform Ownership (#9055) * Move * Update References In Unowned Files * Update References In Vault Files * Update Web AppComponent * Add Import --- .../browser/src/background/main.background.ts | 13 ++- .../src/popup/services/services.module.ts | 2 +- .../components/vault/current-tab.component.ts | 2 +- .../vault/vault-filter.component.ts | 2 +- .../vault/popup/settings/sync.component.ts | 2 +- apps/cli/src/service-container.ts | 10 +-- apps/cli/src/vault/sync.command.ts | 2 +- apps/desktop/src/app/app.component.ts | 2 +- apps/desktop/src/app/services/init.service.ts | 2 +- .../src/vault/app/vault/vault.component.ts | 2 +- apps/web/src/app/app.component.ts | 2 +- .../src/app/layouts/user-layout.component.ts | 2 +- .../organization-options.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../app/vault/org-vault/vault.component.ts | 2 +- .../vault/settings/purge-vault.component.ts | 2 +- .../src/services/jslib-services.module.ts | 22 ++--- libs/common/src/abstractions/api.service.ts | 2 +- .../sync/default-sync.service.ts} | 89 ++++++++++--------- libs/common/src/platform/sync/index.ts | 2 + libs/common/src/platform/sync/internal.ts | 1 + .../sync}/sync-event-args.ts | 2 +- .../sync}/sync.response.ts | 17 ++-- libs/common/src/platform/sync/sync.service.ts | 58 ++++++++++++ libs/common/src/services/api.service.ts | 2 +- .../src/services/notifications.service.ts | 2 +- .../sync/sync-notifier.service.abstraction.ts | 8 -- .../sync/sync.service.abstraction.ts | 21 +---- .../services/sync/sync-notifier.service.ts | 18 ---- 29 files changed, 152 insertions(+), 143 deletions(-) rename libs/common/src/{vault/services/sync/sync.service.ts => platform/sync/default-sync.service.ts} (74%) create mode 100644 libs/common/src/platform/sync/index.ts rename libs/common/src/{vault/types => platform/sync}/sync-event-args.ts (94%) rename libs/common/src/{vault/models/response => platform/sync}/sync.response.ts (69%) create mode 100644 libs/common/src/platform/sync/sync.service.ts delete mode 100644 libs/common/src/vault/abstractions/sync/sync-notifier.service.abstraction.ts delete mode 100644 libs/common/src/vault/services/sync/sync-notifier.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 63721466f6f..a8654c92f03 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -136,6 +136,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; /* eslint-enable import/no-restricted-paths */ +import { SyncService } from "@bitwarden/common/platform/sync"; +// eslint-disable-next-line no-restricted-imports -- Needed for service creation +import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; @@ -166,8 +169,6 @@ import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/co import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync-notifier.service.abstraction"; -import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -176,8 +177,6 @@ import { CollectionService } from "@bitwarden/common/vault/services/collection.s import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; -import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service"; -import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { @@ -268,7 +267,7 @@ export default class MainBackground { collectionService: CollectionServiceAbstraction; vaultTimeoutService: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; - syncService: SyncServiceAbstraction; + syncService: SyncService; passwordGenerationService: PasswordGenerationServiceAbstraction; passwordStrengthService: PasswordStrengthServiceAbstraction; totpService: TotpServiceAbstraction; @@ -306,7 +305,6 @@ export default class MainBackground { policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction; - syncNotifierService: SyncNotifierServiceAbstraction; fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2ClientService: Fido2ClientServiceAbstraction; @@ -638,7 +636,6 @@ export default class MainBackground { this.i18nService, this.stateProvider, ); - this.syncNotifierService = new SyncNotifierService(); this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, @@ -827,7 +824,7 @@ export default class MainBackground { messageListener, ); } else { - this.syncService = new SyncService( + this.syncService = new DefaultSyncService( this.masterPasswordService, this.accountService, this.apiService, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index ace9af3dfa8..d61fa3b19c0 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -80,11 +80,11 @@ import { } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Used for dependency injection import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 4d2674fd703..24ca030284f 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -14,8 +14,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co 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 { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts index deb4434df47..b46b4cf9ff2 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts @@ -9,8 +9,8 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; diff --git a/apps/browser/src/vault/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts index 3fe4de9eb51..16f388804bb 100644 --- a/apps/browser/src/vault/popup/settings/sync.component.ts +++ b/apps/browser/src/vault/popup/settings/sync.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from "@angular/core"; 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 { SyncService } from "@bitwarden/common/platform/sync"; @Component({ selector: "app-sync", diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 53039e91473..ff4eb52b84e 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -97,6 +97,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; /* eslint-enable import/no-restricted-paths */ +import { SyncService } from "@bitwarden/common/platform/sync"; +// eslint-disable-next-line no-restricted-imports -- Needed for service construction +import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; @@ -120,8 +123,6 @@ import { CollectionService } from "@bitwarden/common/vault/services/collection.s import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; -import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service"; -import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { ImportApiService, @@ -216,7 +217,6 @@ export class ServiceContainer { folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; - syncNotifierService: SyncNotifierService; sendApiService: SendApiService; devicesApiService: DevicesApiServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction; @@ -440,8 +440,6 @@ export class ServiceContainer { customUserAgent, ); - this.syncNotifierService = new SyncNotifierService(); - this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); this.containerService = new ContainerService(this.cryptoService, this.encryptService); @@ -648,7 +646,7 @@ export class ServiceContainer { this.avatarService = new AvatarService(this.apiService, this.stateProvider); - this.syncService = new SyncService( + this.syncService = new DefaultSyncService( this.masterPasswordService, this.accountService, this.apiService, diff --git a/apps/cli/src/vault/sync.command.ts b/apps/cli/src/vault/sync.command.ts index 073b9b5df48..c3c6f637538 100644 --- a/apps/cli/src/vault/sync.command.ts +++ b/apps/cli/src/vault/sync.command.ts @@ -1,4 +1,4 @@ -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { Response } from "../models/response"; import { MessageResponse } from "../models/response/message.response"; diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 561e9b2df9c..e4fdd17dc15 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -42,13 +42,13 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UserId } from "@bitwarden/common/types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { DialogService, ToastOptions, ToastService } from "@bitwarden/components"; diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0452e9be837..8793587300f 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -15,10 +15,10 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; +import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { NativeMessagingService } from "../../services/native-messaging.service"; diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index 208bbc70f03..37992ecea0e 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -23,7 +23,7 @@ import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broa import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { SyncService } from "@bitwarden/common/platform/sync"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 254f23eeb23..c9fbf359f0f 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -35,12 +35,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastOptions, ToastService } from "@bitwarden/components"; import { PolicyListService } from "./admin-console/core/policy-list.service"; diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 1ce8d4d2278..757b8220f3a 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { PaymentMethodWarningsModule } from "../billing/shared"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 8dd63e62ddb..5a138c3147b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -13,7 +13,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService } from "@bitwarden/components"; import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index ca04b3aa51f..ae3a0657788 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -45,9 +45,9 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 514cb8150d1..dfdce5c818e 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -48,10 +48,10 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; diff --git a/apps/web/src/app/vault/settings/purge-vault.component.ts b/apps/web/src/app/vault/settings/purge-vault.component.ts index 869cbaab1b4..9a677af7b5d 100644 --- a/apps/web/src/app/vault/settings/purge-vault.component.ts +++ b/apps/web/src/app/vault/settings/purge-vault.component.ts @@ -8,7 +8,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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService } from "@bitwarden/components"; export interface PurgeVaultDialogData { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 048c1829001..8c676bdb9d9 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -187,6 +187,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service"; /* eslint-enable import/no-restricted-paths */ +import { SyncService } from "@bitwarden/common/platform/sync"; +// eslint-disable-next-line no-restricted-imports -- Needed for DI +import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; import { DefaultThemeStateService, ThemeStateService, @@ -226,8 +229,6 @@ import { FolderService as FolderServiceAbstraction, InternalFolderService, } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync-notifier.service.abstraction"; -import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; @@ -235,8 +236,6 @@ import { CollectionService } from "@bitwarden/common/vault/services/collection.s import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; -import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service"; -import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { ToastService } from "@bitwarden/components"; @@ -644,8 +643,8 @@ const safeProviders: SafeProvider[] = [ deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService], }), safeProvider({ - provide: SyncServiceAbstraction, - useClass: SyncService, + provide: SyncService, + useClass: DefaultSyncService, deps: [ InternalMasterPasswordServiceAbstraction, AccountServiceAbstraction, @@ -796,7 +795,7 @@ const safeProviders: SafeProvider[] = [ useClass: devFlagEnabled("noopNotifications") ? NoopNotificationsService : NotificationsService, deps: [ LogService, - SyncServiceAbstraction, + SyncService, AppIdServiceAbstraction, ApiServiceAbstraction, EnvironmentService, @@ -942,12 +941,7 @@ const safeProviders: SafeProvider[] = [ // it depends on SyncService so that new data can be retrieved through the sync // rather than updating the OrganizationService directly. Instead OrganizationService // subscribes to sync notifications and will update itself based on that. - deps: [ApiServiceAbstraction, SyncServiceAbstraction], - }), - safeProvider({ - provide: SyncNotifierServiceAbstraction, - useClass: SyncNotifierService, - deps: [], + deps: [ApiServiceAbstraction, SyncService], }), safeProvider({ provide: DefaultConfigService, @@ -1122,7 +1116,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, - SyncServiceAbstraction, + SyncService, ], }), safeProvider({ diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 73e4f74e63f..ed43849d62b 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -103,6 +103,7 @@ import { EventResponse } from "../models/response/event.response"; import { ListResponse } from "../models/response/list.response"; import { ProfileResponse } from "../models/response/profile.response"; import { UserKeyResponse } from "../models/response/user-key.response"; +import { SyncResponse } from "../platform/sync"; import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; @@ -124,7 +125,6 @@ import { CollectionResponse, } from "../vault/models/response/collection.response"; import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response"; -import { SyncResponse } from "../vault/models/response/sync.response"; /** * @deprecated The `ApiService` class is deprecated and calls should be extracted into individual diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts similarity index 74% rename from libs/common/src/vault/services/sync/sync.service.ts rename to libs/common/src/platform/sync/default-sync.service.ts index 109ecea0358..5058288487f 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -1,50 +1,53 @@ import { firstValueFrom } from "rxjs"; -import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { LogoutReason } from "../../../../auth/src/common/types"; +import { ApiService } from "../../abstractions/api.service"; +import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderService } from "../../admin-console/abstractions/provider.service"; +import { OrganizationUserType } from "../../admin-console/enums"; +import { OrganizationData } from "../../admin-console/models/data/organization.data"; +import { PolicyData } from "../../admin-console/models/data/policy.data"; +import { ProviderData } from "../../admin-console/models/data/provider.data"; +import { PolicyResponse } from "../../admin-console/models/response/policy.response"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AvatarService } from "../../auth/abstractions/avatar.service"; +import { KeyConnectorService } from "../../auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; +import { TokenService } from "../../auth/abstractions/token.service"; +import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; +import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { BillingAccountProfileStateService } from "../../billing/abstractions"; +import { DomainsResponse } from "../../models/response/domains.response"; +import { ProfileResponse } from "../../models/response/profile.response"; +import { SendData } from "../../tools/send/models/data/send.data"; +import { SendResponse } from "../../tools/send/models/response/send.response"; +import { SendApiService } from "../../tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "../../tools/send/services/send.service.abstraction"; +import { UserId } from "../../types/guid"; +import { CipherService } from "../../vault/abstractions/cipher.service"; +import { CollectionService } from "../../vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; +import { CipherData } from "../../vault/models/data/cipher.data"; +import { CollectionData } from "../../vault/models/data/collection.data"; +import { FolderData } from "../../vault/models/data/folder.data"; +import { CipherResponse } from "../../vault/models/response/cipher.response"; +import { CollectionDetailsResponse } from "../../vault/models/response/collection.response"; +import { FolderResponse } from "../../vault/models/response/folder.response"; +import { CryptoService } from "../abstractions/crypto.service"; +import { LogService } from "../abstractions/log.service"; +import { StateService } from "../abstractions/state.service"; +import { MessageSender } from "../messaging"; +import { sequentialize } from "../misc/sequentialize"; -import { ApiService } from "../../../abstractions/api.service"; -import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; -import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; -import { ProviderService } from "../../../admin-console/abstractions/provider.service"; -import { OrganizationUserType } from "../../../admin-console/enums"; -import { OrganizationData } from "../../../admin-console/models/data/organization.data"; -import { PolicyData } from "../../../admin-console/models/data/policy.data"; -import { ProviderData } from "../../../admin-console/models/data/provider.data"; -import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; -import { AccountService } from "../../../auth/abstractions/account.service"; -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AvatarService } from "../../../auth/abstractions/avatar.service"; -import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; -import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; -import { TokenService } from "../../../auth/abstractions/token.service"; -import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; -import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; -import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; -import { DomainsResponse } from "../../../models/response/domains.response"; -import { ProfileResponse } from "../../../models/response/profile.response"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { LogService } from "../../../platform/abstractions/log.service"; -import { StateService } from "../../../platform/abstractions/state.service"; -import { MessageSender } from "../../../platform/messaging"; -import { sequentialize } from "../../../platform/misc/sequentialize"; -import { CoreSyncService } from "../../../platform/sync/core-sync.service"; -import { SendData } from "../../../tools/send/models/data/send.data"; -import { SendResponse } from "../../../tools/send/models/response/send.response"; -import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; -import { InternalSendService } from "../../../tools/send/services/send.service.abstraction"; -import { UserId } from "../../../types/guid"; -import { CipherService } from "../../../vault/abstractions/cipher.service"; -import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; -import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; -import { CipherData } from "../../../vault/models/data/cipher.data"; -import { FolderData } from "../../../vault/models/data/folder.data"; -import { CipherResponse } from "../../../vault/models/response/cipher.response"; -import { FolderResponse } from "../../../vault/models/response/folder.response"; -import { CollectionService } from "../../abstractions/collection.service"; -import { CollectionData } from "../../models/data/collection.data"; -import { CollectionDetailsResponse } from "../../models/response/collection.response"; +import { CoreSyncService } from "./core-sync.service"; + +export class DefaultSyncService extends CoreSyncService { + syncInProgress = false; -export class SyncService extends CoreSyncService { constructor( private masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, diff --git a/libs/common/src/platform/sync/index.ts b/libs/common/src/platform/sync/index.ts new file mode 100644 index 00000000000..641d591ff09 --- /dev/null +++ b/libs/common/src/platform/sync/index.ts @@ -0,0 +1,2 @@ +export { SyncService } from "./sync.service"; +export { SyncResponse } from "./sync.response"; diff --git a/libs/common/src/platform/sync/internal.ts b/libs/common/src/platform/sync/internal.ts index f515e90a07e..d74f200e0da 100644 --- a/libs/common/src/platform/sync/internal.ts +++ b/libs/common/src/platform/sync/internal.ts @@ -1 +1,2 @@ +export { DefaultSyncService } from "./default-sync.service"; export { CoreSyncService } from "./core-sync.service"; diff --git a/libs/common/src/vault/types/sync-event-args.ts b/libs/common/src/platform/sync/sync-event-args.ts similarity index 94% rename from libs/common/src/vault/types/sync-event-args.ts rename to libs/common/src/platform/sync/sync-event-args.ts index 4f7d870a58f..10b7b7c4101 100644 --- a/libs/common/src/vault/types/sync-event-args.ts +++ b/libs/common/src/platform/sync/sync-event-args.ts @@ -1,4 +1,4 @@ -import { SyncResponse } from "../models/response/sync.response"; +import { SyncResponse } from "./sync.response"; type SyncStatus = "Started" | "Completed"; diff --git a/libs/common/src/vault/models/response/sync.response.ts b/libs/common/src/platform/sync/sync.response.ts similarity index 69% rename from libs/common/src/vault/models/response/sync.response.ts rename to libs/common/src/platform/sync/sync.response.ts index 42778a8cef9..9e7173d3ebb 100644 --- a/libs/common/src/vault/models/response/sync.response.ts +++ b/libs/common/src/platform/sync/sync.response.ts @@ -1,12 +1,11 @@ -import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; -import { BaseResponse } from "../../../models/response/base.response"; -import { DomainsResponse } from "../../../models/response/domains.response"; -import { ProfileResponse } from "../../../models/response/profile.response"; -import { SendResponse } from "../../../tools/send/models/response/send.response"; - -import { CipherResponse } from "./cipher.response"; -import { CollectionDetailsResponse } from "./collection.response"; -import { FolderResponse } from "./folder.response"; +import { PolicyResponse } from "../../admin-console/models/response/policy.response"; +import { BaseResponse } from "../../models/response/base.response"; +import { DomainsResponse } from "../../models/response/domains.response"; +import { ProfileResponse } from "../../models/response/profile.response"; +import { SendResponse } from "../../tools/send/models/response/send.response"; +import { CipherResponse } from "../../vault/models/response/cipher.response"; +import { CollectionDetailsResponse } from "../../vault/models/response/collection.response"; +import { FolderResponse } from "../../vault/models/response/folder.response"; export class SyncResponse extends BaseResponse { profile?: ProfileResponse; diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts new file mode 100644 index 00000000000..741657d5353 --- /dev/null +++ b/libs/common/src/platform/sync/sync.service.ts @@ -0,0 +1,58 @@ +import { + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../../models/response/notification.response"; + +/** + * A class encapsulating sync operations and data. + */ +export abstract class SyncService { + /** + * A boolean indicating if a sync is currently in progress via this instance and this instance only. + * + * @deprecated Trusting this property is not safe as it only tells if the current instance is currently + * doing a sync operation but does not tell if another instance of SyncService is doing a sync operation. + */ + abstract syncInProgress: boolean; + + /** + * Gets the date of the last sync for the currently active user. + * + * @returns The date of the last sync or null if there is no active user or the active user has not synced before. + */ + abstract getLastSync(): Promise; + + /** + * Updates a users last sync date. + * @param date The date to be set as the users last sync date. + * @param userId The userId of the user to update the last sync date for. + */ + abstract setLastSync(date: Date, userId?: string): Promise; + + /** + * Optionally does a full sync operation including going to the server to gather the source + * of truth and set that data to state. + * @param forceSync A boolean dictating if a sync should be forced. If `true` a sync will happen + * as long as the current user is authenticated. If `false` it will only sync if either a sync + * has not happened before or the last sync date for the active user is before their account + * revision date. Try to always use `false` if possible. + * + * @param allowThrowOnError A boolean dictating whether or not caught errors should be rethrown. + * `true` if they can be rethrown, `false` if they should not be rethrown. + */ + abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; + + abstract syncUpsertFolder( + notification: SyncFolderNotification, + isEdit: boolean, + ): Promise; + abstract syncDeleteFolder(notification: SyncFolderNotification): Promise; + abstract syncUpsertCipher( + notification: SyncCipherNotification, + isEdit: boolean, + ): Promise; + abstract syncDeleteCipher(notification: SyncFolderNotification): Promise; + abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise; + abstract syncDeleteSend(notification: SyncSendNotification): Promise; +} diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index bae9a34c10c..61cfcb25837 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -121,6 +121,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; import { Utils } from "../platform/misc/utils"; +import { SyncResponse } from "../platform/sync"; import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; @@ -142,7 +143,6 @@ import { CollectionResponse, } from "../vault/models/response/collection.response"; import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response"; -import { SyncResponse } from "../vault/models/response/sync.response"; /** * @deprecated The `ApiService` class is deprecated and calls should be extracted into individual diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index cae6fedbb8b..51589f52fae 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -21,8 +21,8 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { MessagingService } from "../platform/abstractions/messaging.service"; import { StateService } from "../platform/abstractions/state.service"; +import { SyncService } from "../platform/sync/sync.service"; import { UserId } from "../types/guid"; -import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection: signalR.HubConnection; diff --git a/libs/common/src/vault/abstractions/sync/sync-notifier.service.abstraction.ts b/libs/common/src/vault/abstractions/sync/sync-notifier.service.abstraction.ts deleted file mode 100644 index f519850aa66..00000000000 --- a/libs/common/src/vault/abstractions/sync/sync-notifier.service.abstraction.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Observable } from "rxjs"; - -import { SyncEventArgs } from "../../types/sync-event-args"; - -export abstract class SyncNotifierService { - sync$: Observable; - next: (event: SyncEventArgs) => void; -} diff --git a/libs/common/src/vault/abstractions/sync/sync.service.abstraction.ts b/libs/common/src/vault/abstractions/sync/sync.service.abstraction.ts index cfe73317555..1a1b8e7c758 100644 --- a/libs/common/src/vault/abstractions/sync/sync.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/sync/sync.service.abstraction.ts @@ -1,19 +1,2 @@ -import { - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from "../../../models/response/notification.response"; - -export abstract class SyncService { - syncInProgress: boolean; - - getLastSync: () => Promise; - setLastSync: (date: Date, userId?: string) => Promise; - fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; - syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; - syncDeleteFolder: (notification: SyncFolderNotification) => Promise; - syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; - syncDeleteCipher: (notification: SyncFolderNotification) => Promise; - syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise; - syncDeleteSend: (notification: SyncSendNotification) => Promise; -} +// TEMP: Re-export of original SyncService location to allow for team specific PR's +export { SyncService } from "../../../platform/sync"; diff --git a/libs/common/src/vault/services/sync/sync-notifier.service.ts b/libs/common/src/vault/services/sync/sync-notifier.service.ts deleted file mode 100644 index 870ccfb849c..00000000000 --- a/libs/common/src/vault/services/sync/sync-notifier.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Subject } from "rxjs"; - -import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/sync-notifier.service.abstraction"; -import { SyncEventArgs } from "../../types/sync-event-args"; - -/** - * This class should most likely have 0 dependencies because it will hopefully - * be rolled into SyncService once upon a time. - */ -export class SyncNotifierService implements SyncNotifierServiceAbstraction { - private _sync = new Subject(); - - sync$ = this._sync.asObservable(); - - next(event: SyncEventArgs): void { - this._sync.next(event); - } -} From 1cfbcf4ee09b0285767dc5607fb1df99eab88c33 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 5 Jun 2024 08:10:36 -0700 Subject: [PATCH 06/11] [PM-8334] Sort ciphers after autofill (#9511) * [PM-8334] Add localData$ to CipherService and watch it for updates * Fix leftover tw-fixed class * [PM-8334] Fix tests --- .../vault-popup-items.service.spec.ts | 32 ++++++++++++++++++- .../services/vault-popup-items.service.ts | 7 ++-- .../src/vault/abstractions/cipher.service.ts | 3 ++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 6a7cbbfc1a9..b7091eb87bf 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -6,6 +6,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; +import { ObservableTracker } from "@bitwarden/common/spec"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -50,7 +51,8 @@ describe("VaultPopupItemsService", () => { cipherList[3].favorite = true; cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); - cipherServiceMock.ciphers$ = new BehaviorSubject(null).asObservable(); + cipherServiceMock.ciphers$ = new BehaviorSubject(null); + cipherServiceMock.localData$ = new BehaviorSubject(null); searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => ciphers.filter((c) => ["0", "1"].includes(c.id)), @@ -123,6 +125,34 @@ describe("VaultPopupItemsService", () => { }); }); + it("should update cipher list when cipherService.ciphers$ emits", async () => { + const tracker = new ObservableTracker(service.autoFillCiphers$); + + await tracker.expectEmission(); + + (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + + await tracker.expectEmission(); + + // Should only emit twice + expect(tracker.emissions.length).toBe(2); + await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded"); + }); + + it("should update cipher list when cipherService.localData$ emits", async () => { + const tracker = new ObservableTracker(service.autoFillCiphers$); + + await tracker.expectEmission(); + + (cipherServiceMock.localData$ as BehaviorSubject).next(null); + + await tracker.expectEmission(); + + // Should only emit twice + expect(tracker.emissions.length).toBe(2); + await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded"); + }); + describe("autoFillCiphers$", () => { it("should return empty array if there is no current tab", (done) => { jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index ab98722d060..189ce2c09f9 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -5,6 +5,7 @@ import { distinctUntilKeyChanged, from, map, + merge, Observable, of, shareReplay, @@ -78,10 +79,12 @@ export class VaultPopupItemsService { * Observable that contains the list of all decrypted ciphers. * @private */ - private _cipherList$: Observable = this.cipherService.ciphers$.pipe( + private _cipherList$: Observable = merge( + this.cipherService.ciphers$, + this.cipherService.localData$, + ).pipe( runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), - map((ciphers) => Object.values(ciphers)), switchMap((ciphers) => combineLatest([ this.organizationService.organizations$, diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 34bc8193553..2c0676f6442 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,7 @@ import { Observable } from "rxjs"; +import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; + import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; @@ -14,6 +16,7 @@ import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService { cipherViews$: Observable>; ciphers$: Observable>; + localData$: Observable>; /** * An observable monitoring the add/edit cipher info saved to memory. */ From 419c107f87a9b1d57cfc27f643913b9b60330c27 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Wed, 5 Jun 2024 11:39:40 -0400 Subject: [PATCH 07/11] [CL-243] honor initial disabled state in `bitFormButton` (#9510) * honor initial disabled state * update story --- libs/components/src/async-actions/form-button.directive.ts | 4 ++-- libs/components/src/async-actions/in-forms.stories.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index 5fe40423644..4e0facf17b2 100644 --- a/libs/components/src/async-actions/form-button.directive.ts +++ b/libs/components/src/async-actions/form-button.directive.ts @@ -40,13 +40,13 @@ export class BitFormButtonDirective implements OnDestroy { if (this.type === "submit") { buttonComponent.loading = loading; } else { - buttonComponent.disabled = loading; + buttonComponent.disabled = this.disabled || loading; } }); submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { if (this.disabled !== false) { - buttonComponent.disabled = disabled; + buttonComponent.disabled = this.disabled || disabled; } }); } diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index ff060c6a7de..ec6005dd607 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -33,6 +33,7 @@ const template = ` + `; From 24fb3f71f1689d0df46bbc9fad756b6427d32f83 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:29:51 +0530 Subject: [PATCH 08/11] [PM-2057] update two factor email dialog (#8974) * migrating two factor email component * two factor email component migration * two factor email component migration * two factor email component migration --- .../settings/two-factor-email.component.html | 150 ++++++------------ .../settings/two-factor-email.component.ts | 77 ++++++--- .../settings/two-factor-setup.component.ts | 12 +- 3 files changed, 118 insertions(+), 121 deletions(-) diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.html b/apps/web/src/app/auth/settings/two-factor-email.component.html index 93a6b0bb18a..cf1dba98842 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.html +++ b/apps/web/src/app/auth/settings/two-factor-email.component.html @@ -1,101 +1,53 @@ - + + 2. {{ "twoFactorEmailEnterCode" | i18n }} + + + + + + + + + + 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 7a2e6de5801..8a5c0292230 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 @@ -1,4 +1,6 @@ -import { Component } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -19,18 +21,22 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component"; @Component({ selector: "app-two-factor-email", templateUrl: "two-factor-email.component.html", + outputs: ["onUpdated"], }) export class TwoFactorEmailComponent extends TwoFactorBaseComponent { + @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Email; - email: string; - token: string; sentEmail: string; formPromise: Promise; emailPromise: Promise; - override componentName = "app-two-factor-email"; + formGroup = this.formBuilder.group({ + token: [null], + email: ["", [Validators.email, Validators.required]], + }); constructor( + @Inject(DIALOG_DATA) protected data: AuthResponse, apiService: ApiService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -38,6 +44,8 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { userVerificationService: UserVerificationService, private accountService: AccountService, dialogService: DialogService, + private formBuilder: FormBuilder, + private dialogRef: DialogRef, ) { super( apiService, @@ -48,31 +56,49 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { dialogService, ); } + get token() { + return this.formGroup.get("token").value; + } + set token(value: string) { + this.formGroup.get("token").setValue(value); + } + get email() { + return this.formGroup.get("email").value; + } + set email(value: string) { + this.formGroup.get("email").setValue(value); + } + + async ngOnInit() { + await this.auth(this.data); + } auth(authResponse: AuthResponse) { super.auth(authResponse); return this.processResponse(authResponse.response); } - submit() { + submit = async () => { if (this.enabled) { - return super.disable(this.formPromise); + await this.disableEmail(); + this.onChangeStatus.emit(false); } else { - return this.enable(); + await this.enable(); + this.onChangeStatus.emit(true); } + }; + + private disableEmail() { + return super.disable(this.formPromise); } - async sendEmail() { - try { - const request = await this.buildRequestModel(TwoFactorEmailRequest); - request.email = this.email; - this.emailPromise = this.apiService.postTwoFactorEmailSetup(request); - await this.emailPromise; - this.sentEmail = this.email; - } catch (e) { - this.logService.error(e); - } - } + sendEmail = async () => { + const request = await this.buildRequestModel(TwoFactorEmailRequest); + request.email = this.email; + this.emailPromise = this.apiService.postTwoFactorEmailSetup(request); + await this.emailPromise; + this.sentEmail = this.email; + }; protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest); @@ -86,6 +112,10 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { }); } + onClose = () => { + this.dialogRef.close(this.enabled); + }; + private async processResponse(response: TwoFactorEmailResponse) { this.token = null; this.email = response.email; @@ -96,4 +126,15 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { ); } } + /** + * Strongly typed helper to open a TwoFactorEmailComponentComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open( + dialogService: DialogService, + config: DialogConfig>, + ) { + return dialogService.open(TwoFactorEmailComponent, config); + } } diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index dc7871baf94..dd4c69aa277 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; @@ -178,11 +179,14 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent); - await emailComp.auth(result); - emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Email); + const authComp: DialogRef = TwoFactorEmailComponent.open(this.dialogService, { + data: result, }); + authComp.componentInstance.onChangeStatus + .pipe(takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + this.updateStatus(enabled, TwoFactorProviderType.Email); + }); break; } case TwoFactorProviderType.WebAuthn: { From 1cec69e3776b4bb47f9adbe5c0546399fa4dbadb Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:44:33 +0530 Subject: [PATCH 09/11] [PM 2164] api-key component migration (#8562) * api-key component migration * api-key component migration * api-key component migration * api-key component migration --- .../settings/account.component.ts | 44 +++---- .../settings/security/api-key.component.html | 114 +++++++----------- .../settings/security/api-key.component.ts | 60 +++++---- .../security/security-keys.component.ts | 46 +++---- 4 files changed, 126 insertions(+), 138 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 41cf9b9e8f5..5e083de9cc0 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -216,31 +216,33 @@ export class AccountComponent { }; async viewApiKey() { - await this.modalService.openViewRef(ApiKeyComponent, this.apiKeyModalRef, (comp) => { - comp.keyType = "organization"; - comp.entityId = this.organizationId; - comp.postKey = this.organizationApiService.getOrCreateApiKey.bind( - this.organizationApiService, - ); - comp.scope = "api.organization"; - comp.grantType = "client_credentials"; - comp.apiKeyTitle = "apiKey"; - comp.apiKeyWarning = "apiKeyWarning"; - comp.apiKeyDescription = "apiKeyDesc"; + await ApiKeyComponent.open(this.dialogService, { + data: { + keyType: "organization", + entityId: this.organizationId, + postKey: this.organizationApiService.getOrCreateApiKey.bind(this.organizationApiService), + scope: "api.organization", + grantType: "client_credentials", + apiKeyTitle: "apiKey", + apiKeyWarning: "apiKeyWarning", + apiKeyDescription: "apiKeyDesc", + }, }); } async rotateApiKey() { - await this.modalService.openViewRef(ApiKeyComponent, this.rotateApiKeyModalRef, (comp) => { - comp.keyType = "organization"; - comp.isRotation = true; - comp.entityId = this.organizationId; - comp.postKey = this.organizationApiService.rotateApiKey.bind(this.organizationApiService); - comp.scope = "api.organization"; - comp.grantType = "client_credentials"; - comp.apiKeyTitle = "apiKey"; - comp.apiKeyWarning = "apiKeyWarning"; - comp.apiKeyDescription = "apiKeyRotateDesc"; + await ApiKeyComponent.open(this.dialogService, { + data: { + keyType: "organization", + isRotation: true, + entityId: this.organizationId, + postKey: this.organizationApiService.rotateApiKey.bind(this.organizationApiService), + scope: "api.organization", + grantType: "client_credentials", + apiKeyTitle: "apiKey", + apiKeyWarning: "apiKeyWarning", + apiKeyDescription: "apiKeyRotateDesc", + }, }); } } diff --git a/apps/web/src/app/auth/settings/security/api-key.component.html b/apps/web/src/app/auth/settings/security/api-key.component.html index 1402a993881..118b17643cc 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.html +++ b/apps/web/src/app/auth/settings/security/api-key.component.html @@ -1,72 +1,42 @@ - +
+ + {{ data.apiKeyTitle | i18n }} +
+

{{ data.apiKeyDescription | i18n }}

+ + + {{ data.apiKeyWarning | i18n }} + +

+ client_id:
+ {{ clientId }} +

+

+ client_secret:
+ {{ clientSecret }} +

+

+ scope:
+ {{ data.scope }} +

+

+ grant_type:
+ {{ data.grantType }} +

+
+
+
+ + +
+
+
diff --git a/apps/web/src/app/auth/settings/security/api-key.component.ts b/apps/web/src/app/auth/settings/security/api-key.component.ts index 9d005562725..d171bc35617 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.ts +++ b/apps/web/src/app/auth/settings/security/api-key.component.ts @@ -1,46 +1,58 @@ -import { Component } from "@angular/core"; +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response"; import { Verification } from "@bitwarden/common/auth/types/verification"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { DialogService } from "@bitwarden/components"; -@Component({ - selector: "app-api-key", - templateUrl: "api-key.component.html", -}) -export class ApiKeyComponent { +export type ApiKeyDialogData = { keyType: string; - isRotation: boolean; - postKey: (entityId: string, request: SecretVerificationRequest) => Promise; + isRotation?: boolean; entityId: string; + postKey: (entityId: string, request: SecretVerificationRequest) => Promise; scope: string; grantType: string; apiKeyTitle: string; apiKeyWarning: string; apiKeyDescription: string; - - masterPassword: Verification; - formPromise: Promise; +}; +@Component({ + selector: "app-api-key", + templateUrl: "api-key.component.html", +}) +export class ApiKeyComponent { clientId: string; clientSecret: string; + formGroup = this.formBuilder.group({ + masterPassword: [null as Verification, [Validators.required]], + }); constructor( + @Inject(DIALOG_DATA) protected data: ApiKeyDialogData, + private formBuilder: FormBuilder, private userVerificationService: UserVerificationService, - private logService: LogService, ) {} - async submit() { - try { - this.formPromise = this.userVerificationService - .buildRequest(this.masterPassword) - .then((request) => this.postKey(this.entityId, request)); - const response = await this.formPromise; - this.clientSecret = response.apiKey; - this.clientId = `${this.keyType}.${this.entityId}`; - } catch (e) { - this.logService.error(e); + submit = async () => { + if (this.formGroup.invalid) { + this.formGroup.markAllAsTouched(); + return; } - } + const response = await this.userVerificationService + .buildRequest(this.formGroup.value.masterPassword) + .then((request) => this.data.postKey(this.data.entityId, request)); + this.clientSecret = response.apiKey; + this.clientId = `${this.data.keyType}.${this.data.entityId}`; + }; + /** + * Strongly typed helper to open a ApiKeyComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open = (dialogService: DialogService, config: DialogConfig) => { + return dialogService.open(ApiKeyComponent, config); + }; } diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index e29417fad74..8de629dc83e 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DialogService } from "@bitwarden/components"; import { ApiKeyComponent } from "./api-key.component"; @@ -22,8 +22,8 @@ export class SecurityKeysComponent implements OnInit { constructor( private userVerificationService: UserVerificationService, private stateService: StateService, - private modalService: ModalService, private apiService: ApiService, + private dialogService: DialogService, ) {} async ngOnInit() { @@ -32,30 +32,34 @@ export class SecurityKeysComponent implements OnInit { async viewUserApiKey() { const entityId = await this.stateService.getUserId(); - await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => { - comp.keyType = "user"; - comp.entityId = entityId; - comp.postKey = this.apiService.postUserApiKey.bind(this.apiService); - comp.scope = "api"; - comp.grantType = "client_credentials"; - comp.apiKeyTitle = "apiKey"; - comp.apiKeyWarning = "userApiKeyWarning"; - comp.apiKeyDescription = "userApiKeyDesc"; + await ApiKeyComponent.open(this.dialogService, { + data: { + keyType: "user", + entityId: entityId, + postKey: this.apiService.postUserApiKey.bind(this.apiService), + scope: "api", + grantType: "client_credentials", + apiKeyTitle: "apiKey", + apiKeyWarning: "userApiKeyWarning", + apiKeyDescription: "userApiKeyDesc", + }, }); } async rotateUserApiKey() { const entityId = await this.stateService.getUserId(); - await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, (comp) => { - comp.keyType = "user"; - comp.isRotation = true; - comp.entityId = entityId; - comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService); - comp.scope = "api"; - comp.grantType = "client_credentials"; - comp.apiKeyTitle = "apiKey"; - comp.apiKeyWarning = "userApiKeyWarning"; - comp.apiKeyDescription = "apiKeyRotateDesc"; + await ApiKeyComponent.open(this.dialogService, { + data: { + keyType: "user", + isRotation: true, + entityId: entityId, + postKey: this.apiService.postUserRotateApiKey.bind(this.apiService), + scope: "api", + grantType: "client_credentials", + apiKeyTitle: "apiKey", + apiKeyWarning: "userApiKeyWarning", + apiKeyDescription: "apiKeyRotateDesc", + }, }); } } From 6e557338730479766bacb8595efe787c271d7a1b Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:22:04 +0530 Subject: [PATCH 10/11] PM-4951 Migrate Recover Two Factor Component (#9170) * PM-4951 Migrate Recover Two Factor Component * PM-4951 Addressed review comments * PM-4951 Addressed review comments * update route * add type safety to data properties --------- Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com> --- .../auth/recover-two-factor.component.html | 112 ++++++------------ .../app/auth/recover-two-factor.component.ts | 60 +++++----- apps/web/src/app/oss-routing.module.ts | 34 +++++- 3 files changed, 97 insertions(+), 109 deletions(-) diff --git a/apps/web/src/app/auth/recover-two-factor.component.html b/apps/web/src/app/auth/recover-two-factor.component.html index 11d281b742b..e3641765800 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.html +++ b/apps/web/src/app/auth/recover-two-factor.component.html @@ -1,76 +1,40 @@ -
-
-
-

{{ "recoverAccountTwoStep" | i18n }}

-
-
-

- {{ "recoverAccountTwoStepDesc" | i18n }} - {{ "learnMore" | i18n }} -

-
- - -
-
- - -
-
- - -
-
-
- - - {{ "cancel" | i18n }} - -
-
-
-
+ +

+ {{ "recoverAccountTwoStepDesc" | i18n }} + {{ "learnMore" | i18n }} +

+ + {{ "emailAddress" | i18n }} + + + + {{ "masterPass" | i18n }} + + + + {{ "recoveryCodeTitle" | i18n }} + + +
+
+ + + {{ "cancel" | i18n }} +
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 145c46c8df5..4996dbe0a50 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -1,4 +1,5 @@ import { Component } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; @@ -6,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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"; @Component({ @@ -14,10 +14,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl templateUrl: "recover-two-factor.component.html", }) export class RecoverTwoFactorComponent { - email: string; - masterPassword: string; - recoveryCode: string; - formPromise: Promise; + protected formGroup = new FormGroup({ + email: new FormControl(null, [Validators.required]), + masterPassword: new FormControl(null, [Validators.required]), + recoveryCode: new FormControl(null, [Validators.required]), + }); constructor( private router: Router, @@ -26,31 +27,32 @@ export class RecoverTwoFactorComponent { private i18nService: I18nService, private cryptoService: CryptoService, private loginStrategyService: LoginStrategyServiceAbstraction, - private logService: LogService, ) {} - async submit() { - try { - const request = new TwoFactorRecoveryRequest(); - request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase(); - request.email = this.email.trim().toLowerCase(); - const key = await this.loginStrategyService.makePreloginKey( - this.masterPassword, - request.email, - ); - request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key); - this.formPromise = this.apiService.postTwoFactorRecover(request); - await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("twoStepRecoverDisabled"), - ); - // 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.logService.error(e); - } + get email(): string { + return this.formGroup.value.email; } + + get masterPassword(): string { + return this.formGroup.value.masterPassword; + } + + get recoveryCode(): string { + return this.formGroup.value.recoveryCode; + } + + submit = async () => { + const request = new TwoFactorRecoveryRequest(); + request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase(); + request.email = this.email.trim().toLowerCase(); + 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"), + ); + await this.router.navigate(["/"]); + }; } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index c7b4631fa30..8509e987eb4 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -7,7 +7,9 @@ import { redirectGuard, tdeDecryptionRequiredGuard, UnauthGuard, + unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/auth/angular"; import { flagEnabled, Flags } from "../utils/flags"; @@ -40,6 +42,7 @@ import { UpdatePasswordComponent } from "./auth/update-password.component"; import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component"; +import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { DataProperties } from "./core"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; @@ -141,12 +144,6 @@ const routes: Routes = [ data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false } satisfies DataProperties, }, { path: "recover", pathMatch: "full", redirectTo: "recover-2fa" }, - { - path: "recover-2fa", - component: RecoverTwoFactorComponent, - canActivate: [UnauthGuard], - data: { titleId: "recoverAccountTwoStep" } satisfies DataProperties, - }, { path: "recover-delete", component: RecoverDeleteComponent, @@ -203,6 +200,31 @@ const routes: Routes = [ }, ], }, + { + path: "", + component: AnonLayoutWrapperComponent, + children: [ + { + path: "recover-2fa", + canActivate: [unauthGuardFn()], + children: [ + { + path: "", + component: RecoverTwoFactorComponent, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + data: { + pageTitle: "recoverAccountTwoStep", + titleId: "recoverAccountTwoStep", + } satisfies DataProperties & AnonLayoutWrapperData, + }, + ], + }, { path: "", component: UserLayoutComponent, From bf51469404a6fe97c11723c02511a6029f59e2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Wed, 5 Jun 2024 14:58:28 -0400 Subject: [PATCH 11/11] specify generator algorithm during TDE (#9519) --- .../src/auth/components/login-via-auth-request.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 401abab3b19..f898fa4ee8d 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 @@ -246,7 +246,10 @@ export class LoginViaAuthRequestComponent const deviceIdentifier = await this.appIdService.getAppId(); const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey); - const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 }); + const accessCode = await this.passwordGenerationService.generatePassword({ + type: "password", + length: 25, + }); this.fingerprintPhrase = ( await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)