1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-07 11:03:30 +00:00

Move web to apps/web and bitwarden_license/bit-web

This commit is contained in:
Hinton
2022-06-02 11:55:37 +02:00
parent fb35805202
commit 02fe715903
619 changed files with 2 additions and 7 deletions

View File

@@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { IndividualVaultComponent } from "./individual-vault.component";
const routes: Routes = [
{
path: "",
component: IndividualVaultComponent,
data: { titleId: "vaults" },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class IndividualVaultRoutingModule {}

View File

@@ -0,0 +1,121 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="groupings">
<div class="content">
<div class="inner-content">
<app-vault-filter
#vaultFilter
[activeFilter]="activeFilter"
(onFilterChange)="applyVaultFilter($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
(onSearchTextChanged)="filterSearchText($event)"
></app-vault-filter>
</div>
</div>
</div>
</div>
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
<div class="page-header d-flex">
<h1>
{{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="activeFilter.status === 'trash'"
>
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm"
(click)="addCipher()"
*ngIf="activeFilter.status !== 'trash'"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout
type="warning"
*ngIf="activeFilter.status === 'trash'"
icon="bwi-exclamation-triangle"
>
{{ trashCleanupWarning }}
</app-callout>
<app-vault-ciphers
(onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)"
(onAddCipher)="addCipher()"
(onShareClicked)="shareCipher($event)"
(onCollectionsClicked)="editCipherCollections($event)"
(onCloneClicked)="cloneCipher($event)"
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
>
</app-vault-ciphers>
</div>
<div class="col-3">
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
{{ "updateKeyTitle" | i18n }}
</div>
<div class="card-body">
<p>{{ "updateEncryptionKeyShortDesc" | i18n }}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{ "updateEncryptionKey" | i18n }}
</button>
</div>
</div>
<app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email>
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
<div class="card-header bg-warning text-white">
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
{{ "updateBrowser" | i18n }}
</div>
<div class="card-body">
<p>{{ "updateBrowserDesc" | i18n }}</p>
<a
class="btn btn-block btn-outline-secondary"
target="_blank"
href="https://browser-update.org/update-browser.html"
rel="noopener"
>
{{ "updateBrowser" | i18n }}
</a>
</div>
</div>
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
<div class="card-header bg-success text-white">
<i class="bwi bwi-star-f bwi-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
</div>
<div class="card-body">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<a
class="btn btn-block btn-outline-secondary"
routerLink="/settings/subscription/premium"
>
{{ "goPremium" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #folderAddEdit></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #share></ng-template>
<ng-template #collections></ng-template>
<ng-template #updateKeyTemplate></ng-template>

View File

@@ -0,0 +1,415 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
import { ModalService } from "jslib-angular/services/modal.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherView } from "jslib-common/models/view/cipherView";
import { UpdateKeyComponent } from "../../../../settings/update-key.component";
import { AddEditComponent } from "../../../../vault/add-edit.component";
import { AttachmentsComponent } from "../../../../vault/attachments.component";
import { CiphersComponent } from "../../../../vault/ciphers.component";
import { CollectionsComponent } from "../../../../vault/collections.component";
import { FolderAddEditComponent } from "../../../../vault/folder-add-edit.component";
import { ShareComponent } from "../../../../vault/share.component";
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
import { VaultService } from "../../vault.service";
const BroadcasterSubscriptionId = "VaultComponent";
@Component({
selector: "app-vault",
templateUrl: "individual-vault.component.html",
})
export class IndividualVaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild("collections", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
@ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true })
updateKeyModalRef: ViewContainerRef;
favorites = false;
folderId: string = null;
collectionId: string = null;
organizationId: string = null;
myVaultOnly = false;
showVerifyEmail = false;
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
deleted = false;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
constructor(
private syncService: SyncService,
private route: ActivatedRoute,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService,
private modalService: ModalService,
private tokenService: TokenService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private stateService: StateService,
private organizationService: OrganizationService,
private vaultService: VaultService,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService
) {}
async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
: "trashCleanupWarning"
);
this.route.queryParams.pipe(first()).subscribe(async (params) => {
await this.syncService.fullSync(false);
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumCallout =
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
this.filterComponent.reloadOrganizations();
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
if (params.cipherId) {
const cipherView = new CipherView();
cipherView.id = params.cipherId;
if (params.action === "clone") {
await this.cloneCipher(cipherView);
} else if (params.action === "edit") {
await this.editCipher(cipherView);
}
}
await this.ciphersComponent.reload();
this.route.queryParams.subscribe(async (params) => {
if (params.cipherId) {
if ((await this.cipherService.get(params.cipherId)) != null) {
this.editCipherId(params.cipherId);
} else {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unknownCipher")
);
this.router.navigate([], {
queryParams: { cipherId: null },
queryParamsHandling: "merge",
});
}
}
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.filterComponent.reloadCollectionsAndFolders(this.activeFilter),
this.filterComponent.reloadOrganizations(),
this.ciphersComponent.load(this.ciphersComponent.filter),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
});
}
get isShowingCards() {
return (
this.showBrowserOutdated ||
this.showPremiumCallout ||
this.showUpdateKey ||
this.showVerifyEmail
);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async applyVaultFilter(vaultFilter: VaultFilter) {
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
this.activeFilter
);
this.go();
}
async applyOrganizationFilter(orgId: string) {
if (orgId == null) {
this.activeFilter.resetOrganization();
this.activeFilter.myVaultOnly = true;
} else {
this.activeFilter.selectedOrganizationId = orgId;
}
await this.applyVaultFilter(this.activeFilter);
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null &&
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
}
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
async editCipherAttachments(cipher: CipherView) {
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (cipher.organizationId == null && !canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
} else if (cipher.organizationId != null) {
const org = await this.organizationService.get(cipher.organizationId);
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
this.messagingService.send("upgradeOrganization", {
organizationId: cipher.organizationId,
});
return;
}
}
let madeAttachmentChanges = false;
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onReuploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
}
);
modal.onClosed.subscribe(async () => {
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
});
}
async shareCipher(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
ShareComponent,
this.shareModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSharedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSavedCollections.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async addFolder() {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = null;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
});
}
);
}
async editFolder(folderId: string) {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = folderId;
comp.onSavedFolder.subscribe(async () => {
modal.close();
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
});
comp.onDeletedFolder.subscribe(async () => {
modal.close();
await this.filterComponent.reloadCollectionsAndFolders(this.activeFilter);
});
}
);
}
async addCipher() {
const component = await this.editCipher(null);
component.type = this.activeFilter.cipherType;
component.folderId = this.folderId === "none" ? null : this.folderId;
if (this.activeFilter.selectedCollectionId != null) {
const collection = this.filterComponent.collections.fullList.filter(
(c) => c.id === this.activeFilter.selectedCollectionId
);
if (collection.length > 0) {
component.organizationId = collection[0].organizationId;
component.collectionIds = [this.activeFilter.selectedCollectionId];
}
}
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
component.folderId = this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedOrganizationId) {
component.organizationId = this.activeFilter.selectedOrganizationId;
}
}
async editCipher(cipher: CipherView) {
return this.editCipherId(cipher?.id);
}
async editCipherId(id: string) {
const cipher = await this.cipherService.get(id);
if (cipher != null && cipher.reprompt != 0) {
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null });
return;
}
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = id;
comp.onSavedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onRestoredCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
modal.onClosedPromise().then(() => {
this.go({ cipherId: null });
});
return childComponent;
}
async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher);
component.cloneMode = true;
}
async updateKey() {
await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef);
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
favorites: this.favorites ? true : null,
type: this.activeFilter.cipherType,
folderId: this.folderId,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
};
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: "merge",
replaceUrl: true,
});
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from "@angular/core";
import { VaultModule } from "../../vault.module";
import { IndividualVaultRoutingModule } from "./individual-vault-routing.module";
import { IndividualVaultComponent } from "./individual-vault.component";
@NgModule({
imports: [VaultModule, IndividualVaultRoutingModule],
declarations: [IndividualVaultComponent],
exports: [IndividualVaultComponent],
})
export class IndividualVaultModule {}

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../../shared.module";
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
@NgModule({
imports: [SharedModule],
declarations: [OrganizationNameBadgeComponent],
exports: [OrganizationNameBadgeComponent],
})
export class OrganizationBadgeModule {}

View File

@@ -0,0 +1,9 @@
<button
bitBadge
[style.color]="textColor"
[style.background-color]="color"
appA11yTitle="{{ organizationName }}"
(click)="emitOnOrganizationClicked()"
>
{{ organizationName | ellipsis: 13 }}
</button>

View File

@@ -0,0 +1,59 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Component({
selector: "app-org-badge",
templateUrl: "organization-name-badge.component.html",
})
export class OrganizationNameBadgeComponent implements OnInit {
@Input() organizationName: string;
@Input() profileName: string;
@Output() onOrganizationClicked = new EventEmitter<string>();
color: string;
textColor: string;
constructor(private i18nService: I18nService) {}
ngOnInit(): void {
if (this.organizationName == null || this.organizationName === "") {
this.organizationName = this.i18nService.t("me");
this.color = this.stringToColor(this.profileName.toUpperCase());
}
if (this.color == null) {
this.color = this.stringToColor(this.organizationName.toUpperCase());
}
this.textColor = this.pickTextColorBasedOnBgColor();
}
// This value currently isn't stored anywhere, only calculated in the app-avatar component
// Once we are allowing org colors to be changed and saved, change this out
private stringToColor(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ("00" + value.toString(16)).substr(-2);
}
return color;
}
// There are a few ways to calculate text color for contrast, this one seems to fit accessibility guidelines best.
// https://stackoverflow.com/a/3943023/6869691
private pickTextColorBasedOnBgColor() {
const color = this.color.charAt(0) === "#" ? this.color.substring(1, 7) : this.color;
const r = parseInt(color.substring(0, 2), 16); // hexToR
const g = parseInt(color.substring(2, 4), 16); // hexToG
const b = parseInt(color.substring(4, 6), 16); // hexToB
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? "black !important" : "white !important";
}
emitOnOrganizationClicked() {
this.onOrganizationClicked.emit();
}
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { OrganizationVaultComponent } from "./organization-vault.component";
const routes: Routes = [
{
path: "",
component: OrganizationVaultComponent,
data: { titleId: "vaults" },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationVaultRoutingModule {}

View File

@@ -0,0 +1,70 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="groupings">
<div class="content">
<div class="inner-content">
<app-vault-filter
#vaultFilter
[showFolders]="false"
[showFavorites]="false"
[activeFilter]="activeFilter"
[showOrgFilter]="false"
(onFilterChange)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)"
></app-vault-filter>
</div>
</div>
</div>
</div>
<div class="col-9">
<div class="page-header d-flex">
<h1>
{{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="ciphersComponent.actionPromise">
<ng-container *ngIf="actionSpinner.loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</small>
</h1>
<div class="ml-auto d-flex">
<app-vault-bulk-actions
[ciphersComponent]="ciphersComponent"
[deleted]="deleted"
[organization]="organization"
>
</app-vault-bulk-actions>
<button
type="button"
class="btn btn-outline-primary btn-sm ml-auto"
(click)="addCipher()"
*ngIf="!deleted"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
</button>
</div>
</div>
<app-callout type="warning" *ngIf="deleted" icon="bwi bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
<app-org-vault-ciphers
(onCipherClicked)="editCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)"
(onAddCipher)="addCipher()"
(onCollectionsClicked)="editCipherCollections($event)"
(onEventsClicked)="viewEvents($event)"
(onCloneClicked)="cloneCipher($event)"
>
</app-org-vault-ciphers>
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collections></ng-template>
<ng-template #eventsTemplate></ng-template>

View File

@@ -0,0 +1,350 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
import { ModalService } from "jslib-angular/services/modal.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { Organization } from "jslib-common/models/domain/organization";
import { CipherView } from "jslib-common/models/view/cipherView";
import { EntityEventsComponent } from "../../../../organizations/manage/entity-events.component";
import { AddEditComponent } from "../../../../organizations/vault/add-edit.component";
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
import { VaultService } from "../../vault.service";
const BroadcasterSubscriptionId = "OrgVaultComponent";
@Component({
selector: "app-org-vault",
templateUrl: "organization-vault.component.html",
})
export class OrganizationVaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@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;
collectionId: string = null;
type: CipherType = null;
deleted = false;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private i18nService: I18nService,
private modalService: ModalService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private vaultService: VaultService,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService
) {}
ngOnInit() {
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
: "trashCleanupWarning"
);
this.route.parent.params.subscribe(async (params: any) => {
this.organization = await this.organizationService.get(params.organizationId);
this.vaultFilterComponent.organization = this.organization;
this.ciphersComponent.organization = this.organization;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
if (!this.organization.canViewAllCollections) {
await this.syncService.fullSync(false);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
await Promise.all([
this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({
selectedOrganizationId: this.organization.id,
} as Partial<VaultFilter>)
),
this.ciphersComponent.refresh(),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
}
await this.vaultFilterComponent.reloadCollectionsAndFolders(
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
);
await this.ciphersComponent.reload();
if (qParams.viewEvents != null) {
const cipher = this.ciphersComponent.ciphers.filter((c) => c.id === qParams.viewEvents);
if (cipher.length > 0) {
this.viewEvents(cipher[0]);
}
}
this.route.queryParams.subscribe(async (params) => {
if (params.cipherId) {
if (
// Handle users with implicit collection access since they use the admin endpoint
this.organization.canEditAnyCollection ||
(await this.cipherService.get(params.cipherId)) != null
) {
this.editCipherId(params.cipherId);
} else {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("unknownCipher")
);
this.router.navigate([], {
queryParams: { cipherId: null },
queryParamsHandling: "merge",
});
}
}
});
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async applyVaultFilter(vaultFilter: VaultFilter) {
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
this.vaultFilterComponent.searchPlaceholder =
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
this.go();
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null &&
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
}
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.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;
comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
}
);
modal.onClosed.subscribe(async () => {
if (madeAttachmentChanges) {
await this.ciphersComponent.refresh();
}
madeAttachmentChanges = false;
});
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
if (this.organization.canEditAnyCollection) {
comp.collectionIds = cipher.collectionIds;
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
);
}
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onSavedCollections.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
}
async addCipher() {
const component = await this.editCipher(null);
component.organizationId = this.organization.id;
component.type = this.type;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
);
}
if (this.collectionId != null) {
component.collectionIds = [this.collectionId];
}
}
async editCipher(cipher: CipherView) {
return this.editCipherId(cipher?.id);
}
async editCipherId(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.reprompt != 0) {
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
this.go({ cipherId: null });
return;
}
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.organization = this.organization;
comp.cipherId = cipherId;
comp.onSavedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onDeletedCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
comp.onRestoredCipher.subscribe(async () => {
modal.close();
await this.ciphersComponent.refresh();
});
}
);
modal.onClosedPromise().then(() => {
this.go({ cipherId: null });
});
return childComponent;
}
async cloneCipher(cipher: CipherView) {
const component = await this.editCipher(cipher);
component.cloneMode = true;
component.organizationId = this.organization.id;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value
// in the add-edit componenet
component.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 clearFilters() {
this.collectionId = null;
this.type = null;
this.deleted = false;
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
type: this.type,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
};
}
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: "merge",
replaceUrl: true,
});
}
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from "@angular/core";
import { VaultModule } from "../../vault.module";
import { OrganizationVaultRoutingModule } from "./organization-vault-routing.module";
import { OrganizationVaultComponent } from "./organization-vault.component";
@NgModule({
imports: [VaultModule, OrganizationVaultRoutingModule],
declarations: [OrganizationVaultComponent],
exports: [OrganizationVaultComponent],
})
export class OrganizationVaultModule {}

View File

@@ -0,0 +1,19 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "../loose-components.module";
import { SharedModule } from "../shared.module";
import { VaultFilterModule } from "../vault-filter/vault-filter.module";
import { VaultService } from "./vault.service";
@NgModule({
imports: [SharedModule, VaultFilterModule, LooseComponentsModule],
exports: [SharedModule, VaultFilterModule, LooseComponentsModule],
providers: [
{
provide: VaultService,
useClass: VaultService,
},
],
})
export class VaultModule {}

View File

@@ -0,0 +1,29 @@
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
export class VaultService {
calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
if (vaultFilter.status === "favorites") {
return "searchFavorites";
}
if (vaultFilter.status === "trash") {
return "searchTrash";
}
if (vaultFilter.cipherType != null) {
return "searchType";
}
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
return "searchFolder";
}
if (vaultFilter.selectedCollectionId != null) {
return "searchCollection";
}
if (vaultFilter.selectedOrganizationId != null) {
return "searchOrganization";
}
if (vaultFilter.myVaultOnly) {
return "searchMyVault";
}
return "searchVault";
}
}