diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 63721466f6..a8654c92f0 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 ace9af3dfa..d61fa3b19c 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-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 321717285a..e610032160 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/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 4d2674fd70..24ca030284 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 deb4434df4..b46b4cf9ff 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/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index f99d3cbb30..9f38fd61fa 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 5e9487ac88..5e91f196a3 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.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 6a7cbbfc1a..b7091eb87b 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 eacb8e013e..189ce2c09f 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, @@ -38,7 +39,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 @@ -77,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$, @@ -105,7 +109,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 +183,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 +246,7 @@ export class VaultPopupItemsService { } applyFilter(newSearchText: string) { - this.searchText$.next(newSearchText); + this._searchText$.next(newSearchText); } /** diff --git a/apps/browser/src/vault/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts index 3fe4de9eb5..16f388804b 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 53039e9147..ff4eb52b84 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 073b9b5df4..c3c6f63753 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 561e9b2df9..e4fdd17dc1 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 0452e9be83..8793587300 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 208bbc70f0..37992ecea0 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/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 41cf9b9e8f..5e083de9cc 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/app.component.ts b/apps/web/src/app/app.component.ts index 254f23eeb2..c9fbf359f0 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/auth/recover-two-factor.component.html b/apps/web/src/app/auth/recover-two-factor.component.html index 11d281b742..e364176580 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 145c46c8df..4996dbe0a5 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/auth/settings/security/api-key.component.html b/apps/web/src/app/auth/settings/security/api-key.component.html index 1402a99388..118b17643c 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 9d00556272..d171bc3561 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 e29417fad7..8de629dc83 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", + }, }); } } 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 93a6b0bb18..cf1dba9884 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 7a2e6de580..8a5c029223 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 dc7871baf9..dd4c69aa27 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: { 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 0997462ce9..33a534bacf 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 88a37a28aa..6b3a93548b 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 490ebafbff..a95efe32e4 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 25ac3a7a15..e11cf602ad 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 a0db7b5a20..b6282f1e7b 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() { diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 1ce8d4d227..757b8220f3 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/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index c7b4631fa3..8509e987eb 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, 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 8dd63e62dd..5a138c3147 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 ca04b3aa51..ae3a065778 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 514cb8150d..dfdce5c818 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 869cbaab1b..9a677af7b5 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 048c182900..8c676bdb9d 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 73e4f74e63..ed43849d62 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 109ecea035..5058288487 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 0000000000..641d591ff0 --- /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 f515e90a07..d74f200e0d 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 4f7d870a58..10b7b7c410 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 42778a8cef..9e7173d3eb 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 0000000000..741657d535 --- /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 bae9a34c10..61cfcb2583 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 cae6fedbb8..51589f52fa 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/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 34bc819355..2c0676f644 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. */ 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 f519850aa6..0000000000 --- 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 cfe7331755..1a1b8e7c75 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 870ccfb849..0000000000 --- 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); - } -} diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index 5fe4042364..4e0facf17b 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 ff060c6a7d..ec6005dd60 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 = ` + `; diff --git a/libs/components/src/icon/icons/index.ts b/libs/components/src/icon/icons/index.ts index 9de81f1991..ea583031f6 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 0000000000..f68e67f88c --- /dev/null +++ b/libs/components/src/icon/icons/no-results.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "../icon"; + +export const NoResults = svgIcon` + + + + + + + + + + + + + + +`;