mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 02:19:18 +00:00
[PM-29892] - fix bulk share in vault (#18601)
* fix bulk share in vault * clean up types. * remove unnecessary optional chain * add back defensive programming. update restore * fix searchableCollectionNodes * add back optional chains
This commit is contained in:
@@ -472,7 +472,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
collections,
|
||||
filter.collectionId,
|
||||
);
|
||||
searchableCollectionNodes = selectedCollection.children ?? [];
|
||||
searchableCollectionNodes = selectedCollection?.children ?? [];
|
||||
}
|
||||
|
||||
let collectionsToReturn: CollectionAdminView[] = [];
|
||||
@@ -962,10 +962,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
await this.editCipher(cipher, true);
|
||||
}
|
||||
|
||||
restore = async (c: CipherViewLike): Promise<boolean> => {
|
||||
restore = async (c: CipherViewLike): Promise<void> => {
|
||||
const organization = await firstValueFrom(this.organization$);
|
||||
if (!CipherViewLikeUtils.isDeleted(c)) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -974,11 +974,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
!organization.allowAdminAccessToAllCollectionItems
|
||||
) {
|
||||
this.showMissingPermissionsError();
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.repromptCipher([c]))) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow restore of an Unassigned Item
|
||||
@@ -996,10 +996,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
message: this.i18nService.t("restoredItem"),
|
||||
});
|
||||
this.refresh();
|
||||
return true;
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export interface VaultItemDialogParams {
|
||||
/**
|
||||
* Function to restore a cipher from the trash.
|
||||
*/
|
||||
restore?: (c: CipherViewLike) => Promise<boolean>;
|
||||
restore?: (c: CipherViewLike) => Promise<void>;
|
||||
}
|
||||
|
||||
export const VaultItemDialogResult = {
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
lastValueFrom,
|
||||
Observable,
|
||||
Subject,
|
||||
} from "rxjs";
|
||||
import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs";
|
||||
import {
|
||||
concatMap,
|
||||
debounceTime,
|
||||
@@ -18,6 +9,7 @@ import {
|
||||
first,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
@@ -89,7 +81,6 @@ import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
AttachmentDialogCloseResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
CipherFormConfig,
|
||||
@@ -179,14 +170,10 @@ type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
|
||||
],
|
||||
})
|
||||
export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<C>;
|
||||
readonly filterComponent = viewChild(VaultFilterComponent);
|
||||
readonly vaultItemsComponent = viewChild(VaultItemsComponent);
|
||||
|
||||
trashCleanupWarning: string = null;
|
||||
trashCleanupWarning: string = "";
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
|
||||
protected deactivatedOrgIcon = DeactivatedOrg;
|
||||
@@ -198,20 +185,20 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
protected refreshing = false;
|
||||
protected processingEvent = false;
|
||||
protected filter: RoutedVaultFilterModel = {};
|
||||
protected showBulkMove: boolean;
|
||||
protected canAccessPremium: boolean;
|
||||
protected allCollections: CollectionView[];
|
||||
protected showBulkMove: boolean = false;
|
||||
protected canAccessPremium: boolean = false;
|
||||
protected allCollections: CollectionView[] = [];
|
||||
protected allOrganizations: Organization[] = [];
|
||||
protected ciphers: C[];
|
||||
protected collections: CollectionView[];
|
||||
protected isEmpty: boolean;
|
||||
protected ciphers: C[] = [];
|
||||
protected collections: CollectionView[] = [];
|
||||
protected isEmpty: boolean = false;
|
||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||
protected canCreateCollections = false;
|
||||
protected currentSearchText$: Observable<string> = this.route.queryParams.pipe(
|
||||
map((queryParams) => queryParams.search),
|
||||
);
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private refresh$ = new Subject<void>();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
@@ -220,7 +207,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
|
||||
organizations$ = this.accountService.activeAccount$
|
||||
.pipe(map((a) => a?.id))
|
||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||
.pipe(switchMap((id) => (id ? this.organizationService.organizations$(id) : of([]))));
|
||||
|
||||
emptyState$ = combineLatest([
|
||||
this.currentSearchText$,
|
||||
@@ -228,7 +215,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
this.organizations$,
|
||||
]).pipe(
|
||||
map(([searchText, filter, organizations]) => {
|
||||
const selectedOrg = organizations?.find((org) => org.id === filter.organizationId);
|
||||
const selectedOrg = organizations.find((org) => org.id === filter.organizationId);
|
||||
const isOrgDisabled = selectedOrg && !selectedOrg.enabled;
|
||||
|
||||
if (isOrgDisabled) {
|
||||
@@ -586,7 +573,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.refresh$),
|
||||
switchMap(() => this.refresh$.pipe(startWith(undefined))),
|
||||
tap(() => (this.refreshing = true)),
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
@@ -712,7 +699,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
async handleUnknownCipher() {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("unknownCipher"),
|
||||
});
|
||||
await this.router.navigate([], {
|
||||
@@ -842,9 +828,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
if (orgId == null) {
|
||||
orgId = "MyVault";
|
||||
}
|
||||
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
|
||||
const data = this.filterComponent()?.filters?.organizationFilter?.data$;
|
||||
if (data == undefined) {
|
||||
return;
|
||||
}
|
||||
const orgs = await firstValueFrom(data);
|
||||
const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode<OrganizationFilter>;
|
||||
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
|
||||
await this.filterComponent()?.filters?.organizationFilter?.action(orgNode);
|
||||
}
|
||||
|
||||
addFolder = (): void => {
|
||||
@@ -912,7 +902,10 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
canEditCipher: cipher.edit,
|
||||
});
|
||||
|
||||
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
result.action === AttachmentDialogResult.Uploaded ||
|
||||
@@ -966,7 +959,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
*/
|
||||
async addCipher(cipherType?: CipherType) {
|
||||
const type = cipherType ?? this.activeFilter.cipherType;
|
||||
const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type);
|
||||
const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", undefined, type);
|
||||
const collectionId =
|
||||
this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null
|
||||
? this.activeFilter.collectionId
|
||||
@@ -994,7 +987,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) {
|
||||
return this.editCipherId(uuidAsString(cipher?.id), cloneMode);
|
||||
return this.editCipherId(uuidAsString(cipher.id), cloneMode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1088,6 +1081,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
if (result.action === CollectionDialogAction.Saved) {
|
||||
if (result.collection) {
|
||||
// Update CollectionService with the new collection
|
||||
@@ -1104,7 +1100,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
|
||||
const dialog = openCollectionDialog(this.dialogService, {
|
||||
data: {
|
||||
collectionId: c?.id,
|
||||
collectionId: c.id,
|
||||
organizationId: c.organizationId,
|
||||
initialTab: tab,
|
||||
limitNestedCollections: true,
|
||||
@@ -1112,6 +1108,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
if (result.action === CollectionDialogAction.Saved) {
|
||||
if (result.collection) {
|
||||
@@ -1163,7 +1162,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedCollectionId", collection.name),
|
||||
});
|
||||
if (navigateAway) {
|
||||
@@ -1196,12 +1194,12 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
let availableCollections: CollectionView[] = [];
|
||||
const orgId =
|
||||
this.activeFilter.organizationId ||
|
||||
ciphers.find((c) => c.organizationId !== null)?.organizationId;
|
||||
ciphers.find((c) => c.organizationId !== undefined)?.organizationId;
|
||||
|
||||
if (orgId && orgId !== "MyVault") {
|
||||
const organization = this.allOrganizations.find((o) => o.id === orgId);
|
||||
availableCollections = this.allCollections.filter(
|
||||
(c) => c.organizationId === organization.id,
|
||||
(c) => c.organizationId === organization?.id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1229,7 +1227,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
ciphers: ciphersToAssign,
|
||||
organizationId: orgId as OrganizationId,
|
||||
availableCollections,
|
||||
activeCollection: this.activeFilter?.selectedCollectionNode?.node,
|
||||
activeCollection: this.activeFilter.selectedCollectionNode?.node,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1255,7 +1253,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
await this.editCipher(cipher, true);
|
||||
}
|
||||
|
||||
restore = async (c: C): Promise<boolean> => {
|
||||
restore = async (c: CipherViewLike) => {
|
||||
let toastMessage;
|
||||
if (!CipherViewLikeUtils.isDeleted(c)) {
|
||||
return;
|
||||
@@ -1281,13 +1279,14 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: toastMessage,
|
||||
});
|
||||
this.refresh();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
async bulkRestore(ciphers: C[]) {
|
||||
@@ -1311,7 +1310,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
if (selectedCipherIds.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("nothingSelected"),
|
||||
});
|
||||
return;
|
||||
@@ -1321,23 +1319,24 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: toastMessage,
|
||||
});
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async handleDeleteEvent(items: VaultItem<C>[]) {
|
||||
const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher);
|
||||
const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection);
|
||||
const ciphers = items
|
||||
.filter((i) => i.collection === undefined && i.cipher !== undefined)
|
||||
.map((i) => i.cipher as C);
|
||||
const collections = items
|
||||
.filter((i) => i.collection !== undefined)
|
||||
.map((i) => i.collection as CollectionView);
|
||||
if (ciphers.length === 1 && collections.length === 0) {
|
||||
await this.deleteCipher(ciphers[0]);
|
||||
} else if (ciphers.length === 0 && collections.length === 1) {
|
||||
await this.deleteCollection(collections[0]);
|
||||
} else {
|
||||
const orgIds = items
|
||||
.filter((i) => i.cipher === undefined)
|
||||
.map((i) => i.collection.organizationId);
|
||||
const orgIds = collections.map((c) => c.organizationId);
|
||||
const orgs = await firstValueFrom(
|
||||
this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))),
|
||||
);
|
||||
@@ -1345,7 +1344,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCipher(c: C): Promise<boolean> {
|
||||
async deleteCipher(c: C) {
|
||||
if (!(await this.repromptCipher([c]))) {
|
||||
return;
|
||||
}
|
||||
@@ -1364,7 +1363,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1373,7 +1372,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"),
|
||||
});
|
||||
this.refresh();
|
||||
@@ -1390,7 +1388,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
if (ciphers.length === 0 && collections.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("nothingSelected"),
|
||||
});
|
||||
return;
|
||||
@@ -1430,7 +1427,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
if (selectedCipherIds.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("nothingSelected"),
|
||||
});
|
||||
return;
|
||||
@@ -1454,11 +1450,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
|
||||
if (!login) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
this.showErrorToast();
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === "username") {
|
||||
@@ -1471,15 +1464,15 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
typeI18nKey = "password";
|
||||
} else if (field === "totp") {
|
||||
aType = "TOTP";
|
||||
if (!login.totp) {
|
||||
this.showErrorToast();
|
||||
return;
|
||||
}
|
||||
const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp));
|
||||
value = totpResponse.code;
|
||||
typeI18nKey = "verificationCodeTotp";
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
this.showErrorToast();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1494,10 +1487,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this.showErrorToast();
|
||||
return;
|
||||
}
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
|
||||
});
|
||||
|
||||
@@ -1514,6 +1510,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
}
|
||||
}
|
||||
|
||||
showErrorToast() {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the favorite status of the cipher and updates it on the server.
|
||||
*/
|
||||
@@ -1525,7 +1528,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites",
|
||||
),
|
||||
@@ -1540,15 +1542,15 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
: this.cipherService.softDeleteWithServer(id, userId);
|
||||
}
|
||||
|
||||
protected async repromptCipher(ciphers: C[]) {
|
||||
protected async repromptCipher(ciphers: CipherViewLike[]) {
|
||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||
|
||||
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this.refresh$.next();
|
||||
this.vaultItemsComponent?.clearSelection();
|
||||
this.refresh$.next(undefined);
|
||||
this.vaultItemsComponent()?.clearSelection();
|
||||
}
|
||||
|
||||
private async go(queryParams: any = null) {
|
||||
@@ -1573,7 +1575,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
private showMissingPermissionsError() {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("missingPermissions"),
|
||||
});
|
||||
}
|
||||
@@ -1584,13 +1585,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
*/
|
||||
private async getPasswordFromCipherViewLike(cipher: C): Promise<string | undefined> {
|
||||
if (!CipherViewLikeUtils.isCipherListView(cipher)) {
|
||||
return Promise.resolve(cipher.login?.password);
|
||||
return Promise.resolve(cipher?.login?.password);
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId);
|
||||
const cipherView = await this.cipherService.decrypt(_cipher, activeUserId);
|
||||
return cipherView.login?.password;
|
||||
return cipherView.login.password;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user