1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-02 16:43:19 +00:00

[SG-998] and [SG-999] Vault and Autofill team refactor (#4542)

* Move DeprecatedVaultFilterService to vault folder

* [libs] move VaultItemsComponent

* [libs] move AddEditComponent

* [libs] move AddEditCustomFields

* [libs] move attachmentsComponent

* [libs] folderAddEditComponent

* [libs] IconComponent

* [libs] PasswordRepormptComponent

* [libs] PremiumComponent

* [libs] ViewCustomFieldsComponent

* [libs] ViewComponent

* [libs] PasswordRepromptService

* [libs] Move FolderService and FolderApiService abstractions

* [libs] FolderService imports

* [libs] PasswordHistoryComponent

* [libs] move Sync and SyncNotifier abstractions

* [libs] SyncService imports

* [libs] fix file casing for passwordReprompt abstraction

* [libs] SyncNotifier import fix

* [libs] CipherServiceAbstraction

* [libs] PasswordRepromptService abstraction

* [libs] Fix file casing for angular passwordReprompt service

* [libs] fix file casing for SyncNotifierService

* [libs] CipherRepromptType

* [libs] rename CipherRepromptType

* [libs] CipherType

* [libs] Rename CipherType

* [libs] CipherData

* [libs] FolderData

* [libs] PasswordHistoryData

* [libs] AttachmentData

* [libs] CardData

* [libs] FieldData

* [libs] IdentityData

* [libs] LocalData

* [libs] LoginData

* [libs] SecureNoteData

* [libs] LoginUriData

* [libs] Domain classes

* [libs] SecureNote

* [libs] Request models

* [libs] Response models

* [libs] View part 1

* [libs] Views part 2

* [libs] Move folder services

* [libs] Views fixes

* [libs] Move sync services

* [libs] cipher service

* [libs] Types

* [libs] Sync file casing

* [libs] Fix folder service import

* [libs] Move spec files

* [libs] casing fixes on spec files

* [browser] Autofill background, clipboard, commands

* [browser] Fix ContextMenusBackground casing

* [browser] Rename fix

* [browser] Autofill content

* [browser] autofill.js

* [libs] enpass importer spec fix

* [browser] autofill models

* [browser] autofill manifest path updates

* [browser] Autofill notification files

* [browser] autofill services

* [browser] Fix file casing

* [browser] Vault popup loose components

* [browser] Vault components

* [browser] Manifest fixes

* [browser] Vault services

* [cli] vault commands and models

* [browser] File capitilization fixes

* [desktop] Vault components and services

* [web] vault loose components

* [web] Vault components

* [browser] Fix misc-utils import

* [libs] Fix psono spec imports

* [fix] Add comments to address lint rules
This commit is contained in:
Robyn MacCallum
2023-01-31 16:08:37 -05:00
committed by GitHub
parent bf1df6ebf6
commit 7ebedbecfb
472 changed files with 1371 additions and 1328 deletions

View File

@@ -1,176 +0,0 @@
<ng-container>
<h3 class="mt-4">{{ "customFields" | i18n }}</h3>
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div
role="group"
class="row"
cdkDrag
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
attr.aria-label="{{ f.name }}"
>
<div class="col-5 form-group">
<div class="d-flex">
<label for="fieldName{{ i }}">{{ "name" | i18n }}</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/custom-fields/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<input
id="fieldName{{ i }}"
type="text"
name="Field.Name{{ i }}"
[(ngModel)]="f.name"
class="form-control"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-7 form-group">
<label for="fieldValue{{ i }}">{{ "value" | i18n }}</label>
<div class="d-flex align-items-center">
<!-- Text -->
<div class="input-group" *ngIf="f.type === fieldType.Text">
<input
id="fieldValue{{ i }}"
class="form-control"
type="text"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
attr.aria-describedby="fieldName{{ i }}"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(f.value, 'value', 'Field')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Hidden -->
<div class="input-group" *ngIf="f.type === fieldType.Hidden">
<input
id="fieldValue{{ i }}"
type="{{ f.showValue ? 'text' : 'password' }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
class="form-control text-monospace"
appInputVerbatim
autocomplete="new-password"
[disabled]="cipher.isDeleted || viewOnly || (!cipher.viewPassword && !f.newField)"
[readonly]="!cipher.edit && editMode"
attr.aria-describedby="fieldName{{ i }}"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleFieldValue(f)"
[disabled]="!cipher.viewPassword && !f.newField"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
>
</i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
[disabled]="!cipher.viewPassword && !f.newField"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- Linked -->
<div class="input-group" *ngIf="f.type === fieldType.Linked">
<select
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
class="form-control"
[(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
[disabled]="cipher.isDeleted || viewOnly"
attr.aria-describedby="fieldName{{ i }}"
>
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="flex-fill">
<!-- Boolean -->
<input
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
type="checkbox"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean"
appTrueFalseValue
trueValue="true"
falseValue="false"
[disabled]="cipher.isDeleted || viewOnly"
attr.aria-describedby="fieldName{{ i }}"
/>
</div>
<button
type="button"
class="btn btn-link text-danger ml-2"
(click)="removeField(f)"
appA11yTitle="{{ 'remove' | i18n }}"
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-link text-muted cursor-move"
appA11yTitle="{{ 'dragToSort' | i18n }}"
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-hamburger bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Add new custom field -->
<a
href="#"
appStopClick
(click)="addField()"
class="d-inline-block mb-2"
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "newCustomField" | i18n }}
</a>
<div class="row" *ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)">
<div class="col-5">
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option
*ngIf="cipher.linkedFieldOptions != null"
[ngValue]="addFieldLinkedTypeOption.value"
>
{{ addFieldLinkedTypeOption.name }}
</option>
</select>
</div>
</div>
</ng-container>

View File

@@ -1,18 +0,0 @@
import { Component, Input } from "@angular/core";
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/components/add-edit-custom-fields.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@Component({
selector: "app-vault-add-edit-custom-fields",
templateUrl: "add-edit-custom-fields.component.html",
})
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
@Input() viewOnly: boolean;
@Input() copy: (value: string, typeI18nKey: string, aType: string) => void;
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
super(i18nService, eventCollectionService);
}
}

View File

@@ -1,979 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="cipherAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="modal-header">
<h1 class="modal-title" id="cipherAddEditTitle">{{ title }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="cipher">
<app-callout type="info" *ngIf="allowOwnershipAssignment() && !allowPersonal">
{{ "personalOwnershipPolicyInEffect" | i18n }}
</app-callout>
<div class="row" *ngIf="!editMode && !viewOnly">
<div class="col-6 form-group">
<label for="type">{{ "whatTypeOfItem" | i18n }}</label>
<select
id="type"
name="Type"
[(ngModel)]="cipher.type"
class="form-control"
[disabled]="cipher.isDeleted"
appAutofocus
>
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="cipher.name"
required
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group" *ngIf="!organization">
<label for="folder">{{ "folder" | i18n }}</label>
<select
id="folder"
name="FolderId"
[(ngModel)]="cipher.folderId"
class="form-control"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
</div>
<!-- Login -->
<ng-container *ngIf="cipher.type === cipherType.Login">
<div class="row">
<div class="col-6 form-group">
<label for="loginUsername">{{ "username" | i18n }}</label>
<div class="input-group">
<input
id="loginUsername"
class="form-control"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
<div class="input-group-append" *ngIf="!cipher.isDeleted">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy(cipher.login.username, 'username', 'Username')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="col-6 form-group">
<div class="d-flex">
<label for="loginPassword">{{ "password" | i18n }}</label>
<div class="ml-auto d-flex" *ngIf="!cipher.isDeleted && !viewOnly">
<a
href="#"
class="d-block mr-2 bwi-icon-above-input"
appStopClick
appA11yTitle="{{ 'generatePassword' | i18n }}"
(click)="generatePassword()"
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-lg bwi-fw bwi-generate" aria-hidden="true"></i>
</a>
<a
href="#"
class="d-block bwi-icon-above-input"
#checkPasswordBtn
appStopClick
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
>
<i
class="bwi bwi-lg bwi-fw bwi-check-circle"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-fw bwi-spinner bwi-spin"
aria-hidden="true"
[hidden]="!$any(checkPasswordBtn).loading"
title="{{ 'loading' | i18n }}"
></i>
</a>
<a
href="#"
class="d-block bwi-icon-above-input"
appStopClick
[appA11yTitle]="'toggleCharacterCount' | i18n"
(click)="togglePasswordCount()"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-lg bwi-fw bwi-numbered-list" aria-hidden="true"></i>
</a>
</div>
</div>
<div class="input-group">
<input
id="loginPassword"
class="form-control text-monospace"
type="{{ showPassword ? 'text' : 'password' }}"
name="Login.Password"
[(ngModel)]="cipher.login.password"
appInputVerbatim
autocomplete="new-password"
[disabled]="cipher.isDeleted || !cipher.viewPassword || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
[disabled]="!cipher.viewPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher.login.password, 'password', 'Password')"
[disabled]="!cipher.viewPassword"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div *ngIf="showPasswordCount" class="tw-mb-4">
<label>{{ "passwordCharacterCount" | i18n }}</label>
<div class="tw-flex tw-justify-between">
<bit-color-password
[password]="cipher.login.password"
[showCount]="true"
></bit-color-password>
<button type="button" bitLink (click)="togglePasswordCount()">
{{ "hide" | i18n }}
</button>
</div>
</div>
<div class="tw-flex tw-flex-row">
<div class="tw-mb-4 tw-w-1/2">
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
<input
id="loginTotp"
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
name="Login.Totp"
class="form-control text-monospace"
[(ngModel)]="cipher.login.totp"
appInputVerbatim
[disabled]="cipher.isDeleted || !cipher.viewPassword || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="tw-mb-4 tw-ml-4 tw-flex tw-w-1/2 tw-items-end" [ngClass]="{ low: totpLow }">
<div
class="totp tw-flex tw-flex-row tw-items-center"
*ngIf="!cipher.login.totp || !totpCode"
>
<span class="totp-countdown">
<span class="totp-sec tw-text-muted">15</span>
<svg>
<g>
<circle
class="totp-circle-muted inner"
r="12.6"
cy="16"
cx="16"
opacity="0.25"
[ngStyle]="{ 'stroke-dashoffset.px': 40 }"
></circle>
<circle
class="totp-circle-muted outer"
opacity="0.25"
r="14"
cy="16"
cx="16"
></circle>
</g>
</svg>
</span>
<span
class="totp-code tw-mr-3 tw-ml-2 tw-text-muted"
title="{{ 'verificationCodeTotp' | i18n }}"
>--- ---</span
>
<i class="bwi bwi-lg bwi-clone tw-text-muted" aria-hidden="true"></i>
</div>
<div class="tw-pb-2" *ngIf="!cipher.login.totp || !totpCode">
<app-premium-badge
*ngIf="!organization && !cipher.organizationId"
class="ml-3"
></app-premium-badge>
<a
href="#"
appStopClick
bitBadge
badgeType="primary"
class="tw-ml-4"
(click)="upgradeOrganization()"
*ngIf="
(organization && !organization.useTotp) ||
(!organization &&
!canAccessPremium &&
cipher.organizationId &&
!cipher.organizationUseTotp)
"
>
{{ "upgrade" | i18n }}
</a>
</div>
<div
*ngIf="cipher.login.totp && totpCode"
class="totp tw-flex tw-flex-row tw-items-center"
>
<span class="totp-countdown">
<span class="totp-sec">{{ totpSec }}</span>
<svg>
<g>
<circle
class="totp-circle inner"
r="12.6"
cy="16"
cx="16"
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
></circle>
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
</g>
</svg>
</span>
<span
class="totp-code tw-mr-2 tw-ml-2 tw-mt-1"
title="{{ 'verificationCodeTotp' | i18n }}"
>{{ totpCodeFormatted }}</span
>
<button
type="button"
class="tw-items-center tw-border-none tw-bg-transparent tw-text-primary-500"
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<ng-container *ngIf="cipher.login.hasUris">
<div
role="group"
class="row"
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
>
<div class="col-7 form-group">
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
<div class="input-group">
<input
class="form-control"
id="loginUri{{ i }}"
type="text"
name="Login.Uris[{{ i }}].Uri"
[(ngModel)]="u.uri"
[disabled]="cipher.isDeleted || viewOnly"
placeholder="{{ 'ex' | i18n }} https://google.com"
appInputVerbatim
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launch(u)"
[disabled]="!u.canLaunch"
>
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyUri' | i18n }}"
(click)="copy(u.uri, 'uri', 'URI')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="col-5 form-group">
<div class="d-flex">
<label for="loginUriMatch{{ i }}">
{{ "matchDetection" | i18n }}
</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/uri-match-detection/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div class="d-flex">
<select
class="form-control overflow-hidden"
id="loginUriMatch{{ i }}"
name="Login.Uris[{{ i }}].Match"
[(ngModel)]="u.match"
(change)="loginUriMatchChanged(u)"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
<button
type="button"
class="btn btn-link text-danger ml-2"
(click)="removeUri(u)"
appA11yTitle="{{ 'remove' | i18n }}"
*ngIf="!cipher.isDeleted && !viewOnly"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</ng-container>
<a
href="#"
appStopClick
(click)="addUri()"
class="d-inline-block mb-3"
*ngIf="!cipher.isDeleted && !viewOnly && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "newUri" | i18n }}
</a>
</ng-container>
<!-- Card -->
<ng-container *ngIf="cipher.type === cipherType.Card">
<div class="row">
<div class="col-6 form-group">
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
<input
id="cardCardholderName"
class="form-control"
type="text"
name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group">
<label for="cardBrand">{{ "brand" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
<select
id="cardBrand"
class="form-control"
name="Card.Brand"
[(ngModel)]="cipher.card.brand"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyCardBrand>
<input
id="cardBrand"
class="form-control"
name="Card.Brand"
type="text"
[readonly]="true"
[value]="cipher.card.brand"
/>
</ng-template>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="cardNumber">{{ "number" | i18n }}</label>
<div class="input-group">
<input
id="cardNumber"
class="form-control text-monospace"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
[(ngModel)]="cipher.card.number"
appInputVerbatim
autocomplete="new-password"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardNumber()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showCardNumber,
'bwi-eye-slash': showCardNumber
}"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher.card.number, 'number', 'Number')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="col form-group">
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
<select
id="cardExpMonth"
class="form-control"
name="Card.ExpMonth"
[(ngModel)]="cipher.card.expMonth"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyCardExpMonth>
<input
id="cardExpMonth"
class="form-control"
type="text"
name="Card.ExpMonth"
[readonly]="true"
[value]="getCardExpMonthDisplay()"
/>
</ng-template>
</div>
<div class="col form-group">
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
<input
id="cardExpYear"
class="form-control"
type="text"
name="Card.ExpYear"
[(ngModel)]="cipher.card.expYear"
placeholder="{{ 'ex' | i18n }} 2019"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="cardCode">{{ "securityCode" | i18n }}</label>
<div class="input-group">
<input
id="cardCode"
class="form-control text-monospace"
type="{{ showCardCode ? 'text' : 'password' }}"
name="Card.Code"
[(ngModel)]="cipher.card.code"
appInputVerbatim
autocomplete="new-password"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardCode()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'securityCode' | i18n }}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
</ng-container>
<!-- Identity -->
<ng-container *ngIf="cipher.type === cipherType.Identity">
<div class="row">
<div class="col-4 form-group">
<label for="idTitle">{{ "title" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
<select
id="idTitle"
class="form-control"
name="Identity.Title"
[(ngModel)]="cipher.identity.title"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyIdTitle>
<input
id="idTitle"
class="form-control"
name="Identity.Title"
type="text"
[readonly]="true"
[value]="cipher.identity.title"
/>
</ng-template>
</div>
</div>
<div class="row">
<div class="col-4 form-group">
<label for="idFirstName">{{ "firstName" | i18n }}</label>
<input
id="idFirstName"
class="form-control"
type="text"
name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-4 form-group">
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
<input
id="idMiddleName"
class="form-control"
type="text"
name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-4 form-group">
<label for="idLastName">{{ "lastName" | i18n }}</label>
<input
id="idLastName"
class="form-control"
type="text"
name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-4 form-group">
<label for="idUsername">{{ "username" | i18n }}</label>
<input
id="idUsername"
class="form-control"
type="text"
name="Identity.Username"
[(ngModel)]="cipher.identity.username"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-4 form-group">
<label for="idCompany">{{ "company" | i18n }}</label>
<input
id="idCompany"
class="form-control"
type="text"
name="Identity.Company"
[(ngModel)]="cipher.identity.company"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-4 form-group">
<label for="idSsn">{{ "ssn" | i18n }}</label>
<input
id="idSsn"
class="form-control"
type="text"
name="Identity.SSN"
[(ngModel)]="cipher.identity.ssn"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-4 form-group">
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
<input
id="idPassportNumber"
class="form-control"
type="text"
name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-4 form-group">
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
<input
id="idLicenseNumber"
class="form-control"
type="text"
name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idEmail">{{ "email" | i18n }}</label>
<input
id="idEmail"
class="form-control"
type="text"
inputmode="email"
name="Identity.Email"
[(ngModel)]="cipher.identity.email"
appInputVerbatim
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group">
<label for="idPhone">{{ "phone" | i18n }}</label>
<input
id="idPhone"
class="form-control"
type="text"
inputmode="tel"
name="Identity.Phone"
[(ngModel)]="cipher.identity.phone"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idAddress1">{{ "address1" | i18n }}</label>
<input
id="idAddress1"
class="form-control"
type="text"
name="Identity.Address1"
[(ngModel)]="cipher.identity.address1"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group">
<label for="idAddress2">{{ "address2" | i18n }}</label>
<input
id="idAddress2"
class="form-control"
type="text"
name="Identity.Address2"
[(ngModel)]="cipher.identity.address2"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idAddress3">{{ "address3" | i18n }}</label>
<input
id="idAddress3"
class="form-control"
type="text"
name="Identity.Address3"
[(ngModel)]="cipher.identity.address3"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group">
<label for="idCity">{{ "cityTown" | i18n }}</label>
<input
id="idCity"
class="form-control"
type="text"
name="Identity.City"
[(ngModel)]="cipher.identity.city"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idState">{{ "stateProvince" | i18n }}</label>
<input
id="idState"
class="form-control"
type="text"
name="Identity.State"
[(ngModel)]="cipher.identity.state"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="col-6 form-group">
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="idPostalCode"
class="form-control"
type="text"
name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="idCountry">{{ "country" | i18n }}</label>
<input
id="idCountry"
class="form-control"
type="text"
name="Identity.Country"
[(ngModel)]="cipher.identity.country"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
</ng-container>
<div class="form-group">
<label for="notes">{{ "notes" | i18n }}</label>
<textarea
id="notes"
name="Notes"
rows="6"
[(ngModel)]="cipher.notes"
[disabled]="cipher.isDeleted || viewOnly"
[readonly]="!cipher.edit && editMode"
class="form-control"
></textarea>
</div>
<app-vault-add-edit-custom-fields
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
[cipher]="cipher"
[thisCipherType]="cipher.type"
[viewOnly]="viewOnly"
[copy]="copy.bind(this)"
[editMode]="editMode"
></app-vault-add-edit-custom-fields>
<ng-container *ngIf="allowOwnershipAssignment()">
<h3 class="mt-4">{{ "ownership" | i18n }}</h3>
<div class="row">
<div class="col-5">
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
<select
id="organizationId"
class="form-control"
name="OrganizationId"
[(ngModel)]="cipher.organizationId"
(change)="organizationChanged()"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
</ng-container>
<ng-container *ngIf="(!editMode || cloneMode) && cipher.organizationId">
<h3 class="mt-4">{{ "collections" | i18n }}</h3>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<ng-container *ngIf="collections && collections.length">
<div class="form-check" *ngFor="let c of collections; let i = index">
<input
class="form-check-input"
type="checkbox"
[(ngModel)]="$any(c).checked"
id="collection-{{ i }}"
name="Collection[{{ i }}].Checked"
[disabled]="cipher.isDeleted || viewOnly"
/>
<label class="form-check-label" for="collection-{{ i }}">{{ c.name }}</label>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="editMode">
<div class="small text-muted mt-4">
<div>
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
{{ cipher.revisionDate | date: "medium" }}
</div>
<div *ngIf="cipher.creationDate">
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
{{ cipher.creationDate | date: "medium" }}
</div>
<div *ngIf="showRevisionDate">
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
</div>
<div *ngIf="hasPasswordHistory">
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
<a href="#" appStopClick (click)="viewHistory()" title="{{ 'view' | i18n }}">
{{ cipher.passwordHistory.length }}
</a>
</div>
<div class="ml-3" *ngIf="viewingPasswordHistory">
<div *ngFor="let ph of cipher.passwordHistory">
{{ ph.lastUsedDate | date: "short" }} -
<bit-color-password [password]="ph.password"></bit-color-password>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="canUseReprompt">
<h3 class="mt-4">{{ "options" | i18n }}</h3>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[ngModel]="reprompt"
(change)="repromptChanged()"
id="passwordPrompt"
name="passwordPrompt"
[disabled]="cipher.isDeleted || viewOnly || (!cipher.edit && editMode)"
/>
<label class="form-check-label" for="passwordPrompt">{{
"passwordPrompt" | i18n
}}</label>
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading"
*ngIf="!viewOnly"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ (cipher?.isDeleted ? "restore" : "save") | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ (viewOnly ? "close" : "cancel") | i18n }}
</button>
<div class="ml-auto" *ngIf="cipher && !viewOnly">
<button
*ngIf="!organization && !cipher.isDeleted"
type="button"
(click)="toggleFavorite()"
class="btn btn-link"
appA11yTitle="{{ (cipher.favorite ? 'unfavorite' : 'favorite') | i18n }}"
>
<i
class="bwi bwi-lg"
[ngClass]="{ 'bwi-star-f': cipher.favorite, 'bwi-star': !cipher.favorite }"
aria-hidden="true"
></i>
</button>
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
*ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,256 +0,0 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { LoginUriView } from "@bitwarden/common/models/view/login-uri.view";
@Component({
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnDestroy {
canAccessPremium: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
totpSec: number;
totpLow: boolean;
showRevisionDate = false;
hasPasswordHistory = false;
viewingPasswordHistory = false;
viewOnly = false;
showPasswordCount = false;
protected totpInterval: number;
protected override componentName = "app-vault-add-edit";
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
stateService: StateService,
collectionService: CollectionService,
protected totpService: TotpService,
protected passwordGenerationService: PasswordGenerationService,
protected messagingService: MessagingService,
eventCollectionService: EventCollectionService,
protected policyService: PolicyService,
organizationService: OrganizationService,
logService: LogService,
passwordRepromptService: PasswordRepromptService
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
stateService,
collectionService,
messagingService,
eventCollectionService,
policyService,
logService,
passwordRepromptService,
organizationService
);
}
async ngOnInit() {
await super.ngOnInit();
await this.load();
this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null;
this.hasPasswordHistory = this.cipher.hasPasswordHistory;
this.cleanUp();
this.canAccessPremium = await this.stateService.getCanAccessPremium();
if (
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
(this.cipher.organizationUseTotp || this.canAccessPremium)
) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
this.totpInterval = window.setInterval(async () => {
await this.totpTick(interval);
}, 1000);
}
}
ngOnDestroy() {
super.ngOnDestroy();
}
toggleFavorite() {
this.cipher.favorite = !this.cipher.favorite;
}
togglePassword() {
super.togglePassword();
// Hide password count when password is hidden to be safe
if (!this.showPassword && this.showPasswordCount) {
this.togglePasswordCount();
}
}
togglePasswordCount() {
this.showPasswordCount = !this.showPasswordCount;
if (this.editMode && this.showPasswordCount) {
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledPasswordVisible,
this.cipherId
);
}
}
launch(uri: LoginUriView) {
if (!uri.canLaunch) {
return;
}
this.platformUtilsService.launchUri(uri.launchUri);
}
copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
if (this.editMode) {
if (typeI18nKey === "password") {
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
this.cipherId
);
} else if (typeI18nKey === "securityCode") {
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
} else if (aType === "H_Field") {
this.eventCollectionService.collect(
EventType.Cipher_ClientCopiedHiddenField,
this.cipherId
);
}
}
}
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
this.cipher.login.password = await this.passwordGenerationService.generatePassword(options);
}
return confirmed;
}
premiumRequired() {
if (!this.canAccessPremium) {
this.messagingService.send("premiumRequired");
return;
}
}
upgradeOrganization() {
this.messagingService.send("upgradeOrganization", {
organizationId: this.cipher.organizationId,
});
}
showGetPremium() {
if (this.canAccessPremium) {
return;
}
if (this.cipher.organizationUseTotp) {
this.upgradeOrganization();
} else {
this.premiumRequired();
}
}
viewHistory() {
this.viewingPasswordHistory = !this.viewingPasswordHistory;
}
protected cleanUp() {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
protected async totpUpdateCode() {
if (
this.cipher == null ||
this.cipher.type !== CipherType.Login ||
this.cipher.login.totp == null
) {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
return;
}
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
if (this.totpCode != null) {
if (this.totpCode.length > 4) {
const half = Math.floor(this.totpCode.length / 2);
this.totpCodeFormatted =
this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
} else {
this.totpCodeFormatted = this.totpCode;
}
} else {
this.totpCodeFormatted = null;
if (this.totpInterval) {
window.clearInterval(this.totpInterval);
}
}
}
protected allowOwnershipAssignment() {
return (
(!this.editMode || this.cloneMode) &&
this.ownershipOptions != null &&
(this.ownershipOptions.length > 1 || !this.allowPersonal)
);
}
private async totpTick(intervalSeconds: number) {
const epoch = Math.round(new Date().getTime() / 1000.0);
const mod = epoch % intervalSeconds;
this.totpSec = intervalSeconds - mod;
this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2");
this.totpLow = this.totpSec <= 7;
if (mod === 0) {
await this.totpUpdateCode();
}
}
}

View File

@@ -1,120 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="attachmentsTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="attachmentsTitle">
{{ "attachments" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-hover table-list" *ngIf="cipher && cipher.hasAttachments">
<tbody>
<tr *ngFor="let a of cipher.attachments">
<td class="table-list-icon">
<i
class="bwi bwi-fw bwi-lg bwi-file"
*ngIf="!$any(a).downloading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-lg bwi-fw bwi-spin"
*ngIf="$any(a).downloading"
aria-hidden="true"
></i>
</td>
<td class="wrap">
<div class="d-flex">
<a href="#" appStopClick (click)="download(a)">{{ a.fileName }}</a>
<div *ngIf="showFixOldAttachments(a)" class="ml-2">
<a
href="https://bitwarden.com/help/attachments/#fixing-old-attachments"
target="_blank"
rel="noopener"
>
<i
class="bwi bwi-exclamation-triangle text-warning"
title="{{ 'attachmentFixDesc' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachmentFixDesc" | i18n }}</span></a
>
<button
type="button"
class="btn btn-outline-primary btn-sm m-0 py-0 px-2"
(click)="reupload(a)"
#reuploadBtn
[appApiAction]="reuploadPromises[a.id]"
[disabled]="$any(reuploadBtn).loading"
>
{{ "fix" | i18n }}
</button>
</div>
</div>
<small>{{ a.sizeName }}</small>
</td>
<td class="table-list-options" *ngIf="!viewOnly">
<button
class="btn btn-outline-danger"
type="button"
appStopClick
appA11yTitle="{{ 'delete' | i18n }}"
(click)="delete(a)"
#deleteBtn
[appApiAction]="deletePromises[a.id]"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</td>
</tr>
</tbody>
</table>
<div *ngIf="!viewOnly">
<h3>{{ "newAttachment" | i18n }}</h3>
<label for="file" class="sr-only">{{ "file" | i18n }}</label>
<input type="file" id="file" class="form-control-file" name="file" required />
<small class="form-text text-muted">{{ "maxFileSize" | i18n }}</small>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading"
*ngIf="!viewOnly"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,54 +0,0 @@
import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.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 { StateService } from "@bitwarden/common/abstractions/state.service";
import { AttachmentView } from "@bitwarden/common/models/view/attachment.view";
@Component({
selector: "app-vault-attachments",
templateUrl: "attachments.component.html",
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = false;
protected override componentName = "app-vault-attachments";
constructor(
cipherService: CipherService,
i18nService: I18nService,
cryptoService: CryptoService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService,
fileDownloadService: FileDownloadService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService,
fileDownloadService
);
}
protected async reupload(attachment: AttachmentView) {
if (this.showFixOldAttachments(attachment)) {
await this.reuploadCipherAttachment(attachment, false);
}
}
protected showFixOldAttachments(attachment: AttachmentView) {
return attachment.key == null && this.cipher.organizationId == null;
}
}

View File

@@ -1,25 +0,0 @@
<bit-simple-dialog>
<span bitDialogTitle>
{{ (permanent ? "permanentlyDeleteSelected" : "deleteSelected") | i18n }}
</span>
<span bitDialogContent>
<ng-container *ngIf="!permanent">
<span *ngIf="cipherIds?.length">
{{ "deleteSelectedItemsDesc" | i18n: cipherIds.length }}
</span>
<span *ngIf="collectionIds?.length">
{{ "deleteSelectedCollectionsDesc" | i18n: collectionIds.length }}
</span>
{{ "deleteSelectedConfirmation" | i18n }}
</ng-container>
<ng-container *ngIf="permanent">
{{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length }}
</ng-container>
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton type="submit" buttonType="danger" [bitAction]="submit">
{{ (permanent ? "permanentlyDelete" : "delete") | i18n }}
</button>
<button bitButton type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
</bit-simple-dialog>

View File

@@ -1,134 +0,0 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherBulkDeleteRequest } from "@bitwarden/common/models/request/cipher-bulk-delete.request";
import { CollectionBulkDeleteRequest } from "@bitwarden/common/models/request/collection-bulk-delete.request";
import { DialogService } from "@bitwarden/components";
export interface BulkDeleteDialogParams {
cipherIds?: string[];
collectionIds?: string[];
permanent?: boolean;
organization?: Organization;
}
export enum BulkDeleteDialogResult {
Deleted = "deleted",
Canceled = "canceled",
}
/**
* Strongly typed helper to open a BulkDeleteDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openBulkDeleteDialog = (
dialogService: DialogService,
config: DialogConfig<BulkDeleteDialogParams>
) => {
return dialogService.open<BulkDeleteDialogResult, BulkDeleteDialogParams>(
BulkDeleteDialogComponent,
config
);
};
@Component({
selector: "vault-bulk-delete-dialog",
templateUrl: "bulk-delete-dialog.component.html",
})
export class BulkDeleteDialogComponent {
cipherIds: string[];
collectionIds: string[];
permanent = false;
organization: Organization;
constructor(
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
private dialogRef: DialogRef<BulkDeleteDialogResult>,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private apiService: ApiService
) {
this.cipherIds = params.cipherIds ?? [];
this.collectionIds = params.collectionIds ?? [];
this.permanent = params.permanent;
this.organization = params.organization;
}
protected async cancel() {
this.close(BulkDeleteDialogResult.Canceled);
}
protected submit = async () => {
const deletePromises: Promise<void>[] = [];
if (this.cipherIds.length) {
if (!this.organization || !this.organization.canEditAnyCollection) {
deletePromises.push(this.deleteCiphers());
} else {
deletePromises.push(this.deleteCiphersAdmin());
}
}
if (this.collectionIds.length && this.organization) {
deletePromises.push(this.deleteCollections());
}
await Promise.all(deletePromises);
if (this.cipherIds.length) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.permanent ? "permanentlyDeletedItems" : "deletedItems")
);
}
if (this.collectionIds.length) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deletedCollections")
);
}
this.close(BulkDeleteDialogResult.Deleted);
};
private async deleteCiphers(): Promise<any> {
if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds);
} else {
await this.cipherService.softDeleteManyWithServer(this.cipherIds);
}
}
private async deleteCiphersAdmin(): Promise<any> {
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
if (this.permanent) {
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
} else {
return await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
}
}
private async deleteCollections(): Promise<any> {
if (!this.organization.canDeleteAssignedCollections) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("missingPermissions")
);
return;
}
const deleteRequest = new CollectionBulkDeleteRequest(this.collectionIds, this.organization.id);
return await this.apiService.deleteManyCollections(deleteRequest);
}
private close(result: BulkDeleteDialogResult) {
this.dialogRef.close(result);
}
}

View File

@@ -1,25 +0,0 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared";
import { BulkDeleteDialogComponent } from "./bulk-delete-dialog/bulk-delete-dialog.component";
import { BulkMoveDialogComponent } from "./bulk-move-dialog/bulk-move-dialog.component";
import { BulkRestoreDialogComponent } from "./bulk-restore-dialog/bulk-restore-dialog.component";
import { BulkShareDialogComponent } from "./bulk-share-dialog/bulk-share-dialog.component";
@NgModule({
imports: [SharedModule],
declarations: [
BulkDeleteDialogComponent,
BulkMoveDialogComponent,
BulkRestoreDialogComponent,
BulkShareDialogComponent,
],
exports: [
BulkDeleteDialogComponent,
BulkMoveDialogComponent,
BulkRestoreDialogComponent,
BulkShareDialogComponent,
],
})
export class BulkDialogsModule {}

View File

@@ -1,24 +0,0 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="small">
<span bitDialogTitle>
{{ "moveSelected" | i18n }}
</span>
<span bitDialogContent>
<p>{{ "moveSelectedItemsDesc" | i18n: cipherIds.length }}</p>
<bit-form-field>
<bit-label for="folder">{{ "folder" | i18n }}</bit-label>
<select bitInput formControlName="folderId">
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</bit-form-field>
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton bitFormButton type="submit" buttonType="primary">
{{ "save" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</div>
</bit-dialog>
</form>

View File

@@ -1,85 +0,0 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom, Observable } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { DialogService } from "@bitwarden/components";
export interface BulkMoveDialogParams {
cipherIds?: string[];
}
export enum BulkMoveDialogResult {
Moved = "moved",
Canceled = "canceled",
}
/**
* Strongly typed helper to open a BulkMoveDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openBulkMoveDialog = (
dialogService: DialogService,
config: DialogConfig<BulkMoveDialogParams>
) => {
return dialogService.open<BulkMoveDialogResult, BulkMoveDialogParams>(
BulkMoveDialogComponent,
config
);
};
@Component({
selector: "vault-bulk-move-dialog",
templateUrl: "bulk-move-dialog.component.html",
})
export class BulkMoveDialogComponent implements OnInit {
cipherIds: string[] = [];
formGroup = this.formBuilder.group({
folderId: ["", [Validators.required]],
});
folders$: Observable<FolderView[]>;
constructor(
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
private dialogRef: DialogRef<BulkMoveDialogResult>,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private folderService: FolderService,
private formBuilder: FormBuilder
) {
this.cipherIds = params.cipherIds ?? [];
}
async ngOnInit() {
this.folders$ = this.folderService.folderViews$;
this.formGroup.patchValue({
folderId: (await firstValueFrom(this.folders$))[0].id,
});
}
protected cancel() {
this.close(BulkMoveDialogResult.Canceled);
}
protected submit = async () => {
if (this.formGroup.invalid) {
return;
}
await this.cipherService.moveManyWithServer(this.cipherIds, this.formGroup.value.folderId);
this.platformUtilsService.showToast("success", null, this.i18nService.t("movedItems"));
this.close(BulkMoveDialogResult.Moved);
};
private close(result: BulkMoveDialogResult) {
this.dialogRef.close(result);
}
}

View File

@@ -1,14 +0,0 @@
<bit-simple-dialog>
<span bitDialogTitle>
{{ "restoreSelected" | i18n }}
</span>
<span bitDialogContent>
{{ "restoreSelectedItemsDesc" | i18n: cipherIds.length }}
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton type="submit" buttonType="primary" [bitAction]="submit">
{{ "restore" | i18n }}
</button>
<button bitButton type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
</bit-simple-dialog>

View File

@@ -1,63 +0,0 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { DialogService } from "@bitwarden/components";
export interface BulkRestoreDialogParams {
cipherIds: string[];
}
export enum BulkRestoreDialogResult {
Restored = "restored",
Canceled = "canceled",
}
/**
* Strongly typed helper to open a BulkRestoreDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openBulkRestoreDialog = (
dialogService: DialogService,
config: DialogConfig<BulkRestoreDialogParams>
) => {
return dialogService.open<BulkRestoreDialogResult, BulkRestoreDialogParams>(
BulkRestoreDialogComponent,
config
);
};
@Component({
selector: "vault-bulk-restore-dialog",
templateUrl: "bulk-restore-dialog.component.html",
})
export class BulkRestoreDialogComponent {
cipherIds: string[];
constructor(
@Inject(DIALOG_DATA) params: BulkRestoreDialogParams,
private dialogRef: DialogRef<BulkRestoreDialogResult>,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {
this.cipherIds = params.cipherIds ?? [];
}
submit = async () => {
await this.cipherService.restoreManyWithServer(this.cipherIds);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
this.close(BulkRestoreDialogResult.Restored);
};
protected cancel() {
this.close(BulkRestoreDialogResult.Canceled);
}
private close(result: BulkRestoreDialogResult) {
this.dialogRef.close(result);
}
}

View File

@@ -1,73 +0,0 @@
<bit-dialog>
<span bitDialogTitle>
{{ "moveSelectedToOrg" | i18n }}
</span>
<span bitDialogContent>
<p>{{ "moveManyToOrgDesc" | i18n }}</p>
<p>
{{
"moveSelectedItemsCountDesc"
| i18n: this.ciphers.length:shareableCiphers.length:nonShareableCount
}}
</p>
<bit-form-field>
<bit-label for="organization">{{ "organization" | i18n }}</bit-label>
<select
bitInput
[(ngModel)]="organizationId"
id="organization"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</bit-form-field>
<div class="d-flex">
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{
"collections" | i18n
}}</label>
<div class="tw-ml-auto tw-flex tw-gap-2" *ngIf="collections && collections.length">
<button bitLink type="button" (click)="selectAll(true)" class="tw-px-2">
{{ "selectAll" | i18n }}
</button>
<button bitLink type="button" (click)="selectAll(false)" class="tw-px-2">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table
class="table table-hover table-list mb-0"
*ngIf="collections && collections.length"
id="collections"
>
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
bitInput
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
attr.aria-label="Check {{ c.name }}"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton type="submit" buttonType="primary" [bitAction]="submit">
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</div>
</bit-dialog>

View File

@@ -1,152 +0,0 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
import { DialogService } from "@bitwarden/components";
export interface BulkShareDialogParams {
ciphers: CipherView[];
organizationId?: string;
}
export enum BulkShareDialogResult {
Shared = "shared",
Canceled = "canceled",
}
/**
* Strongly typed helper to open a BulkShareDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openBulkShareDialog = (
dialogService: DialogService,
config: DialogConfig<BulkShareDialogParams>
) => {
return dialogService.open<BulkShareDialogResult, BulkShareDialogParams>(
BulkShareDialogComponent,
config
);
};
@Component({
selector: "vault-bulk-share-dialog",
templateUrl: "bulk-share-dialog.component.html",
})
export class BulkShareDialogComponent implements OnInit {
ciphers: CipherView[] = [];
organizationId: string;
nonShareableCount = 0;
collections: Checkable<CollectionView>[] = [];
organizations: Organization[] = [];
shareableCiphers: CipherView[] = [];
private writeableCollections: CollectionView[] = [];
constructor(
@Inject(DIALOG_DATA) params: BulkShareDialogParams,
private dialogRef: DialogRef<BulkShareDialogResult>,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private logService: LogService
) {
this.ciphers = params.ciphers ?? [];
this.organizationId = params.organizationId;
}
async ngOnInit() {
this.shareableCiphers = this.ciphers.filter(
(c) => !c.hasOldAttachments && c.organizationId == null
);
this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length;
const allCollections = await this.collectionService.getAllDecrypted();
this.writeableCollections = allCollections.filter((c) => !c.readOnly);
this.organizations = await this.organizationService.getAll();
if (this.organizationId == null && this.organizations.length > 0) {
this.organizationId = this.organizations[0].id;
}
this.filterCollections();
}
ngOnDestroy() {
this.selectAll(false);
}
filterCollections() {
this.selectAll(false);
if (this.organizationId == null || this.writeableCollections.length === 0) {
this.collections = [];
} else {
this.collections = this.writeableCollections.filter(
(c) => c.organizationId === this.organizationId
);
}
}
submit = async () => {
const checkedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
try {
await this.cipherService.shareManyWithServer(
this.shareableCiphers,
this.organizationId,
checkedCollectionIds
);
const orgName =
this.organizations.find((o) => o.id === this.organizationId)?.name ??
this.i18nService.t("organization");
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("movedItemsToOrg", orgName)
);
this.close(BulkShareDialogResult.Shared);
} catch (e) {
this.logService.error(e);
}
};
check(c: Checkable<CollectionView>, select?: boolean) {
c.checked = select == null ? !c.checked : select;
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach((c) => this.check(c, select));
}
get canSave() {
if (
this.shareableCiphers != null &&
this.shareableCiphers.length > 0 &&
this.collections != null
) {
for (let i = 0; i < this.collections.length; i++) {
if (this.collections[i].checked) {
return true;
}
}
}
return false;
}
protected cancel() {
this.close(BulkShareDialogResult.Canceled);
}
private close(result: BulkShareDialogResult) {
this.dialogRef.close(result);
}
}

View File

@@ -1,63 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h1 class="modal-title" id="collectionsTitle">
{{ "collections" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{ "collectionsDesc" | i18n }}</p>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,37 +0,0 @@
import { Component, OnDestroy } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/components/collections.component";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/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 { CollectionView } from "@bitwarden/common/models/view/collection.view";
@Component({
selector: "app-vault-collections",
templateUrl: "collections.component.html",
})
export class CollectionsComponent extends BaseCollectionsComponent implements OnDestroy {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
logService: LogService
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
}
ngOnDestroy() {
this.selectAll(false);
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
this.collections.forEach((c) => this.check(c, select));
}
}

View File

@@ -1,68 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="folderAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-sm" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h1 class="modal-title" id="folderAddEditTitle">{{ title }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="folder.name"
required
appAutofocus
/>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { Component } from "@angular/core";
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component";
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({
selector: "app-folder-add-edit",
templateUrl: "folder-add-edit.component.html",
})
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
protected override componentName = "app-folder-add-edit";
constructor(
folderService: FolderService,
folderApiService: FolderApiServiceAbstraction,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(folderService, folderApiService, i18nService, platformUtilsService, logService);
}
}

View File

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

View File

@@ -1,11 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<button
bitBadge
[style.color]="textColor"
[style.background-color]="color"
appA11yTitle="{{ organizationName }}"
(click)="emitOnOrganizationClicked()"
>
{{ organizationName | ellipsis: 13 }}
</button>

View File

@@ -1,49 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { Utils } from "@bitwarden/common/misc/utils";
@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;
isMe: boolean;
constructor(
private i18nService: I18nService,
private avatarService: AvatarUpdateService,
private tokenService: TokenService
) {}
async ngOnInit(): Promise<void> {
if (this.organizationName == null || this.organizationName === "") {
this.organizationName = this.i18nService.t("me");
this.isMe = true;
}
if (this.isMe) {
this.color = await this.avatarService.loadColorFromState();
if (this.color == null) {
const userName =
(await this.tokenService.getName()) ?? (await this.tokenService.getEmail());
this.color = Utils.stringToColor(userName.toUpperCase());
}
} else {
this.color = Utils.stringToColor(this.organizationName.toUpperCase());
}
this.textColor = Utils.pickTextColorBasedOnBgColor(this.color, 135, true) + "!important";
}
emitOnOrganizationClicked() {
this.onOrganizationClicked.emit();
}
}

View File

@@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from "@angular/core";
import { CollectionView } from "@bitwarden/common/src/models/view/collection.view";
@Pipe({
name: "collectionNameFromId",
pure: true,
})
export class GetCollectionNameFromIdPipe implements PipeTransform {
transform(value: string, collections: CollectionView[]) {
return collections.find((o) => o.id === value)?.name;
}
}

View File

@@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from "@angular/core";
import { GroupView } from "../../organizations/core";
@Pipe({
name: "groupNameFromId",
pure: true,
})
export class GetGroupNameFromIdPipe implements PipeTransform {
transform(value: string, groups: GroupView[]) {
return groups.find((o) => o.id === value)?.name;
}
}

View File

@@ -1,14 +0,0 @@
import { Pipe, PipeTransform } from "@angular/core";
import { Organization } from "@bitwarden/common/models/domain/organization";
@Pipe({
name: "orgNameFromId",
pure: true,
})
export class GetOrgNameFromIdPipe implements PipeTransform {
transform(value: string, organizations: Organization[]) {
const orgName = organizations.find((o) => o.id === value)?.name;
return orgName;
}
}

View File

@@ -1,11 +0,0 @@
import { NgModule } from "@angular/core";
import { GetCollectionNameFromIdPipe } from "./get-collection-name.pipe";
import { GetGroupNameFromIdPipe } from "./get-group-name.pipe";
import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe";
@NgModule({
declarations: [GetOrgNameFromIdPipe, GetCollectionNameFromIdPipe, GetGroupNameFromIdPipe],
exports: [GetOrgNameFromIdPipe, GetCollectionNameFromIdPipe, GetGroupNameFromIdPipe],
})
export class PipesModule {}

View File

@@ -1,101 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="shareTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h1 class="modal-title" id="shareTitle">
{{ "moveToOrganization" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<ng-container *ngIf="organizations$ | async as organizations">
<div class="modal-body" *ngIf="!organizations || !organizations.length">
{{ "noOrganizationsList" | i18n }}
</div>
<div class="modal-body" *ngIf="organizations && organizations.length">
<p>{{ "moveToOrgDesc" | i18n }}</p>
<div class="form-group">
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
class="form-control"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table
class="table table-hover table-list mb-0"
*ngIf="collections && collections.length"
>
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[disabled]="form.loading || !canSave"
[ngClass]="{ loading: form.loading }"
*ngIf="organizations && organizations.length"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "save" | i18n }}</span>
</button>
<a
href="#"
routerLink="/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>
{{ "newOrganization" | i18n }}
</a>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</ng-container>
</form>
</div>
</div>

View File

@@ -1,47 +0,0 @@
import { Component, OnDestroy } from "@angular/core";
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
@Component({
selector: "app-vault-share",
templateUrl: "share.component.html",
})
export class ShareComponent extends BaseShareComponent implements OnDestroy {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
logService: LogService
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService
);
}
ngOnDestroy() {
this.selectAll(false);
}
check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select;
}
selectAll(select: boolean) {
const collections = select ? this.collections : this.writeableCollections;
collections.forEach((c) => this.check(c, select));
}
}

View File

@@ -1,9 +0,0 @@
<a
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main tw-no-underline hover:tw-bg-secondary-100 hover:tw-no-underline focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
href="#"
appStopClick
(click)="submit(returnUri, true)"
>
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
{{ "linkSso" | i18n }}
</a>

View File

@@ -1,59 +0,0 @@
import { AfterContentInit, Component, Input } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent } from "@bitwarden/angular/components/sso.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({
selector: "app-link-sso",
templateUrl: "link-sso.component.html",
})
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
@Input() organization: Organization;
returnUri = "/settings/organizations";
constructor(
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
apiService: ApiService,
authService: AuthService,
router: Router,
route: ActivatedRoute,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService,
stateService: StateService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
authService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
);
this.returnUri = "/settings/organizations";
this.redirectUri = window.location.origin + "/sso-connector.html";
this.clientId = "web";
}
async ngAfterContentInit() {
this.identifier = this.organization.identifier;
}
}

View File

@@ -1,52 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted tw-m-2"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<div
*ngIf="loaded"
class="tw-flex tw-min-w-[200px] tw-max-w-[300px] tw-flex-col"
[appApiAction]="actionPromise"
>
<button
*ngIf="allowEnrollmentChanges(organization) && !organization.resetPasswordEnrolled"
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
(click)="toggleResetPasswordEnrollment(organization)"
>
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
{{ "enrollPasswordReset" | i18n }}
</button>
<button
*ngIf="allowEnrollmentChanges(organization) && organization.resetPasswordEnrolled"
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
(click)="toggleResetPasswordEnrollment(organization)"
>
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "withdrawPasswordReset" | i18n }}
</button>
<ng-container *ngIf="organization.useSso && organization.identifier">
<button
*ngIf="organization.ssoBound; else linkSso"
class="tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left !tw-text-main hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
(click)="unlinkSso(organization)"
>
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
{{ "unlinkSso" | i18n }}
</button>
<ng-template #linkSso>
<app-link-sso [organization]="organization"> </app-link-sso>
</ng-template>
</ng-container>
<button
class="text-danger tw-block tw-cursor-pointer tw-border-none tw-bg-background tw-px-4 tw-py-2 tw-text-left hover:tw-bg-secondary-100 focus:tw-z-50 focus:tw-bg-secondary-100 focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2 active:!tw-ring-0 active:!tw-ring-offset-0"
(click)="leave(organization)"
>
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
{{ "leave" | i18n }}
</button>
</div>

View File

@@ -1,157 +0,0 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { map, Subject, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { EnrollMasterPasswordReset } from "../../../organizations/users/enroll-master-password-reset.component";
import { OptionsInput } from "../shared/components/vault-filter-section.component";
import { OrganizationFilter } from "../shared/models/vault-filter.type";
@Component({
selector: "app-organization-options",
templateUrl: "organization-options.component.html",
})
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
actionPromise: Promise<void | boolean>;
policies: Policy[];
loaded = false;
private destroy$ = new Subject<void>();
constructor(
@Inject(OptionsInput) protected organization: OrganizationFilter,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private apiService: ApiService,
private syncService: SyncService,
private policyService: PolicyService,
private modalService: ModalService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService
) {}
async ngOnInit() {
this.policyService.policies$
.pipe(
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
takeUntil(this.destroy$)
)
.subscribe((policies) => {
this.policies = policies;
this.loaded = true;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
allowEnrollmentChanges(org: OrganizationFilter): boolean {
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
const policy = this.policies.find((p) => p.organizationId === org.id);
if (policy != null && policy.enabled) {
return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
}
}
return false;
}
showEnrolledStatus(org: Organization): boolean {
return (
org.useResetPassword &&
org.resetPasswordEnrolled &&
this.policies.some((p) => p.organizationId === org.id && p.enabled)
);
}
async unlinkSso(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("unlinkSsoConfirmation"),
org.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
return this.syncService.fullSync(true);
});
await this.actionPromise;
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
} catch (e) {
this.logService.error(e);
}
}
async leave(org: Organization) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("leaveOrganizationConfirmation"),
org.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.organizationApiService.leave(org.id);
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
} catch (e) {
this.logService.error(e);
}
}
async toggleResetPasswordEnrollment(org: Organization) {
if (!this.organization.resetPasswordEnrolled) {
this.modalService.open(EnrollMasterPasswordReset, {
allowMultipleModals: true,
data: {
organization: org,
},
});
} else {
// Remove reset password
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.masterPasswordHash = "ignored";
request.resetPasswordKey = null;
this.actionPromise = this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
this.organization.id,
this.organization.userId,
request
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("withdrawPasswordResetSuccess")
);
this.syncService.fullSync(true);
} catch (e) {
this.logService.error(e);
}
}
}
}

View File

@@ -1,35 +0,0 @@
<div class="card vault-filters">
<div class="container loading-spinner" *ngIf="!isLoaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<div *ngIf="isLoaded">
<div class="card-header d-flex">
{{ "filters" | i18n }}
<a
class="ml-auto"
href="https://bitwarden.com/help/searching-vault/"
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div class="card-body">
<input
type="search"
placeholder="{{ searchPlaceholder | i18n }}"
id="search"
class="form-control"
(input)="searchTextChanged($any($event.target).value)"
autocomplete="off"
appAutofocus
/>
<ng-container *ngFor="let f of filtersList">
<div class="filter">
<app-filter-section [activeFilter]="activeFilter" [section]="f"> </app-filter-section>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -1,353 +0,0 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
import {
VaultFilterList,
VaultFilterSection,
VaultFilterType,
} from "../shared/models/vault-filter-section.type";
import { VaultFilter } from "../shared/models/vault-filter.model";
import {
CipherStatus,
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "../shared/models/vault-filter.type";
import { OrganizationOptionsComponent } from "./organization-options.component";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent implements OnInit, OnDestroy {
filters?: VaultFilterList;
@Input() activeFilter: VaultFilter = new VaultFilter();
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
@Output() onSearchTextChanged = new EventEmitter<string>();
@Output() onAddFolder = new EventEmitter<never>();
@Output() onEditFolder = new EventEmitter<FolderFilter>();
isLoaded = false;
searchText = "";
protected destroy$: Subject<void> = new Subject<void>();
get filtersList() {
return this.filters ? Object.values(this.filters) : [];
}
get searchPlaceholder() {
if (this.activeFilter.isFavorites) {
return "searchFavorites";
}
if (this.activeFilter.isDeleted) {
return "searchTrash";
}
if (this.activeFilter.cipherType === CipherType.Login) {
return "searchLogin";
}
if (this.activeFilter.cipherType === CipherType.Card) {
return "searchCard";
}
if (this.activeFilter.cipherType === CipherType.Identity) {
return "searchIdentity";
}
if (this.activeFilter.cipherType === CipherType.SecureNote) {
return "searchSecureNote";
}
if (this.activeFilter.selectedFolderNode?.node) {
return "searchFolder";
}
if (this.activeFilter.selectedCollectionNode?.node) {
return "searchCollection";
}
if (this.activeFilter.organizationId === "MyVault") {
return "searchMyVault";
}
if (this.activeFilter.organizationId) {
return "searchOrganization";
}
return "searchVault";
}
constructor(
protected vaultFilterService: VaultFilterService,
protected policyService: PolicyService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService
) {
this.loadSubscriptions();
}
async ngOnInit(): Promise<void> {
this.filters = await this.buildAllFilters();
this.activeFilter.selectedCipherTypeNode =
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
this.isLoaded = true;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
protected loadSubscriptions() {
this.vaultFilterService.filteredFolders$
.pipe(
switchMap(async (folders) => {
this.removeInvalidFolderSelection(folders);
}),
takeUntil(this.destroy$)
)
.subscribe();
this.vaultFilterService.filteredCollections$
.pipe(
switchMap(async (collections) => {
this.removeInvalidCollectionSelection(collections);
}),
takeUntil(this.destroy$)
)
.subscribe();
}
searchTextChanged(t: string) {
this.searchText = t;
this.onSearchTextChanged.emit(t);
}
protected applyVaultFilter(filter: VaultFilter) {
this.activeFilterChanged.emit(filter);
}
applyOrganizationFilter = async (orgNode: TreeNode<OrganizationFilter>): Promise<void> => {
if (!orgNode?.node.enabled) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("disabledOrganizationFilterError")
);
return;
}
const filter = this.activeFilter;
filter.resetOrganization();
if (orgNode?.node.id !== "AllVaults") {
filter.selectedOrganizationNode = orgNode;
}
this.vaultFilterService.setOrganizationFilter(orgNode.node);
await this.vaultFilterService.expandOrgFilter();
this.applyVaultFilter(filter);
};
applyTypeFilter = async (filterNode: TreeNode<CipherTypeFilter>): Promise<void> => {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCipherTypeNode = filterNode;
this.applyVaultFilter(filter);
};
applyFolderFilter = async (folderNode: TreeNode<FolderFilter>): Promise<void> => {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedFolderNode = folderNode;
this.applyVaultFilter(filter);
};
applyCollectionFilter = async (collectionNode: TreeNode<CollectionFilter>): Promise<void> => {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCollectionNode = collectionNode;
this.applyVaultFilter(filter);
};
addFolder = async (): Promise<void> => {
this.onAddFolder.emit();
};
editFolder = async (folder: FolderFilter): Promise<void> => {
this.onEditFolder.emit(folder);
};
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
return await firstValueFrom(this.filters?.typeFilter.data$);
}
protected async removeInvalidFolderSelection(folders: FolderView[]) {
if (this.activeFilter.selectedFolderNode) {
if (!folders.some((f) => f.id === this.activeFilter.folderId)) {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCipherTypeNode =
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
this.applyVaultFilter(filter);
}
}
}
protected async removeInvalidCollectionSelection(collections: CollectionView[]) {
if (this.activeFilter.selectedCollectionNode) {
if (!collections.some((f) => f.id === this.activeFilter.collectionId)) {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCipherTypeNode =
(await this.getDefaultFilter()) as TreeNode<CipherTypeFilter>;
this.applyVaultFilter(filter);
}
}
}
async buildAllFilters(): Promise<VaultFilterList> {
const builderFilter = {} as VaultFilterList;
builderFilter.organizationFilter = await this.addOrganizationFilter();
builderFilter.typeFilter = await this.addTypeFilter();
builderFilter.folderFilter = await this.addFolderFilter();
builderFilter.collectionFilter = await this.addCollectionFilter();
builderFilter.trashFilter = await this.addTrashFilter();
return builderFilter;
}
protected async addOrganizationFilter(): Promise<VaultFilterSection> {
const singleOrgPolicy = await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
const personalVaultPolicy = await this.policyService.policyAppliesToUser(
PolicyType.PersonalOwnership
);
const addAction = !singleOrgPolicy
? { text: "newOrganization", route: "/create-organization" }
: null;
const orgFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.organizationTree$,
header: {
showHeader: !(singleOrgPolicy && personalVaultPolicy),
isSelectable: true,
},
action: this.applyOrganizationFilter,
options: { component: OrganizationOptionsComponent },
add: addAction,
divider: true,
};
return orgFilterSection;
}
protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise<VaultFilterSection> {
const allTypeFilters: CipherTypeFilter[] = [
{
id: "favorites",
name: this.i18nService.t("favorites"),
type: "favorites",
icon: "bwi-star",
},
{
id: "login",
name: this.i18nService.t("typeLogin"),
type: CipherType.Login,
icon: "bwi-globe",
},
{
id: "card",
name: this.i18nService.t("typeCard"),
type: CipherType.Card,
icon: "bwi-credit-card",
},
{
id: "identity",
name: this.i18nService.t("typeIdentity"),
type: CipherType.Identity,
icon: "bwi-id-card",
},
{
id: "note",
name: this.i18nService.t("typeSecureNote"),
type: CipherType.SecureNote,
icon: "bwi-sticky-note",
},
];
const typeFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.buildTypeTree(
{ id: "AllItems", name: "allItems", type: "all", icon: "" },
allTypeFilters.filter((f) => !excludeTypes.includes(f.type))
),
header: {
showHeader: true,
isSelectable: true,
},
action: this.applyTypeFilter,
};
return typeFilterSection;
}
protected async addFolderFilter(): Promise<VaultFilterSection> {
const folderFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.folderTree$,
header: {
showHeader: true,
isSelectable: false,
},
action: this.applyFolderFilter,
edit: {
text: "editFolder",
action: this.editFolder,
},
add: {
text: "Add Folder",
action: this.addFolder,
},
};
return folderFilterSection;
}
protected async addCollectionFilter(): Promise<VaultFilterSection> {
const collectionFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.collectionTree$,
header: {
showHeader: true,
isSelectable: true,
},
action: this.applyCollectionFilter,
};
return collectionFilterSection;
}
protected async addTrashFilter(): Promise<VaultFilterSection> {
const trashFilterSection: VaultFilterSection = {
data$: this.vaultFilterService.buildTypeTree(
{
id: "headTrash",
name: "HeadTrash",
type: "trash",
icon: "bwi-trash",
},
[
{
id: "trash",
name: this.i18nService.t("trash"),
type: "trash",
icon: "bwi-trash",
},
]
),
header: {
showHeader: false,
isSelectable: true,
},
action: this.applyTypeFilter,
};
return trashFilterSection;
}
}

View File

@@ -1,31 +0,0 @@
import { Observable } from "rxjs";
import { Organization } from "@bitwarden/common/src/models/domain/organization";
import { TreeNode } from "@bitwarden/common/src/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/src/models/view/collection.view";
import { FolderView } from "@bitwarden/common/src/models/view/folder.view";
import {
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "../../shared/models/vault-filter.type";
export abstract class VaultFilterService {
collapsedFilterNodes$: Observable<Set<string>>;
filteredFolders$: Observable<FolderView[]>;
filteredCollections$: Observable<CollectionView[]>;
organizationTree$: Observable<TreeNode<OrganizationFilter>>;
folderTree$: Observable<TreeNode<FolderFilter>>;
collectionTree$: Observable<TreeNode<CollectionFilter>>;
reloadCollections: () => Promise<void>;
getCollectionNodeFromTree: (id: string) => Promise<TreeNode<CollectionFilter>>;
setCollapsedFilterNodes: (collapsedFilterNodes: Set<string>) => Promise<void>;
expandOrgFilter: () => Promise<void>;
setOrganizationFilter: (organization: Organization) => void;
buildTypeTree: (
head: CipherTypeFilter,
array: CipherTypeFilter[]
) => Observable<TreeNode<CipherTypeFilter>>;
}

View File

@@ -1,288 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, ReplaySubject, take } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { VaultFilterService } from "./vault-filter.service";
describe("vault filter service", () => {
let vaultFilterService: VaultFilterService;
let stateService: MockProxy<StateService>;
let organizationService: MockProxy<OrganizationService>;
let folderService: MockProxy<FolderService>;
let cipherService: MockProxy<CipherService>;
let collectionService: MockProxy<CollectionService>;
let policyService: MockProxy<PolicyService>;
let i18nService: MockProxy<I18nService>;
let organizations: ReplaySubject<Organization[]>;
let folderViews: ReplaySubject<FolderView[]>;
beforeEach(() => {
stateService = mock<StateService>();
organizationService = mock<OrganizationService>();
folderService = mock<FolderService>();
cipherService = mock<CipherService>();
collectionService = mock<CollectionService>();
policyService = mock<PolicyService>();
i18nService = mock<I18nService>();
i18nService.collator = new Intl.Collator("en-US");
organizations = new ReplaySubject<Organization[]>(1);
folderViews = new ReplaySubject<FolderView[]>(1);
organizationService.organizations$ = organizations;
folderService.folderViews$ = folderViews;
vaultFilterService = new VaultFilterService(
stateService,
organizationService,
folderService,
cipherService,
collectionService,
policyService,
i18nService
);
});
describe("collapsed filter nodes", () => {
const nodes = new Set(["1", "2"]);
it("updates observable when saving", (complete) => {
vaultFilterService.collapsedFilterNodes$.pipe(take(1)).subscribe((value) => {
if (value === nodes) {
complete();
}
});
vaultFilterService.setCollapsedFilterNodes(nodes);
});
it("loads from state on initialization", async () => {
stateService.getCollapsedGroupings.mockResolvedValue(["1", "2"]);
await expect(firstValueFrom(vaultFilterService.collapsedFilterNodes$)).resolves.toEqual(
nodes
);
});
});
describe("organizations", () => {
beforeEach(() => {
const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")];
organizations.next(storedOrgs);
});
it("returns a nested tree", async () => {
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
expect(tree.children.length).toBe(3);
expect(tree.children.find((o) => o.node.name === "org1"));
expect(tree.children.find((o) => o.node.name === "org2"));
});
it("hides My Vault if personal ownership policy is enabled", async () => {
policyService.policyAppliesToUser
.calledWith(PolicyType.PersonalOwnership)
.mockResolvedValue(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
expect(tree.children.length).toBe(2);
expect(!tree.children.find((o) => o.node.id === "MyVault"));
});
it("returns 1 organization and My Vault if single organization policy is enabled", async () => {
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
expect(tree.children.length).toBe(2);
expect(tree.children.find((o) => o.node.name === "org1"));
expect(tree.children.find((o) => o.node.id === "MyVault"));
});
it("returns 1 organization if both single organization and personal ownership policies are enabled", async () => {
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
policyService.policyAppliesToUser
.calledWith(PolicyType.PersonalOwnership)
.mockResolvedValue(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
expect(tree.children.length).toBe(1);
expect(tree.children.find((o) => o.node.name === "org1"));
});
});
describe("folders", () => {
describe("filtered folders with organization", () => {
beforeEach(() => {
// Org must be updated before folderService else the subscription uses the null org default value
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
});
it("returns folders filtered by current organization", async () => {
const storedCiphers = [
createCipherView("1", "org test id", "folder test id"),
createCipherView("2", "non matching org id", "non matching folder id"),
];
cipherService.getAllDecrypted.mockResolvedValue(storedCiphers);
const storedFolders = [
createFolderView("folder test id", "test"),
createFolderView("non matching folder id", "test2"),
];
folderViews.next(storedFolders);
await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toEqual([
createFolderView("folder test id", "test"),
]);
});
});
describe("folder tree", () => {
it("returns a nested tree", async () => {
const storedFolders = [
createFolderView("Folder 1 Id", "Folder 1"),
createFolderView("Folder 2 Id", "Folder 1/Folder 2"),
createFolderView("Folder 3 Id", "Folder 1/Folder 3"),
];
folderViews.next(storedFolders);
const result = await firstValueFrom(vaultFilterService.folderTree$);
expect(result.children[0].node.id === "Folder 1 Id");
expect(result.children[0].children.find((c) => c.node.id === "Folder 2 Id"));
expect(result.children[0].children.find((c) => c.node.id === "Folder 3 Id"));
}, 10000);
});
});
describe("collections", () => {
describe("filtered collections", () => {
it("returns collections filtered by current organization", async () => {
vaultFilterService.setOrganizationFilter(createOrganization("org test id", "Test Org"));
const storedCollections = [
createCollectionView("1", "collection 1", "org test id"),
createCollectionView("2", "collection 2", "non matching org id"),
];
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
vaultFilterService.reloadCollections();
await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([
createCollectionView("1", "collection 1", "org test id"),
]);
});
});
describe("collection tree", () => {
it("returns tree with children", async () => {
const storedCollections = [
createCollectionView("id-1", "Collection 1", "org test id"),
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
createCollectionView("id-3", "Collection 1/Collection 3", "org test id"),
];
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
vaultFilterService.reloadCollections();
const result = await firstValueFrom(vaultFilterService.collectionTree$);
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-2", "id-3"]);
});
it("returns tree where non-existing collections are excluded from children", async () => {
const storedCollections = [
createCollectionView("id-1", "Collection 1", "org test id"),
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
];
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
vaultFilterService.reloadCollections();
const result = await firstValueFrom(vaultFilterService.collectionTree$);
expect(result.children.map((c) => c.node.id)).toEqual(["id-1"]);
expect(result.children[0].children.map((c) => c.node.id)).toEqual(["id-3"]);
expect(result.children[0].children[0].node.name).toBe("Collection 2/Collection 3");
});
it("returns tree with parents", async () => {
const storedCollections = [
createCollectionView("id-1", "Collection 1", "org test id"),
createCollectionView("id-2", "Collection 1/Collection 2", "org test id"),
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
createCollectionView("id-4", "Collection 1/Collection 4", "org test id"),
];
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
vaultFilterService.reloadCollections();
const result = await firstValueFrom(vaultFilterService.collectionTree$);
const c1 = result.children[0];
const c2 = c1.children[0];
const c3 = c2.children[0];
const c4 = c1.children[1];
expect(c2.parent.node.id).toEqual("id-1");
expect(c3.parent.node.id).toEqual("id-2");
expect(c4.parent.node.id).toEqual("id-1");
});
it("returns tree where non-existing collections are excluded from parents", async () => {
const storedCollections = [
createCollectionView("id-1", "Collection 1", "org test id"),
createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"),
];
collectionService.getAllDecrypted.mockResolvedValue(storedCollections);
vaultFilterService.reloadCollections();
const result = await firstValueFrom(vaultFilterService.collectionTree$);
const c1 = result.children[0];
const c3 = c1.children[0];
expect(c3.parent.node.id).toEqual("id-1");
});
});
});
function createOrganization(id: string, name: string) {
const org = new Organization();
org.id = id;
org.name = name;
org.identifier = name;
return org;
}
function createCipherView(id: string, orgId: string, folderId: string) {
const cipher = new CipherView();
cipher.id = id;
cipher.organizationId = orgId;
cipher.folderId = folderId;
return cipher;
}
function createFolderView(id: string, name: string): FolderView {
const folder = new FolderView();
folder.id = id;
folder.name = name;
return folder;
}
function createCollectionView(id: string, name: string, orgId: string): CollectionView {
const collection = new CollectionView();
collection.id = id;
collection.name = name;
collection.organizationId = orgId;
return collection;
}
});

View File

@@ -1,256 +0,0 @@
import { Injectable } from "@angular/core";
import {
BehaviorSubject,
combineLatestWith,
firstValueFrom,
map,
Observable,
of,
ReplaySubject,
switchMap,
} from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { CollectionAdminView } from "../../../organizations/core";
import {
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "../shared/models/vault-filter.type";
import { VaultFilterService as VaultFilterServiceAbstraction } from "./abstractions/vault-filter.service";
const NestingDelimiter = "/";
@Injectable()
export class VaultFilterService implements VaultFilterServiceAbstraction {
protected _collapsedFilterNodes = new BehaviorSubject<Set<string>>(null);
collapsedFilterNodes$: Observable<Set<string>> = this._collapsedFilterNodes.pipe(
switchMap(async (nodes) => nodes ?? (await this.getCollapsedFilterNodes()))
);
organizationTree$: Observable<TreeNode<OrganizationFilter>> =
this.organizationService.organizations$.pipe(
switchMap((orgs) => this.buildOrganizationTree(orgs))
);
protected _organizationFilter = new BehaviorSubject<Organization>(null);
filteredFolders$: Observable<FolderView[]> = this.folderService.folderViews$.pipe(
combineLatestWith(this._organizationFilter),
switchMap(([folders, org]) => {
return this.filterFolders(folders, org);
})
);
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
map((folders) => this.buildFolderTree(folders))
);
// TODO: Remove once collections is refactored with observables
// replace with collection service observable
private collectionViews$ = new ReplaySubject<CollectionView[]>(1);
filteredCollections$: Observable<CollectionView[]> = this.collectionViews$.pipe(
combineLatestWith(this._organizationFilter),
switchMap(([collections, org]) => {
return this.filterCollections(collections, org);
})
);
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe(
map((collections) => this.buildCollectionTree(collections))
);
constructor(
protected stateService: StateService,
protected organizationService: OrganizationService,
protected folderService: FolderService,
protected cipherService: CipherService,
protected collectionService: CollectionService,
protected policyService: PolicyService,
protected i18nService: I18nService
) {}
// TODO: Remove once collections is refactored with observables
async reloadCollections() {
this.collectionViews$.next(await this.collectionService.getAllDecrypted());
}
async getCollectionNodeFromTree(id: string) {
const collections = await firstValueFrom(this.collectionTree$);
return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode<CollectionFilter>;
}
async setCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
this._collapsedFilterNodes.next(collapsedFilterNodes);
}
protected async getCollapsedFilterNodes(): Promise<Set<string>> {
const nodes = new Set(await this.stateService.getCollapsedGroupings());
return nodes;
}
setOrganizationFilter(organization: Organization) {
if (organization?.id != "AllVaults") {
this._organizationFilter.next(organization);
} else {
this._organizationFilter.next(null);
}
}
async expandOrgFilter() {
const collapsedFilterNodes = await firstValueFrom(this.collapsedFilterNodes$);
if (!collapsedFilterNodes.has("AllVaults")) {
return;
}
collapsedFilterNodes.delete("AllVaults");
await this.setCollapsedFilterNodes(collapsedFilterNodes);
}
protected async buildOrganizationTree(
orgs?: Organization[]
): Promise<TreeNode<OrganizationFilter>> {
const headNode = this.getOrganizationFilterHead();
if (!(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))) {
const myVaultNode = this.getOrganizationFilterMyVault();
headNode.children.push(myVaultNode);
}
if (await this.policyService.policyAppliesToUser(PolicyType.SingleOrg)) {
orgs = orgs.slice(0, 1);
}
if (orgs) {
orgs.forEach((org) => {
const orgCopy = org as OrganizationFilter;
orgCopy.icon = "bwi-business";
const node = new TreeNode<OrganizationFilter>(orgCopy, headNode, orgCopy.name);
headNode.children.push(node);
});
}
return headNode;
}
protected getOrganizationFilterHead(): TreeNode<OrganizationFilter> {
const head = new Organization() as OrganizationFilter;
head.enabled = true;
return new TreeNode<OrganizationFilter>(head, null, "allVaults", "AllVaults");
}
protected getOrganizationFilterMyVault(): TreeNode<OrganizationFilter> {
const myVault = new Organization() as OrganizationFilter;
myVault.id = "MyVault";
myVault.icon = "bwi-user";
myVault.enabled = true;
myVault.hideOptions = true;
return new TreeNode<OrganizationFilter>(myVault, null, this.i18nService.t("myVault"));
}
buildTypeTree(
head: CipherTypeFilter,
array?: CipherTypeFilter[]
): Observable<TreeNode<CipherTypeFilter>> {
const headNode = new TreeNode<CipherTypeFilter>(head, null);
array?.forEach((filter) => {
const node = new TreeNode<CipherTypeFilter>(filter, headNode, filter.name);
headNode.children.push(node);
});
return of(headNode);
}
protected async filterCollections(
storedCollections: CollectionView[],
org?: Organization
): Promise<CollectionView[]> {
return org?.id != null
? storedCollections.filter((c) => c.organizationId === org.id)
: storedCollections;
}
protected buildCollectionTree(collections?: CollectionView[]): TreeNode<CollectionFilter> {
const headNode = this.getCollectionFilterHead();
if (!collections) {
return headNode;
}
const nodes: TreeNode<CollectionFilter>[] = [];
collections
.sort((a, b) => this.i18nService.collator.compare(a.name, b.name))
.forEach((c) => {
const collectionCopy = new CollectionView() as CollectionFilter;
collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId;
collectionCopy.icon = "bwi-collection";
if (c instanceof CollectionAdminView) {
collectionCopy.groups = c.groups;
collectionCopy.assigned = c.assigned;
}
const parts =
c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
});
nodes.forEach((n) => {
n.parent = headNode;
headNode.children.push(n);
});
return headNode;
}
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
const head = new CollectionView() as CollectionFilter;
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
}
protected async filterFolders(
storedFolders: FolderView[],
org?: Organization
): Promise<FolderView[]> {
if (org?.id == null) {
return storedFolders;
}
const ciphers = await this.cipherService.getAllDecrypted();
const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id);
return storedFolders.filter(
(f) =>
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
ciphers.filter((c) => c.folderId == f.id).length < 1
);
}
protected buildFolderTree(folders?: FolderView[]): TreeNode<FolderFilter> {
const headNode = this.getFolderFilterHead();
if (!folders) {
return headNode;
}
const nodes: TreeNode<FolderFilter>[] = [];
folders.forEach((f) => {
const folderCopy = new FolderView() as FolderFilter;
folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate;
folderCopy.icon = "bwi-folder";
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
});
nodes.forEach((n) => {
n.parent = headNode;
headNode.children.push(n);
});
return headNode;
}
protected getFolderFilterHead(): TreeNode<FolderFilter> {
const head = new FolderView() as FolderFilter;
return new TreeNode<FolderFilter>(head, null, "folders", "AllFolders");
}
}

View File

@@ -1,143 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<ng-container *ngIf="filters && filters.length">
<div *ngIf="headerInfo.showHeader" class="filter-heading">
<button
class="toggle-button"
(click)="toggleCollapse(headerNode.node)"
[attr.aria-expanded]="!isCollapsed(headerNode.node)"
appA11yTitle="{{ 'toggleCollapse' | i18n }}: {{ headerNode.node.name | i18n }}"
aria-controls="sub-filters"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="isCollapsed(headerNode.node) ? 'bwi-angle-right' : 'bwi-angle-down'"
></i>
</button>
<button
*ngIf="headerInfo.isSelectable"
appA11yTitle="{{ isOrganizationFilter ? 'vault' : ('filter' | i18n) }}: {{
headerNode.node.name | i18n
}}"
class="filter-button"
(click)="onFilterSelect(headerNode)"
>
<h3
[ngClass]="{
active: isAllVaultsSelected || isNodeSelected(headerNode)
}"
>
&nbsp;{{ headerNode.node.name | i18n }}
</h3>
</button>
<h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
&nbsp;{{ headerNode.node.name | i18n }}
</h3>
<button
*ngIf="showAddButton"
(click)="onAdd()"
class="text-muted ml-auto add-button"
appA11yTitle="{{ addInfo.text | i18n }}"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</button>
</div>
<ul
id="{{ headerNode.node.name }}-filters"
*ngIf="!isCollapsed(headerNode.node)"
class="filter-options"
>
<ng-template #recursiveFilters let-filters>
<li
*ngFor="let f of filters"
[ngClass]="{
active: isNodeSelected(f)
}"
class="filter-option"
>
<span class="filter-buttons">
<button
*ngIf="f.children.length"
appA11yTitle="{{ 'toggleCollapse' | i18n }}: {{ f.node.name }}"
(click)="toggleCollapse(f.node)"
[attr.aria-expanded]="!isCollapsed(f.node)"
[attr.aria-controls]="f.node.name + '_children'"
class="toggle-button"
>
<i
class="bwi bwi-fw"
[ngClass]="{
'bwi-angle-right': isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node)
}"
aria-hidden="true"
></i>
</button>
<button
class="filter-button"
appA11yTitle="{{ isOrganizationFilter ? 'vault' : ('filter' | i18n) }}: {{
f.node.name
}}"
[ngClass]="{ 'disabled-organization': isOrganizationFilter && !f.node.enabled }"
(click)="onFilterSelect(f)"
>
<i
*ngIf="f.children.length === 0"
class="bwi bwi-fw {{ f.node.icon }}"
aria-hidden="true"
></i>
&nbsp;{{ f.node.name }}
</button>
<span class="ml-auto">
<button
*ngIf="editInfo && f.node.id"
class="edit-button"
(click)="onEdit(f)"
appA11yTitle="{{ editInfo.text | i18n }}"
>
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
</button>
<i
*ngIf="isOrganizationFilter && !f.node.enabled"
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
[attr.aria-label]="'organizationIsDisabled' | i18n"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i
><ng-container *ngIf="optionsInfo && !f.node.hideOptions"
><button [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon">
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
</button>
<bit-menu class="filter-organization-options" #optionsMenu>
<ng-container
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
></ng-container>
</bit-menu>
</ng-container>
</span>
</span>
<ul
[id]="f.node.name + '_children'"
class="nested-filter-options"
*ngIf="f.children.length && !isCollapsed(f.node)"
>
<ng-container *ngTemplateOutlet="recursiveFilters; context: { $implicit: f.children }">
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveFilters; context: { $implicit: filters }"
></ng-container>
<li class="filter-option" *ngIf="showAddLink">
<span class="filter-buttons">
<a href="#" routerLink="{{ addInfo.route }}" class="filter-button">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ addInfo.text | i18n }}
</a>
</span>
</li>
</ul>
<hr *ngIf="divider" />
</ng-container>

View File

@@ -1,137 +0,0 @@
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { VaultFilterService } from "../../services/abstractions/vault-filter.service";
import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type";
import { VaultFilter } from "../models/vault-filter.model";
@Component({
selector: "app-filter-section",
templateUrl: "vault-filter-section.component.html",
})
export class VaultFilterSectionComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
@Input() activeFilter: VaultFilter;
@Input() section: VaultFilterSection;
data: TreeNode<VaultFilterType>;
collapsedFilterNodes: Set<string> = new Set();
private injectors = new Map<string, Injector>();
constructor(private vaultFilterService: VaultFilterService, private injector: Injector) {
this.vaultFilterService.collapsedFilterNodes$
.pipe(takeUntil(this.destroy$))
.subscribe((nodes) => {
this.collapsedFilterNodes = nodes;
});
}
ngOnInit() {
this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => {
this.data = data;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get headerNode() {
return this.data;
}
get headerInfo() {
return this.section.header;
}
get filters() {
return this.data?.children;
}
get isOrganizationFilter() {
return this.data.node instanceof Organization;
}
get isAllVaultsSelected() {
return this.isOrganizationFilter && !this.activeFilter.selectedOrganizationNode;
}
isNodeSelected(filterNode: TreeNode<VaultFilterType>) {
return (
this.activeFilter.organizationId === filterNode?.node.id ||
this.activeFilter.cipherTypeId === filterNode?.node.id ||
this.activeFilter.folderId === filterNode?.node.id ||
this.activeFilter.collectionId === filterNode?.node.id
);
}
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
await this.section?.action(filterNode);
}
get editInfo() {
return this.section?.edit;
}
onEdit(filterNode: TreeNode<VaultFilterType>) {
this.section?.edit?.action(filterNode.node);
}
get addInfo() {
return this.section.add;
}
get showAddButton() {
return this.section.add && !this.section.add.route;
}
get showAddLink() {
return this.section.add && this.section.add.route;
}
async onAdd() {
this.section?.add?.action();
}
get optionsInfo() {
return this.section?.options;
}
get divider() {
return this.section?.divider;
}
isCollapsed(node: ITreeNodeObject) {
return this.collapsedFilterNodes.has(node.id);
}
async toggleCollapse(node: ITreeNodeObject) {
if (this.collapsedFilterNodes.has(node.id)) {
this.collapsedFilterNodes.delete(node.id);
} else {
this.collapsedFilterNodes.add(node.id);
}
await this.vaultFilterService.setCollapsedFilterNodes(this.collapsedFilterNodes);
}
// an injector is necessary to pass data into a dynamic component
// here we are creating a new injector for each filter that has options
createInjector(data: VaultFilterType) {
let inject = this.injectors.get(data.id);
if (!inject) {
inject = Injector.create({
providers: [{ provide: OptionsInput, useValue: data }],
parent: this.injector,
});
this.injectors.set(data.id, inject);
}
return inject;
}
}
export const OptionsInput = new InjectionToken<VaultFilterType>("OptionsInput");

View File

@@ -1,50 +0,0 @@
import { Observable } from "rxjs";
import { TreeNode } from "@bitwarden/common/src/models/domain/tree-node";
import {
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "./vault-filter.type";
export type VaultFilterType =
| OrganizationFilter
| CipherTypeFilter
| FolderFilter
| CollectionFilter;
export enum VaultFilterLabel {
OrganizationFilter = "organizationFilter",
TypeFilter = "typeFilter",
FolderFilter = "folderFilter",
CollectionFilter = "collectionFilter",
TrashFilter = "trashFilter",
}
export type VaultFilterSection = {
data$: Observable<TreeNode<VaultFilterType>>;
header: {
showHeader: boolean;
isSelectable: boolean;
};
action: (filterNode: TreeNode<VaultFilterType>) => Promise<void>;
edit?: {
text: string;
action: (filter: VaultFilterType) => void;
};
add?: {
text: string;
route?: string;
action?: () => void;
};
options?: {
component: any;
};
divider?: boolean;
};
export type VaultFilterList = {
[key in VaultFilterLabel]?: VaultFilterSection;
};

View File

@@ -1,332 +0,0 @@
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { FolderView } from "@bitwarden/common/models/view/folder.view";
import { VaultFilter } from "./vault-filter.model";
import {
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "./vault-filter.type";
describe("VaultFilter", () => {
describe("filterFunction", () => {
const allCiphersFilter = new TreeNode<CipherTypeFilter>(
{
id: "AllItems",
name: "allItems",
type: "all",
icon: "",
},
null
);
const favoriteCiphersFilter = new TreeNode<CipherTypeFilter>(
{
id: "favorites",
name: "favorites",
type: "favorites",
icon: "bwi-star",
},
null
);
const identityCiphersFilter = new TreeNode<CipherTypeFilter>(
{
id: "identity",
name: "identity",
type: CipherType.Identity,
icon: "bwi-id-card",
},
null
);
const trashFilter = new TreeNode<CipherTypeFilter>(
{
id: "trash",
name: "trash",
type: "trash",
icon: "bwi-trash",
},
null
);
describe("generic cipher", () => {
it("should return true when no filter is applied", () => {
const cipher = createCipher();
const filterFunction = createFilterFunction({});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given a favorite cipher", () => {
const cipher = createCipher({ favorite: true });
it("should return true when filtering for favorites", () => {
const filterFunction = createFilterFunction({ selectedCipherTypeNode: allCiphersFilter });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filtering for trash", () => {
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a deleted cipher", () => {
const cipher = createCipher({ deletedDate: new Date() });
it("should return true when filtering for trash", () => {
const filterFunction = createFilterFunction({ selectedCipherTypeNode: trashFilter });
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filtering for favorites", () => {
const filterFunction = createFilterFunction({
selectedCipherTypeNode: favoriteCiphersFilter,
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher with type", () => {
it("should return true when filter matches cipher type", () => {
const cipher = createCipher({ type: CipherType.Identity });
const filterFunction = createFilterFunction({
selectedCipherTypeNode: identityCiphersFilter,
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match cipher type", () => {
const cipher = createCipher({ type: CipherType.Card });
const filterFunction = createFilterFunction({
selectedCipherTypeNode: identityCiphersFilter,
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher with folder id", () => {
it("should return true when filter matches folder id", () => {
const cipher = createCipher({ folderId: "folderId" });
const filterFunction = createFilterFunction({
selectedFolderNode: createFolderFilterNode({ id: "folderId" }),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match folder id", () => {
const cipher = createCipher({ folderId: "folderId" });
const filterFunction = createFilterFunction({
selectedFolderNode: createFolderFilterNode({ id: "differentFolderId" }),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given a cipher without folder", () => {
const cipher = createCipher({ folderId: null });
it("should return true when filtering on unassigned folder", () => {
const filterFunction = createFilterFunction({
selectedFolderNode: createFolderFilterNode({ id: null }),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given an organizational cipher (with organization and collections)", () => {
const cipher = createCipher({
organizationId: "organizationId",
collectionIds: ["collectionId", "anotherId"],
});
it("should return true when filter matches collection id", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({
id: "collectionId",
organizationId: "organizationId",
}),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return false when filter does not match collection id", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({
id: "nonMatchingCollectionId",
organizationId: "organizationId",
}),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return false when filter does not match organization id", () => {
const filterFunction = createFilterFunction({
selectedOrganizationNode: createOrganizationFilterNode({
id: "nonMatchingOrganizationId",
}),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return false when filtering for my vault only", () => {
const filterFunction = createFilterFunction({
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return false when filtering by All Collections", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({ id: "AllCollections" }),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
});
describe("given an unassigned organizational cipher (with organization, without collection)", () => {
const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] });
it("should return true when filtering for unassigned collection", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({ id: null }),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
it("should return true when filter matches organization id", () => {
const filterFunction = createFilterFunction({
selectedOrganizationNode: createOrganizationFilterNode({ id: "organizationId" }),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
describe("given an individual cipher (without organization or collection)", () => {
const cipher = createCipher({ organizationId: null, collectionIds: [] });
it("should return false when filtering for unassigned collection", () => {
const filterFunction = createFilterFunction({
selectedCollectionNode: createCollectionFilterNode({ id: null }),
});
const result = filterFunction(cipher);
expect(result).toBe(false);
});
it("should return true when filtering for my vault only", () => {
const cipher = createCipher({ organizationId: null });
const filterFunction = createFilterFunction({
selectedOrganizationNode: createOrganizationFilterNode({ id: "MyVault" }),
});
const result = filterFunction(cipher);
expect(result).toBe(true);
});
});
});
});
function createFilterFunction(options: Partial<VaultFilter> = {}) {
return new VaultFilter(options).buildFilter();
}
function createOrganizationFilterNode(
options: Partial<OrganizationFilter>
): TreeNode<OrganizationFilter> {
const org = new Organization() as OrganizationFilter;
org.id = options.id;
org.icon = options.icon ?? "";
return new TreeNode<OrganizationFilter>(org, null);
}
function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<FolderFilter> {
const folder = new FolderView() as FolderFilter;
folder.id = options.id;
folder.name = options.name;
folder.icon = options.icon ?? "";
folder.revisionDate = options.revisionDate ?? new Date();
return new TreeNode<FolderFilter>(folder, null);
}
function createCollectionFilterNode(
options: Partial<CollectionFilter>
): TreeNode<CollectionFilter> {
const collection = new CollectionView() as CollectionFilter;
collection.id = options.id;
collection.name = options.name ?? "";
collection.icon = options.icon ?? "";
collection.organizationId = options.organizationId;
collection.externalId = options.externalId ?? "";
collection.readOnly = options.readOnly ?? false;
collection.hidePasswords = options.hidePasswords ?? false;
return new TreeNode<CollectionFilter>(collection, null);
}
function createCipher(options: Partial<CipherView> = {}) {
const cipher = new CipherView();
cipher.favorite = options.favorite ?? false;
cipher.deletedDate = options.deletedDate;
cipher.type = options.type;
cipher.folderId = options.folderId;
cipher.collectionIds = options.collectionIds;
cipher.organizationId = options.organizationId;
return cipher;
}

View File

@@ -1,127 +0,0 @@
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import {
CipherStatus,
CipherTypeFilter,
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "./vault-filter.type";
export type VaultFilterFunction = (cipher: CipherView) => boolean;
// TODO: Replace shared VaultFilter Model with this one and
// refactor browser and desktop code to use this model.
export class VaultFilter {
selectedOrganizationNode: TreeNode<OrganizationFilter>;
selectedCipherTypeNode: TreeNode<CipherTypeFilter>;
selectedFolderNode: TreeNode<FolderFilter>;
selectedCollectionNode: TreeNode<CollectionFilter>;
get isFavorites(): boolean {
return this.selectedCipherTypeNode?.node.type === "favorites";
}
get isDeleted(): boolean {
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
}
get organizationId(): string {
return this.selectedOrganizationNode?.node.id;
}
get cipherType(): CipherType {
return this.selectedCipherTypeNode?.node.type in CipherType
? (this.selectedCipherTypeNode?.node.type as CipherType)
: null;
}
get cipherStatus(): CipherStatus {
return this.selectedCipherTypeNode?.node.type;
}
get cipherTypeId(): string {
return this.selectedCipherTypeNode?.node.id;
}
get folderId(): string {
return this.selectedFolderNode?.node.id;
}
get collectionId(): string {
return this.selectedCollectionNode?.node.id;
}
constructor(init?: Partial<VaultFilter>) {
Object.assign(this, init);
}
resetFilter() {
this.selectedCipherTypeNode = null;
this.selectedFolderNode = null;
this.selectedCollectionNode = null;
}
resetOrganization() {
this.selectedOrganizationNode = null;
}
buildFilter(): VaultFilterFunction {
return (cipher) => {
let cipherPassesFilter = true;
if (this.isFavorites && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.isDeleted && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.cipherType && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.cipherType;
}
if (this.selectedFolderNode) {
// No folder
if (this.folderId === null && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId === null;
}
// Folder
if (this.folderId !== null && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId === this.folderId;
}
}
if (this.selectedCollectionNode) {
// All Collections
if (this.collectionId === "AllCollections" && cipherPassesFilter) {
cipherPassesFilter = false;
}
// Unassigned
if (this.collectionId === null && cipherPassesFilter) {
cipherPassesFilter =
cipher.organizationId != null &&
(cipher.collectionIds == null || cipher.collectionIds.length === 0);
}
// Collection
if (
this.collectionId !== null &&
this.collectionId !== "AllCollections" &&
cipherPassesFilter
) {
cipherPassesFilter =
cipher.collectionIds != null && cipher.collectionIds.includes(this.collectionId);
}
}
if (this.selectedOrganizationNode) {
// My Vault
if (this.organizationId === "MyVault" && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
// Organization
else if (this.organizationId !== null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.organizationId;
}
}
return cipherPassesFilter;
};
}
}

View File

@@ -1,15 +0,0 @@
import { CipherType } from "@bitwarden/common/src/enums/cipherType";
import { Organization } from "@bitwarden/common/src/models/domain/organization";
import { ITreeNodeObject } from "@bitwarden/common/src/models/domain/tree-node";
import { FolderView } from "@bitwarden/common/src/models/view/folder.view";
import { CollectionAdminView } from "../../../../organizations/core";
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
export type CollectionFilter = CollectionAdminView & {
icon: string;
};
export type FolderFilter = FolderView & { icon: string };
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };

View File

@@ -1,12 +0,0 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../../shared";
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
@NgModule({
imports: [SharedModule],
declarations: [VaultFilterSectionComponent],
exports: [SharedModule, VaultFilterSectionComponent],
})
export class VaultFilterSharedModule {}

View File

@@ -1,21 +0,0 @@
import { NgModule } from "@angular/core";
import { LinkSsoComponent } from "./components/link-sso.component";
import { OrganizationOptionsComponent } from "./components/organization-options.component";
import { VaultFilterComponent } from "./components/vault-filter.component";
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
import { VaultFilterService } from "./services/vault-filter.service";
import { VaultFilterSharedModule } from "./shared/vault-filter-shared.module";
@NgModule({
imports: [VaultFilterSharedModule],
declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
exports: [VaultFilterComponent],
providers: [
{
provide: VaultFilterServiceAbstraction,
useClass: VaultFilterService,
},
],
})
export class VaultFilterModule {}

View File

@@ -1,341 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<ng-container>
<bit-table
*ngIf="filteredCiphers.length || filteredCollections.length"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th bitCell class="tw-min-w-fit" colspan="2">
<input
class="tw-mr-2"
type="checkbox"
bitCheckbox
id="checkAll"
(change)="checkAll($any($event.target).checked)"
[(ngModel)]="isAllChecked"
/>
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="checkAll">{{
"all" | i18n
}}</label>
</th>
<th bitCell class="tw-w-1/2">{{ "name" | i18n }}</th>
<th bitCell class="tw-w-max">
<ng-container *ngIf="!organization">{{ "owner" | i18n }}</ng-container>
<ng-container *ngIf="organization">
{{ (activeFilter.selectedCollectionNode ? "groups" : "collections") | i18n }}
</ng-container>
</th>
<th bitCell class="tw-min-w-fit tw-text-right">
<button
[bitMenuTriggerFor]="headerMenu"
bitIconButton="bwi-ellipsis-v"
size="small"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<ng-container>
<button
class="dropdown-item"
appStopClick
(click)="bulkMove()"
*ngIf="!activeFilter.isDeleted && !organization"
>
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "moveSelected" | i18n }}
</button>
<button
class="dropdown-item"
appStopClick
(click)="bulkShare()"
*ngIf="!activeFilter.isDeleted && !organization"
>
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
{{ "moveSelectedToOrg" | i18n }}
</button>
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="activeFilter.isDeleted">
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restoreSelected" | i18n }}
</button>
<button class="dropdown-item text-danger" (click)="bulkDelete()">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{
(activeFilter.isDeleted ? "permanentlyDeleteSelected" : "deleteSelected") | i18n
}}
</button>
</ng-container>
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body>
<tr
bitRow
*ngFor="let col of filteredCollections"
(click)="navigateCollection(col)"
class="tw-cursor-pointer"
alignContent="middle"
>
<td bitCell (click)="checkRow(col)" appStopProp>
<input
*ngIf="canDeleteCollection(col.node)"
class="tw-cursor-pointer"
type="checkbox"
bitCheckbox
[(ngModel)]="$any(col).checked"
appStopProp
/>
</td>
<td bitCell>
<div class="icon" aria-hidden="true">
<i class="bwi bwi-fw bwi-lg bwi-collection"></i>
</div>
</td>
<td bitCell>
<button
bitLink
class="tw-text-start"
linkType="secondary"
(click)="navigateCollection(col)"
>
{{ col.node.name }}
</button>
</td>
<td bitCell>
<ng-container *ngIf="!organization">
<app-org-badge
organizationName="{{ col.node.organizationId | orgNameFromId: organizations }}"
[profileName]="profileName"
(onOrganizationClicked)="onOrganizationClicked(col.node.organizationId)"
appStopProp
>
</app-org-badge>
</ng-container>
<ng-container *ngIf="organization && activeFilter.selectedCollectionNode">
<app-group-badge
*ngIf="col.node.groups"
[selectedGroups]="col.node.groups"
[allGroups]="groups"
></app-group-badge>
</ng-container>
</td>
<td bitCell class="tw-text-right">
<button
*ngIf="canEditCollection(col.node) || canDeleteCollection(col.node)"
[bitMenuTriggerFor]="collectionOptions"
size="small"
bitIconButton="bwi-ellipsis-v"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
appStopProp
></button>
<bit-menu #collectionOptions>
<button
*ngIf="canEditCollection(col.node)"
bitMenuItem
(click)="editCollection(col.node, 'info')"
>
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
{{ "editInfo" | i18n }}
</button>
<button
*ngIf="canEditCollection(col.node)"
bitMenuItem
(click)="editCollection(col.node, 'access')"
>
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
</button>
<button
*ngIf="canDeleteCollection(col.node)"
bitMenuItem
(click)="deleteCollection(col.node)"
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
<tr
bitRow
*ngFor="let c of filteredCiphers"
class="tw-cursor-pointer"
(click)="selectCipher(c)"
alignContent="middle"
>
<td bitCell (click)="checkRow(c)" appStopProp>
<input type="checkbox" bitCheckbox [(ngModel)]="$any(c).checked" appStopProp />
</td>
<td bitCell>
<app-vault-icon [cipher]="c"></app-vault-icon>
</td>
<td bitCell class="tw-break-all">
<button
bitLink
class="tw-text-start"
[routerLink]="[]"
[queryParams]="{ itemId: c.id }"
queryParamsHandling="merge"
title="{{ 'editItem' | i18n }}"
>
{{ c.name }}
</button>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
<ng-container *ngIf="showFixOldAttachments(c)">
<i
class="bwi bwi-exclamation-triangle text-warning"
appStopProp
title="{{ 'attachmentsNeedFix' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachmentsNeedFix" | i18n }}</span>
</ng-container>
</ng-container>
<br />
<span class="tw-text-sm tw-text-muted" appStopProp>{{ c.subTitle }}</span>
</td>
<td bitCell>
<ng-container *ngIf="!organization">
<app-org-badge
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
profileName="{{ profileName }}"
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
appStopProp
>
</app-org-badge>
</ng-container>
<ng-container *ngIf="organization && !activeFilter.selectedCollectionNode">
<app-collection-badge
*ngIf="c.collectionIds"
[collectionIds]="c.collectionIds"
[collections]="vaultFilterService.filteredCollections$ | async"
></app-collection-badge>
</ng-container>
</td>
<td bitCell class="tw-text-right">
<button
[bitMenuTriggerFor]="cipherOptions"
size="small"
bitIconButton="bwi-ellipsis-v"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
appStopProp
></button>
<bit-menu #cipherOptions>
<ng-container *ngIf="c.type === cipherType.Login && !c.isDeleted">
<button bitMenuItem (click)="copy(c, c.login.username, 'username', 'Username')">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyUsername" | i18n }}
</button>
<button
bitMenuItem
(click)="copy(c, c.login.password, 'password', 'Password')"
*ngIf="c.viewPassword"
>
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyPassword" | i18n }}
</button>
<button
bitMenuItem
(click)="copy(c, c.login.totp, 'verificationCodeTotp', 'TOTP')"
*ngIf="displayTotpCopyButton(c)"
>
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
{{ "copyVerificationCode" | i18n }}
</button>
<button bitMenuItem *ngIf="c.login.canLaunch" (click)="launch(c.login.launchUri)">
<i class="bwi bwi-fw bwi-share-square" aria-hidden="true"></i>
{{ "launch" | i18n }}
</button>
</ng-container>
<button bitMenuItem (click)="attachments(c)">
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</button>
<button
bitMenuItem
*ngIf="((!organization && !c.organizationId) || organization) && !c.isDeleted"
(click)="clone(c)"
>
<i class="bwi bwi-fw bwi-files" aria-hidden="true"></i>
{{ "clone" | i18n }}
</button>
<button
bitMenuItem
*ngIf="!organization && !c.organizationId && !c.isDeleted"
(click)="share(c)"
>
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
{{ "moveToOrganization" | i18n }}
</button>
<button
bitMenuItem
*ngIf="c.organizationId && !c.isDeleted"
(click)="editCipherCollections(c)"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collections" | i18n }}
</button>
<button bitMenuItem *ngIf="c.organizationId && accessEvents" (click)="events(c)">
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</button>
<button bitMenuItem (click)="restore(c)" *ngIf="c.isDeleted">
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restore" | i18n }}
</button>
<button bitMenuItem (click)="deleteCipher(c)">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ (c.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="
showMissingCollectionPermissionMessage ||
(!filteredCiphers.length && !filteredCollections.length)
"
>
<ng-container *ngIf="!loaded">
<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>
<ng-container *ngIf="loaded">
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<ng-container *ngIf="showMissingCollectionPermissionMessage">
<p>{{ "noPermissionToViewAllCollectionItems" | i18n }}</p>
</ng-container>
<ng-container *ngIf="!showMissingCollectionPermissionMessage">
<p>{{ "noItemsInList" | i18n }}</p>
<button (click)="addCipher()" class="btn btn-outline-primary" *ngIf="showAddNew">
<i class="bwi bwi-plus bwi-fw"></i>{{ "addItem" | i18n }}
</button>
</ng-container>
</ng-container>
</div>
</ng-container>

View File

@@ -1,569 +0,0 @@
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/components/vault-items.component";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { CipherService } from "@bitwarden/common/abstractions/cipher.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 { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.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 { TokenService } from "@bitwarden/common/abstractions/token.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
import { DialogService, Icons } from "@bitwarden/components";
import { CollectionAdminView, GroupView } from "../organizations/core";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
} from "./bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
import {
BulkMoveDialogResult,
openBulkMoveDialog,
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
import {
BulkRestoreDialogResult,
openBulkRestoreDialog,
} from "./bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component";
import {
BulkShareDialogResult,
openBulkShareDialog,
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
import { CollectionFilter } from "./vault-filter/shared/models/vault-filter.type";
const MaxCheckedCount = 500;
export type VaultItemRow = (CipherView | TreeNode<CollectionFilter>) & { checked?: boolean };
@Component({
selector: "app-vault-items",
templateUrl: "vault-items.component.html",
})
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy {
@Input() showAddNew = true;
@Input() activeFilter: VaultFilter;
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
@Output() onAttachmentsClicked = new EventEmitter<CipherView>();
@Output() onShareClicked = new EventEmitter<CipherView>();
@Output() onEditCipherCollectionsClicked = new EventEmitter<CipherView>();
@Output() onCloneClicked = new EventEmitter<CipherView>();
@Output() onOrganzationBadgeClicked = new EventEmitter<string>();
cipherType = CipherType;
actionPromise: Promise<any>;
userHasPremiumAccess = false;
organizations: Organization[] = [];
profileName: string;
noItemIcon = Icons.Search;
groups: GroupView[] = [];
protected pageSizeLimit = 200;
protected isAllChecked = false;
protected didScroll = false;
protected currentPagedCiphersCount = 0;
protected currentPagedCollectionsCount = 0;
protected refreshing = false;
protected pagedCiphers: CipherView[] = [];
protected pagedCollections: TreeNode<CollectionFilter>[] = [];
protected searchedCollections: TreeNode<CollectionFilter>[] = [];
get collections(): TreeNode<CollectionFilter>[] {
return this.activeFilter?.selectedCollectionNode?.children ?? [];
}
get filteredCollections(): TreeNode<CollectionFilter>[] {
if (this.isPaging()) {
return this.pagedCollections;
}
if (this.searchService.isSearchable(this.searchText)) {
return this.searchedCollections;
}
return this.collections;
}
get filteredCiphers(): CipherView[] {
return this.isPaging() ? this.pagedCiphers : this.ciphers;
}
constructor(
searchService: SearchService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected vaultFilterService: VaultFilterService,
protected cipherService: CipherService,
protected eventCollectionService: EventCollectionService,
protected totpService: TotpService,
protected stateService: StateService,
protected passwordRepromptService: PasswordRepromptService,
protected dialogService: DialogService,
protected logService: LogService,
private searchPipe: SearchPipe,
private organizationService: OrganizationService,
private tokenService: TokenService
) {
super(searchService);
}
ngOnDestroy() {
this.checkAll(false);
}
async applyFilter(filter: (cipher: CipherView) => boolean = null) {
this.checkAll(false);
this.isAllChecked = false;
this.pagedCollections = [];
if (!this.refreshing && this.isPaging()) {
this.currentPagedCollectionsCount = 0;
this.currentPagedCiphersCount = 0;
}
await super.applyFilter(filter);
}
// load() is called after the page loads and the first sync has completed.
// Do not use ngOnInit() for anything that requires sync data.
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
await super.load(filter, deleted);
this.updateSearchedCollections(this.collections);
this.profileName = await this.tokenService.getName();
this.organizations = await this.organizationService.getAll();
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
}
async refresh() {
try {
this.refreshing = true;
await this.reload(this.filter, this.deleted);
} finally {
this.refreshing = false;
}
}
loadMore() {
// If we have less rows than the page size, we don't need to page anything
if (this.ciphers.length + (this.collections?.length || 0) <= this.pageSizeLimit) {
return;
}
let pageSpaceLeft = this.pageSizeLimit;
if (
this.refreshing &&
this.pagedCiphers.length + this.pagedCollections.length === 0 &&
this.currentPagedCiphersCount + this.currentPagedCollectionsCount > this.pageSizeLimit
) {
// When we refresh, we want to load the previous amount of items, not restart the paging
pageSpaceLeft = this.currentPagedCiphersCount + this.currentPagedCollectionsCount;
}
// if there are still collections to show
if (this.collections?.length > this.pagedCollections.length) {
const collectionsToAdd = this.collections.slice(
this.pagedCollections.length,
this.currentPagedCollectionsCount + pageSpaceLeft
);
this.pagedCollections = this.pagedCollections.concat(collectionsToAdd);
// set the current count to the new count of paged collections
this.currentPagedCollectionsCount = this.pagedCollections.length;
// subtract the available page size by the amount of collections we just added, default to 0 if negative
pageSpaceLeft =
collectionsToAdd.length > pageSpaceLeft ? 0 : pageSpaceLeft - collectionsToAdd.length;
}
// if we have room left to show ciphers and we have ciphers to show
if (pageSpaceLeft > 0 && this.ciphers.length > this.pagedCiphers.length) {
this.pagedCiphers = this.pagedCiphers.concat(
this.ciphers.slice(this.pagedCiphers.length, this.currentPagedCiphersCount + pageSpaceLeft)
);
// set the current count to the new count of paged ciphers
this.currentPagedCiphersCount = this.pagedCiphers.length;
}
// set a flag if we actually loaded the second page while paging
this.didScroll = this.pagedCiphers.length + this.pagedCollections.length > this.pageSizeLimit;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
const totalRows =
this.ciphers.length + (this.activeFilter?.selectedCollectionNode?.children.length || 0);
return !searching && totalRows > this.pageSizeLimit;
}
async resetPaging() {
this.pagedCollections = [];
this.pagedCiphers = [];
this.loadMore();
}
async doSearch(indexedCiphers?: CipherView[]) {
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
[this.filter, this.deletedFilter],
indexedCiphers
);
this.updateSearchedCollections(this.collections);
this.resetPaging();
}
launch(uri: string) {
this.platformUtilsService.launchUri(uri);
}
async attachments(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onAttachmentsClicked.emit(c);
}
async share(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onShareClicked.emit(c);
}
editCipherCollections(c: CipherView) {
this.onEditCipherCollectionsClicked.emit(c);
}
async clone(c: CipherView) {
if (!(await this.repromptCipher(c))) {
return;
}
this.onCloneClicked.emit(c);
}
async deleteCipher(c: CipherView): Promise<boolean> {
if (!(await this.repromptCipher(c))) {
return;
}
if (this.actionPromise != null) {
return;
}
const permanent = c.isDeleted;
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t(
permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation"
),
this.i18nService.t(permanent ? "permanentlyDeleteItem" : "deleteItem"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.deleteCipherWithServer(c.id, permanent);
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem")
);
this.refresh();
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
}
async bulkDelete() {
if (!(await this.repromptCipher())) {
return;
}
const selectedIds = this.selectedCipherIds;
if (selectedIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkDeleteDialog(this.dialogService, {
data: { permanent: this.deleted, cipherIds: selectedIds },
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkDeleteDialogResult.Deleted) {
this.actionPromise = this.refresh();
await this.actionPromise;
this.actionPromise = null;
}
}
async restore(c: CipherView): Promise<boolean> {
if (this.actionPromise != null || !c.isDeleted) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("restoreItemConfirmation"),
this.i18nService.t("restoreItem"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.actionPromise = this.cipherService.restoreWithServer(c.id);
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh();
} catch (e) {
this.logService.error(e);
}
this.actionPromise = null;
}
async bulkRestore() {
if (!(await this.repromptCipher())) {
return;
}
const selectedCipherIds = this.selectedCipherIds;
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkRestoreDialog(this.dialogService, {
data: { cipherIds: selectedCipherIds },
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkRestoreDialogResult.Restored) {
this.actionPromise = this.refresh();
await this.actionPromise;
this.actionPromise = null;
}
}
async bulkShare() {
if (!(await this.repromptCipher())) {
return;
}
const selectedCiphers = this.selectedCiphers;
if (selectedCiphers.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers: selectedCiphers } });
const result = await lastValueFrom(dialog.closed);
if (result === BulkShareDialogResult.Shared) {
this.actionPromise = this.refresh();
await this.actionPromise;
this.actionPromise = null;
}
}
async bulkMove() {
if (!(await this.repromptCipher())) {
return;
}
const selectedCipherIds = this.selectedCipherIds;
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkMoveDialog(this.dialogService, {
data: { cipherIds: selectedCipherIds },
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkMoveDialogResult.Moved) {
this.actionPromise = this.refresh();
await this.actionPromise;
this.actionPromise = null;
}
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.repromptCipher(cipher))
) {
return;
}
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
return;
} else if (value === cipher.login.totp) {
value = await this.totpService.getCode(value);
}
if (!cipher.viewPassword) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey))
);
if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") {
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
cipher.id
);
} else if (typeI18nKey === "securityCode") {
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
}
navigateCollection(node: TreeNode<CollectionFilter>) {
const filter = this.activeFilter;
filter.selectedCollectionNode = node;
this.activeFilterChanged.emit(filter);
}
checkAll(select: boolean) {
if (select) {
this.checkAll(false);
}
const items: VaultItemRow[] = this.ciphers;
if (!items) {
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) {
// Collections can't be managed in end user vault
if (!(item instanceof CipherView)) {
return;
}
item.checked = select ?? !item.checked;
}
get selectedCiphers(): CipherView[] {
if (!this.ciphers) {
return [];
}
return this.ciphers.filter((c) => !!(c as VaultItemRow).checked);
}
get selectedCipherIds(): string[] {
return this.selectedCiphers.map((c) => c.id);
}
displayTotpCopyButton(cipher: CipherView) {
return (
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
);
}
onOrganizationClicked(organizationId: string) {
this.onOrganzationBadgeClicked.emit(organizationId);
}
events(c: CipherView) {
// TODO: This should be removed but is needed since we reuse the same template
}
canDeleteCollection(c: CollectionAdminView): boolean {
// TODO: This should be removed but is needed since we reuse the same template
return false; // Always return false for non org vault
}
async deleteCollection(collection: CollectionView): Promise<void> {
// TODO: This should be removed but is needed since we reuse the same template
}
canEditCollection(c: CollectionAdminView): boolean {
// TODO: This should be removed but is needed since we reuse the same template
return false; // Always return false for non org vault
}
async editCollection(c: CollectionView, tab: "info" | "access"): Promise<void> {
// TODO: This should be removed but is needed since we reuse the same template
}
get showMissingCollectionPermissionMessage(): boolean {
// TODO: This should be removed but is needed since we reuse the same template
return false; // Always return false for non org vault
}
protected updateSearchedCollections(collections: TreeNode<CollectionFilter>[]) {
if (this.searchService.isSearchable(this.searchText)) {
this.searchedCollections = this.searchPipe.transform(
collections,
this.searchText,
(collection) => collection.node.name,
(collection) => collection.node.id
);
}
}
protected deleteCipherWithServer(id: string, permanent: boolean) {
return permanent
? this.cipherService.deleteWithServer(id)
: this.cipherService.softDeleteWithServer(id);
}
protected showFixOldAttachments(c: CipherView) {
return c.hasOldAttachments && c.organizationId == null;
}
protected async repromptCipher(c?: CipherView) {
if (c) {
return (
c.reprompt === CipherRepromptType.None ||
(await this.passwordRepromptService.showPasswordPrompt())
);
} else {
const selectedCiphers = this.selectedCiphers;
const notProtected = !selectedCiphers.find(
(cipher) => cipher.reprompt !== CipherRepromptType.None
);
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
}
}
}

View File

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

View File

@@ -1,134 +0,0 @@
<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"
(activeFilterChanged)="applyVaultFilter($event)"
(onSearchTextChanged)="filterSearchText($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event)"
></app-vault-filter>
</div>
</div>
</div>
</div>
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
<bit-breadcrumbs *ngIf="breadcrumbs.length > 0">
<bit-breadcrumb
*ngFor="let collection of breadcrumbs; let first = first"
[icon]="first ? undefined : 'bwi-collection'"
(click)="applyCollectionFilter(collection)"
>
<!-- First node in the tree contains a translation key. The rest come from user input. -->
<ng-container *ngIf="first">{{ collection.node.name | i18n }}</ng-container>
<ng-container *ngIf="!first">{{ collection.node.name }}</ng-container>
</bit-breadcrumb>
</bit-breadcrumbs>
<div class="tw-mb-4 tw-flex">
<h1>
{{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
<ng-container *ngIf="$any(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">
<button
type="button"
bitButton
buttonType="primary"
(click)="addCipher()"
*ngIf="!activeFilter.isDeleted"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
</button>
</div>
</div>
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
{{ trashCleanupWarning }}
</app-callout>
<app-vault-items
[activeFilter]="activeFilter"
(activeFilterChanged)="applyVaultFilter($event)"
(onCipherClicked)="navigateToCipher($event)"
(onAttachmentsClicked)="editCipherAttachments($event)"
(onAddCipher)="addCipher()"
(onShareClicked)="shareCipher($event)"
(onEditCipherCollectionsClicked)="editCipherCollections($event)"
(onCloneClicked)="cloneCipher($event)"
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
>
</app-vault-items>
</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-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
<app-verify-email
*ngIf="showVerifyEmail"
class="d-block mb-4"
(onVerified)="emailVerified($event)"
></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

@@ -1,443 +0,0 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { 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 { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.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 { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { KdfType, DEFAULT_PBKDF2_ITERATIONS } from "@bitwarden/common/enums/kdfType";
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { UpdateKeyComponent } from "../settings/update-key.component";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
import { CollectionsComponent } from "./collections.component";
import { FolderAddEditComponent } from "./folder-add-edit.component";
import { ShareComponent } from "./share.component";
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
import {
CollectionFilter,
FolderFilter,
OrganizationFilter,
} from "./vault-filter/shared/models/vault-filter.type";
import { VaultItemsComponent } from "./vault-items.component";
const BroadcasterSubscriptionId = "VaultComponent";
@Component({
selector: "app-vault",
templateUrl: "vault.component.html",
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent;
@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;
showVerifyEmail = false;
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
showLowKdf = false;
trashCleanupWarning: string = null;
kdfIterations: number;
activeFilter: VaultFilter = new VaultFilter();
private destroy$ = new Subject<void>();
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 vaultFilterService: VaultFilterService,
private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService
) {}
async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
this.showLowKdf = await this.isLowKdfIteration();
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
: "trashCleanupWarning"
);
this.route.queryParams
.pipe(
first(),
switchMap(async (params: Params) => {
await this.syncService.fullSync(false);
await this.vaultFilterService.reloadCollections();
await this.vaultItemsComponent.reload();
const canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumCallout =
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
this.showUpdateKey = !(await this.cryptoService.hasEncKey());
const cipherId = getCipherIdFromParams(params);
if (!cipherId) {
return;
}
const cipherView = new CipherView();
cipherView.id = cipherId;
if (params.action === "clone") {
await this.cloneCipher(cipherView);
} else if (params.action === "edit") {
await this.editCipher(cipherView);
}
}),
switchMap(() => this.route.queryParams),
switchMap(async (params) => {
const cipherId = getCipherIdFromParams(params);
if (cipherId) {
if ((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: { itemId: null, cipherId: null },
queryParamsHandling: "merge",
});
}
}
}),
takeUntil(this.destroy$)
)
.subscribe();
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.load(this.vaultItemsComponent.filter),
]);
this.changeDetectorRef.detectChanges();
}
break;
}
});
});
}
get isShowingCards() {
return (
this.showBrowserOutdated ||
this.showPremiumCallout ||
this.showUpdateKey ||
this.showVerifyEmail ||
this.showLowKdf
);
}
emailVerified(verified: boolean) {
this.showVerifyEmail = !verified;
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.complete();
}
async applyVaultFilter(filter: VaultFilter) {
this.activeFilter = filter;
this.vaultItemsComponent.showAddNew = !this.activeFilter.isDeleted;
await this.vaultItemsComponent.reload(
this.activeFilter.buildFilter(),
this.activeFilter.isDeleted
);
this.go();
}
async applyOrganizationFilter(orgId: string) {
if (orgId == null) {
orgId = "MyVault";
}
const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$);
const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode<OrganizationFilter>;
this.filterComponent.filters?.organizationFilter?.action(orgNode);
}
addFolder = async (): Promise<void> => {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = null;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onSavedFolder.subscribe(async () => {
modal.close();
});
}
);
};
editFolder = async (folder: FolderFilter): Promise<void> => {
const [modal] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => {
comp.folderId = folder.id;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onSavedFolder.subscribe(async () => {
modal.close();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onDeletedFolder.subscribe(async () => {
modal.close();
});
}
);
};
filterSearchText(searchText: string) {
this.vaultItemsComponent.searchText = searchText;
this.vaultItemsComponent.search(200);
}
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 = 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;
// 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
comp.onReuploadedAttachment.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 shareCipher(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
ShareComponent,
this.shareModalRef,
(comp) => {
comp.cipherId = cipher.id;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onSharedCipher.subscribe(async () => {
modal.close();
await this.vaultItemsComponent.refresh();
});
}
);
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
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 component = await this.editCipher(null);
component.type = this.activeFilter.cipherType;
if (this.activeFilter.organizationId !== "MyVault") {
component.organizationId = this.activeFilter.organizationId;
component.collections = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).filter((c) => !c.readOnly && c.id != null);
}
const selectedColId = this.activeFilter.collectionId;
if (selectedColId !== "AllCollections") {
component.collectionIds = [selectedColId];
}
component.folderId = this.activeFilter.folderId;
}
async navigateToCipher(cipher: CipherView) {
this.go({ itemId: cipher?.id });
}
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, itemId: null });
return;
}
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = id;
// 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();
});
}
);
modal.onClosedPromise().then(() => {
this.go({ cipherId: null, itemId: 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);
}
async isLowKdfIteration() {
const kdfType = await this.stateService.getKdfType();
const kdfOptions = await this.stateService.getKdfConfig();
return kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < DEFAULT_PBKDF2_ITERATIONS;
}
get breadcrumbs(): TreeNode<CollectionFilter>[] {
if (!this.activeFilter.selectedCollectionNode) {
return [];
}
const collections = [this.activeFilter.selectedCollectionNode];
while (collections[collections.length - 1].parent != undefined) {
collections.push(collections[collections.length - 1].parent);
}
return collections.map((c) => c).reverse();
}
protected applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCollectionNode = collection;
this.applyVaultFilter(filter);
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
favorites: this.activeFilter.isFavorites || null,
type: this.activeFilter.cipherType,
folderId: this.activeFilter.folderId,
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"];
};

View File

@@ -1,33 +0,0 @@
import { NgModule } from "@angular/core";
import { BreadcrumbsModule } from "@bitwarden/components";
import { CollectionBadgeModule } from "../organizations/vault/collection-badge/collection-badge.module";
import { GroupBadgeModule } from "../organizations/vault/group-badge/group-badge.module";
import { SharedModule, LooseComponentsModule } from "../shared";
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
import { PipesModule } from "./pipes/pipes.module";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultItemsComponent } from "./vault-items.component";
import { VaultRoutingModule } from "./vault-routing.module";
import { VaultComponent } from "./vault.component";
@NgModule({
imports: [
VaultFilterModule,
VaultRoutingModule,
OrganizationBadgeModule,
GroupBadgeModule,
CollectionBadgeModule,
PipesModule,
SharedModule,
LooseComponentsModule,
BulkDialogsModule,
BreadcrumbsModule,
],
declarations: [VaultComponent, VaultItemsComponent],
exports: [VaultComponent],
})
export class VaultModule {}