334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
|
import { lastValueFrom } from "rxjs";
|
|
|
|
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
|
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import { DialogService } from "@bitwarden/components";
|
|
|
|
import { CollectionAdminView } from "../../admin-console/organizations/core";
|
|
import { GroupService } from "../../admin-console/organizations/core/services/group/group.service";
|
|
import {
|
|
CollectionDialogResult,
|
|
CollectionDialogTabType,
|
|
openCollectionDialog,
|
|
} from "../../admin-console/organizations/shared/components/collection-dialog";
|
|
import {
|
|
BulkDeleteDialogResult,
|
|
openBulkDeleteDialog,
|
|
} from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
|
|
import { VaultFilterService } from "../individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
|
import { CollectionFilter } from "../individual-vault/vault-filter/shared/models/vault-filter.type";
|
|
import {
|
|
VaultItemRow,
|
|
VaultItemsComponent as BaseVaultItemsComponent,
|
|
} from "../individual-vault/vault-items.component";
|
|
|
|
const MaxCheckedCount = 500;
|
|
|
|
@Component({
|
|
selector: "app-org-vault-items",
|
|
templateUrl: "../individual-vault/vault-items.component.html",
|
|
})
|
|
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy {
|
|
@Input() set initOrganization(value: Organization) {
|
|
this.organization = value;
|
|
this.changeOrganization();
|
|
}
|
|
@Output() onEventsClicked = new EventEmitter<CipherView>();
|
|
|
|
protected allCiphers: CipherView[] = [];
|
|
|
|
constructor(
|
|
searchService: SearchService,
|
|
i18nService: I18nService,
|
|
platformUtilsService: PlatformUtilsService,
|
|
cipherService: CipherService,
|
|
vaultFilterService: VaultFilterService,
|
|
eventCollectionService: EventCollectionService,
|
|
totpService: TotpService,
|
|
passwordRepromptService: PasswordRepromptService,
|
|
dialogService: DialogService,
|
|
logService: LogService,
|
|
stateService: StateService,
|
|
organizationService: OrganizationService,
|
|
tokenService: TokenService,
|
|
searchPipe: SearchPipe,
|
|
protected groupService: GroupService,
|
|
private apiService: ApiService
|
|
) {
|
|
super(
|
|
searchService,
|
|
i18nService,
|
|
platformUtilsService,
|
|
vaultFilterService,
|
|
cipherService,
|
|
eventCollectionService,
|
|
totpService,
|
|
stateService,
|
|
passwordRepromptService,
|
|
dialogService,
|
|
logService,
|
|
searchPipe,
|
|
organizationService,
|
|
tokenService
|
|
);
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
super.ngOnDestroy();
|
|
}
|
|
|
|
async changeOrganization() {
|
|
this.groups = await this.groupService.getAll(this.organization?.id);
|
|
await this.loadCiphers();
|
|
await this.reload(this.activeFilter.buildFilter());
|
|
}
|
|
|
|
async loadCiphers() {
|
|
if (this.organization?.canEditAnyCollection) {
|
|
this.accessEvents = this.organization?.useEvents;
|
|
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(
|
|
this.organization?.id
|
|
);
|
|
} else {
|
|
this.allCiphers = (await this.cipherService.getAllDecrypted()).filter(
|
|
(c) => c.organizationId === this.organization?.id
|
|
);
|
|
}
|
|
|
|
this.searchService.indexCiphers(this.allCiphers, this.organization?.id);
|
|
}
|
|
|
|
async refreshCollections(): Promise<void> {
|
|
await this.vaultFilterService.reloadCollections();
|
|
if (this.activeFilter.selectedCollectionNode) {
|
|
this.activeFilter.selectedCollectionNode =
|
|
await this.vaultFilterService.getCollectionNodeFromTree(
|
|
this.activeFilter.selectedCollectionNode.node.id
|
|
);
|
|
}
|
|
}
|
|
|
|
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
|
this.deleted = deleted ?? false;
|
|
await this.applyFilter(filter);
|
|
this.loaded = true;
|
|
}
|
|
|
|
async refresh() {
|
|
await this.loadCiphers();
|
|
await this.refreshCollections();
|
|
super.refresh();
|
|
}
|
|
|
|
async search(timeout: number = null) {
|
|
await super.search(timeout, this.allCiphers);
|
|
}
|
|
|
|
events(c: CipherView) {
|
|
this.onEventsClicked.emit(c);
|
|
}
|
|
|
|
protected showFixOldAttachments(c: CipherView) {
|
|
return this.organization?.canEditAnyCollection && c.hasOldAttachments;
|
|
}
|
|
|
|
checkAll(select: boolean) {
|
|
if (select) {
|
|
this.checkAll(false);
|
|
}
|
|
|
|
const items: VaultItemRow[] = [...this.collections, ...this.ciphers];
|
|
if (!items.length) {
|
|
return;
|
|
}
|
|
|
|
const selectCount = select && items.length > MaxCheckedCount ? MaxCheckedCount : items.length;
|
|
for (let i = 0; i < selectCount; i++) {
|
|
this.checkRow(items[i], select);
|
|
}
|
|
}
|
|
|
|
checkRow(item: VaultItemRow, select?: boolean) {
|
|
if (item instanceof TreeNode && item.node.id == null) {
|
|
return;
|
|
}
|
|
|
|
// Do not allow checking a collection we cannot delete
|
|
if (item instanceof TreeNode && !this.canDeleteCollection(item.node)) {
|
|
return;
|
|
}
|
|
|
|
item.checked = select ?? !item.checked;
|
|
}
|
|
|
|
get selectedCollections(): TreeNode<CollectionFilter>[] {
|
|
if (!this.collections) {
|
|
return [];
|
|
}
|
|
return this.collections.filter((c) => !!(c as VaultItemRow).checked);
|
|
}
|
|
|
|
get selectedCollectionIds(): string[] {
|
|
return this.selectedCollections.map((c) => c.node.id);
|
|
}
|
|
|
|
canEditCollection(c: CollectionAdminView): boolean {
|
|
// Only edit collections if we're in the org vault and not editing "Unassigned"
|
|
if (this.organization === undefined || c.id === null) {
|
|
return false;
|
|
}
|
|
|
|
// Otherwise, check if we can edit the specified collection
|
|
return (
|
|
this.organization.canEditAnyCollection ||
|
|
(this.organization.canEditAssignedCollections && c.assigned)
|
|
);
|
|
}
|
|
|
|
async editCollection(c: CollectionView, tab: "info" | "access"): Promise<void> {
|
|
const tabType = tab == "info" ? CollectionDialogTabType.Info : CollectionDialogTabType.Access;
|
|
|
|
const dialog = openCollectionDialog(this.dialogService, {
|
|
data: { collectionId: c?.id, organizationId: this.organization?.id, initialTab: tabType },
|
|
});
|
|
|
|
const result = await lastValueFrom(dialog.closed);
|
|
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
|
|
this.actionPromise = this.refresh();
|
|
await this.actionPromise;
|
|
this.actionPromise = null;
|
|
}
|
|
}
|
|
|
|
get showMissingCollectionPermissionMessage(): boolean {
|
|
// Not filtering by collections, so no need to show message
|
|
if (this.activeFilter.selectedCollectionNode == null) {
|
|
return false;
|
|
}
|
|
|
|
// Filtering by all collections, so no need to show message
|
|
if (this.activeFilter.selectedCollectionNode.node.id == "AllCollections") {
|
|
return false;
|
|
}
|
|
|
|
// Filtering by a collection, so show message if user is not assigned
|
|
return !this.activeFilter.selectedCollectionNode.node.assigned && !this.organization.isAdmin;
|
|
}
|
|
|
|
canDeleteCollection(c: CollectionAdminView): boolean {
|
|
// Only delete collections if we're in the org vault and not deleting "Unassigned"
|
|
if (this.organization === undefined || c.id === null) {
|
|
return false;
|
|
}
|
|
|
|
// Otherwise, check if we can delete the specified collection
|
|
return (
|
|
this.organization?.canDeleteAnyCollection ||
|
|
(this.organization?.canDeleteAssignedCollections && c.assigned)
|
|
);
|
|
}
|
|
|
|
async deleteCollection(collection: CollectionView): Promise<void> {
|
|
if (
|
|
!this.organization.canDeleteAssignedCollections &&
|
|
!this.organization.canDeleteAnyCollection
|
|
) {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccurred"),
|
|
this.i18nService.t("missingPermissions")
|
|
);
|
|
return;
|
|
}
|
|
const confirmed = await this.platformUtilsService.showDialog(
|
|
this.i18nService.t("deleteCollectionConfirmation"),
|
|
collection.name,
|
|
this.i18nService.t("yes"),
|
|
this.i18nService.t("no"),
|
|
"warning"
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
try {
|
|
this.actionPromise = this.apiService.deleteCollection(this.organization?.id, collection.id);
|
|
await this.actionPromise;
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t("deletedCollectionId", collection.name)
|
|
);
|
|
this.actionPromise = null;
|
|
await this.refresh();
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
}
|
|
|
|
async bulkDelete() {
|
|
if (!(await this.repromptCipher())) {
|
|
return;
|
|
}
|
|
|
|
const selectedCipherIds = this.selectedCipherIds;
|
|
const selectedCollectionIds = this.deleted ? null : this.selectedCollectionIds;
|
|
|
|
if (!selectedCipherIds?.length && !selectedCollectionIds?.length) {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccurred"),
|
|
this.i18nService.t("nothingSelected")
|
|
);
|
|
return;
|
|
}
|
|
|
|
const dialog = openBulkDeleteDialog(this.dialogService, {
|
|
data: {
|
|
permanent: this.deleted,
|
|
cipherIds: selectedCipherIds,
|
|
collectionIds: selectedCollectionIds,
|
|
organization: this.organization,
|
|
},
|
|
});
|
|
|
|
const result = await lastValueFrom(dialog.closed);
|
|
if (result === BulkDeleteDialogResult.Deleted) {
|
|
this.actionPromise = this.refresh();
|
|
await this.actionPromise;
|
|
this.actionPromise = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated Block interaction using long running modal dialog instead
|
|
*/
|
|
protected get isProcessingAction() {
|
|
return this.actionPromise != null;
|
|
}
|
|
|
|
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
|
if (!this.organization?.canEditAnyCollection) {
|
|
return super.deleteCipherWithServer(id, this.deleted);
|
|
}
|
|
return permanent
|
|
? this.apiService.deleteCipherAdmin(id)
|
|
: this.apiService.putDeleteCipherAdmin(id);
|
|
}
|
|
}
|