From be51f1934a34360c5a54faa3d75c3f30ee1b64e9 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 7 May 2024 11:02:50 -0400 Subject: [PATCH 01/14] [AC-1121] Collections Add Access filter and badge (#8404) * added bit toggle group for add access filter to AC collections --- .../vault-collection-row.component.html | 12 ++- .../vault-collection-row.component.ts | 1 + .../vault-items/vault-items.component.html | 6 +- .../vault-items/vault-items.component.ts | 51 +++++++++ .../vault/core/views/collection-admin.view.ts | 29 +++++ .../app/vault/org-vault/vault.component.html | 16 +++ .../app/vault/org-vault/vault.component.ts | 100 ++++++++++++++++-- apps/web/src/locales/en/messages.json | 6 ++ .../vault/models/domain/collection.spec.ts | 1 + .../src/vault/models/view/collection.view.ts | 1 + 10 files changed, 214 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index d03b6dcc38..897d360b4b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -20,7 +20,7 @@ bitLink [disabled]="disabled" type="button" - class="tw-w-full tw-truncate tw-text-start tw-leading-snug" + class="tw-flex tw-w-full tw-text-start tw-leading-snug" linkType="secondary" title="{{ 'viewCollectionWithName' | i18n: collection.name }}" [routerLink]="[]" @@ -28,7 +28,15 @@ queryParamsHandling="merge" appStopProp > - {{ collection.name }} + {{ collection.name }} +
+ {{ "addAccess" | i18n }} +
diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 8bf7779f88..4a9667f8b8 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -21,6 +21,7 @@ import { RowHeightClass } from "./vault-items.component"; }) export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; + protected Unassigned = "unassigned"; @Input() disabled: boolean; @Input() collection: CollectionView; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index c63273fabd..ba69c038fb 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -99,8 +99,12 @@ (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > + o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user without edit access should not see the Edit option unless that user has "Can Manage" access to a collection + if ( + !collection.manage && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.editAnyCollection + ) { + return false; + } + //Owner/Admin and Custom Users with Edit can see Edit and Access of Orphaned Collections + if ( + collection.addAccess && + collection.id !== Unassigned && + ((organization?.type === OrganizationUserType.Custom && + organization?.permissions.editAnyCollection) || + organization.isAdmin || + organization.isOwner) + ) { + return true; + } + } return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); } @@ -111,6 +136,32 @@ export class VaultItemsComponent { } const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user with only edit access should not see the Delete button for orphaned collections + if ( + collection.addAccess && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.deleteAnyCollection && + organization?.permissions.editAnyCollection + ) { + return false; + } + + // Owner/Admin with no access to a collection will not see Delete + if ( + !collection.assigned && + !collection.addAccess && + (organization.isAdmin || organization.isOwner) && + !( + organization?.type === OrganizationUserType.Custom && + organization?.permissions.deleteAnyCollection + ) + ) { + return false; + } + } + return collection.canDelete(organization); } diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 2be84b0d24..cc217fc9ce 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -1,3 +1,4 @@ +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -7,6 +8,7 @@ import { CollectionAccessSelectionView } from "../../../admin-console/organizati export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; users: CollectionAccessSelectionView[] = []; + addAccess: boolean; /** * Flag indicating the user has been explicitly assigned to this Collection @@ -31,6 +33,33 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } + groupsCanManage() { + if (this.groups.length === 0) { + return this.groups; + } + + const returnedGroups = this.groups.filter((group) => { + if (group.manage) { + return group; + } + }); + return returnedGroups; + } + + usersCanManage(revokedUsers: OrganizationUserUserDetailsResponse[]) { + if (this.users.length === 0) { + return this.users; + } + + const returnedUsers = this.users.filter((user) => { + const isRevoked = revokedUsers.some((revoked) => revoked.id === user.id); + if (user.manage && !isRevoked) { + return user; + } + }); + return returnedUsers; + } + /** * Whether the current user can edit the collection, including user and group access */ diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index f815fccb21..af7b5059e5 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -26,6 +26,20 @@
+ + + {{ "all" | i18n }} + + + + {{ "addAccess" | i18n }} + + {{ trashCleanupWarning }} @@ -54,6 +68,8 @@ [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" + [addAccessStatus]="addAccessStatus$ | async" + [addAccessToggle]="showAddAccessToggle" > 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 243dedef93..4e06f7668c 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -36,6 +36,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -102,6 +105,11 @@ import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; +enum AddAccessStatusType { + All = 0, + AddAccess = 1, +} + @Component({ selector: "app-org-vault", templateUrl: "vault.component.html", @@ -122,6 +130,7 @@ export class VaultComponent implements OnInit, OnDestroy { trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); + protected showAddAccessToggle = false; protected noItemIcon = Icons.Search; protected performingInitialLoad = true; protected refreshing = false; @@ -149,10 +158,12 @@ export class VaultComponent implements OnInit, OnDestroy { protected get flexibleCollectionsV1Enabled(): boolean { return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; } + protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); + protected addAccessStatus$ = new BehaviorSubject(0); constructor( private route: ActivatedRoute, @@ -181,6 +192,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, + private organizationUserService: OrganizationUserService, protected configService: ConfigService, ) {} @@ -241,6 +253,11 @@ export class VaultComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe((activeFilter) => { this.activeFilter = activeFilter; + + // watch the active filters. Only show toggle when viewing the collections filter + if (!this.activeFilter.collectionId) { + this.showAddAccessToggle = false; + } }); this.searchText$ @@ -309,6 +326,10 @@ export class VaultComponent implements OnInit, OnDestroy { const allCiphers$ = organization$.pipe( concatMap(async (organization) => { + // If user swaps organization reset the addAccessToggle + if (!this.showAddAccessToggle || organization) { + this.addAccessToggle(0); + } let ciphers; if (this.flexibleCollectionsV1Enabled) { @@ -348,9 +369,21 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( + // This will be passed into the usersCanManage call + this.orgRevokedUsers = ( + await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$)) + ).data.filter((user: OrganizationUserUserDetailsResponse) => { + return user.status === -1; + }); + + const collections$ = combineLatest([ + nestedCollections$, + filter$, + this.currentSearchText$, + this.addAccessStatus$, + ]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText, addAccessStatus]) => { if ( filter.collectionId === Unassigned || (filter.collectionId === undefined && filter.type !== undefined) @@ -358,26 +391,30 @@ export class VaultComponent implements OnInit, OnDestroy { return []; } + this.showAddAccessToggle = false; let collectionsToReturn = []; if (filter.collectionId === undefined || filter.collectionId === All) { - collectionsToReturn = collections.map((c) => c.node); + collectionsToReturn = await this.addAccessCollectionsMap(collections); } else { const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( collections, filter.collectionId, ); - collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + collectionsToReturn = await this.addAccessCollectionsMap(selectedCollection?.children); } if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, - (collection) => collection.name, - (collection) => collection.id, + (collection: CollectionAdminView) => collection.name, + (collection: CollectionAdminView) => collection.id, ); } + if (addAccessStatus === 1 && this.showAddAccessToggle) { + collectionsToReturn = collectionsToReturn.filter((c: any) => c.addAccess); + } return collectionsToReturn; }), takeUntil(this.destroy$), @@ -586,6 +623,57 @@ export class VaultComponent implements OnInit, OnDestroy { ); } + // Update the list of collections to see if any collection is orphaned + // and will receive the addAccess badge / be filterable by the user + async addAccessCollectionsMap(collections: TreeNode[]) { + let mappedCollections; + const { type, allowAdminAccessToAllCollectionItems, permissions } = this.organization; + + const canEditCiphersCheck = + this._flexibleCollectionsV1FlagEnabled && + !this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled); + + // This custom type check will show addAccess badge for + // Custom users with canEdit access AND owner/admin manage access setting is OFF + const customUserCheck = + this._flexibleCollectionsV1FlagEnabled && + !allowAdminAccessToAllCollectionItems && + type === OrganizationUserType.Custom && + permissions.editAnyCollection; + + // If Custom user has Delete Only access they will not see Add Access toggle + const customUserOnlyDelete = + this.flexibleCollectionsV1Enabled && + type === OrganizationUserType.Custom && + permissions.deleteAnyCollection && + !permissions.editAnyCollection; + + if (!customUserOnlyDelete && (canEditCiphersCheck || customUserCheck)) { + mappedCollections = collections.map((c: TreeNode) => { + const groupsCanManage = c.node.groupsCanManage(); + const usersCanManage = c.node.usersCanManage(this.orgRevokedUsers); + if ( + groupsCanManage.length === 0 && + usersCanManage.length === 0 && + c.node.id !== Unassigned + ) { + c.node.addAccess = true; + this.showAddAccessToggle = true; + } else { + c.node.addAccess = false; + } + return c.node; + }); + } else { + mappedCollections = collections.map((c: TreeNode) => c.node); + } + return mappedCollections; + } + + addAccessToggle(e: any) { + this.addAccessStatus$.next(e); + } + get loading() { return this.refreshing || this.processingEvent; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4840003abd..f032e822f8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index cd1cab8b42..4ee725be57 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -61,6 +61,7 @@ describe("Collection", () => { const view = await collection.decrypt(); expect(view).toEqual({ + addAccess: false, externalId: "extId", hidePasswords: false, id: "id", diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 86766bdeac..f742b283bd 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject { readOnly: boolean = null; hidePasswords: boolean = null; manage: boolean = null; + addAccess: boolean = false; assigned: boolean = null; constructor(c?: Collection | CollectionAccessDetailsResponse) { From c241aba025bfa75fe589767d766cd53c73f5acb0 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 7 May 2024 10:00:47 -0700 Subject: [PATCH 02/14] [AC-2555] Cipher collections dialog merge fixes (#9036) * [AC-2555] Fix missing feature flags in CollectionsComponent * [AC-2555] Do not filter collections when opening the cipher collections dialog in the org vault --- .../components/vault/collections.component.ts | 3 +++ .../vault/app/vault/collections.component.ts | 3 +++ .../individual-vault/collections.component.ts | 5 +++- .../vault/org-vault/collections.component.ts | 3 +++ .../app/vault/org-vault/vault.component.ts | 23 ++----------------- .../components/collections.component.ts | 6 +++++ 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index c8f85a8b7a..cb37f0fdad 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -5,6 +5,7 @@ import { first } from "rxjs/operators"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -26,6 +27,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { private route: ActivatedRoute, private location: Location, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -34,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index cd08427016..4b6a88f325 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -20,6 +21,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { platformUtilsService: PlatformUtilsService, organizationService: OrganizationService, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -28,6 +30,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } } diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 6add775b4a..3bf9181905 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -1,8 +1,9 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, Inject } from "@angular/core"; +import { Component, Inject, OnDestroy } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -23,6 +24,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService: CipherService, organizationSerivce: OrganizationService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: CollectionsDialogParams, ) { @@ -33,6 +35,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService, organizationSerivce, logService, + configService, ); this.cipherId = params?.cipherId; } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 67eac2098f..89e4884559 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -35,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { organizationService: OrganizationService, private apiService: ApiService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams, ) { @@ -45,6 +47,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, dialogRef, params, ); 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 4e06f7668c..f037170dda 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -802,33 +802,14 @@ export class VaultComponent implements OnInit, OnDestroy { const dialog = openOrgVaultCollectionsDialog(this.dialogService, { data: { collectionIds: cipher.collectionIds, - collections: collections.filter((c) => !c.readOnly && c.id != Unassigned), + collections: collections, organization: this.organization, cipherId: cipher.id, }, }); - /** - - const [modal] = await this.modalService.openViewRef( - CollectionsComponent, - this.collectionsModalRef, - (comp) => { - comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; - comp.collectionIds = cipher.collectionIds; - comp.collections = collections; - comp.organization = this.organization; - comp.cipherId = cipher.id; - comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); - - */ if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) { - await this.refresh(); + this.refresh(); } } diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 5f8c4145cb..d1f4f93072 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -2,6 +2,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -33,9 +35,13 @@ export class CollectionsComponent implements OnInit { protected cipherService: CipherService, protected organizationService: OrganizationService, private logService: LogService, + private configService: ConfigService, ) {} async ngOnInit() { + this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( + FeatureFlag.FlexibleCollectionsV1, + ); await this.load(); } From de0852431adf467e828da17b0d8038804c1c30dc Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 7 May 2024 13:25:49 -0400 Subject: [PATCH 03/14] [PM-7917] Remove session sync (#9024) * Remove session sync and MemoryStorageService * Fix merge --- .../browser/src/background/main.background.ts | 8 +- .../storage-service.factory.ts | 9 +- .../browser-session.decorator.spec.ts | 88 ----- .../browser-session.decorator.ts | 75 ----- .../session-sync-observable/index.ts | 2 - .../session-storable.ts | 7 - .../session-sync.decorator.spec.ts | 57 ---- .../session-sync.decorator.ts | 54 ---- .../session-syncer.spec.ts | 301 ------------------ .../session-sync-observable/session-syncer.ts | 125 -------- .../sync-item-metadata.ts | 25 -- .../synced-item-metadata.spec.ts | 42 --- .../browser-memory-storage.service.ts | 11 +- .../services/browser-state.service.spec.ts | 7 +- .../services/default-browser-state.service.ts | 7 +- ...cal-backed-session-storage.service.spec.ts | 26 +- .../local-backed-session-storage.service.ts | 19 +- .../background-memory-storage.service.ts | 1 - .../foreground-memory-storage.service.ts | 7 +- ...emory-storage-service-interactions.spec.ts | 4 +- .../src/platform/storage/port-messages.d.ts | 4 +- .../src/popup/services/services.module.ts | 5 +- apps/web/src/app/core/state/state.service.ts | 7 +- libs/angular/src/services/injection-tokens.ts | 7 +- .../platform/abstractions/storage.service.ts | 11 +- .../platform/models/domain/storage-options.ts | 4 - .../services/memory-storage.service.ts | 8 +- .../src/platform/services/state.service.ts | 14 +- .../state/storage/memory-storage.service.ts | 8 +- 29 files changed, 41 insertions(+), 902 deletions(-) delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/index.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5cd4113bae..713dfe801c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -84,7 +84,6 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -246,10 +245,9 @@ export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; - memoryStorageService: AbstractMemoryStorageService; - memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; - largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService & - ObservableStorageService; + memoryStorageService: AbstractStorageService; + memoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index e63e39944d..764842d751 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -1,5 +1,4 @@ import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -66,9 +65,9 @@ export function sessionStorageServiceFactory( } export function memoryStorageServiceFactory( - cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices, + cache: { memoryStorageService?: AbstractStorageService } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( @@ -97,10 +96,10 @@ export function memoryStorageServiceFactory( export function observableMemoryStorageServiceFactory( cache: { - memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService; + memoryStorageService?: AbstractStorageService & ObservableStorageService; } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { return new BackgroundMemoryStorageService(); }); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts deleted file mode 100644 index 2092f6992b..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; - -import { browserSession } from "./browser-session.decorator"; -import { SessionStorable } from "./session-storable"; -import { sessionSync } from "./session-sync.decorator"; - -// browserSession initializes SessionSyncers for each sessionSync decorated property -// We don't want to test SessionSyncers, so we'll mock them -jest.mock("./session-syncer"); - -describe("browserSession decorator", () => { - it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => { - @browserSession - class TestClass {} - expect(() => { - new TestClass(); - }).toThrowError( - "Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters", - ); - }); - - it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(DefaultBrowserStateService.prototype, { - memoryStorageService: { - value: Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }), - }, - }); - - @browserSession - class TestClass { - constructor(private stateService: DefaultBrowserStateService) {} - } - - expect(new TestClass(stateService)).toBeDefined(); - }); - - it("should create if MemoryStorageService is a constructor argument", () => { - const memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - - @browserSession - class TestClass { - constructor(private memoryStorageService: AbstractMemoryStorageService) {} - } - - expect(new TestClass(memoryStorageService)).toBeDefined(); - }); - - describe("interaction with @sessionSync decorator", () => { - let memoryStorageService: MemoryStorageService; - - @browserSession - class TestClass { - @sessionSync({ initializer: (s: string) => s }) - private behaviorSubject = new BehaviorSubject(""); - - constructor(private memoryStorageService: MemoryStorageService) {} - - fromJSON(json: any) { - this.behaviorSubject.next(json); - } - } - - beforeEach(() => { - memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - }); - - it("should create a session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers.length).toEqual(1); - }); - - it("should initialize the session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts deleted file mode 100644 index 8cf84ef153..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Constructor } from "type-fest"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; - -import { SessionStorable } from "./session-storable"; -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -/** - * Mark the class as syncing state across the browser session. This decorator finds rxjs BehaviorSubject properties - * marked with @sessionSync and syncs these values across the browser session. - * - * @param constructor - * @returns A new constructor that extends the original one to add session syncing. - */ -export function browserSession>(constructor: TCtor) { - return class extends constructor implements SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; - - constructor(...args: any[]) { - super(...args); - - // Require state service to be injected - const storageService: AbstractMemoryStorageService = this.findStorageService( - [this as any].concat(args), - ); - - if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) { - return; - } - - this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) => - this.buildSyncer(metadata, storageService), - ); - } - - buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) { - const syncer = new SessionSyncer( - (this as any)[metadata.propertyKey], - storageSerice, - metadata, - ); - // 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 - syncer.init(); - return syncer; - } - - findStorageService(args: any[]): AbstractMemoryStorageService { - const storageService = args.find(this.isMemoryStorageService); - - if (storageService) { - return storageService; - } - - const stateService = args.find( - (arg) => - arg?.memoryStorageService != null && - this.isMemoryStorageService(arg.memoryStorageService), - ); - if (stateService) { - return stateService.memoryStorageService; - } - - throw new Error( - `Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`, - ); - } - - isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService { - return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE; - } - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/index.ts b/apps/browser/src/platform/decorators/session-sync-observable/index.ts deleted file mode 100644 index c0c547192e..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { browserSession } from "./browser-session.decorator"; -export { sessionSync } from "./session-sync.decorator"; diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts deleted file mode 100644 index f5838b86ef..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export interface SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts deleted file mode 100644 index 7a6e726608..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { sessionSync } from "./session-sync.decorator"; - -describe("sessionSync decorator", () => { - const initializer = (s: string) => "test"; - class TestClass { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - @sessionSync({ initializer: initializer, initializeAs: "array" }) - private secondTestProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - this.secondTestProperty.complete(); - } - } - - it("should add __syncedItemKeys to prototype", () => { - const testClass = new TestClass(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_0", - initializer: initializer, - }), - expect.objectContaining({ - propertyKey: "secondTestProperty", - sessionKey: "secondTestProperty_1", - initializer: initializer, - initializeAs: "array", - }), - ]); - testClass.complete(); - }); - - class TestClass2 { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - } - } - - it("should maintain sessionKey index count for other test classes", () => { - const testClass = new TestClass2(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_2", - initializer: initializer, - }), - ]); - testClass.complete(); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts deleted file mode 100644 index e439cea45a..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { SessionStorable } from "./session-storable"; -import { InitializeOptions } from "./sync-item-metadata"; - -class BuildOptions> { - initializer?: (keyValuePair: TJson) => T; - initializeAs?: InitializeOptions; -} - -// Used to ensure uniqueness for each synced observable -let index = 0; - -/** - * A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts. - * - * >**Note** This decorator does nothing if the enclosing class is not decorated with @browserSession. - * - * >**Note** The Behavior subject must be initialized with a default or in the constructor of the class. If it is not, an error will be thrown. - * - * >**!!Warning!!** If the property is overwritten at any time, the new value will not be synced across the browser session. - * - * @param buildOptions - * Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an - * initializer function that takes a key value pair representation of the BehaviorSubject data - * and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate - * the provided initializer function should be used to build an array of values. For example, - * ```ts - * \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' }) - * ``` - * is equivalent to - * ``` - * \@sessionSync({ initializer: (obj: any[]) => obj.map((f) => Foo.fromJSON }) - * ``` - * - * @returns decorator function - */ -export function sessionSync(buildOptions: BuildOptions) { - return (prototype: unknown, propertyKey: string) => { - // Force prototype into SessionStorable and implement it. - const p = prototype as SessionStorable; - - if (p.__syncedItemMetadata == null) { - p.__syncedItemMetadata = []; - } - - p.__syncedItemMetadata.push({ - propertyKey, - sessionKey: `${propertyKey}_${index++}`, - initializer: buildOptions.initializer, - initializeAs: buildOptions.initializeAs ?? "object", - }); - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts deleted file mode 100644 index 18f0ceac60..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { awaitAsync } from "@bitwarden/common/../spec/utils"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, ReplaySubject } from "rxjs"; - -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("session syncer", () => { - const propertyKey = "behaviorSubject"; - const sessionKey = "Test__" + propertyKey; - const metaData: SyncedItemMetadata = { - propertyKey, - sessionKey, - initializer: (s: string) => s, - initializeAs: "object", - }; - let storageService: MockProxy; - let sut: SessionSyncer; - let behaviorSubject: BehaviorSubject; - - beforeEach(() => { - behaviorSubject = new BehaviorSubject(""); - jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({ - name: "bitwarden-test", - version: "0.0.0", - manifest_version: 3, - }); - - storageService = mock(); - storageService.has.mockResolvedValue(false); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - }); - - afterEach(() => { - jest.resetAllMocks(); - - behaviorSubject.complete(); - }); - - describe("constructor", () => { - it("should throw if subject is not an instance of Subject", () => { - expect(() => { - new SessionSyncer({} as any, storageService, null); - }).toThrowError("subject must inherit from Subject"); - }); - - it("should create if either ctor or initializer is provided", () => { - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: () => null, - }), - ).toBeDefined(); - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializer: (s: any) => s, - initializeAs: "object", - }), - ).toBeDefined(); - }); - it("should throw if neither ctor or initializer is provided", () => { - expect(() => { - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: null, - }); - }).toThrowError("initializer must be provided"); - }); - }); - - describe("init", () => { - it("should ignore all updates currently in a ReplaySubject's buffer", () => { - const replaySubject = new ReplaySubject(Infinity); - replaySubject.next("1"); - replaySubject.next("2"); - replaySubject.next("3"); - sut = new SessionSyncer(replaySubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(3); - }); - - it("should ignore BehaviorSubject's initial value", () => { - const behaviorSubject = new BehaviorSubject("initial"); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(1); - }); - - it("should grab an initial value from storage if it exists", async () => { - storageService.has.mockResolvedValue(true); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).toHaveBeenCalledWith(); - }); - - it("should not grab an initial value from storage if it does not exist", async () => { - storageService.has.mockResolvedValue(false); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).not.toHaveBeenCalled(); - }); - }); - - describe("a value is emitted on the observable", () => { - let sendMessageSpy: jest.SpyInstance; - const value = "test"; - const serializedValue = JSON.stringify(value); - - beforeEach(() => { - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - - behaviorSubject.next(value); - }); - - it("should update sessionSyncers in other contexts", async () => { - // await finishing of fire-and-forget operation - await new Promise((resolve) => setTimeout(resolve, 100)); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, { - id: sut.id, - serializedValue, - }); - }); - }); - - describe("A message is received", () => { - let nextSpy: jest.SpyInstance; - let sendMessageSpy: jest.SpyInstance; - - beforeEach(() => { - nextSpy = jest.spyOn(behaviorSubject, "next"); - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should ignore messages with the wrong command", async () => { - await sut.updateFromMessage({ command: "wrong_command", id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should ignore messages from itself", async () => { - await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should update from message on emit from another instance", async () => { - const builder = jest.fn(); - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - const value = "test"; - const serializedValue = JSON.stringify(value); - builder.mockReturnValue(value); - - // Expect no circular messaging - await awaitAsync(); - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(storageService.getBypassCache).toHaveBeenCalledTimes(0); - - expect(nextSpy).toHaveBeenCalledTimes(1); - expect(nextSpy).toHaveBeenCalledWith(value); - expect(behaviorSubject.value).toBe(value); - - // Expect no circular messaging - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - }); - }); - - describe("memory storage", () => { - const value = "test"; - const serializedValue = JSON.stringify(value); - let saveSpy: jest.SpyInstance; - const builder = jest.fn().mockReturnValue(value); - const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage"); - - beforeEach(async () => { - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - saveSpy = jest.spyOn(storageService, "save"); - - // 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 - sut.init(); - await awaitAsync(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should always store on observed next for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(2); - }); - - it("should not store on message receive for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(0); - }); - - it("should store on message receive for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - - it("should store on observed next for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts deleted file mode 100644 index 6561d5074c..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export class SessionSyncer { - subscription: Subscription; - id = Utils.newGuid(); - - // ignore initial values - private ignoreNUpdates = 0; - - constructor( - private subject: Subject, - private memoryStorageService: AbstractMemoryStorageService, - private metaData: SyncedItemMetadata, - ) { - if (!(subject instanceof Subject)) { - throw new Error("subject must inherit from Subject"); - } - - if (metaData.initializer == null) { - throw new Error("initializer must be provided"); - } - } - - async init() { - switch (this.subject.constructor) { - case ReplaySubject: - // ignore all updates currently in the buffer - this.ignoreNUpdates = (this.subject as any)._buffer.length; - break; - case BehaviorSubject: - this.ignoreNUpdates = 1; - break; - default: - break; - } - - await this.observe(); - // must be synchronous - const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey); - if (hasInSessionMemory) { - await this.updateFromMemory(); - } - - this.listenForUpdates(); - } - - private async observe() { - const stream = this.subject.pipe(skip(this.ignoreNUpdates)); - this.ignoreNUpdates = 0; - - // This may be a memory leak. - // There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary - // contexts. If so, this is handled by destruction of the context. - this.subscription = stream - .pipe( - concatMap(async (next) => { - if (this.ignoreNUpdates > 0) { - this.ignoreNUpdates -= 1; - return; - } - await this.updateSession(next); - }), - ) - .subscribe(); - } - - private listenForUpdates() { - // This is an unawaited promise, but it will be executed asynchronously in the background. - BrowserApi.messageListener(this.updateMessageCommand, (message) => { - // 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.updateFromMessage(message); - }); - } - - async updateFromMessage(message: any) { - if (message.command != this.updateMessageCommand || message.id === this.id) { - return; - } - await this.update(message.serializedValue); - } - - async updateFromMemory() { - const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey); - await this.update(value); - } - - async update(serializedValue: any) { - if (!serializedValue) { - return; - } - - const unBuiltValue = JSON.parse(serializedValue); - if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - const builder = SyncedItemMetadata.builder(this.metaData); - const value = builder(unBuiltValue); - this.ignoreNUpdates = 1; - this.subject.next(value); - } - - private async updateSession(value: any) { - if (!value) { - return; - } - - const serializedValue = JSON.stringify(value); - if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue }); - } - - private get updateMessageCommand() { - return `${this.metaData.sessionKey}_update`; - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts deleted file mode 100644 index fe2b393923..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type InitializeOptions = "array" | "record" | "object"; - -export class SyncedItemMetadata { - propertyKey: string; - sessionKey: string; - initializer: (keyValuePair: any) => any; - initializeAs: InitializeOptions; - - static builder(metadata: SyncedItemMetadata): (o: any) => any { - const itemBuilder = metadata.initializer; - if (metadata.initializeAs === "array") { - return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); - } else if (metadata.initializeAs === "record") { - return (keyValuePair: any) => { - const record: Record = {}; - for (const key in keyValuePair) { - record[key] = itemBuilder(keyValuePair[key]); - } - return record; - }; - } else { - return (keyValuePair: any) => itemBuilder(keyValuePair); - } - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts deleted file mode 100644 index 61eb63eaac..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("builder", () => { - const propertyKey = "propertyKey"; - const key = "key"; - const initializer = (s: any) => "used initializer"; - - it("should use initializer", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer, - initializeAs: "object", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({})).toBe("used initializer"); - }); - - it("should honor initialize as array", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "array", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder([{}])).toBeInstanceOf(Array); - expect(builder([{}])[0]).toBe("used initializer"); - }); - - it("should honor initialize as record", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "record", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({ key: "" })).toBeInstanceOf(Object); - expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" }); - }); -}); diff --git a/apps/browser/src/platform/services/browser-memory-storage.service.ts b/apps/browser/src/platform/services/browser-memory-storage.service.ts index b067dc5a12..f824a1df0d 100644 --- a/apps/browser/src/platform/services/browser-memory-storage.service.ts +++ b/apps/browser/src/platform/services/browser-memory-storage.service.ts @@ -1,16 +1,7 @@ -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; - import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service"; -export default class BrowserMemoryStorageService - extends AbstractChromeStorageService - implements AbstractMemoryStorageService -{ +export default class BrowserMemoryStorageService extends AbstractChromeStorageService { constructor() { super(chrome.storage.session); } - type = "MemoryStorageService" as const; - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index a0a52ff622..9077305f44 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -3,10 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { State } from "@bitwarden/common/platform/models/domain/state"; @@ -56,7 +53,7 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: MockProxy; + let memoryStorageService: MockProxy; beforeEach(() => { memoryStorageService = mock(); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index d7bc45bcc3..92da28efa2 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -2,10 +2,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractStorageService, - AbstractMemoryStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; @@ -25,7 +22,7 @@ export class DefaultBrowserStateService constructor( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, stateFactory: StateFactory, accountService: AccountService, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 7114bda06e..8d43c8f2fe 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -59,24 +59,12 @@ describe("LocalBackedSessionStorage", () => { await sut.get("test"); expect(sut["cache"]["test"]).toEqual("decrypted"); }); - }); - - describe("getBypassCache", () => { - it("ignores cached values", async () => { - sut["cache"]["test"] = "cached"; - const encrypted = makeEncString("encrypted"); - localStorage.internalStore["session_test"] = encrypted.encryptedString; - encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const result = await sut.getBypassCache("test"); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); - expect(result).toEqual("decrypted"); - }); it("returns a decrypted value when one is stored in local storage", async () => { const encrypted = makeEncString("encrypted"); localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const result = await sut.getBypassCache("test"); + const result = await sut.get("test"); expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); expect(result).toEqual("decrypted"); }); @@ -85,19 +73,9 @@ describe("LocalBackedSessionStorage", () => { const encrypted = makeEncString("encrypted"); localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - await sut.getBypassCache("test"); + await sut.get("test"); expect(sut["cache"]["test"]).toEqual("decrypted"); }); - - it("deserializes when a deserializer is provided", async () => { - const encrypted = makeEncString("encrypted"); - localStorage.internalStore["session_test"] = encrypted.encryptedString; - encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const deserializer = jest.fn().mockReturnValue("deserialized"); - const result = await sut.getBypassCache("test", { deserializer }); - expect(deserializer).toHaveBeenCalledWith("decrypted"); - expect(result).toEqual("deserialized"); - }); }); describe("has", () => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index c29b9c69dc..2c14ac2833 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,18 +1,16 @@ import { Subject } from "rxjs"; -import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../browser/browser-api"; @@ -20,7 +18,7 @@ import { MemoryStoragePortMessage } from "../storage/port-messages"; import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { private ports: Set = new Set([]); @@ -65,20 +63,12 @@ export class LocalBackedSessionStorageService }); } - async get(key: string, options?: MemoryStorageOptions): Promise { + async get(key: string, options?: StorageOptions): Promise { if (this.cache[key] !== undefined) { return this.cache[key] as T; } - return await this.getBypassCache(key, options); - } - - async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { - let value = await this.getLocalSessionValue(await this.sessionKey.get(), key); - - if (options?.deserializer != null) { - value = options.deserializer(value as Jsonify); - } + const value = await this.getLocalSessionValue(await this.sessionKey.get(), key); this.cache[key] = value; return value as T; @@ -159,7 +149,6 @@ export class LocalBackedSessionStorageService switch (message.action) { case "get": - case "getBypassCache": case "has": { result = await this[message.action](message.key); break; diff --git a/apps/browser/src/platform/storage/background-memory-storage.service.ts b/apps/browser/src/platform/storage/background-memory-storage.service.ts index 9203d2aacb..a1d333affa 100644 --- a/apps/browser/src/platform/storage/background-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/background-memory-storage.service.ts @@ -51,7 +51,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService { switch (message.action) { case "get": - case "getBypassCache": case "has": { result = await this[message.action](message.key); break; diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index b3ac8de55e..bd6a52c82f 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -1,7 +1,7 @@ import { Observable, Subject, filter, firstValueFrom, map } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,7 +11,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { MemoryStoragePortMessage } from "./port-messages"; import { portName } from "./port-name"; -export class ForegroundMemoryStorageService extends AbstractMemoryStorageService { +export class ForegroundMemoryStorageService extends AbstractStorageService { private _port: chrome.runtime.Port; private _backgroundResponses$: Observable; private updatesSubject = new Subject(); @@ -59,9 +59,6 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService async get(key: string): Promise { return await this.delegateToBackground("get", key); } - async getBypassCache(key: string): Promise { - return await this.delegateToBackground("getBypassCache", key); - } async has(key: string): Promise { return await this.delegateToBackground("has", key); } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index 43ffb6a065..c462f24269 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -25,9 +25,9 @@ describe("foreground background memory storage interaction", () => { jest.resetAllMocks(); }); - test.each(["has", "get", "getBypassCache"])( + test.each(["has", "get"])( "background should respond with the correct value for %s", - async (action: "get" | "has" | "getBypassCache") => { + async (action: "get" | "has") => { const key = "key"; const value = "value"; background[action] = jest.fn().mockResolvedValue(value); diff --git a/apps/browser/src/platform/storage/port-messages.d.ts b/apps/browser/src/platform/storage/port-messages.d.ts index a64a9b2ef7..60817c98a4 100644 --- a/apps/browser/src/platform/storage/port-messages.d.ts +++ b/apps/browser/src/platform/storage/port-messages.d.ts @@ -1,5 +1,5 @@ import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -14,7 +14,7 @@ type MemoryStoragePortMessage = { data: string | string[] | StorageUpdate; originator: "foreground" | "background"; action?: - | keyof Pick + | keyof Pick | "subject_update" | "initialization"; }; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index ee08ed84b7..7dc79fa01e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -59,7 +59,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -411,7 +410,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, useFactory: ( - regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService, + regularMemoryStorageService: AbstractStorageService & ObservableStorageService, ) => { if (BrowserApi.isManifestVersion(2)) { return regularMemoryStorageService; @@ -439,7 +438,7 @@ const safeProviders: SafeProvider[] = [ useFactory: ( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 185509e150..de47a69555 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -9,10 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -26,7 +23,7 @@ export class StateService extends BaseStateService { constructor( storageService: AbstractStorageService, @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, - @Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService, + @Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService, logService: LogService, @Inject(STATE_FACTORY) stateFactory: StateFactory, accountService: AccountService, diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index b7989e7f32..c58931ce55 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -3,7 +3,6 @@ import { Observable, Subject } from "rxjs"; import { ClientType } from "@bitwarden/common/enums"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -24,7 +23,7 @@ export class SafeInjectionToken extends InjectionToken { export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< - AbstractMemoryStorageService & ObservableStorageService + AbstractStorageService & ObservableStorageService >("OBSERVABLE_MEMORY_STORAGE"); export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService @@ -32,9 +31,7 @@ export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("OBSERVABLE_DISK_LOCAL_STORAGE"); -export const MEMORY_STORAGE = new SafeInjectionToken( - "MEMORY_STORAGE", -); +export const MEMORY_STORAGE = new SafeInjectionToken("MEMORY_STORAGE"); export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); export const LOGOUT_CALLBACK = new SafeInjectionToken< diff --git a/libs/common/src/platform/abstractions/storage.service.ts b/libs/common/src/platform/abstractions/storage.service.ts index f380420c39..390d71ae2a 100644 --- a/libs/common/src/platform/abstractions/storage.service.ts +++ b/libs/common/src/platform/abstractions/storage.service.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { MemoryStorageOptions, StorageOptions } from "../models/domain/storage-options"; +import { StorageOptions } from "../models/domain/storage-options"; export type StorageUpdateType = "save" | "remove"; export type StorageUpdate = { @@ -24,12 +24,3 @@ export abstract class AbstractStorageService { abstract save(key: string, obj: T, options?: StorageOptions): Promise; abstract remove(key: string, options?: StorageOptions): Promise; } - -export abstract class AbstractMemoryStorageService extends AbstractStorageService { - // Used to identify the service in the session sync decorator framework - static readonly TYPE = "MemoryStorageService"; - readonly type = AbstractMemoryStorageService.TYPE; - - abstract get(key: string, options?: MemoryStorageOptions): Promise; - abstract getBypassCache(key: string, options?: MemoryStorageOptions): Promise; -} diff --git a/libs/common/src/platform/models/domain/storage-options.ts b/libs/common/src/platform/models/domain/storage-options.ts index 6ed430ac50..e27628b850 100644 --- a/libs/common/src/platform/models/domain/storage-options.ts +++ b/libs/common/src/platform/models/domain/storage-options.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { HtmlStorageLocation, StorageLocation } from "../../enums"; export type StorageOptions = { @@ -9,5 +7,3 @@ export type StorageOptions = { htmlStorageLocation?: HtmlStorageLocation; keySuffix?: string; }; - -export type MemoryStorageOptions = StorageOptions & { deserializer?: (obj: Jsonify) => T }; diff --git a/libs/common/src/platform/services/memory-storage.service.ts b/libs/common/src/platform/services/memory-storage.service.ts index 9cecee7538..d5debf46cc 100644 --- a/libs/common/src/platform/services/memory-storage.service.ts +++ b/libs/common/src/platform/services/memory-storage.service.ts @@ -1,8 +1,8 @@ import { Subject } from "rxjs"; -import { AbstractMemoryStorageService, StorageUpdate } from "../abstractions/storage.service"; +import { AbstractStorageService, StorageUpdate } from "../abstractions/storage.service"; -export class MemoryStorageService extends AbstractMemoryStorageService { +export class MemoryStorageService extends AbstractStorageService { protected store = new Map(); private updatesSubject = new Subject(); @@ -42,8 +42,4 @@ export class MemoryStorageService extends AbstractMemoryStorageService { this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 156a871a2d..aa245f8688 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -14,10 +14,7 @@ import { InitOptions, StateService as StateServiceAbstraction, } from "../abstractions/state.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "../abstractions/storage.service"; +import { AbstractStorageService } from "../abstractions/storage.service"; import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; @@ -61,7 +58,7 @@ export class StateService< constructor( protected storageService: AbstractStorageService, protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractMemoryStorageService, + protected memoryStorageService: AbstractStorageService, protected logService: LogService, protected stateFactory: StateFactory, protected accountService: AccountService, @@ -1111,9 +1108,10 @@ export class StateService< } protected async state(): Promise> { - const state = await this.memoryStorageService.get>(keys.state, { - deserializer: (s) => State.fromJSON(s, this.accountDeserializer), - }); + let state = await this.memoryStorageService.get>(keys.state); + if (this.memoryStorageService.valuesRequireDeserialization) { + state = State.fromJSON(state, this.accountDeserializer); + } return state; } diff --git a/libs/common/src/platform/state/storage/memory-storage.service.ts b/libs/common/src/platform/state/storage/memory-storage.service.ts index 36116f5e4e..ab45c101f9 100644 --- a/libs/common/src/platform/state/storage/memory-storage.service.ts +++ b/libs/common/src/platform/state/storage/memory-storage.service.ts @@ -1,13 +1,13 @@ import { Subject } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, ObservableStorageService, StorageUpdate, } from "../../abstractions/storage.service"; export class MemoryStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { protected store: Record = {}; @@ -49,8 +49,4 @@ export class MemoryStorageService this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } From 27d4178287fca5f75fb3dbe48c0e6c294e6f17e1 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 7 May 2024 14:48:23 -0400 Subject: [PATCH 04/14] [PS] fix broken local Storybook (#9075) * add disableRoutesGraph to compdocArgs * fix popup-layout story routes --- angular.json | 10 +++++++++- .../platform/popup/layout/popup-layout.stories.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/angular.json b/angular.json index 4b62c771cb..cdf213e39d 100644 --- a/angular.json +++ b/angular.json @@ -142,7 +142,15 @@ "configDir": ".storybook", "browserTarget": "components:build", "compodoc": true, - "compodocArgs": ["-p", "./tsconfig.json", "-e", "json", "-d", "."], + "compodocArgs": [ + "-p", + "./tsconfig.json", + "-e", + "json", + "-d", + ".", + "--disableRoutesGraph" + ], "port": 6006 } }, diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 77530d06e5..28692c79e1 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -315,13 +315,13 @@ export default { importProvidersFrom( RouterModule.forRoot( [ - { path: "", redirectTo: "vault", pathMatch: "full" }, - { path: "vault", component: MockVaultPageComponent }, - { path: "generator", component: MockGeneratorPageComponent }, - { path: "send", component: MockSendPageComponent }, - { path: "settings", component: MockSettingsPageComponent }, + { path: "", redirectTo: "tabs/vault", pathMatch: "full" }, + { path: "tabs/vault", component: MockVaultPageComponent }, + { path: "tabs/generator", component: MockGeneratorPageComponent }, + { path: "tabs/send", component: MockSendPageComponent }, + { path: "tabs/settings", component: MockSettingsPageComponent }, // in case you are coming from a story that also uses the router - { path: "**", redirectTo: "vault" }, + { path: "**", redirectTo: "tabs/vault" }, ], { useHash: true }, ), From 3a71322510adaa517cc98b773133c8b11f1704f7 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 7 May 2024 12:35:28 -0700 Subject: [PATCH 05/14] [AC-1707] Restrict provider access to items (#8265) * [AC-1707] Add feature flag * [AC-1707] Prevent loading ciphers for provider users in the org vault when the feature flag is enabled * [AC-1707] Ensure new canEditAllCiphers logic only applies to organizations that have FC enabled * [AC-1707] Update editAllCiphers helper to check for restrictProviderAccess feature flag * [AC-1707] Remove un-used vaultFilterComponent reference * [AC-1707] Hide vault filter for providers * [AC-1707] Add search to vault header for provider users * [AC-1707] Hide New Item button for Providers when restrict provider access feature flag is enabled * [AC-1707] Remove leftover debug statement * [AC-1707] Update canEditAllCiphers references to consider the restrictProviderAccessFlag * [AC-1707] Fix collections component changes from main * [AC-1707] Fix some feature flag issues from merge with main * [AC-1707] Avoid 'readonly' collection dialog for providers * [AC-1707] Fix broken Browser component * [AC-1707] Fix broken Desktop component * [AC-1707] Add restrict provider flag to add access badge logic --- .../bulk-delete-dialog.component.ts | 15 +++- .../collections.component.html | 8 +- .../individual-vault/collections.component.ts | 8 +- .../app/vault/org-vault/add-edit.component.ts | 26 +++++- .../vault/org-vault/attachments.component.ts | 33 ++++++-- ...-collection-assignment-dialog.component.ts | 5 +- .../collection-access-restricted.component.ts | 10 ++- .../vault/org-vault/collections.component.ts | 10 ++- .../vault-header/vault-header.component.html | 23 ++++- .../vault-header/vault-header.component.ts | 26 ++++++ .../app/vault/org-vault/vault.component.html | 14 +++- .../app/vault/org-vault/vault.component.ts | 83 +++++++++++++++---- .../src/app/vault/org-vault/vault.module.ts | 3 +- .../components/collections.component.ts | 11 ++- .../vault/components/add-edit.component.ts | 21 ++++- .../models/domain/organization.ts | 24 ++++-- libs/common/src/enums/feature-flag.enum.ts | 2 + .../src/vault/models/view/collection.view.ts | 8 +- 18 files changed, 273 insertions(+), 57 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index a678a05ae3..f49c54ac32 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -1,4 +1,4 @@ -import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { firstValueFrom } from "rxjs"; @@ -56,6 +56,10 @@ export class BulkDeleteDialogComponent { FeatureFlag.FlexibleCollectionsV1, ); + private restrictProviderAccess$ = this.configService.getFeatureFlag$( + FeatureFlag.RestrictProviderAccess, + ); + constructor( @Inject(DIALOG_DATA) params: BulkDeleteDialogParams, private dialogRef: DialogRef, @@ -81,10 +85,11 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; if (this.cipherIds.length) { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); if ( !this.organization || - !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled) + !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess) ) { deletePromises.push(this.deleteCiphers()); } else { @@ -118,7 +123,11 @@ export class BulkDeleteDialogComponent { private async deleteCiphers(): Promise { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); - const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); + const asAdmin = this.organization?.canEditAllCiphers( + flexibleCollectionsV1Enabled, + restrictProviderAccess, + ); if (this.permanent) { await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); } else { diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index 5adf9c4e58..d9c2145f0b 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -32,7 +32,13 @@ [(ngModel)]="$any(c).checked" name="Collection[{{ i }}].Checked" appStopProp - [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" + [disabled]=" + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess + ) + " /> {{ c.name }} diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 3bf9181905..af9c3476bd 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -50,7 +50,13 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On } check(c: CollectionView, select?: boolean) { - if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) { + if ( + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return; } (c as any).checked = select == null ? !(c as any).checked : select; diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 01e4dbaadf..82055cc916 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -82,7 +82,12 @@ export class AddEditComponent extends BaseAddEditComponent { } protected loadCollections() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.loadCollections(); } return Promise.resolve(this.collections); @@ -93,7 +98,10 @@ export class AddEditComponent extends BaseAddEditComponent { const firstCipherCheck = await super.loadCipher(); if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && firstCipherCheck != null ) { return firstCipherCheck; @@ -108,14 +116,24 @@ export class AddEditComponent extends BaseAddEditComponent { } protected encryptCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.encryptCipher(); } return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher); } protected async deleteCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipher(); } return this.cipher.isDeleted diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 2aecf277e6..30189e8021 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -29,6 +29,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On organization: Organization; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccess = false; constructor( cipherService: CipherService, @@ -62,11 +63,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On this.flexibleCollectionsV1Enabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); + this.restrictProviderAccess = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess), + ); } protected async reupload(attachment: AttachmentView) { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.showFixOldAttachments(attachment) ) { await super.reuploadCipherAttachment(attachment, true); @@ -74,7 +81,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On } protected async loadCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -85,12 +97,20 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On return this.cipherService.saveAttachmentWithServer( this.cipherDomain, file, - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled), + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ), ); } protected deleteCipherAttachment(attachmentId: string) { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); @@ -99,7 +119,10 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected showFixOldAttachments(attachment: AttachmentView) { return ( attachment.key == null && - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) ); } } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index e9f8401d73..e13ef49fc3 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -71,9 +71,12 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni async ngOnInit() { const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); + const restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); const org = await this.organizationService.get(this.params.organizationId); - if (org.canEditAllCiphers(v1FCEnabled)) { + if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) { this.editableItems = this.params.ciphers; } else { this.editableItems = this.params.ciphers.filter((c) => c.edit); diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 337d73b315..7a51f01577 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from "@angular/core"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components"; @@ -22,12 +22,18 @@ const icon = svgIcon` - {{ "viewCollection" | i18n }} + {{ buttonText | i18n }} `, }) export class CollectionAccessRestrictedComponent { protected icon = icon; + @Input() canEditCollection = false; + @Output() viewCollectionClicked = new EventEmitter(); + + get buttonText() { + return this.canEditCollection ? "editCollection" : "viewCollection"; + } } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 89e4884559..557b048a7b 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -61,7 +61,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected async loadCipher() { // if cipher is unassigned use apiService. We can see this by looking at this.collectionIds if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.collectionIds.length !== 0 ) { return await super.loadCipher(); @@ -86,7 +89,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected saveCollections() { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) || + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) || this.collectionIds.length === 0 ) { const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 97d99d5821..8388f4ea9d 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -73,8 +73,16 @@ + +
-
+
+ +
diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index a5cd468008..eecd2f434a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit { /** Currently selected collection */ @Input() collection?: TreeNode; + /** The current search text in the header */ + @Input() searchText: string; + /** Emits an event when the new item button is clicked in the header */ @Output() onAddCipher = new EventEmitter(); @@ -55,10 +58,14 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); + /** Emits an event when the search text changes in the header*/ + @Output() searchTextChanged = new EventEmitter(); + protected CollectionDialogTabType = CollectionDialogTabType; protected organizations$ = this.organizationService.organizations$; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccessFlag = false; constructor( private organizationService: OrganizationService, @@ -73,6 +80,9 @@ export class VaultHeaderComponent implements OnInit { this.flexibleCollectionsV1Enabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); + this.restrictProviderAccessFlag = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); } get title() { @@ -197,7 +207,23 @@ export class VaultHeaderComponent implements OnInit { return this.collection.node.canDelete(this.organization); } + get canCreateCollection(): boolean { + return this.organization?.canCreateNewCollections; + } + + get canCreateCipher(): boolean { + if (this.organization?.isProviderUser && this.restrictProviderAccessFlag) { + return false; + } + return true; + } + deleteCollection() { this.onDeleteCollection.emit(); } + + onSearchTextChanged(t: string) { + this.searchText = t; + this.searchTextChanged.emit(t); + } } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index af7b5059e5..096389021f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -3,19 +3,20 @@ [loading]="refreshing" [organization]="organization" [collection]="selectedCollection" + [searchText]="currentSearchText$ | async" (onAddCipher)="addCipher()" (onAddCollection)="addCollection()" (onEditCollection)="editCollection(selectedCollection.node, $event.tab)" (onDeleteCollection)="deleteCollection(selectedCollection.node)" + (searchTextChanged)="filterSearchText($event)" >
-
+
-
+
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 f037170dda..103b29fad7 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -100,7 +100,6 @@ import { BulkCollectionsDialogResult, } from "./bulk-collections-dialog"; import { openOrgVaultCollectionsDialog } from "./collections.component"; -import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; @@ -118,8 +117,6 @@ enum AddAccessStatusType { export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; - @ViewChild("vaultFilter", { static: true }) - vaultFilterComponent: VaultFilterComponent; @ViewChild("attachments", { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef; @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) @@ -151,6 +148,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected showMissingCollectionPermissionMessage: boolean; protected showCollectionAccessRestricted: boolean; protected currentSearchText$: Observable; + /** + * A list of collections that the user can assign items to and edit those items within. + * @protected + */ protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; private _flexibleCollectionsV1FlagEnabled: boolean; @@ -160,6 +161,11 @@ export class VaultComponent implements OnInit, OnDestroy { } protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; + private _restrictProviderAccessFlagEnabled: boolean; + protected get restrictProviderAccessEnabled(): boolean { + return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled; + } + private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -207,6 +213,10 @@ export class VaultComponent implements OnInit, OnDestroy { FeatureFlag.FlexibleCollectionsV1, ); + this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); + const filter$ = this.routedVaultFilterService.filter$; const organizationId$ = filter$.pipe( map((filter) => filter.organizationId), @@ -297,10 +307,20 @@ export class VaultComponent implements OnInit, OnDestroy { this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe( map((collections) => { - // Users that can edit all ciphers can implicitly edit all collections - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + // If restricted, providers can not add items to any collections or edit those items + if (this.organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + // Users that can edit all ciphers can implicitly add to / edit within any collection + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { return collections; } + // The user is only allowed to add/edit items to assigned collections that are not readonly return collections.filter((c) => c.assigned && !c.readOnly); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -332,10 +352,19 @@ export class VaultComponent implements OnInit, OnDestroy { } let ciphers; + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + if (this.flexibleCollectionsV1Enabled) { // Flexible collections V1 logic. // If the user can edit all ciphers for the organization then fetch them ALL. - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { // Otherwise, only fetch ciphers they have access to (includes unassigned for admins). @@ -343,7 +372,12 @@ export class VaultComponent implements OnInit, OnDestroy { } } else { // Pre-flexible collections logic, to be removed after flexible collections is fully released - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { ciphers = (await this.cipherService.getAllDecrypted()).filter( @@ -443,9 +477,17 @@ export class VaultComponent implements OnInit, OnDestroy { organization$, ]).pipe( map(([filter, collection, organization]) => { + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return collection != undefined || filter.collectionId === Unassigned; + } + return ( - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || - (!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || + (!organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) && collection != undefined && !collection.node.assigned) ); @@ -490,7 +532,8 @@ export class VaultComponent implements OnInit, OnDestroy { map(([filter, collection, organization]) => { return ( // Filtering by unassigned, show message if not admin - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || // Filtering by a collection, so show message if user is not assigned (collection != undefined && !collection.node.assigned && @@ -513,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy { if (this.flexibleCollectionsV1Enabled) { canEditCipher = - organization.canEditAllCiphers(true) || + organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) || (await firstValueFrom(allCipherMap$))[cipherId] != undefined; } else { canEditCipher = @@ -631,7 +674,10 @@ export class VaultComponent implements OnInit, OnDestroy { const canEditCiphersCheck = this._flexibleCollectionsV1FlagEnabled && - !this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled); + !this.organization.canEditAllCiphers( + this._flexibleCollectionsV1FlagEnabled, + this.restrictProviderAccessEnabled, + ); // This custom type check will show addAccess badge for // Custom users with canEdit access AND owner/admin manage access setting is OFF @@ -780,13 +826,13 @@ export class VaultComponent implements OnInit, OnDestroy { map((c) => { return c.sort((a, b) => { if ( - a.canEditItems(this.organization, true) && - !b.canEditItems(this.organization, true) + a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + !b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return -1; } else if ( - !a.canEditItems(this.organization, true) && - b.canEditItems(this.organization, true) + !a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return 1; } else { @@ -1247,7 +1293,10 @@ export class VaultComponent implements OnInit, OnDestroy { } protected deleteCipherWithServer(id: string, permanent: boolean) { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ); return permanent ? this.cipherService.deleteWithServer(id, asAdmin) : this.cipherService.softDeleteWithServer(id, asAdmin); diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index 47365bb4b1..a478307123 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components"; +import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components"; import { LooseComponentsModule } from "../../shared/loose-components.module"; import { SharedModule } from "../../shared/shared.module"; @@ -32,6 +32,7 @@ import { VaultComponent } from "./vault.component"; CollectionDialogModule, CollectionAccessRestrictedComponent, NoItemsModule, + SearchModule, ], declarations: [VaultComponent, VaultHeaderComponent], exports: [VaultComponent], diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index d1f4f93072..445727ac61 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -25,6 +25,7 @@ export class CollectionsComponent implements OnInit { collections: CollectionView[] = []; organization: Organization; flexibleCollectionsV1Enabled: boolean; + restrictProviderAccess: boolean; protected cipherDomain: Cipher; @@ -42,6 +43,9 @@ export class CollectionsComponent implements OnInit { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, ); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); await this.load(); } @@ -68,7 +72,12 @@ export class CollectionsComponent implements OnInit { async submit(): Promise { const selectedCollectionIds = this.collections .filter((c) => { - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return !!(c as any).checked; } else { return !!(c as any).checked && c.readOnly == null; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 0397a7a663..74c368d726 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -91,6 +91,7 @@ export class AddEditComponent implements OnInit, OnDestroy { private previousCipherId: string; protected flexibleCollectionsV1Enabled = false; + protected restrictProviderAccess = false; get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -183,6 +184,9 @@ export class AddEditComponent implements OnInit, OnDestroy { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, ); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -668,11 +672,14 @@ export class AddEditComponent implements OnInit, OnDestroy { protected saveCipher(cipher: Cipher) { const isNotClone = this.editMode && !this.cloneMode; - let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + let orgAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection if (!cipher.collectionIds) { - orgAdmin = this.organization?.canEditUnassignedCiphers(); + orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess); } return this.cipher.id == null @@ -681,14 +688,20 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected deleteCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); } protected restoreCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipherService.restoreWithServer(this.cipher.id, asAdmin); } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index bdf0b8fbbf..04840477df 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -203,22 +203,32 @@ export class Organization { ); } - canEditUnassignedCiphers() { - // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 + canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) { + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } return this.isAdmin || this.permissions.editAnyCollection; } - canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { + canEditAllCiphers( + flexibleCollectionsV1Enabled: boolean, + restrictProviderAccessFlagEnabled: boolean, + ) { // Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers - if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { + if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) { return this.isAdmin || this.permissions.editAnyCollection; } + + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } + // Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins - // Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag + // Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag return ( - this.isProviderUser || (this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) || - (this.allowAdminAccessToAllCollectionItems && this.isAdmin) + (this.allowAdminAccessToAllCollectionItems && + (this.type === OrganizationUserType.Admin || this.type === OrganizationUserType.Owner)) ); } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5ed3724f2f..221b251f3c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -17,6 +17,7 @@ export enum FeatureFlag { UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", + RestrictProviderAccess = "restrict-provider-access", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -44,6 +45,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, + [FeatureFlag.RestrictProviderAccess]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index f742b283bd..ebc0229f4e 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -39,7 +39,11 @@ export class CollectionView implements View, ITreeNodeObject { } } - canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean { + canEditItems( + org: Organization, + v1FlexibleCollections: boolean, + restrictProviderAccess: boolean, + ): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", @@ -48,7 +52,7 @@ export class CollectionView implements View, ITreeNodeObject { if (org?.flexibleCollections) { return ( - org?.canEditAllCiphers(v1FlexibleCollections) || + org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) || this.manage || (this.assigned && !this.readOnly) ); From 29bd03e64ea3d88109308d44547450a65c654d31 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 22:23:03 +0200 Subject: [PATCH 06/14] [PM-7172] Create account security settings component (navigational changes) (#8817) * Move about.component into tools ownership * Split out account security settings Move settings.component.ts to auth/popup/settings and rename to account-security.component.ts Move controls from settings.component.html and create account-security.component.html Move settings.component.html to tools/popup/settings.component.html Create settings.component.ts under tools/popup/settings Fixup module imports and routing Add new strings to en/message.json * Move vault-timeout-input.component to auth * Move await-desktop-dialog.component to auth * Add transition for account-security --------- Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 12 ++ .../settings/account-security.component.html | 140 ++++++++++++++++++ .../settings/account-security.component.ts} | 109 +++----------- .../await-desktop-dialog.component.html | 0 .../await-desktop-dialog.component.ts | 0 .../vault-timeout-input.component.html | 0 .../settings/vault-timeout-input.component.ts | 0 .../src/popup/app-routing.animations.ts | 3 + apps/browser/src/popup/app-routing.module.ts | 9 +- apps/browser/src/popup/app.module.ts | 6 +- .../settings/about}/about.component.html | 0 .../popup/settings/about}/about.component.ts | 0 .../popup/settings/settings.component.html | 125 ++-------------- .../popup/settings/settings.component.ts | 101 +++++++++++++ 14 files changed, 300 insertions(+), 205 deletions(-) create mode 100644 apps/browser/src/auth/popup/settings/account-security.component.html rename apps/browser/src/{popup/settings/settings.component.ts => auth/popup/settings/account-security.component.ts} (83%) rename apps/browser/src/{ => auth}/popup/settings/await-desktop-dialog.component.html (100%) rename apps/browser/src/{ => auth}/popup/settings/await-desktop-dialog.component.ts (100%) rename apps/browser/src/{ => auth}/popup/settings/vault-timeout-input.component.html (100%) rename apps/browser/src/{ => auth}/popup/settings/vault-timeout-input.component.ts (100%) rename apps/browser/src/{popup/settings => tools/popup/settings/about}/about.component.html (100%) rename apps/browser/src/{popup/settings => tools/popup/settings/about}/about.component.ts (100%) rename apps/browser/src/{ => tools}/popup/settings/settings.component.html (55%) create mode 100644 apps/browser/src/tools/popup/settings/settings.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bd62b825e7..493a909f8a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,9 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html new file mode 100644 index 0000000000..dff9675743 --- /dev/null +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -0,0 +1,140 @@ + +
+ +
+

+ {{ "accountSecurity" | i18n }} +

+
+ +
+
+
+
+

{{ "unlockMethods" | i18n }}

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

{{ "sessionTimeoutHeader" | i18n }}

+
+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) + }} + + + {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} + + + + +
+ + +
+ +
+
+
+

{{ "otherOptions" | i18n }}

+
+ + + + + +
+
+
diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts similarity index 83% rename from apps/browser/src/popup/settings/settings.component.ts rename to apps/browser/src/auth/popup/settings/account-security.component.ts index c7e5b7dc95..88365e7b47 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -23,7 +22,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -34,35 +32,20 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { DialogService } from "@bitwarden/components"; -import { SetPinComponent } from "../../auth/popup/components/set-pin.component"; -import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../platform/flags"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { enableAccountSwitching } from "../../../platform/flags"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { SetPinComponent } from "../components/set-pin.component"; -import { AboutComponent } from "./about.component"; import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; -const RateUrls = { - [DeviceType.ChromeExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.FirefoxExtension]: - "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", - [DeviceType.OperaExtension]: - "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", - [DeviceType.EdgeExtension]: - "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", - [DeviceType.VivaldiExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", -}; - @Component({ - selector: "app-settings", - templateUrl: "settings.component.html", + selector: "auth-account-security", + templateUrl: "account-security.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SettingsComponent implements OnInit { +export class AccountSecurityComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; @@ -95,7 +78,6 @@ export class SettingsComponent implements OnInit { private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, public messagingService: MessagingService, - private router: Router, private environmentService: EnvironmentService, private cryptoService: CryptoService, private stateService: StateService, @@ -425,23 +407,6 @@ export class SettingsComponent implements OnInit { ); } - async lock() { - await this.vaultTimeoutService.lock(); - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - type: "info", - }); - - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (confirmed) { - this.messagingService.send("logout", { userId: userId }); - } - } - async changePassword() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "continueToWebApp" }, @@ -468,44 +433,6 @@ export class SettingsComponent implements OnInit { } } - async share() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "learnOrg" }, - content: { key: "learnOrgConfirmation" }, - type: "info", - }); - if (confirmed) { - // 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 - BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/"); - } - } - - async webVault() { - const env = await firstValueFrom(this.environmentService.environment$); - const url = env.getWebVaultUrl(); - await BrowserApi.createNewTab(url); - } - - async import() { - await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - // 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 - BrowserPopupUtils.openCurrentPagePopout(window); - } - } - - export() { - // 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(["/export"]); - } - - about() { - this.dialogService.open(AboutComponent); - } - async fingerprint() { const fingerprint = await this.cryptoService.getFingerprint( await this.stateService.getUserId(), @@ -518,11 +445,21 @@ export class SettingsComponent implements OnInit { return firstValueFrom(dialogRef.closed); } - rate() { - const deviceType = this.platformUtilsService.getDevice(); - // 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 - BrowserApi.createNewTab((RateUrls as any)[deviceType]); + async lock() { + await this.vaultTimeoutService.lock(); + } + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + type: "info", + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (confirmed) { + this.messagingService.send("logout", { userId: userId }); + } } ngOnDestroy() { diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.html b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.html rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.ts rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.html b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.html similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.html rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.html diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.ts b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.ts rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 13403545fd..e37c640bf9 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -174,6 +174,9 @@ export const routerTransition = trigger("routerTransition", [ transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft), transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight), + transition("tabs => account-security", inSlideLeft), + transition("account-security => tabs", outSlideRight), + transition("tabs => import", inSlideLeft), transition("import => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 0dcf496457..059e2e605d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -21,6 +21,7 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; @@ -35,6 +36,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; @@ -53,7 +55,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -246,6 +247,12 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "autofill" }, }, + { + path: "account-security", + component: AccountSecurityComponent, + canActivate: [AuthGuard], + data: { state: "account-security" }, + }, { path: "folders", component: FoldersComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index bed40dfddc..40cdd29754 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -30,6 +30,8 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; +import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; @@ -49,6 +51,7 @@ import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.componen import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; @@ -77,9 +80,7 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; -import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -156,6 +157,7 @@ import "../platform/popup/locales"; SendListComponent, SendTypeComponent, SetPasswordComponent, + AccountSecurityComponent, SettingsComponent, ShareComponent, SsoComponent, diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/tools/popup/settings/about/about.component.html similarity index 100% rename from apps/browser/src/popup/settings/about.component.html rename to apps/browser/src/tools/popup/settings/about/about.component.html diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/tools/popup/settings/about/about.component.ts similarity index 100% rename from apps/browser/src/popup/settings/about.component.ts rename to apps/browser/src/tools/popup/settings/about/about.component.ts diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html similarity index 55% rename from apps/browser/src/popup/settings/settings.component.html rename to apps/browser/src/tools/popup/settings/settings.component.html index 98c218b0db..0b7773019b 100644 --- a/apps/browser/src/popup/settings/settings.component.html +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -7,10 +7,18 @@
-
+

{{ "manage" | i18n }}

+
-
-

{{ "security" | i18n }}

-
- - - {{ - "vaultTimeoutPolicyWithActionInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) - }} - - - {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} - - - {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} - - - - -
- - -
- -
- - -
-
- - -
-
- - -
- - -
-

{{ "account" | i18n }}

@@ -145,35 +67,6 @@
- - -
diff --git a/apps/browser/src/tools/popup/settings/settings.component.ts b/apps/browser/src/tools/popup/settings/settings.component.ts new file mode 100644 index 0000000000..81727c442c --- /dev/null +++ b/apps/browser/src/tools/popup/settings/settings.component.ts @@ -0,0 +1,101 @@ +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Subject } from "rxjs"; + +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +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 { DialogService } from "@bitwarden/components"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +import { AboutComponent } from "./about/about.component"; + +const RateUrls = { + [DeviceType.ChromeExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.FirefoxExtension]: + "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", + [DeviceType.OperaExtension]: + "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", + [DeviceType.EdgeExtension]: + "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", + [DeviceType.VivaldiExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", +}; + +@Component({ + selector: "tools-settings", + templateUrl: "settings.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class SettingsComponent implements OnInit { + private destroy$ = new Subject(); + + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private vaultTimeoutService: VaultTimeoutService, + public messagingService: MessagingService, + private router: Router, + private environmentService: EnvironmentService, + private dialogService: DialogService, + ) {} + + async ngOnInit() {} + + async share() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "learnOrg" }, + content: { key: "learnOrgConfirmation" }, + type: "info", + }); + if (confirmed) { + // 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 + BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/"); + } + } + + async webVault() { + const env = await firstValueFrom(this.environmentService.environment$); + const url = env.getWebVaultUrl(); + await BrowserApi.createNewTab(url); + } + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + // 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 + BrowserPopupUtils.openCurrentPagePopout(window); + } + } + + export() { + // 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(["/export"]); + } + + about() { + this.dialogService.open(AboutComponent); + } + + rate() { + const deviceType = this.platformUtilsService.getDevice(); + // 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 + BrowserApi.createNewTab((RateUrls as any)[deviceType]); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} From 7bb37877eeb001a0e7302c1b3a22310e4fb12df7 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 23:04:17 +0200 Subject: [PATCH 07/14] Fix test after session-removal with https://github.com/bitwarden/clients/pull/9024 (#9076) Co-authored-by: Daniel James Smith --- .../src/platform/services/browser-state.service.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 9077305f44..506f185b64 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -15,9 +15,6 @@ import { Account } from "../../models/account"; import { DefaultBrowserStateService } from "./default-browser-state.service"; -// disable session syncing to just test class -jest.mock("../decorators/session-sync-observable/"); - describe("Browser State Service", () => { let secureStorageService: MockProxy; let diskStorageService: MockProxy; From ea7d1ff6ed79bf6854647d2e84138fae204c00f4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 7 May 2024 17:43:42 -0400 Subject: [PATCH 08/14] Handle error object for biometric lock (#9070) --- apps/browser/src/auth/popup/lock.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 86352e2c82..782e37b864 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -143,15 +143,17 @@ export class LockComponent extends BaseLockComponent { try { success = await super.unlockBiometric(); } catch (e) { - const error = BiometricErrors[e as BiometricErrorTypes]; + const error = BiometricErrors[e?.message as BiometricErrorTypes]; if (error == null) { this.logService.error("Unknown error: " + e); + return false; } this.biometricError = this.i18nService.t(error.description); + } finally { + this.pendingBiometric = false; } - this.pendingBiometric = false; return success; } From 5682e38384edbed20963b43a67f7d57b73376400 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 23:58:31 +0200 Subject: [PATCH 09/14] [PM-7175] Create vault settings component (navigational changes) (#8840) * Move about.component into tools ownership * Split out account security settings Move settings.component.ts to auth/popup/settings and rename to account-security.component.ts Move controls from settings.component.html and create account-security.component.html Move settings.component.html to tools/popup/settings.component.html Create settings.component.ts under tools/popup/settings Fixup module imports and routing Add new strings to en/message.json * Move vault-timeout-input.component to auth * Move await-desktop-dialog.component to auth * Move folder.component to vault/popup/settings * Move sync.component to vault/popup/settings * Create vault settings component Move controls from settings.component.html to vault-settings.component.html Register VaultSettingsComponent within app.module Register route for VaultSettingsComponent Add new string in en/messages.json * Fix routing for back navigation on child pages of vault settings * Add transitions to vault-settings sub-pages * Add transition for account-security * Add an await to popping out the extension * Use "Vault" instead of "Vault settings" as title --------- Co-authored-by: Daniel James Smith --- .../src/popup/app-routing.animations.ts | 20 ++++--- apps/browser/src/popup/app-routing.module.ts | 11 +++- apps/browser/src/popup/app.module.ts | 6 +- .../popup/settings/export.component.html | 2 +- .../import/import-browser.component.html | 2 +- .../popup/settings/settings.component.html | 33 +---------- .../popup/settings/folders.component.html | 2 +- .../popup/settings/folders.component.ts | 0 .../popup/settings/sync.component.html | 2 +- .../popup/settings/sync.component.ts | 0 .../settings/vault-settings.component.html | 56 +++++++++++++++++++ .../settings/vault-settings.component.ts | 25 +++++++++ 12 files changed, 112 insertions(+), 47 deletions(-) rename apps/browser/src/{ => vault}/popup/settings/folders.component.html (95%) rename apps/browser/src/{ => vault}/popup/settings/folders.component.ts (100%) rename apps/browser/src/{ => vault}/popup/settings/sync.component.html (94%) rename apps/browser/src/{ => vault}/popup/settings/sync.component.ts (100%) create mode 100644 apps/browser/src/vault/popup/settings/vault-settings.component.html create mode 100644 apps/browser/src/vault/popup/settings/vault-settings.component.ts diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index e37c640bf9..9bad33f744 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -177,20 +177,24 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => account-security", inSlideLeft), transition("account-security => tabs", outSlideRight), - transition("tabs => import", inSlideLeft), - transition("import => tabs", outSlideRight), + // Vault settings + transition("tabs => vault-settings", inSlideLeft), + transition("vault-settings => tabs", outSlideRight), - transition("tabs => export", inSlideLeft), - transition("export => tabs", outSlideRight), + transition("vault-settings => import", inSlideLeft), + transition("import => vault-settings", outSlideRight), - transition("tabs => folders", inSlideLeft), - transition("folders => tabs", outSlideRight), + transition("vault-settings => export", inSlideLeft), + transition("export => vault-settings", outSlideRight), + + transition("vault-settings => folders", inSlideLeft), + transition("folders => vault-settings", outSlideRight), transition("folders => edit-folder, folders => add-folder", inSlideUp), transition("edit-folder => folders, add-folder => folders", outSlideDown), - transition("tabs => sync", inSlideLeft), - transition("sync => tabs", outSlideRight), + transition("vault-settings => sync", inSlideLeft), + transition("sync => vault-settings", outSlideRight), transition("tabs => excluded-domains", inSlideLeft), transition("excluded-domains => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 059e2e605d..c4e9acbd75 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,14 +48,15 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -253,6 +254,12 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "account-security" }, }, + { + path: "vault-settings", + component: VaultSettingsComponent, + canActivate: [AuthGuard], + data: { state: "vault-settings" }, + }, { path: "folders", component: FoldersComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 40cdd29754..71e6ed4f17 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -70,6 +70,9 @@ import { VaultSelectComponent } from "../vault/popup/components/vault/vault-sele import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; @@ -77,10 +80,8 @@ import { PopOutComponent } from "./components/pop-out.component"; import { UserVerificationComponent } from "./components/user-verification.component"; import { ServicesModule } from "./services/services.module"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -159,6 +160,7 @@ import "../platform/popup/locales"; SetPasswordComponent, AccountSecurityComponent, SettingsComponent, + VaultSettingsComponent, ShareComponent, SsoComponent, SyncComponent, diff --git a/apps/browser/src/tools/popup/settings/export.component.html b/apps/browser/src/tools/popup/settings/export.component.html index aae3584f6c..1b2ea1eb1d 100644 --- a/apps/browser/src/tools/popup/settings/export.component.html +++ b/apps/browser/src/tools/popup/settings/export.component.html @@ -1,7 +1,7 @@
- diff --git a/apps/browser/src/tools/popup/settings/import/import-browser.component.html b/apps/browser/src/tools/popup/settings/import/import-browser.component.html index df4f3f09aa..67b5eb348a 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser.component.html +++ b/apps/browser/src/tools/popup/settings/import/import-browser.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/tools/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html index 0b7773019b..71f4f1b991 100644 --- a/apps/browser/src/tools/popup/settings/settings.component.html +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -30,17 +30,9 @@ - - diff --git a/apps/browser/src/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts similarity index 100% rename from apps/browser/src/popup/settings/folders.component.ts rename to apps/browser/src/vault/popup/settings/folders.component.ts diff --git a/apps/browser/src/popup/settings/sync.component.html b/apps/browser/src/vault/popup/settings/sync.component.html similarity index 94% rename from apps/browser/src/popup/settings/sync.component.html rename to apps/browser/src/vault/popup/settings/sync.component.html index 6743f12a1a..6d0a1c31a8 100644 --- a/apps/browser/src/popup/settings/sync.component.html +++ b/apps/browser/src/vault/popup/settings/sync.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts similarity index 100% rename from apps/browser/src/popup/settings/sync.component.ts rename to apps/browser/src/vault/popup/settings/sync.component.ts diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.html b/apps/browser/src/vault/popup/settings/vault-settings.component.html new file mode 100644 index 0000000000..4928720e46 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.html @@ -0,0 +1,56 @@ + +
+ +
+

+ {{ "vault" | i18n }} +

+
+ +
+
+
+
+
+ + + + +
+
+
diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.ts new file mode 100644 index 0000000000..a12f6d1d5b --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +@Component({ + selector: "vault-settings", + templateUrl: "vault-settings.component.html", +}) +export class VaultSettingsComponent { + constructor( + public messagingService: MessagingService, + private router: Router, + ) {} + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + await BrowserPopupUtils.openCurrentPagePopout(window); + } + } +} From 5097a67b284357a300654560a126719337f25fdf Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 8 May 2024 09:36:42 -0400 Subject: [PATCH 10/14] Change close to cancel (#9062) --- .../providers/clients/create-client-organization.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html index 4c5d9fca9b..87169b6d9c 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html @@ -62,7 +62,7 @@ {{ "addOrganization" | i18n }} From 6c05aacbbac58d16744e368a95b66faf2fde0285 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 8 May 2024 10:18:19 -0400 Subject: [PATCH 11/14] [PM-6822] Create new Vault V2 component for extension refresh (#9065) * created v2 component. added it to app routing --- apps/browser/src/popup/app-routing.module.ts | 6 +++--- apps/browser/src/popup/app.module.ts | 2 ++ .../popup/components/vault/vault-v2.component.html | 1 + .../popup/components/vault/vault-v2.component.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault/vault-v2.component.html create mode 100644 apps/browser/src/vault/popup/components/vault/vault-v2.component.ts diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index c4e9acbd75..9e1872f1ce 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -46,6 +46,7 @@ import { PasswordHistoryComponent } from "../vault/popup/components/vault/passwo import { ShareComponent } from "../vault/popup/components/vault/share.component"; import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component"; import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; +import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; @@ -355,12 +356,11 @@ const routes: Routes = [ data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, - { + ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", - component: VaultFilterComponent, canActivate: [AuthGuard], data: { state: "tabs_vault" }, - }, + }), { path: "generator", component: GeneratorComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 71e6ed4f17..8ff4f9e4a2 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -67,6 +67,7 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component" import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component"; import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component"; +import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; @@ -181,6 +182,7 @@ import "../platform/popup/locales"; EnvironmentSelectorComponent, CurrentAccountComponent, AccountSwitcherComponent, + VaultV2Component, ], providers: [CurrencyPipe, DatePipe], bootstrap: [AppComponent], 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 new file mode 100644 index 0000000000..a653f46332 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -0,0 +1 @@ +

Vault V2 Extension Refresh

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 new file mode 100644 index 0000000000..332e5d1a4e --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -0,0 +1,13 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; + +@Component({ + selector: "app-vault", + templateUrl: "vault-v2.component.html", +}) +export class VaultV2Component implements OnInit, OnDestroy { + constructor() {} + + ngOnInit(): void {} + + ngOnDestroy(): void {} +} From 7d5575882e331b1f3d63a4f9e0e4d6644772af89 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 8 May 2024 11:34:07 -0400 Subject: [PATCH 12/14] Allow selecting of version numbers for copy-pasting in issues (#9078) * Allow selecting of version numbers for copy-pasting in issues * Simplify user-select override Expands selectable area to labels of versions --- apps/browser/src/popup/scss/misc.scss | 4 ++++ .../src/tools/popup/settings/about/about.component.html | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index c106931c1f..61f22be9ef 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -424,6 +424,10 @@ img, .modal-title, .overlay-container { user-select: none; + + &.user-select { + user-select: auto; + } } app-about .modal-body > *, diff --git a/apps/browser/src/tools/popup/settings/about/about.component.html b/apps/browser/src/tools/popup/settings/about/about.component.html index e68a664ba7..bad39a53d3 100644 --- a/apps/browser/src/tools/popup/settings/about/about.component.html +++ b/apps/browser/src/tools/popup/settings/about/about.component.html @@ -5,7 +5,7 @@
Bitwarden

© Bitwarden Inc. 2015-{{ year }}

-

{{ "version" | i18n }}: {{ version$ | async }}

+

{{ "version" | i18n }}: {{ version$ | async }}

{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} @@ -16,7 +16,7 @@ -

+

{{ "serverVersion" | i18n }} ({{ "thirdParty" | i18n }}): {{ data.serverConfig?.version }} @@ -28,7 +28,7 @@

-

+

{{ "serverVersion" | i18n }} ({{ "selfHostedServer" | i18n }}): {{ data.serverConfig?.version }} From 350ad890decc28cdd6a183a1af3e66769d802864 Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 8 May 2024 18:24:18 +0200 Subject: [PATCH 13/14] Check MasterPassword in web app during change (#8293) --- .../auth/settings/change-password.component.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 454d96f2bd..fb428def9f 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -165,7 +165,22 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { newMasterKey: MasterKey, newUserKey: [UserKey, EncString], ) { - const masterKey = await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword); + const masterKey = await this.cryptoService.makeMasterKey( + this.currentMasterPassword, + await this.stateService.getEmail(), + await this.kdfConfigService.getKdfConfig(), + ); + + const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); + if (userKey == null) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("invalidMasterPassword"), + ); + return; + } + const request = new PasswordRequest(); request.masterPasswordHash = await this.cryptoService.hashMasterKey( this.currentMasterPassword, From c2812fc21d345783fd0e3c21a50d499d4af8012a Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 8 May 2024 19:25:24 +0200 Subject: [PATCH 14/14] [PM-7176] Create appearance settings component (navigational changes) (#8911) * Move about.component into tools ownership * Split out account security settings Move settings.component.ts to auth/popup/settings and rename to account-security.component.ts Move controls from settings.component.html and create account-security.component.html Move settings.component.html to tools/popup/settings.component.html Create settings.component.ts under tools/popup/settings Fixup module imports and routing Add new strings to en/message.json * Move vault-timeout-input.component to auth * Move await-desktop-dialog.component to auth * Add transition for account-security * Create appearance settings component * Add entry in settings to navigate to the appearance settings page * Add transition animation for settings to appearance and back * Remove settings from options that are now under appearance --------- Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 3 + .../src/popup/app-routing.animations.ts | 4 ++ apps/browser/src/popup/app-routing.module.ts | 7 ++ apps/browser/src/popup/app.module.ts | 2 + .../src/popup/settings/options.component.html | 51 -------------- .../src/popup/settings/options.component.ts | 35 ---------- .../popup/settings/settings.component.html | 8 +++ .../popup/settings/appearance.component.html | 67 +++++++++++++++++++ .../popup/settings/appearance.component.ts | 62 +++++++++++++++++ 9 files changed, 153 insertions(+), 86 deletions(-) create mode 100644 apps/browser/src/vault/popup/settings/appearance.component.html create mode 100644 apps/browser/src/vault/popup/settings/appearance.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 493a909f8a..fcc80e5ff7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3035,6 +3035,9 @@ "accountSecurity": { "message": "Account security" }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 9bad33f744..55d2687a43 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -202,6 +202,10 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => options", inSlideLeft), transition("options => tabs", outSlideRight), + // Appearance settings + transition("tabs => appearance", inSlideLeft), + transition("appearance => tabs", outSlideRight), + transition("tabs => premium", inSlideLeft), transition("premium => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 9e1872f1ce..8fb397fe24 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,6 +48,7 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; +import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { SyncComponent } from "../vault/popup/settings/sync.component"; @@ -303,6 +304,12 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "options" }, }, + { + path: "appearance", + component: AppearanceComponent, + canActivate: [AuthGuard], + data: { state: "appearance" }, + }, { path: "clone-cipher", component: AddEditComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 8ff4f9e4a2..4a310027c1 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -70,6 +70,7 @@ import { VaultSelectComponent } from "../vault/popup/components/vault/vault-sele import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; +import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { SyncComponent } from "../vault/popup/settings/sync.component"; @@ -148,6 +149,7 @@ import "../platform/popup/locales"; LoginViaAuthRequestComponent, LoginDecryptionOptionsComponent, OptionsComponent, + AppearanceComponent, GeneratorComponent, PasswordGeneratorHistoryComponent, PasswordHistoryComponent, diff --git a/apps/browser/src/popup/settings/options.component.html b/apps/browser/src/popup/settings/options.component.html index 93c018233a..fa2b7514db 100644 --- a/apps/browser/src/popup/settings/options.component.html +++ b/apps/browser/src/popup/settings/options.component.html @@ -192,56 +192,5 @@ {{ "showIdentitiesCurrentTabDesc" | i18n }}

-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index dbdb94c586..0344362d36 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types"; @@ -12,8 +11,6 @@ import { } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { enableAccountSwitching } from "../../platform/flags"; @@ -23,8 +20,6 @@ import { enableAccountSwitching } from "../../platform/flags"; templateUrl: "options.component.html", }) export class OptionsComponent implements OnInit { - enableFavicon = false; - enableBadgeCounter = true; enableAutoFillOnPageLoad = false; autoFillOnPageLoadDefault = false; autoFillOnPageLoadOptions: any[]; @@ -36,8 +31,6 @@ export class OptionsComponent implements OnInit { showCardsCurrentTab = false; showIdentitiesCurrentTab = false; showClearClipboard = true; - theme: ThemeType; - themeOptions: any[]; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; uriMatchOptions: any[]; clearClipboard: ClearClipboardDelaySetting; @@ -52,18 +45,9 @@ export class OptionsComponent implements OnInit { private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, private domainSettingsService: DomainSettingsService, - private badgeSettingsService: BadgeSettingsServiceAbstraction, i18nService: I18nService, - private themeStateService: ThemeStateService, private vaultSettingsService: VaultSettingsService, ) { - this.themeOptions = [ - { name: i18nService.t("default"), value: ThemeType.System }, - { name: i18nService.t("light"), value: ThemeType.Light }, - { name: i18nService.t("dark"), value: ThemeType.Dark }, - { name: "Nord", value: ThemeType.Nord }, - { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, - ]; this.uriMatchOptions = [ { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, { name: i18nService.t("host"), value: UriMatchStrategy.Host }, @@ -117,14 +101,8 @@ export class OptionsComponent implements OnInit { this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); - this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); - - this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); - this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); - this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); - const defaultUriMatch = await firstValueFrom( this.domainSettingsService.defaultUriMatchStrategy$, ); @@ -166,15 +144,6 @@ export class OptionsComponent implements OnInit { await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); } - async updateFavicon() { - await this.domainSettingsService.setShowFavicons(this.enableFavicon); - } - - async updateBadgeCounter() { - await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter); - this.messagingService.send("bgUpdateContextMenu"); - } - async updateShowCardsCurrentTab() { await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); } @@ -183,10 +152,6 @@ export class OptionsComponent implements OnInit { await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); } - async saveTheme() { - await this.themeStateService.setSelectedTheme(this.theme); - } - async saveClearClipboard() { await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); } diff --git a/apps/browser/src/tools/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html index 71f4f1b991..997bc557b6 100644 --- a/apps/browser/src/tools/popup/settings/settings.component.html +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -86,6 +86,14 @@
{{ "options" | i18n }}
+ +
+

+ {{ "appearance" | i18n }} +

+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
diff --git a/apps/browser/src/vault/popup/settings/appearance.component.ts b/apps/browser/src/vault/popup/settings/appearance.component.ts new file mode 100644 index 0000000000..154d4e426d --- /dev/null +++ b/apps/browser/src/vault/popup/settings/appearance.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +import { enableAccountSwitching } from "../../../platform/flags"; + +@Component({ + selector: "vault-appearance", + templateUrl: "appearance.component.html", +}) +export class AppearanceComponent implements OnInit { + enableFavicon = false; + enableBadgeCounter = true; + theme: ThemeType; + themeOptions: any[]; + accountSwitcherEnabled = false; + + constructor( + private messagingService: MessagingService, + private domainSettingsService: DomainSettingsService, + private badgeSettingsService: BadgeSettingsServiceAbstraction, + i18nService: I18nService, + private themeStateService: ThemeStateService, + ) { + this.themeOptions = [ + { name: i18nService.t("default"), value: ThemeType.System }, + { name: i18nService.t("light"), value: ThemeType.Light }, + { name: i18nService.t("dark"), value: ThemeType.Dark }, + { name: "Nord", value: ThemeType.Nord }, + { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, + ]; + + this.accountSwitcherEnabled = enableAccountSwitching(); + } + + async ngOnInit() { + this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); + + this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); + + this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); + } + + async updateFavicon() { + await this.domainSettingsService.setShowFavicons(this.enableFavicon); + } + + async updateBadgeCounter() { + await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter); + this.messagingService.send("bgUpdateContextMenu"); + } + + async saveTheme() { + await this.themeStateService.setSelectedTheme(this.theme); + } +}