mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
* [EC-775] feat: add compatibility layer from #4154 * [EC-775] fix: ciphers not reloading on filter change * [EC-775] feat: add support for cipher types * [EC-775] feat: implement organization switching * [EC-775] feat: remove invalid folder and collection checks Had to remove these becuase they were causing double navigations on each click. * [EC-775] fix: fix reverse data flow race condition vault-filter.component was pushing up old filter models which would sometimes overwrite new filter models that came from the routed filter service. * [EC-775] fix: No folder use-case not working * [EC-775] feat: make navigation behave like master * [EC-775] feat: add support for trash * [EC-775] chore: simplify findNode * [EC-775] feat: add support for org vault * [EC-775] feat: add support for orgId in path * [EC-775] feat: use proper treenode constructor * [EC-775] chore: remove unnecessary variable * [EC-775] docs: add docs to relevant classes * [EC-775] chore: use existing function for searching tree * [EC-775] fix: hide "new" button in trash view * [EC-775] feat: add explicit handling for `AllItems` * [EC-775] fix: prune folderId when changing organization * [EC-775] fix: properly use `undefined` instead of `null` * [EC-775] chore: simplify setters using ternary operator * [EC-775] feat: add static typing to `type` filter * [EC-775] feat: use new `All` variable for collections * [EC-775] feat: return `RouterLink` compatible link from `createRoute` * [EC-775] feat: add ordId path support to `createRoute` * [EC-775] fix: interpret params differently in org vault This is needed due to how defaults used to work when using `state-in-code`. We really want to get rid of this type of logic going forward. * [EC-775] doc: clarify `createRoute` * [EC-775] fix: better `type` typing * [EC-775] feat: remove support for path navigation It's better that we circle back to this type of navigationt when we're working on the VVR and have more knowledge about how this is supposed to work. * [EC-775] fix: refactor bridge service to improve readability Refactor follows feedback from PR review
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
import {
|
|
ChangeDetectorRef,
|
|
Component,
|
|
NgZone,
|
|
OnDestroy,
|
|
OnInit,
|
|
ViewChild,
|
|
ViewContainerRef,
|
|
} from "@angular/core";
|
|
import { ActivatedRoute, Params, Router } from "@angular/router";
|
|
import { combineLatest, firstValueFrom, Subject } from "rxjs";
|
|
import { first, switchMap, takeUntil } from "rxjs/operators";
|
|
|
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
|
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import { DialogService } from "@bitwarden/components";
|
|
|
|
import { EntityEventsComponent } from "../../organizations/manage/entity-events.component";
|
|
import { CollectionsComponent } from "../../organizations/vault/collections.component";
|
|
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
|
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
|
import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
|
import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service";
|
|
|
|
import { AddEditComponent } from "./add-edit.component";
|
|
import { AttachmentsComponent } from "./attachments.component";
|
|
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
|
import { VaultItemsComponent } from "./vault-items.component";
|
|
|
|
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
|
|
|
@Component({
|
|
selector: "app-org-vault",
|
|
templateUrl: "vault.component.html",
|
|
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
|
|
})
|
|
export class VaultComponent implements OnInit, OnDestroy {
|
|
@ViewChild("vaultFilter", { static: true })
|
|
vaultFilterComponent: VaultFilterComponent;
|
|
@ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent;
|
|
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
|
attachmentsModalRef: ViewContainerRef;
|
|
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
|
cipherAddEditModalRef: ViewContainerRef;
|
|
@ViewChild("collections", { read: ViewContainerRef, static: true })
|
|
collectionsModalRef: ViewContainerRef;
|
|
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
|
|
eventsModalRef: ViewContainerRef;
|
|
|
|
organization: Organization;
|
|
trashCleanupWarning: string = null;
|
|
activeFilter: VaultFilter = new VaultFilter();
|
|
private destroy$ = new Subject<void>();
|
|
|
|
constructor(
|
|
private route: ActivatedRoute,
|
|
private organizationService: OrganizationService,
|
|
protected vaultFilterService: VaultFilterService,
|
|
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
|
private router: Router,
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
private syncService: SyncService,
|
|
private i18nService: I18nService,
|
|
private modalService: ModalService,
|
|
private dialogService: DialogService,
|
|
private messagingService: MessagingService,
|
|
private broadcasterService: BroadcasterService,
|
|
private ngZone: NgZone,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private cipherService: CipherService,
|
|
private passwordRepromptService: PasswordRepromptService
|
|
) {}
|
|
|
|
async ngOnInit() {
|
|
this.trashCleanupWarning = this.i18nService.t(
|
|
this.platformUtilsService.isSelfHost()
|
|
? "trashCleanupWarningSelfHosted"
|
|
: "trashCleanupWarning"
|
|
);
|
|
|
|
this.route.parent.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
|
this.organization = this.organizationService.get(params.organizationId);
|
|
});
|
|
|
|
this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
|
|
this.vaultItemsComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
|
|
});
|
|
|
|
// verifies that the organization has been set
|
|
combineLatest([this.route.queryParams, this.route.parent.params])
|
|
.pipe(
|
|
switchMap(async ([qParams]) => {
|
|
const cipherId = getCipherIdFromParams(qParams);
|
|
if (!cipherId) {
|
|
return;
|
|
}
|
|
if (
|
|
// Handle users with implicit collection access since they use the admin endpoint
|
|
this.organization.canUseAdminCollections ||
|
|
(await this.cipherService.get(cipherId)) != null
|
|
) {
|
|
this.editCipherId(cipherId);
|
|
} else {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccurred"),
|
|
this.i18nService.t("unknownCipher")
|
|
);
|
|
this.router.navigate([], {
|
|
queryParams: { cipherId: null, itemId: null },
|
|
queryParamsHandling: "merge",
|
|
});
|
|
}
|
|
}),
|
|
takeUntil(this.destroy$)
|
|
)
|
|
.subscribe();
|
|
|
|
if (!this.organization.canUseAdminCollections) {
|
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
|
this.ngZone.run(async () => {
|
|
switch (message.command) {
|
|
case "syncCompleted":
|
|
if (message.successfully) {
|
|
await Promise.all([
|
|
this.vaultFilterService.reloadCollections(),
|
|
this.vaultItemsComponent.refresh(),
|
|
]);
|
|
this.changeDetectorRef.detectChanges();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
await this.syncService.fullSync(false);
|
|
}
|
|
|
|
this.routedVaultFilterBridgeService.activeFilter$
|
|
.pipe(takeUntil(this.destroy$))
|
|
.subscribe((activeFilter) => {
|
|
this.activeFilter = activeFilter;
|
|
});
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
async refreshItems() {
|
|
this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh();
|
|
await this.vaultItemsComponent.actionPromise;
|
|
this.vaultItemsComponent.actionPromise = null;
|
|
}
|
|
|
|
filterSearchText(searchText: string) {
|
|
this.vaultItemsComponent.searchText = searchText;
|
|
this.vaultItemsComponent.search(200);
|
|
}
|
|
|
|
async editCipherAttachments(cipher: CipherView) {
|
|
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
|
|
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
|
|
return;
|
|
}
|
|
|
|
let madeAttachmentChanges = false;
|
|
|
|
const [modal] = await this.modalService.openViewRef(
|
|
AttachmentsComponent,
|
|
this.attachmentsModalRef,
|
|
(comp) => {
|
|
comp.organization = this.organization;
|
|
comp.cipherId = cipher.id;
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
|
|
}
|
|
);
|
|
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
modal.onClosed.subscribe(async () => {
|
|
if (madeAttachmentChanges) {
|
|
await this.vaultItemsComponent.refresh();
|
|
}
|
|
madeAttachmentChanges = false;
|
|
});
|
|
}
|
|
|
|
async editCipherCollections(cipher: CipherView) {
|
|
const currCollections = await firstValueFrom(this.vaultFilterService.filteredCollections$);
|
|
const [modal] = await this.modalService.openViewRef(
|
|
CollectionsComponent,
|
|
this.collectionsModalRef,
|
|
(comp) => {
|
|
comp.collectionIds = cipher.collectionIds;
|
|
comp.collections = currCollections.filter((c) => !c.readOnly && c.id != null);
|
|
comp.organization = this.organization;
|
|
comp.cipherId = cipher.id;
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
comp.onSavedCollections.subscribe(async () => {
|
|
modal.close();
|
|
await this.vaultItemsComponent.refresh();
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async addCipher() {
|
|
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
|
|
(c) => !c.readOnly && c.id != null
|
|
);
|
|
|
|
await this.editCipher(null, (comp) => {
|
|
comp.organizationId = this.organization.id;
|
|
comp.type = this.activeFilter.cipherType;
|
|
comp.collections = collections;
|
|
if (this.activeFilter.collectionId) {
|
|
comp.collectionIds = [this.activeFilter.collectionId];
|
|
}
|
|
});
|
|
}
|
|
|
|
async navigateToCipher(cipher: CipherView) {
|
|
this.go({ itemId: cipher?.id });
|
|
}
|
|
|
|
async editCipher(
|
|
cipher: CipherView,
|
|
additionalComponentParameters?: (comp: AddEditComponent) => void
|
|
) {
|
|
return this.editCipherId(cipher?.id, additionalComponentParameters);
|
|
}
|
|
|
|
async editCipherId(
|
|
cipherId: string,
|
|
additionalComponentParameters?: (comp: AddEditComponent) => void
|
|
) {
|
|
const cipher = await this.cipherService.get(cipherId);
|
|
if (cipher != null && cipher.reprompt != 0) {
|
|
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
|
this.go({ cipherId: null, itemId: null });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const defaultComponentParameters = (comp: AddEditComponent) => {
|
|
comp.organization = this.organization;
|
|
comp.cipherId = cipherId;
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
comp.onSavedCipher.subscribe(async () => {
|
|
modal.close();
|
|
await this.vaultItemsComponent.refresh();
|
|
});
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
comp.onDeletedCipher.subscribe(async () => {
|
|
modal.close();
|
|
await this.vaultItemsComponent.refresh();
|
|
});
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
|
comp.onRestoredCipher.subscribe(async () => {
|
|
modal.close();
|
|
await this.vaultItemsComponent.refresh();
|
|
});
|
|
};
|
|
|
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
|
AddEditComponent,
|
|
this.cipherAddEditModalRef,
|
|
additionalComponentParameters == null
|
|
? defaultComponentParameters
|
|
: (comp) => {
|
|
defaultComponentParameters(comp);
|
|
additionalComponentParameters(comp);
|
|
}
|
|
);
|
|
|
|
modal.onClosedPromise().then(() => {
|
|
this.go({ cipherId: null, itemId: null });
|
|
});
|
|
|
|
return childComponent;
|
|
}
|
|
|
|
async cloneCipher(cipher: CipherView) {
|
|
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
|
|
(c) => !c.readOnly && c.id != null
|
|
);
|
|
|
|
await this.editCipher(cipher, (comp) => {
|
|
comp.cloneMode = true;
|
|
comp.collections = collections;
|
|
comp.organizationId = this.organization.id;
|
|
comp.collectionIds = cipher.collectionIds;
|
|
});
|
|
}
|
|
|
|
async viewEvents(cipher: CipherView) {
|
|
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
|
comp.name = cipher.name;
|
|
comp.organizationId = this.organization.id;
|
|
comp.entityId = cipher.id;
|
|
comp.showUser = true;
|
|
comp.entity = "cipher";
|
|
});
|
|
}
|
|
|
|
private go(queryParams: any = null) {
|
|
if (queryParams == null) {
|
|
queryParams = {
|
|
type: this.activeFilter.cipherType,
|
|
collectionId: this.activeFilter.collectionId,
|
|
deleted: this.activeFilter.isDeleted || null,
|
|
};
|
|
}
|
|
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: queryParams,
|
|
queryParamsHandling: "merge",
|
|
replaceUrl: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allows backwards compatibility with
|
|
* old links that used the original `cipherId` param
|
|
*/
|
|
const getCipherIdFromParams = (params: Params): string => {
|
|
return params["itemId"] || params["cipherId"];
|
|
};
|