mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 23:33:31 +00:00
[PM-12048] Wire up vNextCollectionService (#14871)
* remove derived state, add cache in service. Fix ts strict errors
* cleanup
* promote vNextCollectionService
* wip
* replace callers in web WIP
* refactor tests for web
* update callers to use vNextCollectionServcie methods in CLI
* WIP make decryptMany public again, fix callers, imports
* wip cli
* wip desktop
* update callers in browser, fix tests
* remove in service cache
* cleanup
* fix test
* clean up
* address cr feedback
* remove duplicate userId
* clean up
* remove unused import
* fix vault-settings-import-nudge.service
* fix caching issue
* clean up
* refactor decryption, cleanup, update callers
* clean up
* Use in-memory statedefinition
* Ac/pm 12048 v next collection service pairing (#15239)
* Draft from pairing with Gibson
* Add todos
* Add comment
* wip
* refactor upsert
---------
Co-authored-by: Brandon <btreston@bitwarden.com>
* clean up
* fix state definitions
* fix linter error
* cleanup
* add test, fix shareReplay
* fix item-more-options component
* fix desktop build
* refactor state to account for null as an initial value, remove caching
* add proper cache, add unit test, update callers
* clean up
* fix routing when deleting collections
* cleanup
* use combineLatest
* fix ts-strict errors, fix error handling
* refactor Collection and CollectionView properties for ts-strict
* Revert "refactor Collection and CollectionView properties for ts-strict"
This reverts commit a5c63aab76.
---------
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
@@ -1030,18 +1030,23 @@ export default class NotificationBackground {
|
||||
private async getCollectionData(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
): Promise<CollectionView[]> {
|
||||
const collections = (await this.collectionService.getAllDecrypted()).reduce<CollectionView[]>(
|
||||
(acc, collection) => {
|
||||
if (collection.organizationId === message?.orgId) {
|
||||
acc.push({
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
organizationId: collection.organizationId,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
const collections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
map((collections) =>
|
||||
collections.reduce<CollectionView[]>((acc, collection) => {
|
||||
if (collection.organizationId === message?.orgId) {
|
||||
acc.push({
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
organizationId: collection.organizationId,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
),
|
||||
),
|
||||
);
|
||||
return collections;
|
||||
}
|
||||
|
||||
@@ -1620,7 +1620,6 @@ export default class MainBackground {
|
||||
this.keyService.clearKeys(userBeingLoggedOut),
|
||||
this.cipherService.clear(userBeingLoggedOut),
|
||||
this.folderService.clear(userBeingLoggedOut),
|
||||
this.collectionService.clear(userBeingLoggedOut),
|
||||
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
|
||||
this.vaultFilterService.clear(),
|
||||
this.biometricStateService.logout(userBeingLoggedOut),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Observable, combineLatest, filter, first, map, switchMap } from "rxjs";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -70,7 +71,12 @@ export class AssignCollections {
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest([cipher$, this.collectionService.decryptedCollections$])
|
||||
const decryptedCollection$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
);
|
||||
|
||||
combineLatest([cipher$, decryptedCollection$])
|
||||
.pipe(takeUntilDestroyed(), first())
|
||||
.subscribe(([cipherView, collections]) => {
|
||||
let availableCollections = collections;
|
||||
|
||||
@@ -93,7 +93,7 @@ export class ItemMoreOptionsComponent {
|
||||
switchMap((userId) => {
|
||||
return combineLatest([
|
||||
this.organizationService.hasOrganizations(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
map(([hasOrgs, collections]) => {
|
||||
const canEditCollections = collections.some((c) => !c.readOnly);
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("VaultPopupItemsService", () => {
|
||||
];
|
||||
|
||||
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
|
||||
collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections);
|
||||
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
|
||||
|
||||
activeUserLastSync$ = new BehaviorSubject(new Date());
|
||||
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);
|
||||
|
||||
@@ -72,6 +72,11 @@ export class VaultPopupItemsService {
|
||||
private organizations$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
);
|
||||
|
||||
private decryptedCollections$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that contains the list of other cipher types that should be shown
|
||||
* in the autofill section of the Vault tab. Depends on vault settings.
|
||||
@@ -130,7 +135,7 @@ export class VaultPopupItemsService {
|
||||
|
||||
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
|
||||
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
@@ -291,7 +296,7 @@ export class VaultPopupItemsService {
|
||||
*/
|
||||
deletedCiphers$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
|
||||
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
|
||||
@@ -20,6 +20,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import {
|
||||
@@ -58,7 +59,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
};
|
||||
|
||||
const collectionService = {
|
||||
decryptedCollections$,
|
||||
decryptedCollections$: () => decryptedCollections$,
|
||||
getAllNested: () => Promise.resolve([]),
|
||||
} as unknown as CollectionService;
|
||||
|
||||
@@ -106,7 +107,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
signal: jest.fn(() => mockCachedSignal),
|
||||
};
|
||||
|
||||
collectionService.getAllNested = () => Promise.resolve([]);
|
||||
collectionService.getAllNested = () => [];
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
@@ -382,14 +383,7 @@ describe("VaultPopupListFiltersService", () => {
|
||||
beforeEach(() => {
|
||||
decryptedCollections$.next(testCollections);
|
||||
|
||||
collectionService.getAllNested = () =>
|
||||
Promise.resolve(
|
||||
testCollections.map((c) => ({
|
||||
children: [],
|
||||
node: c,
|
||||
parent: null,
|
||||
})),
|
||||
);
|
||||
collectionService.getAllNested = () => testCollections.map((c) => new TreeNode(c, null));
|
||||
});
|
||||
|
||||
it("returns all collections", (done) => {
|
||||
@@ -755,15 +749,13 @@ function createSeededVaultPopupListFiltersService(
|
||||
} as any;
|
||||
|
||||
const collectionServiceMock = {
|
||||
decryptedCollections$: seededCollections$,
|
||||
decryptedCollections$: () => seededCollections$,
|
||||
getAllNested: () =>
|
||||
Promise.resolve(
|
||||
seededCollections$.value.map((c) => ({
|
||||
children: [],
|
||||
node: c,
|
||||
parent: null,
|
||||
})),
|
||||
),
|
||||
seededCollections$.value.map((c) => ({
|
||||
children: [],
|
||||
node: c,
|
||||
parent: null,
|
||||
})),
|
||||
} as any;
|
||||
|
||||
const folderServiceMock = {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
@@ -446,7 +445,7 @@ export class VaultPopupListFiltersService {
|
||||
this.filters$.pipe(
|
||||
distinctUntilChanged((prev, curr) => prev.organization?.id === curr.organization?.id),
|
||||
),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
this.organizationService.memberOrganizations$(userId),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation),
|
||||
]),
|
||||
@@ -463,16 +462,11 @@ export class VaultPopupListFiltersService {
|
||||
}
|
||||
return sortDefaultCollections(filtered, orgs, this.i18nService.collator);
|
||||
}),
|
||||
switchMap((collections) => {
|
||||
return from(this.collectionService.getAllNested(collections)).pipe(
|
||||
map(
|
||||
(nested) =>
|
||||
new DynamicTreeNode<CollectionView>({
|
||||
fullList: collections,
|
||||
nestedList: nested,
|
||||
}),
|
||||
),
|
||||
);
|
||||
map((fullList) => {
|
||||
return new DynamicTreeNode<CollectionView>({
|
||||
fullList,
|
||||
nestedList: this.collectionService.getAllNested(fullList),
|
||||
});
|
||||
}),
|
||||
map((tree) =>
|
||||
tree.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")),
|
||||
|
||||
@@ -24,6 +24,7 @@ import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export
|
||||
import { LoginExport } from "@bitwarden/common/models/export/login.export";
|
||||
import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -442,8 +443,11 @@ export class GetCommand extends DownloadCommand {
|
||||
|
||||
private async getCollection(id: string) {
|
||||
let decCollection: CollectionView = null;
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
if (Utils.isGuid(id)) {
|
||||
const collection = await this.collectionService.get(id);
|
||||
const collection = await firstValueFrom(
|
||||
this.collectionService.encryptedCollections$(activeUserId).pipe(getById(id)),
|
||||
);
|
||||
if (collection != null) {
|
||||
const orgKeys = await firstValueFrom(this.keyService.activeUserOrgKeys$);
|
||||
decCollection = await collection.decrypt(
|
||||
@@ -451,7 +455,9 @@ export class GetCommand extends DownloadCommand {
|
||||
);
|
||||
}
|
||||
} else if (id.trim() !== "") {
|
||||
let collections = await this.collectionService.getAllDecrypted();
|
||||
let collections = await firstValueFrom(
|
||||
this.collectionService.decryptedCollections$(activeUserId),
|
||||
);
|
||||
collections = CliUtils.searchCollections(collections, id);
|
||||
if (collections.length > 1) {
|
||||
return Response.multipleResults(collections.map((c) => c.id));
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionResponse } from "../admin-console/models/response/collection.response";
|
||||
import { OrganizationUserResponse } from "../admin-console/models/response/organization-user.response";
|
||||
@@ -42,6 +43,7 @@ export class ListCommand {
|
||||
private apiService: ApiService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private accountService: AccountService,
|
||||
private keyService: KeyService,
|
||||
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
|
||||
) {}
|
||||
|
||||
@@ -158,7 +160,10 @@ export class ListCommand {
|
||||
}
|
||||
|
||||
private async listCollections(options: Options) {
|
||||
let collections = await this.collectionService.getAllDecrypted();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
let collections = await firstValueFrom(
|
||||
this.collectionService.decryptedCollections$(activeUserId),
|
||||
);
|
||||
|
||||
if (options.organizationId != null) {
|
||||
collections = collections.filter((c) => {
|
||||
@@ -178,13 +183,13 @@ export class ListCommand {
|
||||
}
|
||||
|
||||
private async listOrganizationCollections(options: Options) {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
if (options.organizationId == null || options.organizationId === "") {
|
||||
return Response.badRequest("`organizationid` option is required.");
|
||||
}
|
||||
if (!Utils.isGuid(options.organizationId)) {
|
||||
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
|
||||
}
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
if (!userId) {
|
||||
return Response.badRequest("No user found.");
|
||||
}
|
||||
@@ -207,7 +212,13 @@ export class ListCommand {
|
||||
const collections = response.data
|
||||
.filter((c) => c.organizationId === options.organizationId)
|
||||
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
|
||||
let decCollections = await this.collectionService.decryptMany(collections);
|
||||
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId));
|
||||
if (orgKeys == null) {
|
||||
throw new Error("Organization keys not found.");
|
||||
}
|
||||
let decCollections = await firstValueFrom(
|
||||
this.collectionService.decryptMany$(collections, orgKeys),
|
||||
);
|
||||
if (options.search != null && options.search.trim() !== "") {
|
||||
decCollections = CliUtils.searchCollections(decCollections, options.search);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.eventCollectionService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.cliRestrictedItemTypesService,
|
||||
);
|
||||
this.createCommand = new CreateCommand(
|
||||
|
||||
@@ -901,7 +901,6 @@ export class ServiceContainer {
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
]);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||
|
||||
@@ -114,6 +114,7 @@ export class VaultProgram extends BaseProgram {
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.eventCollectionService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.cliRestrictedItemTypesService,
|
||||
);
|
||||
const response = await command.run(object, cmd);
|
||||
|
||||
@@ -677,7 +677,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.keyService.clearKeys(userBeingLoggedOut);
|
||||
await this.cipherService.clear(userBeingLoggedOut);
|
||||
await this.folderService.clear(userBeingLoggedOut);
|
||||
await this.collectionService.clear(userBeingLoggedOut);
|
||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut);
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
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 { getByIds } from "@bitwarden/common/platform/misc";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
@@ -360,7 +361,12 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
this.allOrganizations = orgs;
|
||||
});
|
||||
|
||||
this.collectionService.decryptedCollections$
|
||||
if (!this.activeUserId) {
|
||||
throw new Error("No user found.");
|
||||
}
|
||||
|
||||
this.collectionService
|
||||
.decryptedCollections$(this.activeUserId)
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((collections) => {
|
||||
this.allCollections = collections;
|
||||
@@ -701,9 +707,17 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
this.cipherId = null;
|
||||
this.action = "view";
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
|
||||
if (!this.activeUserId) {
|
||||
throw new Error("No userId provided.");
|
||||
}
|
||||
|
||||
this.collections = await firstValueFrom(
|
||||
this.collectionService.decryptedCollectionViews$(cipher.collectionIds as CollectionId[]),
|
||||
this.collectionService
|
||||
.decryptedCollections$(this.activeUserId)
|
||||
.pipe(getByIds(cipher.collectionIds)),
|
||||
);
|
||||
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
if (this.activeUserId) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
@@ -264,6 +265,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
private collectionService: CollectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -1133,6 +1135,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
try {
|
||||
await this.apiService.deleteCollection(this.organization?.id, collection.id);
|
||||
await this.collectionService.delete([collection.id as CollectionId], this.userId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
@@ -25,10 +26,13 @@ import {
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { GroupDetailsView, InternalGroupApiService as GroupService } from "../core";
|
||||
|
||||
@@ -100,6 +104,8 @@ export class GroupsComponent {
|
||||
private logService: LogService,
|
||||
private collectionService: CollectionService,
|
||||
private toastService: ToastService,
|
||||
private keyService: KeyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.route.params
|
||||
.pipe(
|
||||
@@ -244,16 +250,22 @@ export class GroupsComponent {
|
||||
this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow);
|
||||
}
|
||||
|
||||
private async toCollectionMap(response: ListResponse<CollectionResponse>) {
|
||||
private toCollectionMap(
|
||||
response: ListResponse<CollectionResponse>,
|
||||
): Observable<Record<string, CollectionView>> {
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
);
|
||||
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||
|
||||
// Convert to an object using collection Ids as keys for faster name lookups
|
||||
const collectionMap: Record<string, CollectionView> = {};
|
||||
decryptedCollections.forEach((c) => (collectionMap[c.id] = c));
|
||||
|
||||
return collectionMap;
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
switchMap((orgKeys) => this.collectionService.decryptMany$(collections, orgKeys)),
|
||||
map((collections) => {
|
||||
const collectionMap: Record<string, CollectionView> = {};
|
||||
collections.forEach((c) => (collectionMap[c.id] = c));
|
||||
return collectionMap;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
@@ -307,17 +308,27 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
* Retrieve a map of all collection IDs <-> names for the organization.
|
||||
*/
|
||||
async getCollectionNameMap() {
|
||||
const collectionMap = new Map<string, string>();
|
||||
const response = await this.apiService.getCollections(this.organization.id);
|
||||
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
|
||||
map((res) =>
|
||||
res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))),
|
||||
),
|
||||
);
|
||||
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||
|
||||
decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name));
|
||||
const decryptedCollections$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
withLatestFrom(response),
|
||||
switchMap(([orgKeys, collections]) =>
|
||||
this.collectionService.decryptMany$(collections, orgKeys),
|
||||
),
|
||||
map((collections) => {
|
||||
const collectionMap = new Map<string, string>();
|
||||
collections.forEach((c) => collectionMap.set(c.id, c.name));
|
||||
return collectionMap;
|
||||
}),
|
||||
);
|
||||
|
||||
return collectionMap;
|
||||
return await firstValueFrom(decryptedCollections$);
|
||||
}
|
||||
|
||||
removeUser(id: string): Promise<void> {
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
CollectionService,
|
||||
Collection,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -38,6 +37,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
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 { getById } from "@bitwarden/common/platform/misc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -141,7 +141,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected PermissionMode = PermissionMode;
|
||||
protected showDeleteButton = false;
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
@@ -166,14 +165,12 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit() {
|
||||
// Opened from the individual vault
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
if (this.params.showOrgSelector) {
|
||||
this.showOrgSelector = true;
|
||||
this.formGroup.controls.selectedOrg.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => this.loadOrg(id));
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.organizations$ = this.organizationService.organizations$(userId).pipe(
|
||||
first(),
|
||||
map((orgs) =>
|
||||
@@ -195,9 +192,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
this.collections = await this.collectionService.getAll();
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(this.organizations$, this.collections, this.i18nService),
|
||||
freeOrgCollectionLimitValidator(
|
||||
this.organizations$,
|
||||
this.collectionService
|
||||
.encryptedCollections$(userId)
|
||||
.pipe(map((collections) => collections ?? [])),
|
||||
this.i18nService,
|
||||
),
|
||||
);
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
@@ -212,7 +214,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
filter(() => this.organizationSelected.errors?.cannotCreateCollections),
|
||||
switchMap((value) => this.findOrganizationById(value)),
|
||||
switchMap((organizationId) => this.organizations$.pipe(getById(organizationId))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((org) => {
|
||||
@@ -222,11 +224,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
async findOrganizationById(orgId: string): Promise<Organization | undefined> {
|
||||
const organizations = await firstValueFrom(this.organizations$);
|
||||
return organizations.find((org) => org.id === orgId);
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const organization$ = this.organizationService
|
||||
@@ -413,7 +410,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
collectionView.name = this.formGroup.controls.name.value;
|
||||
}
|
||||
|
||||
const savedCollection = await this.collectionAdminService.save(collectionView);
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const savedCollection = await this.collectionAdminService.save(collectionView, userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
|
||||
it("returns null if organization is not found", async () => {
|
||||
const orgs: Organization[] = [];
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), of([]), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result: Observable<ValidationErrors> = validator(control) as Observable<ValidationErrors>;
|
||||
@@ -28,7 +28,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
});
|
||||
|
||||
it("returns null if control is not an instance of FormControl", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService);
|
||||
const control = {} as AbstractControl;
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
@@ -40,7 +40,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
});
|
||||
|
||||
it("returns null if control is not provided", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService);
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
undefined as any,
|
||||
@@ -53,7 +53,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
it("returns null if organization has not reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 2 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
@@ -65,7 +65,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
it("returns error if organization has reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 1 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { map, Observable, of } from "rxjs";
|
||||
import { combineLatest, map, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
export function freeOrgCollectionLimitValidator(
|
||||
orgs: Observable<Organization[]>,
|
||||
collections: Collection[],
|
||||
organizations$: Observable<Organization[]>,
|
||||
collections$: Observable<Collection[]>,
|
||||
i18nService: I18nService,
|
||||
): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
@@ -21,15 +22,16 @@ export function freeOrgCollectionLimitValidator(
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return orgs.pipe(
|
||||
map((organizations) => organizations.find((org) => org.id === orgId)),
|
||||
map((org) => {
|
||||
if (!org) {
|
||||
return combineLatest([organizations$.pipe(getById(orgId)), collections$]).pipe(
|
||||
map(([organization, collections]) => {
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgCollections = collections.filter((c) => c.organizationId === org.id);
|
||||
const hasReachedLimit = org.maxCollections === orgCollections.length;
|
||||
const orgCollections = collections.filter(
|
||||
(collection: Collection) => collection.organizationId === organization.id,
|
||||
);
|
||||
const hasReachedLimit = organization.maxCollections === orgCollections.length;
|
||||
|
||||
if (hasReachedLimit) {
|
||||
return {
|
||||
|
||||
@@ -285,7 +285,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
this.biometricStateService.logout(userId),
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
@@ -68,7 +68,6 @@ export class BulkDeleteDialogComponent {
|
||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
||||
private cipherService: CipherService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private collectionService: CollectionService,
|
||||
@@ -116,7 +115,11 @@ export class BulkDeleteDialogComponent {
|
||||
});
|
||||
}
|
||||
if (this.collections.length) {
|
||||
await this.collectionService.delete(this.collections.map((c) => c.id));
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.collectionService.delete(
|
||||
this.collections.map((c) => c.id as CollectionId),
|
||||
userId,
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("vault filter service", () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
organizationService.memberOrganizations$.mockReturnValue(organizations);
|
||||
folderService.folderViews$.mockReturnValue(folderViews);
|
||||
collectionService.decryptedCollections$ = collectionViews;
|
||||
collectionService.decryptedCollections$.mockReturnValue(collectionViews);
|
||||
policyService.policyAppliesToUser$
|
||||
.calledWith(PolicyType.OrganizationDataOwnership, mockUserId)
|
||||
.mockReturnValue(organizationDataOwnershipPolicy);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Injectable } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
combineLatestWith,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
@@ -100,13 +99,13 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
map((folders) => this.buildFolderTree(folders)),
|
||||
);
|
||||
|
||||
filteredCollections$: Observable<CollectionView[]> =
|
||||
this.collectionService.decryptedCollections$.pipe(
|
||||
combineLatestWith(this._organizationFilter),
|
||||
switchMap(([collections, org]) => {
|
||||
return this.filterCollections(collections, org);
|
||||
}),
|
||||
);
|
||||
filteredCollections$: Observable<CollectionView[]> = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
this._organizationFilter,
|
||||
]).pipe(switchMap(([collections, org]) => this.filterCollections(collections, org)));
|
||||
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = combineLatest([
|
||||
this.filteredCollections$,
|
||||
|
||||
@@ -334,7 +334,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
});
|
||||
|
||||
const filter$ = this.routedVaultFilterService.filter$;
|
||||
const allCollections$ = this.collectionService.decryptedCollections$;
|
||||
|
||||
const allCollections$ = this.collectionService.decryptedCollections$(activeUserId);
|
||||
const nestedCollections$ = allCollections$.pipe(
|
||||
map((collections) => getNestedCollectionTree(collections)),
|
||||
);
|
||||
@@ -861,7 +862,10 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
if (result.collection) {
|
||||
// Update CollectionService with the new collection
|
||||
const c = new CollectionData(result.collection as CollectionDetailsResponse);
|
||||
await this.collectionService.upsert(c);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
await this.collectionService.upsert(c, activeUserId);
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
@@ -878,20 +882,23 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
if (result.action === CollectionDialogAction.Saved) {
|
||||
if (result.collection) {
|
||||
// Update CollectionService with the new collection
|
||||
const c = new CollectionData(result.collection as CollectionDetailsResponse);
|
||||
await this.collectionService.upsert(c);
|
||||
await this.collectionService.upsert(c, activeUserId);
|
||||
}
|
||||
this.refresh();
|
||||
} else if (result.action === CollectionDialogAction.Deleted) {
|
||||
await this.collectionService.delete(result.collection?.id);
|
||||
this.refresh();
|
||||
const parent = this.selectedCollection?.parent;
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
if (this.selectedCollection?.node.id === c?.id) {
|
||||
const navigateAway = this.selectedCollection && this.selectedCollection.node.id === c.id;
|
||||
await this.collectionService.delete([result.collection?.id as CollectionId], activeUserId);
|
||||
this.refresh();
|
||||
if (navigateAway) {
|
||||
await this.router.navigate([], {
|
||||
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
|
||||
queryParams: { collectionId: parent?.node.id ?? null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
@@ -916,18 +923,22 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parent = this.selectedCollection?.parent;
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
const navigateAway =
|
||||
this.selectedCollection && this.selectedCollection.node.id === collection.id;
|
||||
await this.apiService.deleteCollection(collection.organizationId, collection.id);
|
||||
await this.collectionService.delete(collection.id);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.collectionService.delete([collection.id as CollectionId], activeUserId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedCollectionId", collection.name),
|
||||
});
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
if (this.selectedCollection?.node.id === collection.id) {
|
||||
if (navigateAway) {
|
||||
await this.router.navigate([], {
|
||||
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
|
||||
queryParams: { collectionId: parent?.node.id ?? null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user