mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-19562] remove flag pm-18520-desktop-cipher-forms (#15524)
* removing the desktop flag and replacing the route in desktop app routing. removing code from old desktop
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import {
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
@@ -50,7 +49,6 @@ import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
@@ -102,15 +100,11 @@ const routes: Routes = [
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: VaultComponent,
|
||||
flaggedComponent: VaultV2Component,
|
||||
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
|
||||
routeOptions: {
|
||||
path: "vault",
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "vault",
|
||||
component: VaultV2Component,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
{
|
||||
path: "send",
|
||||
|
||||
@@ -18,19 +18,8 @@ import { UpdateTempPasswordComponent } from "../auth/update-temp-password.compon
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/app/vault/collections.component";
|
||||
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
|
||||
import { PasswordHistoryComponent } from "../vault/app/vault/password-history.component";
|
||||
import { ShareComponent } from "../vault/app/vault/share.component";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultItemsComponent } from "../vault/app/vault/vault-items.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component";
|
||||
import { ViewComponent } from "../vault/app/vault/view.component";
|
||||
|
||||
import { SettingsComponent } from "./accounts/settings.component";
|
||||
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
|
||||
@@ -61,28 +50,17 @@ import { SharedModule } from "./shared/shared.module";
|
||||
],
|
||||
declarations: [
|
||||
AccountSwitcherComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
FolderAddEditComponent,
|
||||
HeaderComponent,
|
||||
PasswordHistoryComponent,
|
||||
PremiumComponent,
|
||||
RemovePasswordComponent,
|
||||
SearchComponent,
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultComponent,
|
||||
VaultItemsComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
ViewCustomFieldsComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -441,10 +441,6 @@ img,
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
app-vault-view .box-footer {
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* override for vault icon in desktop */
|
||||
app-vault-icon > div {
|
||||
display: flex;
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi box-draggable-row"
|
||||
cdkDrag
|
||||
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
|
||||
[ngClass]="{ 'box-content-row-checkbox': f.type === fieldType.Boolean }"
|
||||
attr.aria-label="{{ f.name }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removeField(f)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<label for="fieldName{{ i }}" class="sr-only">{{ "name" | i18n }}</label>
|
||||
<label for="fieldValue{{ i }}" class="sr-only">{{ "value" | i18n }}</label>
|
||||
<div class="row-main">
|
||||
<input
|
||||
id="fieldName{{ i }}"
|
||||
type="text"
|
||||
name="Field.Name{{ i }}"
|
||||
[(ngModel)]="f.name"
|
||||
class="row-label"
|
||||
placeholder="{{ 'name' | i18n }}"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Text -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="text"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Text"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
appInputVerbatim
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Password -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="{{ f.showValue ? 'text' : 'password' }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
class="monospaced"
|
||||
*ngIf="f.type === fieldType.Hidden"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
[disabled]="!cipher.viewPassword && !f.newField"
|
||||
appInputVerbatim
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Linked -->
|
||||
<select
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.linkedId"
|
||||
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
>
|
||||
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Boolean -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Boolean"
|
||||
appTrueFalseValue
|
||||
trueValue="true"
|
||||
falseValue="false"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div
|
||||
class="action-buttons"
|
||||
*ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="f.showValue"
|
||||
(click)="toggleFieldValue(f)"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="drag-handle"
|
||||
appA11yTitle="{{ 'dragToSort' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
cdkDragHandle
|
||||
>
|
||||
<i class="bwi bwi-drag-and-drop" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add new custom field -->
|
||||
<div class="box-content-row" *ngIf="!(!cipher.edit && editMode)" appBoxRow>
|
||||
<button type="button" appStopClick (click)="addField()">
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
{{ "newCustomField" | i18n }}
|
||||
</button>
|
||||
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
|
||||
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
|
||||
<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>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit-custom-fields",
|
||||
templateUrl: "add-edit-custom-fields.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
|
||||
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
|
||||
super(i18nService, eventCollectionService);
|
||||
}
|
||||
}
|
||||
@@ -1,826 +0,0 @@
|
||||
<form #form="ngForm" (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="content">
|
||||
<div class="inner-content" *ngIf="cipher">
|
||||
<div class="box">
|
||||
<bit-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
|
||||
{{ "personalOwnershipPolicyInEffect" | i18n }}
|
||||
</bit-callout>
|
||||
<h2 class="box-header">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
|
||||
<label for="type">{{ "type" | i18n }}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" (change)="typeChange()">
|
||||
<option *ngFor="let item of menuItems$ | async" [ngValue]="item.type">
|
||||
{{ item.labelKey | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="cipher.name"
|
||||
[appAutofocus]="!editMode"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="loginUsername"
|
||||
type="text"
|
||||
name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generateUsername' | i18n }}"
|
||||
(click)="generateUsername()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginPassword">{{ "password" | i18n }}</label>
|
||||
<input
|
||||
id="loginPassword"
|
||||
class="monospaced"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password"
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons" *ngIf="cipher.viewPassword">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generatePassword' | i18n }}"
|
||||
(click)="generatePassword()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box-content-row box-content-row-multi text-muted"
|
||||
*ngIf="cipher.login.hasFido2Credentials && !cloneMode"
|
||||
appBoxRow
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removePasskey()"
|
||||
appA11yTitle="{{ 'removePasskey' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ fido2CredentialCreationDateValue }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
|
||||
<input
|
||||
id="cardCardholderName"
|
||||
type="text"
|
||||
name="Card.CardCardholderName"
|
||||
[(ngModel)]="cipher.card.cardholderName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardNumber">{{ "number" | i18n }}</label>
|
||||
<input
|
||||
id="cardNumber"
|
||||
class="monospaced"
|
||||
type="{{ showCardNumber ? 'text' : 'password' }}"
|
||||
name="Card.Number"
|
||||
(input)="onCardNumberChange()"
|
||||
[(ngModel)]="cipher.card.number"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
(click)="toggleCardNumber()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardBrand">{{ "brand" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
|
||||
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardBrand>
|
||||
<input
|
||||
id="cardBrand"
|
||||
name="Card.Brand"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.card.brand"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
|
||||
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardExpMonth>
|
||||
<input
|
||||
id="cardExpMonth"
|
||||
type="text"
|
||||
name="Card.ExpMonth"
|
||||
[readonly]="true"
|
||||
[value]="getCardExpMonthDisplay()"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
|
||||
<input
|
||||
id="cardExpYear"
|
||||
type="text"
|
||||
name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear"
|
||||
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardCode">{{ "securityCode" | i18n }}</label>
|
||||
<input
|
||||
id="cardCode"
|
||||
class="monospaced"
|
||||
type="{{ showCardCode ? 'text' : 'password' }}"
|
||||
name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
(click)="toggleCardCode()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.type === cipherType.Identity">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idTitle">{{ "title" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
|
||||
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyIdTitle>
|
||||
<input
|
||||
id="idTitle"
|
||||
name="Identity.Title"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.identity.title"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idFirstName">{{ "firstName" | i18n }}</label>
|
||||
<input
|
||||
id="idFirstName"
|
||||
type="text"
|
||||
name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
|
||||
<input
|
||||
id="idMiddleName"
|
||||
type="text"
|
||||
name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLastName">{{ "lastName" | i18n }}</label>
|
||||
<input
|
||||
id="idLastName"
|
||||
type="text"
|
||||
name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="idUsername"
|
||||
type="text"
|
||||
name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCompany">{{ "company" | i18n }}</label>
|
||||
<input
|
||||
id="idCompany"
|
||||
type="text"
|
||||
name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idSsn">{{ "ssn" | i18n }}</label>
|
||||
<input
|
||||
id="idSsn"
|
||||
type="text"
|
||||
name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idPassportNumber"
|
||||
type="text"
|
||||
name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idLicenseNumber"
|
||||
type="text"
|
||||
name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idEmail">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="idEmail"
|
||||
type="text"
|
||||
name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPhone">{{ "phone" | i18n }}</label>
|
||||
<input
|
||||
id="idPhone"
|
||||
type="text"
|
||||
name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress1">{{ "address1" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress1"
|
||||
type="text"
|
||||
name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress2">{{ "address2" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress2"
|
||||
type="text"
|
||||
name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress3">{{ "address3" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress3"
|
||||
type="text"
|
||||
name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCity">{{ "cityTown" | i18n }}</label>
|
||||
<input
|
||||
id="idCity"
|
||||
type="text"
|
||||
name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idState">{{ "stateProvince" | i18n }}</label>
|
||||
<input
|
||||
id="idState"
|
||||
type="text"
|
||||
name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
|
||||
<input
|
||||
id="idPostalCode"
|
||||
type="text"
|
||||
name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCountry">{{ "country" | i18n }}</label>
|
||||
<input
|
||||
id="idCountry"
|
||||
type="text"
|
||||
name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ssh Key -->
|
||||
<div *ngIf="cipher.type === cipherType.SshKey">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="sshPrivateKey">{{ "sshPrivateKey" | i18n }}</label>
|
||||
<div
|
||||
*ngIf="!showPrivateKey"
|
||||
class="monospaced"
|
||||
style="white-space: pre-line"
|
||||
[innerText]="cipher.sshKey.maskedPrivateKey"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="showPrivateKey"
|
||||
class="monospaced"
|
||||
style="white-space: pre-line"
|
||||
[innerText]="cipher.sshKey.privateKey"
|
||||
></div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copySshPrivateKey' | i18n }}"
|
||||
(click)="copy(this.cipher.sshKey.privateKey, 'sshPrivateKey', 'SshPrivateKey')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePrivateKey()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'importSshKeyFromClipboard' | i18n }}"
|
||||
(click)="importSshKeyFromClipboard()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-import" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="sshPublicKey">{{ "sshPublicKey" | i18n }}</label>
|
||||
<input
|
||||
id="sshPublicKey"
|
||||
type="text"
|
||||
name="SSHKey.SSHPublicKey"
|
||||
[ngModel]="cipher.sshKey.publicKey"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
(click)="copy(cipher.sshKey.publicKey, 'sshPublicKey', 'SSHPublicKey')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="sshKeyFingerprint">{{ "sshFingerprint" | i18n }}</label>
|
||||
<input
|
||||
id="sshKeyFingerprint"
|
||||
type="text"
|
||||
name="SSHKey.SSHKeyFingerprint"
|
||||
[ngModel]="cipher.sshKey.keyFingerprint"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
(click)="copy(cipher.sshKey.keyFingerprint, 'sshFingerprint', 'SSHFingerprint')"
|
||||
appA11yTitle="{{ 'generateSSHKey' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content">
|
||||
<ng-container *ngIf="cipher.login.hasUris">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi"
|
||||
appBoxRow
|
||||
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
|
||||
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removeUri(u)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
|
||||
<input
|
||||
id="loginUri{{ i }}"
|
||||
type="text"
|
||||
name="Login.Uris[{{ i }}].Uri"
|
||||
[(ngModel)]="u.uri"
|
||||
placeholder="{{ 'ex' | i18n }} https://google.com"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<label for="loginUriMatch{{ i }}" class="sr-only">
|
||||
{{ "matchDetection" | i18n }} {{ i + 1 }}
|
||||
</label>
|
||||
<select
|
||||
id="loginUriMatch{{ i }}"
|
||||
name="Login.Uris[{{ i }}].Match"
|
||||
[(ngModel)]="u.match"
|
||||
[hidden]="
|
||||
$any(u).showOptions === false ||
|
||||
($any(u).showOptions == null && u.match == null)
|
||||
"
|
||||
(change)="loginUriMatchChanged(u)"
|
||||
>
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleOptions' | i18n }}"
|
||||
(click)="toggleUriOptions(u)"
|
||||
[attr.aria-expanded]="
|
||||
!(
|
||||
$any(u).showOptions === false ||
|
||||
($any(u).showOptions == null && u.match == null)
|
||||
)
|
||||
"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="addUri()"
|
||||
class="box-content-row"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
{{ "newUri" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="folder">{{ "folder" | i18n }}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
|
||||
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favorite">{{ "favorite" | i18n }}</label>
|
||||
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
|
||||
<label for="passwordPrompt"
|
||||
>{{ "passwordPrompt" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
(click)="openHelpReprompt()"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
id="passwordPrompt"
|
||||
type="checkbox"
|
||||
name="PasswordPrompt"
|
||||
[ngModel]="reprompt"
|
||||
(change)="repromptChanged()"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="attachments()"
|
||||
*ngIf="editMode && !cloneMode"
|
||||
>
|
||||
<div class="row-main">{{ "attachments" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="editCollections()"
|
||||
*ngIf="editMode && !cloneMode && cipher.organizationId"
|
||||
>
|
||||
<div class="row-main">{{ "collections" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
<label for="notes">{{ "notes" | i18n }}</label>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<textarea
|
||||
id="notes"
|
||||
name="Notes"
|
||||
rows="6"
|
||||
[(ngModel)]="cipher.notes"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-vault-add-edit-custom-fields
|
||||
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
|
||||
[cipher]="cipher"
|
||||
[thisCipherType]="cipher.type"
|
||||
[editMode]="editMode"
|
||||
>
|
||||
</app-vault-add-edit-custom-fields>
|
||||
<div class="box" *ngIf="allowOwnershipOptions()">
|
||||
<h2 class="box-header">
|
||||
{{ "ownership" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
|
||||
<select
|
||||
id="organizationId"
|
||||
class="form-control"
|
||||
name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId"
|
||||
(change)="organizationChanged()"
|
||||
>
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button type="submit" class="primary" [disabled]="$any(form).loading">
|
||||
<span [hidden]="$any(form).loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(form).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button type="button" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="right">
|
||||
<button
|
||||
type="button"
|
||||
(click)="share()"
|
||||
*ngIf="
|
||||
editMode &&
|
||||
cipher &&
|
||||
!cipher.organizationId &&
|
||||
!cloneMode &&
|
||||
writeableCollections.length > 0
|
||||
"
|
||||
>
|
||||
{{ "move" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)"
|
||||
[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"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,185 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { NgForm } from "@angular/forms";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "AddEditComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@ViewChild("form")
|
||||
private form: NgForm;
|
||||
menuItems$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the default CIPHER_MENU_ITEMS array
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(typeOption) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) => restrictedType.cipherType === typeOption.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
accountService: AccountService,
|
||||
collectionService: CollectionService,
|
||||
messagingService: MessagingService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
logService: LogService,
|
||||
organizationService: OrganizationService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
toastService: ToastService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
sdkService: SdkService,
|
||||
sshImportPromptService: SshImportPromptService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
accountService,
|
||||
collectionService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
passwordRepromptService,
|
||||
organizationService,
|
||||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
toastService,
|
||||
sdkService,
|
||||
sshImportPromptService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
await this.load();
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
// We use ngOnChanges for everything else instead.
|
||||
}
|
||||
|
||||
async ngOnChanges() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (
|
||||
document.querySelectorAll("app-vault-add-edit .ng-dirty").length === 0 ||
|
||||
(this.cipher != null && this.cipherId !== this.cipher.id)
|
||||
) {
|
||||
this.cipher = null;
|
||||
}
|
||||
|
||||
await super.load();
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
this.showCardNumber = false;
|
||||
this.showCardCode = false;
|
||||
if (this.cipher !== null && this.cipher.hasFields) {
|
||||
this.cipher.fields.forEach((field) => {
|
||||
field.showValue = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
allowOwnershipOptions(): boolean {
|
||||
return (
|
||||
(!this.editMode || this.cloneMode) &&
|
||||
this.ownershipOptions &&
|
||||
(this.ownershipOptions.length > 1 || !this.allowPersonal)
|
||||
);
|
||||
}
|
||||
|
||||
markPasswordAsDirty() {
|
||||
this.form.controls["Login.Password"].markAsDirty();
|
||||
}
|
||||
|
||||
openHelpReprompt() {
|
||||
this.platformUtilsService.launchUri(
|
||||
"https://bitwarden.com/help/managing-items/#protect-individual-items",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cipher when an attachment is altered.
|
||||
* Note: This only updates the `attachments` and `revisionDate`
|
||||
* properties to ensure any in-progress edits are not lost.
|
||||
*/
|
||||
patchCipherAttachments(cipher: CipherView) {
|
||||
this.cipher.attachments = cipher.attachments;
|
||||
this.cipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
truncateString(value: string, length: number) {
|
||||
return value.length > length ? value.substring(0, length) + "..." : value;
|
||||
}
|
||||
|
||||
togglePrivateKey() {
|
||||
this.showPrivateKey = !this.showPrivateKey;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="attachmentsTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-body">
|
||||
<div class="box" *ngIf="cipher && cipher.hasAttachments">
|
||||
<h1 class="box-header" id="attachmentsTitle">
|
||||
{{ "attachments" | i18n }}
|
||||
</h1>
|
||||
<div class="box-content no-hover">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
|
||||
<div class="row-main">
|
||||
{{ a.fileName }}
|
||||
</div>
|
||||
<small class="row-sub-label">{{ a.sizeName }}</small>
|
||||
<div class="action-buttons no-pad">
|
||||
<button
|
||||
class="row-btn btn"
|
||||
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"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "newAttachment" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content no-hover">
|
||||
<div class="box-content-row">
|
||||
<label for="file">{{ "file" | i18n }}</label>
|
||||
<input type="file" id="file" name="file" aria-describedby="fileHelp" required />
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileHelp" class="box-footer">
|
||||
{{ "maxFileSize" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-attachments",
|
||||
templateUrl: "attachments.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-body">
|
||||
<div class="box">
|
||||
<h1 class="box-header" id="collectionsTitle">
|
||||
{{ "collections" | i18n }}
|
||||
</h1>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
[disabled]="!cipher.canAssignToCollections"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-collections",
|
||||
templateUrl: "collections.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class CollectionsComponent extends BaseCollectionsComponent {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
organizationService: OrganizationService,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
organizationService,
|
||||
logService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="folderAddEditTitle">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<form
|
||||
#form
|
||||
class="modal-content"
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="box">
|
||||
<h1 class="box-header" id="folderAddEditTitle">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" type="text" formControlName="name" [appAutofocus]="!editMode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
|
||||
<div class="right">
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="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"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-folder-add-edit",
|
||||
templateUrl: "folder-add-edit.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
|
||||
constructor(
|
||||
folderService: FolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
keyService: KeyService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
folderService,
|
||||
folderApiService,
|
||||
accountService,
|
||||
keyService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="passwordHistoryTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="box">
|
||||
<h1 class="box-header" id="passwordHistoryTitle">
|
||||
{{ "passwordHistory" | i18n }}
|
||||
</h1>
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||
<div class="row-main">
|
||||
<span class="text monospaced" [innerHTML]="h.password | colorPassword"></span>
|
||||
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(h.password)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="!history.length">
|
||||
{{ "noPasswordsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-password-history",
|
||||
templateUrl: "password-history.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordHistoryComponent extends BasePasswordHistoryComponent {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(cipherService, platformUtilsService, i18nService, accountService, window, toastService);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="moveToOrgTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<div class="modal-body">
|
||||
<div class="box">
|
||||
<h1 class="box-header" id="moveToOrgTitle">
|
||||
{{ "moveToOrganization" | i18n }}
|
||||
</h1>
|
||||
<div class="box-content" *ngIf="!organizations || !organizations.length">
|
||||
<div class="box-content-row">
|
||||
{{ "noOrganizationsList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="organizations && organizations.length">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organization">{{ "organization" | i18n }}</label>
|
||||
<select
|
||||
id="organization"
|
||||
name="OrganizationId"
|
||||
aria-describedby="organizationHelp"
|
||||
[(ngModel)]="organizationId"
|
||||
(change)="filterCollections()"
|
||||
>
|
||||
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="organizationHelp" class="box-footer">
|
||||
{{ "moveToOrgDesc" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="organizations && organizations.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="c.checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
[disabled]="form.loading || !canSave"
|
||||
*ngIf="organizations && organizations.length"
|
||||
>
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button type="button" (click)="close()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-share",
|
||||
templateUrl: "share.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ShareComponent extends BaseShareComponent {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
organizationService: OrganizationService,
|
||||
accountService: AccountService,
|
||||
private modalRef: ModalRef,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
logService,
|
||||
organizationService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
protected close() {
|
||||
this.modalRef.close();
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<div class="container loading-spinner" *ngIf="!loaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="loaded">
|
||||
<div class="content">
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="42"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers.length"
|
||||
>
|
||||
<div class="list">
|
||||
<button
|
||||
type="button"
|
||||
*cdkVirtualFor="let c of ciphers; trackBy: trackByFn"
|
||||
appStopClick
|
||||
(click)="selectCipher(c)"
|
||||
(contextmenu)="rightClickCipher(c)"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
[ngClass]="{ active: c.id === activeCipherId }"
|
||||
[attr.aria-pressed]="c.id === activeCipherId"
|
||||
class="flex-list-item virtual-scroll-item"
|
||||
>
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
<div class="flex-cipher-list-item">
|
||||
<span class="text">
|
||||
<span class="truncate-box">
|
||||
<span class="truncate">{{ c.name }}</span>
|
||||
<ng-container *ngIf="c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection-shared text-muted"
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip text-muted"
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<div class="no-items" *ngIf="!ciphers.length">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button type="button" (click)="addCipher()" class="btn block primary link">
|
||||
{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
type="button"
|
||||
(click)="addCipher()"
|
||||
(contextmenu)="addCipherOptions()"
|
||||
class="block primary"
|
||||
appA11yTitle="{{ 'addItem' | i18n }}"
|
||||
[disabled]="deleted"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,39 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component } from "@angular/core";
|
||||
import { distinctUntilChanged } from "rxjs";
|
||||
|
||||
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultItemsComponent extends BaseVaultItemsComponent {
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
searchBarService: SearchBarService,
|
||||
cipherService: CipherService,
|
||||
accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(searchService, cipherService, accountService, restrictedItemTypesService);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {
|
||||
this.searchText = searchText;
|
||||
});
|
||||
}
|
||||
|
||||
trackByFn(index: number, c: CipherView) {
|
||||
return c.id;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<div id="vault" class="vault" attr.aria-hidden="{{ showingModal }}">
|
||||
<app-vault-items
|
||||
id="items"
|
||||
class="items"
|
||||
[activeCipherId]="cipherId"
|
||||
(onCipherClicked)="viewCipher($event)"
|
||||
(onCipherRightClicked)="viewCipherMenu($event)"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCipherOptions)="addCipherOptions()"
|
||||
>
|
||||
</app-vault-items>
|
||||
<app-vault-view
|
||||
id="details"
|
||||
class="details"
|
||||
*ngIf="cipherId && action === 'view'"
|
||||
[cipherId]="cipherId"
|
||||
[collectionId]="activeFilter?.selectedCollectionId"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
(onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)"
|
||||
(onEditCipher)="editCipher($event)"
|
||||
(onViewCipherPasswordHistory)="viewCipherPasswordHistory($event)"
|
||||
(onRestoredCipher)="restoredCipher($event)"
|
||||
(onDeletedCipher)="deletedCipher($event)"
|
||||
>
|
||||
</app-vault-view>
|
||||
<app-vault-add-edit
|
||||
id="addEdit"
|
||||
class="details"
|
||||
*ngIf="action === 'add' || action === 'edit' || action === 'clone'"
|
||||
[cloneMode]="action === 'clone'"
|
||||
[folderId]="action === 'add' && folderId !== 'none' ? folderId : null"
|
||||
[organizationId]="action === 'add' ? addOrganizationId : null"
|
||||
[collectionIds]="action === 'add' ? addCollectionIds : null"
|
||||
[collectionId]="activeFilter?.selectedCollectionId"
|
||||
[type]="action === 'add' ? (addType ? addType : type) : null"
|
||||
[cipherId]="action === 'edit' || action === 'clone' ? cipherId : null"
|
||||
(onSavedCipher)="savedCipher($event)"
|
||||
(onDeletedCipher)="deletedCipher($event)"
|
||||
(onEditAttachments)="editCipherAttachments($event)"
|
||||
(onCancelled)="cancelledAddEdit($event)"
|
||||
(onShareCipher)="shareCipher($event)"
|
||||
(onEditCollections)="cipherCollections($event)"
|
||||
(onGeneratePassword)="openGenerator(true)"
|
||||
(onGenerateUsername)="openGenerator(false)"
|
||||
>
|
||||
</app-vault-add-edit>
|
||||
<div
|
||||
id="logo"
|
||||
class="logo"
|
||||
*ngIf="action !== 'add' && action !== 'edit' && action !== 'view' && action !== 'clone'"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav">
|
||||
<app-vault-filter
|
||||
class="vault-filters"
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onAddFolder)="addFolder()"
|
||||
(onEditFolder)="editFolder($event.id)"
|
||||
></app-vault-filter>
|
||||
<app-nav class="nav"></app-nav>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #generator></ng-template>
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #share></ng-template>
|
||||
<ng-template #folderAddEdit></ng-template>
|
||||
<ng-template #passwordHistory></ng-template>
|
||||
@@ -1,870 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom } from "rxjs";
|
||||
import { filter, first, map, take } from "rxjs/operators";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType, toCipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
AddEditFolderDialogResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
import { CollectionsComponent } from "./collections.component";
|
||||
import { CredentialGeneratorDialogComponent } from "./credential-generator-dialog.component";
|
||||
import { PasswordHistoryComponent } from "./password-history.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
import { ViewComponent } from "./view.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "vault.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(ViewComponent) viewComponent: ViewComponent;
|
||||
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
|
||||
@ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent;
|
||||
@ViewChild("generator", { read: ViewContainerRef, static: true })
|
||||
generatorModalRef: ViewContainerRef;
|
||||
@ViewChild(VaultFilterComponent, { static: true }) vaultFilterComponent: VaultFilterComponent;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
|
||||
passwordHistoryModalRef: ViewContainerRef;
|
||||
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
|
||||
@ViewChild("collections", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
|
||||
action: string;
|
||||
cipherId: string = null;
|
||||
favorites = false;
|
||||
type: CipherType = null;
|
||||
folderId: string = null;
|
||||
collectionId: string = null;
|
||||
organizationId: string = null;
|
||||
myVaultOnly = false;
|
||||
addType: CipherType = null;
|
||||
addOrganizationId: string = null;
|
||||
addCollectionIds: string[] = null;
|
||||
showingModal = false;
|
||||
deleted = false;
|
||||
userHasPremiumAccess = false;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
activeUserId: UserId;
|
||||
cipherRepromptId: string | null = null;
|
||||
|
||||
private modal: ModalRef = null;
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
private syncService: SyncService,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private totpService: TotpService,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private searchBarService: SearchBarService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((canAccessPremium: boolean) => {
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
let detectChanges = true;
|
||||
|
||||
switch (message.command) {
|
||||
case "newLogin":
|
||||
await this.addCipher(CipherType.Login);
|
||||
break;
|
||||
case "newCard":
|
||||
await this.addCipher(CipherType.Card);
|
||||
break;
|
||||
case "newIdentity":
|
||||
await this.addCipher(CipherType.Identity);
|
||||
break;
|
||||
case "newSecureNote":
|
||||
await this.addCipher(CipherType.SecureNote);
|
||||
break;
|
||||
case "newSshKey":
|
||||
await this.addCipher(CipherType.SshKey);
|
||||
break;
|
||||
case "focusSearch":
|
||||
(document.querySelector("#search") as HTMLInputElement).select();
|
||||
detectChanges = false;
|
||||
break;
|
||||
case "syncCompleted":
|
||||
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter());
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
await this.vaultFilterComponent.reloadOrganizations();
|
||||
break;
|
||||
case "modalShown":
|
||||
this.showingModal = true;
|
||||
break;
|
||||
case "modalClosed":
|
||||
this.showingModal = false;
|
||||
break;
|
||||
case "copyUsername": {
|
||||
const uComponent =
|
||||
this.addEditComponent == null ? this.viewComponent : this.addEditComponent;
|
||||
const uCipher = uComponent != null ? uComponent.cipher : null;
|
||||
if (
|
||||
this.cipherId != null &&
|
||||
uCipher != null &&
|
||||
uCipher.id === this.cipherId &&
|
||||
uCipher.login != null &&
|
||||
uCipher.login.username != null
|
||||
) {
|
||||
this.copyValue(uCipher, uCipher.login.username, "username", "Username");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copyPassword": {
|
||||
const pComponent =
|
||||
this.addEditComponent == null ? this.viewComponent : this.addEditComponent;
|
||||
const pCipher = pComponent != null ? pComponent.cipher : null;
|
||||
if (
|
||||
this.cipherId != null &&
|
||||
pCipher != null &&
|
||||
pCipher.id === this.cipherId &&
|
||||
pCipher.login != null &&
|
||||
pCipher.login.password != null &&
|
||||
pCipher.viewPassword
|
||||
) {
|
||||
this.copyValue(pCipher, pCipher.login.password, "password", "Password");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copyTotp": {
|
||||
const tComponent =
|
||||
this.addEditComponent == null ? this.viewComponent : this.addEditComponent;
|
||||
const tCipher = tComponent != null ? tComponent.cipher : null;
|
||||
if (
|
||||
this.cipherId != null &&
|
||||
tCipher != null &&
|
||||
tCipher.id === this.cipherId &&
|
||||
tCipher.login != null &&
|
||||
tCipher.login.hasTotp &&
|
||||
this.userHasPremiumAccess
|
||||
) {
|
||||
const value = await firstValueFrom(this.totpService.getCode$(tCipher.login.totp));
|
||||
this.copyValue(tCipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
detectChanges = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (detectChanges) {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
this.searchBarService.setEnabled(true);
|
||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||
|
||||
const browserLoginApprovalFeatureFlag = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
|
||||
);
|
||||
if (browserLoginApprovalFeatureFlag === true) {
|
||||
const authRequests = await firstValueFrom(this.authRequestService.getPendingAuthRequests$());
|
||||
// There is a chance that there is more than one auth request in the response we only show the most recent one
|
||||
if (authRequests.length > 0) {
|
||||
const mostRecentAuthRequest = authRequests.reduce((latest, current) => {
|
||||
const latestDate = new Date(latest.creationDate).getTime();
|
||||
const currentDate = new Date(current.creationDate).getTime();
|
||||
return currentDate > latestDate ? current : latest;
|
||||
});
|
||||
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: mostRecentAuthRequest.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const authRequest = await this.apiService.getLastAuthRequest();
|
||||
if (authRequest != null) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
this.cipherService
|
||||
.failedToDecryptCiphers$(this.activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchBarService.setEnabled(false);
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.componentIsDestroyed$.next(true);
|
||||
this.componentIsDestroyed$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = params.cipherId;
|
||||
if (params.action === "clone") {
|
||||
await this.cloneCipher(cipherView);
|
||||
} else if (params.action === "edit") {
|
||||
await this.editCipher(cipherView);
|
||||
} else {
|
||||
await this.viewCipher(cipherView);
|
||||
}
|
||||
} else if (params.action === "add") {
|
||||
this.addType = toCipherType(params.addType);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.addCipher(this.addType);
|
||||
}
|
||||
|
||||
const paramCipherType = toCipherType(params.type);
|
||||
this.activeFilter = new VaultFilter({
|
||||
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
|
||||
cipherType: params.action === "add" || paramCipherType == null ? null : paramCipherType,
|
||||
selectedFolderId: params.folderId,
|
||||
selectedCollectionId: params.selectedCollectionId,
|
||||
selectedOrganizationId: params.selectedOrganizationId,
|
||||
myVaultOnly: params.myVaultOnly ?? false,
|
||||
});
|
||||
await this.vaultItemsComponent.reload(this.activeFilter.buildFilter());
|
||||
});
|
||||
}
|
||||
|
||||
async viewCipher(cipher: CipherView) {
|
||||
if (!(await this.canNavigateAway("view", cipher))) {
|
||||
return;
|
||||
} else if (!(await this.passwordReprompt(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cipherId = cipher.id;
|
||||
this.action = "view";
|
||||
this.go();
|
||||
}
|
||||
|
||||
viewCipherMenu(cipher: CipherView) {
|
||||
const menu: RendererMenuItem[] = [
|
||||
{
|
||||
label: this.i18nService.t("view"),
|
||||
click: () =>
|
||||
this.functionWithChangeDetection(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.viewCipher(cipher);
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (cipher.decryptionFailure) {
|
||||
invokeMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cipher.isDeleted) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("edit"),
|
||||
click: () =>
|
||||
this.functionWithChangeDetection(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.editCipher(cipher);
|
||||
}),
|
||||
});
|
||||
if (!cipher.organizationId) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("clone"),
|
||||
click: () =>
|
||||
this.functionWithChangeDetection(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.cloneCipher(cipher);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (cipher.type) {
|
||||
case CipherType.Login:
|
||||
if (
|
||||
cipher.login.canLaunch ||
|
||||
cipher.login.username != null ||
|
||||
cipher.login.password != null
|
||||
) {
|
||||
menu.push({ type: "separator" });
|
||||
}
|
||||
if (cipher.login.canLaunch) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("launch"),
|
||||
click: () => this.platformUtilsService.launchUri(cipher.login.launchUri),
|
||||
});
|
||||
}
|
||||
if (cipher.login.username != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyUsername"),
|
||||
click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"),
|
||||
});
|
||||
}
|
||||
if (cipher.login.password != null && cipher.viewPassword) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyPassword"),
|
||||
click: () => {
|
||||
this.copyValue(cipher, cipher.login.password, "password", "Password");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyVerificationCodeTotp"),
|
||||
click: async () => {
|
||||
const value = await firstValueFrom(this.totpService.getCode$(cipher.login.totp));
|
||||
this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP");
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CipherType.Card:
|
||||
if (cipher.card.number != null || cipher.card.code != null) {
|
||||
menu.push({ type: "separator" });
|
||||
}
|
||||
if (cipher.card.number != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copyNumber"),
|
||||
click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"),
|
||||
});
|
||||
}
|
||||
if (cipher.card.code != null) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("copySecurityCode"),
|
||||
click: () => {
|
||||
this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
invokeMenu(menu);
|
||||
}
|
||||
|
||||
async editCipher(cipher: CipherView) {
|
||||
if (!(await this.canNavigateAway("edit", cipher))) {
|
||||
return;
|
||||
} else if (!(await this.passwordReprompt(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.editCipherWithoutPasswordPrompt(cipher);
|
||||
}
|
||||
|
||||
async editCipherWithoutPasswordPrompt(cipher: CipherView) {
|
||||
if (!(await this.canNavigateAway("edit", cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cipherId = cipher.id;
|
||||
this.action = "edit";
|
||||
this.go();
|
||||
}
|
||||
|
||||
async cloneCipher(cipher: CipherView) {
|
||||
if (!(await this.canNavigateAway("clone", cipher))) {
|
||||
return;
|
||||
} else if (!(await this.passwordReprompt(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.cloneCipherWithoutPasswordPrompt(cipher);
|
||||
}
|
||||
|
||||
async cloneCipherWithoutPasswordPrompt(cipher: CipherView) {
|
||||
if (!(await this.canNavigateAway("edit", cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cipherId = cipher.id;
|
||||
this.action = "clone";
|
||||
this.go();
|
||||
}
|
||||
|
||||
async addCipher(type: CipherType = null) {
|
||||
if (!(await this.canNavigateAway("add", null))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addType = type || this.activeFilter.cipherType;
|
||||
this.action = "add";
|
||||
this.cipherId = null;
|
||||
this.prefillNewCipherFromFilter();
|
||||
this.go();
|
||||
|
||||
if (type === CipherType.SshKey) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("sshKeyGenerated"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addCipherOptions() {
|
||||
const menu: RendererMenuItem[] = [
|
||||
{
|
||||
label: this.i18nService.t("typeLogin"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Login),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeCard"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Card),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeIdentity"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.Identity),
|
||||
},
|
||||
{
|
||||
label: this.i18nService.t("typeSecureNote"),
|
||||
click: () => this.addCipherWithChangeDetection(CipherType.SecureNote),
|
||||
},
|
||||
];
|
||||
|
||||
invokeMenu(menu);
|
||||
}
|
||||
|
||||
async savedCipher(cipher: CipherView) {
|
||||
this.cipherId = null;
|
||||
this.action = "view";
|
||||
await this.vaultItemsComponent.refresh();
|
||||
this.cipherId = cipher.id;
|
||||
await this.cipherService.clearCache(this.activeUserId);
|
||||
await this.vaultItemsComponent.load(this.activeFilter.buildFilter());
|
||||
this.go();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
}
|
||||
|
||||
async deletedCipher(cipher: CipherView) {
|
||||
this.cipherId = null;
|
||||
this.action = null;
|
||||
this.go();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
}
|
||||
|
||||
async restoredCipher(cipher: CipherView) {
|
||||
this.cipherId = null;
|
||||
this.action = null;
|
||||
this.go();
|
||||
await this.vaultItemsComponent.refresh();
|
||||
}
|
||||
|
||||
async editCipherAttachments(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
AttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => (comp.cipherId = cipher.id),
|
||||
);
|
||||
this.modal = modal;
|
||||
|
||||
let madeAttachmentChanges = false;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onUploadedAttachment.subscribe((cipher) => {
|
||||
madeAttachmentChanges = true;
|
||||
// Update the edit component cipher with the updated cipher,
|
||||
// which is needed because the revision date is updated when an attachment is altered
|
||||
this.addEditComponent.patchCipherAttachments(cipher);
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onDeletedAttachment.subscribe((cipher) => {
|
||||
madeAttachmentChanges = true;
|
||||
// Update the edit component cipher with the updated cipher,
|
||||
// which is needed because the revision date is updated when an attachment is altered
|
||||
this.addEditComponent.patchCipherAttachments(cipher);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
if (madeAttachmentChanges) {
|
||||
await this.vaultItemsComponent.refresh();
|
||||
}
|
||||
madeAttachmentChanges = false;
|
||||
});
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
ShareComponent,
|
||||
this.shareModalRef,
|
||||
(comp) => (comp.cipherId = cipher.id),
|
||||
);
|
||||
this.modal = modal;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
childComponent.onSharedCipher.subscribe(async () => {
|
||||
this.modal.close();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.viewCipher(cipher);
|
||||
await this.vaultItemsComponent.refresh();
|
||||
await this.cipherService.clearCache(this.activeUserId);
|
||||
await this.vaultItemsComponent.load(this.activeFilter.buildFilter());
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async cipherCollections(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => (comp.cipherId = cipher.id),
|
||||
);
|
||||
this.modal = modal;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onSavedCollections.subscribe(() => {
|
||||
this.modal.close();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.viewCipher(cipher);
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async viewCipherPasswordHistory(cipher: CipherView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
[this.modal] = await this.modalService.openViewRef(
|
||||
PasswordHistoryComponent,
|
||||
this.passwordHistoryModalRef,
|
||||
(comp) => (comp.cipherId = cipher.id),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
cancelledAddEdit(cipher: CipherView) {
|
||||
this.cipherId = cipher.id;
|
||||
this.action = this.cipherId != null ? "view" : null;
|
||||
this.go();
|
||||
}
|
||||
|
||||
async applyVaultFilter(vaultFilter: VaultFilter) {
|
||||
this.searchBarService.setPlaceholderText(
|
||||
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)),
|
||||
);
|
||||
this.activeFilter = vaultFilter;
|
||||
await this.vaultItemsComponent.reload(
|
||||
this.activeFilter.buildFilter(),
|
||||
vaultFilter.status === "trash",
|
||||
);
|
||||
this.go();
|
||||
}
|
||||
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
return "searchFavorites";
|
||||
}
|
||||
if (vaultFilter.status === "trash") {
|
||||
return "searchTrash";
|
||||
}
|
||||
if (vaultFilter.cipherType != null) {
|
||||
return "searchType";
|
||||
}
|
||||
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
|
||||
return "searchFolder";
|
||||
}
|
||||
if (vaultFilter.selectedCollectionId != null) {
|
||||
return "searchCollection";
|
||||
}
|
||||
if (vaultFilter.selectedOrganizationId != null) {
|
||||
return "searchOrganization";
|
||||
}
|
||||
if (vaultFilter.myVaultOnly) {
|
||||
return "searchMyVault";
|
||||
}
|
||||
|
||||
return "searchVault";
|
||||
}
|
||||
|
||||
async openGenerator(passwordType = true) {
|
||||
CredentialGeneratorDialogComponent.open(this.dialogService, {
|
||||
onCredentialGenerated: (value?: string) => {
|
||||
if (this.addEditComponent != null) {
|
||||
this.addEditComponent.markPasswordAsDirty();
|
||||
if (passwordType) {
|
||||
this.addEditComponent.cipher.login.password = value ?? "";
|
||||
} else {
|
||||
this.addEditComponent.cipher.login.username = value ?? "";
|
||||
}
|
||||
}
|
||||
},
|
||||
type: passwordType ? "password" : "username",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async addFolder() {
|
||||
this.messagingService.send("newFolder");
|
||||
}
|
||||
|
||||
async editFolder(folderId: string) {
|
||||
const folderView = await firstValueFrom(
|
||||
this.folderService.getDecrypted$(folderId, this.activeUserId),
|
||||
);
|
||||
|
||||
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
...folderView,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (
|
||||
result === AddEditFolderDialogResult.Deleted ||
|
||||
result === AddEditFolderDialogResult.Created
|
||||
) {
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private dirtyInput(): boolean {
|
||||
return (
|
||||
(this.action === "add" || this.action === "edit" || this.action === "clone") &&
|
||||
document.querySelectorAll("app-vault-add-edit .ng-dirty").length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private async wantsToSaveChanges(): Promise<boolean> {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "unsavedChangesTitle" },
|
||||
content: { key: "unsavedChangesConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
return !confirmed;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
if (queryParams == null) {
|
||||
queryParams = {
|
||||
action: this.action,
|
||||
cipherId: this.cipherId,
|
||||
favorites: this.favorites ? true : null,
|
||||
type: this.type,
|
||||
folderId: this.folderId,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
organizationId: this.organizationId,
|
||||
myVaultOnly: this.myVaultOnly,
|
||||
};
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
private addCipherWithChangeDetection(type: CipherType = null) {
|
||||
this.functionWithChangeDetection(() => this.addCipher(type));
|
||||
}
|
||||
|
||||
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
|
||||
this.functionWithChangeDetection(async () => {
|
||||
if (
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.passwordReprompt(cipher))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(value);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)),
|
||||
});
|
||||
if (this.action === "view") {
|
||||
this.messagingService.send("minimizeOnCopy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private functionWithChangeDetection(func: () => void) {
|
||||
this.ngZone.run(() => {
|
||||
func();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
private prefillNewCipherFromFilter() {
|
||||
if (this.activeFilter.selectedCollectionId != null) {
|
||||
const collection = this.vaultFilterComponent.collections.fullList.filter(
|
||||
(c) => c.id === this.activeFilter.selectedCollectionId,
|
||||
);
|
||||
if (collection.length > 0) {
|
||||
this.addOrganizationId = collection[0].organizationId;
|
||||
this.addCollectionIds = [this.activeFilter.selectedCollectionId];
|
||||
}
|
||||
} else if (this.activeFilter.selectedOrganizationId) {
|
||||
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
}
|
||||
}
|
||||
|
||||
private async canNavigateAway(action: string, cipher?: CipherView) {
|
||||
// Don't navigate to same route
|
||||
if (this.action === action && (cipher == null || this.cipherId === cipher.id)) {
|
||||
return false;
|
||||
} else if (this.dirtyInput() && (await this.wantsToSaveChanges())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async passwordReprompt(cipher: CipherView) {
|
||||
if (cipher.reprompt === CipherRepromptType.None) {
|
||||
this.cipherRepromptId = null;
|
||||
return true;
|
||||
}
|
||||
if (this.cipherRepromptId === cipher.id) {
|
||||
return true;
|
||||
}
|
||||
const repromptResult = await this.passwordRepromptService.showPasswordPrompt();
|
||||
if (repromptResult) {
|
||||
this.cipherRepromptId = cipher.id;
|
||||
}
|
||||
return repromptResult;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let field of cipher.fields; index as i"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
*ngIf="field.type != fieldType.Linked"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, field.value)"
|
||||
[id]="'customField-' + i"
|
||||
>
|
||||
{{ field.name }}
|
||||
</span>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">
|
||||
{{ "cfTypeLinked" | i18n }}: {{ field.name }}
|
||||
</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{ field.value || " " }}
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Hidden">
|
||||
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
|
||||
<span
|
||||
*ngIf="field.showValue && !field.showCount"
|
||||
class="monospaced show-whitespace"
|
||||
[innerHTML]="field.value | colorPassword"
|
||||
></span>
|
||||
<span
|
||||
*ngIf="field.showValue && field.showCount"
|
||||
[innerHTML]="field.value | colorPasswordCount"
|
||||
></span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="field.value === 'true'"
|
||||
disabled="true"
|
||||
[attr.aria-labelledby]="'customField-' + i"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
|
||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons action-buttons-fixed">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
|
||||
(click)="toggleFieldCount(field)"
|
||||
[attr.aria-pressed]="field.showCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword"
|
||||
(click)="toggleFieldValue(field)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !field.showValue, 'bwi-eye-slash': field.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
*ngIf="
|
||||
field.value &&
|
||||
field.type !== fieldType.Boolean &&
|
||||
field.type !== fieldType.Linked &&
|
||||
!(field.type === fieldType.Hidden && !cipher.viewPassword)
|
||||
"
|
||||
(click)="
|
||||
copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')
|
||||
"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view-custom-fields",
|
||||
templateUrl: "view-custom-fields.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
|
||||
constructor(eventCollectionService: EventCollectionService) {
|
||||
super(eventCollectionService);
|
||||
}
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
<div class="content">
|
||||
<div class="inner-content" *ngIf="cipher">
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "itemInformation" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.name)"
|
||||
>{{ "name" | i18n }}</span
|
||||
>
|
||||
{{ cipher.name }}
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.login">
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.username)"
|
||||
>{{ "username" | i18n }}</span
|
||||
>
|
||||
{{ cipher.login.username }}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
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 class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.password)"
|
||||
>{{ "password" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="!showPassword" class="monospaced">
|
||||
{{ cipher.login.maskedPassword }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="showPassword && !showPasswordCount"
|
||||
class="monospaced password-wrapper"
|
||||
[appCopyText]="cipher.login.password"
|
||||
[innerHTML]="cipher.login.password | colorPassword"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="showPassword && showPasswordCount"
|
||||
[innerHTML]="cipher.login.password | colorPasswordCount"
|
||||
></div>
|
||||
</div>
|
||||
<div class="action-buttons" *ngIf="cipher.viewPassword">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
(click)="togglePasswordCount()"
|
||||
*ngIf="showPassword"
|
||||
[attr.aria-pressed]="showPasswordCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box-content-row text-muted"
|
||||
*ngIf="cipher.login.hasFido2Credentials"
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ fido2CredentialCreationDateValue }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="cipher.login.totp && totpInfo$ | async as totpInfo">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex totp"
|
||||
[ngClass]="{ low: totpInfo.totpLow }"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, totpInfo.totpCode)"
|
||||
>{{ "verificationCodeTotp" | i18n }}</span
|
||||
>
|
||||
<span class="totp-code">{{ totpInfo.totpCodeFormatted }}</span>
|
||||
</div>
|
||||
<span class="totp-countdown" aria-hidden="true">
|
||||
<span class="totp-sec">{{ totpInfo.totpSec }}</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle
|
||||
class="totp-circle inner"
|
||||
r="12.6"
|
||||
cy="16"
|
||||
cx="16"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpInfo.totpDash }"
|
||||
></circle>
|
||||
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
title="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(totpInfo.totpCode, 'verificationCodeTotp', 'TOTP')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "copyValue" | i18n }}</span>
|
||||
<span
|
||||
class="sr-only exists-only-on-parent-focus"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>{{ totpInfo.totpSec }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="box-content-row box-content-row-flex totp" *ngIf="showPremiumRequiredTotp">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
|
||||
<span class="row-label">
|
||||
<a [routerLink]="" (click)="showGetPremium()"
|
||||
>{{ "premiumSubcriptionRequired" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.card">
|
||||
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.cardholderName)"
|
||||
>{{ "cardholderName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.cardholderName }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.number)"
|
||||
>{{ "number" | i18n }}</span
|
||||
>
|
||||
<span *ngIf="!showCardNumber" class="monospaced">{{
|
||||
cipher.card.maskedNumber | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
<span *ngIf="showCardNumber" class="monospaced">{{
|
||||
cipher.card.number | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
(click)="toggleCardNumber()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Card Number')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.brand">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.brand)"
|
||||
>{{ "brand" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.brand }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.expiration">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.expiration)"
|
||||
>{{ "expiration" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.expiration }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.code)"
|
||||
>{{ "securityCode" | i18n }}</span
|
||||
>
|
||||
<span *ngIf="!showCardCode" class="monospaced">{{ cipher.card.maskedCode }}</span>
|
||||
<span *ngIf="showCardCode" class="monospaced">{{ cipher.card.code }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
(click)="toggleCardCode()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.identity">
|
||||
<div class="box-content-row" *ngIf="cipher.identity.fullName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.fullName)"
|
||||
>{{ "identityName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.fullName }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.username">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.username)"
|
||||
>{{ "username" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.username }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.company">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.company)"
|
||||
>{{ "company" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.company }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.ssn">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.ssn)"
|
||||
>{{ "ssn" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.ssn }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.passportNumber)"
|
||||
>{{ "passportNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.passportNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.licenseNumber)"
|
||||
>{{ "licenseNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.licenseNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.email">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.email)"
|
||||
>{{ "email" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.email }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.phone">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.phone)"
|
||||
>{{ "phone" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.phone }}
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country"
|
||||
>
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="
|
||||
setTextDataOnDrag(
|
||||
$event,
|
||||
(cipher.identity.address1 ? cipher.identity.address1 + '\n' : '') +
|
||||
(cipher.identity.address2 ? cipher.identity.address2 + '\n' : '') +
|
||||
(cipher.identity.address3 ? cipher.identity.address3 + '\n' : '') +
|
||||
(cipher.identity.fullAddressPart2
|
||||
? cipher.identity.fullAddressPart2 + '\n'
|
||||
: '') +
|
||||
(cipher.identity.country ? cipher.identity.country : '')
|
||||
)
|
||||
"
|
||||
>{{ "address" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="cipher.identity.address1">{{ cipher.identity.address1 }}</div>
|
||||
<div *ngIf="cipher.identity.address2">{{ cipher.identity.address2 }}</div>
|
||||
<div *ngIf="cipher.identity.address3">{{ cipher.identity.address3 }}</div>
|
||||
<div *ngIf="cipher.identity.fullAddressPart2">
|
||||
{{ cipher.identity.fullAddressPart2 }}
|
||||
</div>
|
||||
<div *ngIf="cipher.identity.country">{{ cipher.identity.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ssh Key -->
|
||||
<div *ngIf="cipher.sshKey">
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.sshKey.privateKey">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "sshPrivateKey" | i18n }}</span>
|
||||
<div
|
||||
*ngIf="!showPrivateKey"
|
||||
class="monospaced"
|
||||
style="white-space: pre-line"
|
||||
[innerText]="cipher.sshKey.maskedPrivateKey"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="showPrivateKey"
|
||||
class="monospaced"
|
||||
style="white-space: pre-line"
|
||||
[innerText]="cipher.sshKey.privateKey"
|
||||
></div>
|
||||
</div>
|
||||
<div class="action-buttons" *ngIf="cipher.viewPassword">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPrivateKey"
|
||||
(click)="togglePrivateKey()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copySSHPrivateKey' | i18n }}"
|
||||
(click)="copy(cipher.sshKey.privateKey, 'sshPrivateKey', 'SshPrivateKey')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngIf="cipher.sshKey.publicKey"
|
||||
appBoxRow
|
||||
>
|
||||
<div class="row-main">
|
||||
<label for="sshPublicKey">{{ "sshPublicKey" | i18n }}</label>
|
||||
<input
|
||||
id="sshPublicKey"
|
||||
type="text"
|
||||
name="SshKey.SshPublicKey"
|
||||
[ngModel]="cipher.sshKey.publicKey"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
(click)="copy(cipher.sshKey.publicKey, 'sshPublicKey', 'SshPublicKey')"
|
||||
appA11yTitle="{{ 'generateSshKey' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngIf="cipher.sshKey.keyFingerprint"
|
||||
appBoxRow
|
||||
>
|
||||
<div class="row-main">
|
||||
<label for="sshKeyFingerprint">{{ "sshFingerprint" | i18n }}</label>
|
||||
<input
|
||||
id="sshKeyFingerprint"
|
||||
type="text"
|
||||
name="SshKey.SshKeyFingerprint"
|
||||
[ngModel]="cipher.sshKey.keyFingerprint"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
(click)="copy(cipher.sshKey.keyFingerprint, 'sshFingerprint', 'SshFingerprint')"
|
||||
appA11yTitle="{{ 'generateSshKey' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let u of cipher.login.uris; let i = index"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="!u.isWebsite"
|
||||
>{{ "uri" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="u.isWebsite"
|
||||
>{{ "website" | i18n }}</span
|
||||
>
|
||||
<span title="{{ u.uri }}">{{ u.hostOrUri }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyUri' | i18n }}"
|
||||
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.folderId && folder">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<label
|
||||
for="folderName"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, folder.name)"
|
||||
>{{ "folder" | i18n }}</label
|
||||
>
|
||||
<input id="folderName" type="text" name="folderName" [value]="folder.name" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.notes">
|
||||
<h2 class="box-header">
|
||||
<span
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.notes)"
|
||||
>{{ "notes" | i18n }}</span
|
||||
>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row pre-wrap">{{ cipher.notes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-vault-view-custom-fields
|
||||
*ngIf="cipher.hasFields"
|
||||
[cipher]="cipher"
|
||||
[promptPassword]="promptPassword.bind(this)"
|
||||
[copy]="copy.bind(this)"
|
||||
>
|
||||
</app-vault-view-custom-fields>
|
||||
<div class="box" *ngIf="cipher.hasAttachments && (canAccessPremium || cipher.organizationId)">
|
||||
<h2 class="box-header">
|
||||
{{ "attachments" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
*ngFor="let attachment of cipher.attachments"
|
||||
appStopClick
|
||||
(click)="downloadAttachment(attachment)"
|
||||
>
|
||||
<span class="row-main">{{ attachment.fileName }}</span>
|
||||
<small class="row-sub-label">{{ attachment.sizeName }}</small>
|
||||
<i
|
||||
class="bwi bwi-download bwi-fw row-sub-icon"
|
||||
*ngIf="!$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
|
||||
*ngIf="$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-footer">
|
||||
<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="cipher.passwordRevisionDisplayDate">
|
||||
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
|
||||
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.hasPasswordHistory">
|
||||
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
|
||||
<button
|
||||
type="button"
|
||||
(click)="viewHistory()"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'passwordHistory' | i18n }}, {{ cipher.passwordHistory.length }}"
|
||||
>
|
||||
<span aria-hidden="true">{{ cipher.passwordHistory.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer" *ngIf="cipher">
|
||||
<ng-container *ngIf="!cipher.decryptionFailure">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="(canRestoreCipher$ | async) && cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="!cipher?.organizationId && !cipher.isDeleted"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="canDeleteCipher$ | async">
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,176 +0,0 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "ViewComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view",
|
||||
templateUrl: "view.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Output() onViewCipherPasswordHistory = new EventEmitter<CipherView>();
|
||||
@Input() masterPasswordAlreadyPrompted: boolean = false;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
totpService: TotpService,
|
||||
tokenService: TokenService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
broadcasterService: BroadcasterService,
|
||||
ngZone: NgZone,
|
||||
changeDetectorRef: ChangeDetectorRef,
|
||||
eventCollectionService: EventCollectionService,
|
||||
apiService: ApiService,
|
||||
private messagingService: MessagingService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
totpService,
|
||||
tokenService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
window,
|
||||
broadcasterService,
|
||||
ngZone,
|
||||
changeDetectorRef,
|
||||
eventCollectionService,
|
||||
apiService,
|
||||
passwordRepromptService,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
datePipe,
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
toastService,
|
||||
cipherAuthorizationService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.cipher?.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [this.cipherId as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
|
||||
if (changes["cipherId"]) {
|
||||
if (changes["cipherId"].currentValue !== changes["cipherId"].previousValue) {
|
||||
this.showPrivateKey = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewHistory() {
|
||||
this.onViewCipherPasswordHistory.emit(this.cipher);
|
||||
}
|
||||
|
||||
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
|
||||
const hasCopied = await super.copy(value, typeI18nKey, aType);
|
||||
if (hasCopied) {
|
||||
this.messagingService.send("minimizeOnCopy");
|
||||
}
|
||||
|
||||
return hasCopied;
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
this.showCardNumber = false;
|
||||
this.showCardCode = false;
|
||||
if (this.cipher !== null && this.cipher.hasFields) {
|
||||
this.cipher.fields.forEach((field) => {
|
||||
field.showValue = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showGetPremium() {
|
||||
if (!this.canAccessPremium) {
|
||||
this.messagingService.send("premiumRequired");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class CollectionsComponent implements OnInit {
|
||||
@Input() cipherId: string;
|
||||
@Input() allowSelectNone = false;
|
||||
@Output() onSavedCollections = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
cipher: CipherView;
|
||||
collectionIds: string[];
|
||||
collections: CollectionView[] = [];
|
||||
organization: Organization;
|
||||
|
||||
protected cipherDomain: Cipher;
|
||||
|
||||
constructor(
|
||||
protected collectionService: CollectionService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected cipherService: CipherService,
|
||||
protected organizationService: OrganizationService,
|
||||
private logService: LogService,
|
||||
private accountService: AccountService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
this.collections = await this.loadCollections();
|
||||
|
||||
this.collections.forEach((c) => ((c as any).checked = false));
|
||||
if (this.collectionIds != null) {
|
||||
this.collections.forEach((c) => {
|
||||
(c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.organization == null) {
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(activeUserId)
|
||||
.pipe(
|
||||
map((organizations) =>
|
||||
organizations.find((org) => org.id === this.cipher.organizationId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const selectedCollectionIds = this.collections
|
||||
.filter((c) => {
|
||||
if (this.organization.canEditAllCiphers) {
|
||||
return !!(c as any).checked;
|
||||
} else {
|
||||
return !!(c as any).checked && !c.readOnly;
|
||||
}
|
||||
})
|
||||
.map((c) => c.id);
|
||||
if (!this.allowSelectNone && selectedCollectionIds.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectOneCollection"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
this.cipherDomain.collectionIds = selectedCollectionIds;
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.formPromise = this.saveCollections(activeUserId);
|
||||
await this.formPromise;
|
||||
this.onSavedCollections.emit();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("editedItem"),
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected loadCipherCollections() {
|
||||
return this.cipherDomain.collectionIds;
|
||||
}
|
||||
|
||||
protected async loadCollections() {
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
return allCollections.filter(
|
||||
(c) => !c.readOnly && c.organizationId === this.cipher.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
protected saveCollections(userId: UserId) {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId);
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Directive()
|
||||
export class ShareComponent implements OnInit, OnDestroy {
|
||||
@Input() cipherId: string;
|
||||
@Input() organizationId: string;
|
||||
@Output() onSharedCipher = new EventEmitter();
|
||||
|
||||
formPromise: Promise<void>;
|
||||
cipher: CipherView;
|
||||
collections: Checkable<CollectionView>[] = [];
|
||||
organizations$: Observable<Organization[]>;
|
||||
|
||||
protected writeableCollections: Checkable<CollectionView>[] = [];
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected collectionService: CollectionService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected cipherService: CipherService,
|
||||
private logService: LogService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly);
|
||||
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe(
|
||||
map((orgs) => {
|
||||
return orgs
|
||||
.filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed)
|
||||
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
}),
|
||||
);
|
||||
|
||||
this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
|
||||
if (this.organizationId == null && orgs.length > 0) {
|
||||
this.organizationId = orgs[0].id;
|
||||
this.filterCollections();
|
||||
}
|
||||
});
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||
}
|
||||
|
||||
filterCollections() {
|
||||
this.writeableCollections.forEach((c) => (c.checked = false));
|
||||
if (this.organizationId == null || this.writeableCollections.length === 0) {
|
||||
this.collections = [];
|
||||
} else {
|
||||
this.collections = this.writeableCollections.filter(
|
||||
(c) => c.organizationId === this.organizationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const selectedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
|
||||
if (selectedCollectionIds.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("selectOneCollection"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||
const orgs = await firstValueFrom(this.organizations$);
|
||||
const orgName =
|
||||
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");
|
||||
|
||||
try {
|
||||
this.formPromise = this.cipherService
|
||||
.shareWithServer(cipherView, this.organizationId, selectedCollectionIds, activeUserId)
|
||||
.then(async () => {
|
||||
this.onSharedCipher.emit();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("movedItemToOrg", cipherView.name, orgName),
|
||||
);
|
||||
});
|
||||
await this.formPromise;
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get canSave() {
|
||||
if (this.collections != null) {
|
||||
for (let i = 0; i < this.collections.length; i++) {
|
||||
if (this.collections[i].checked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
|
||||
import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
|
||||
@Directive()
|
||||
export class AddEditCustomFieldsComponent implements OnChanges {
|
||||
@Input() cipher: CipherView;
|
||||
@Input() thisCipherType: CipherType;
|
||||
@Input() editMode: boolean;
|
||||
|
||||
addFieldType: FieldType = FieldType.Text;
|
||||
addFieldTypeOptions: any[];
|
||||
addFieldLinkedTypeOption: any;
|
||||
linkedFieldOptions: any[] = [];
|
||||
|
||||
cipherType = CipherType;
|
||||
fieldType = FieldType;
|
||||
eventType = EventType;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
) {
|
||||
this.addFieldTypeOptions = [
|
||||
{ name: i18nService.t("cfTypeText"), value: FieldType.Text },
|
||||
{ name: i18nService.t("cfTypeHidden"), value: FieldType.Hidden },
|
||||
{ name: i18nService.t("cfTypeBoolean"), value: FieldType.Boolean },
|
||||
];
|
||||
this.addFieldLinkedTypeOption = {
|
||||
name: this.i18nService.t("cfTypeLinked"),
|
||||
value: FieldType.Linked,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.thisCipherType != null) {
|
||||
this.setLinkedFieldOptions();
|
||||
|
||||
if (!changes.thisCipherType.firstChange) {
|
||||
this.resetCipherLinkedFields();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addField() {
|
||||
if (this.cipher.fields == null) {
|
||||
this.cipher.fields = [];
|
||||
}
|
||||
|
||||
const f = new FieldView();
|
||||
f.type = this.addFieldType;
|
||||
f.newField = true;
|
||||
|
||||
if (f.type === FieldType.Linked) {
|
||||
f.linkedId = this.linkedFieldOptions[0].value;
|
||||
}
|
||||
|
||||
this.cipher.fields.push(f);
|
||||
}
|
||||
|
||||
removeField(field: FieldView) {
|
||||
const i = this.cipher.fields.indexOf(field);
|
||||
if (i > -1) {
|
||||
this.cipher.fields.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFieldValue(field: FieldView) {
|
||||
const f = field as any;
|
||||
f.showValue = !f.showValue;
|
||||
if (this.editMode && f.showValue) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||
this.cipher.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
trackByFunction(index: number, item: any) {
|
||||
return index;
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
private setLinkedFieldOptions() {
|
||||
if (this.cipher.linkedFieldOptions == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options: any = [];
|
||||
this.cipher.linkedFieldOptions.forEach((linkedFieldOption, id) =>
|
||||
options.push({ name: this.i18nService.t(linkedFieldOption.i18nKey), value: id }),
|
||||
);
|
||||
this.linkedFieldOptions = options.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
}
|
||||
|
||||
private resetCipherLinkedFields() {
|
||||
if (this.cipher.fields == null || this.cipher.fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete any Linked custom fields if the item type does not support them
|
||||
if (this.cipher.linkedFieldOptions == null) {
|
||||
this.cipher.fields = this.cipher.fields.filter((f) => f.type !== FieldType.Linked);
|
||||
return;
|
||||
}
|
||||
|
||||
this.cipher.fields
|
||||
.filter((f) => f.type === FieldType.Linked)
|
||||
.forEach((f) => (f.linkedId = this.linkedFieldOptions[0].value));
|
||||
}
|
||||
}
|
||||
@@ -1,855 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { concatMap, firstValueFrom, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
CipherService,
|
||||
EncryptionContext,
|
||||
} from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@Directive()
|
||||
export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@Input() cloneMode = false;
|
||||
@Input() folderId: string = null;
|
||||
@Input() cipherId: string;
|
||||
@Input() type: CipherType;
|
||||
@Input() collectionIds: string[];
|
||||
@Input() organizationId: string = null;
|
||||
@Input() collectionId: string = null;
|
||||
@Output() onSavedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||
@Output() onCancelled = new EventEmitter<CipherView>();
|
||||
@Output() onEditAttachments = new EventEmitter<CipherView>();
|
||||
@Output() onShareCipher = new EventEmitter<CipherView>();
|
||||
@Output() onEditCollections = new EventEmitter<CipherView>();
|
||||
@Output() onGeneratePassword = new EventEmitter();
|
||||
@Output() onGenerateUsername = new EventEmitter();
|
||||
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
|
||||
editMode = false;
|
||||
cipher: CipherView;
|
||||
folders$: Observable<FolderView[]>;
|
||||
collections: CollectionView[] = [];
|
||||
title: string;
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
restorePromise: Promise<any>;
|
||||
checkPasswordPromise: Promise<number>;
|
||||
showPassword = false;
|
||||
showPrivateKey = false;
|
||||
showTotpSeed = false;
|
||||
showCardNumber = false;
|
||||
showCardCode = false;
|
||||
cipherType = CipherType;
|
||||
cardBrandOptions: any[];
|
||||
cardExpMonthOptions: any[];
|
||||
identityTitleOptions: any[];
|
||||
uriMatchOptions: any[];
|
||||
ownershipOptions: any[] = [];
|
||||
autofillOnPageLoadOptions: any[];
|
||||
currentDate = new Date();
|
||||
allowPersonal = true;
|
||||
reprompt = false;
|
||||
canUseReprompt = true;
|
||||
organization: Organization;
|
||||
/**
|
||||
* Flag to determine if the action is being performed from the admin console.
|
||||
*/
|
||||
isAdminConsoleAction: boolean = false;
|
||||
|
||||
protected componentName = "";
|
||||
protected destroy$ = new Subject<void>();
|
||||
protected writeableCollections: CollectionView[];
|
||||
private organizationDataOwnershipAppliesToUser: boolean;
|
||||
private previousCipherId: string;
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
this.cipher?.login?.fido2Credentials?.[0]?.creationDate,
|
||||
"short",
|
||||
);
|
||||
return `${dateCreated} ${creationDate}`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected folderService: FolderService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected auditService: AuditService,
|
||||
protected accountService: AccountService,
|
||||
protected collectionService: CollectionService,
|
||||
protected messagingService: MessagingService,
|
||||
protected eventCollectionService: EventCollectionService,
|
||||
protected policyService: PolicyService,
|
||||
protected logService: LogService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
private organizationService: OrganizationService,
|
||||
protected dialogService: DialogService,
|
||||
protected win: Window,
|
||||
protected datePipe: DatePipe,
|
||||
protected configService: ConfigService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected toastService: ToastService,
|
||||
protected sdkService: SdkService,
|
||||
private sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
this.cardBrandOptions = [
|
||||
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||
{ name: "Visa", value: "Visa" },
|
||||
{ name: "Mastercard", value: "Mastercard" },
|
||||
{ name: "American Express", value: "Amex" },
|
||||
{ name: "Discover", value: "Discover" },
|
||||
{ name: "Diners Club", value: "Diners Club" },
|
||||
{ name: "JCB", value: "JCB" },
|
||||
{ name: "Maestro", value: "Maestro" },
|
||||
{ name: "UnionPay", value: "UnionPay" },
|
||||
{ name: "RuPay", value: "RuPay" },
|
||||
{ name: i18nService.t("other"), value: "Other" },
|
||||
];
|
||||
this.cardExpMonthOptions = [
|
||||
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||
{ name: "01 - " + i18nService.t("january"), value: "1" },
|
||||
{ name: "02 - " + i18nService.t("february"), value: "2" },
|
||||
{ name: "03 - " + i18nService.t("march"), value: "3" },
|
||||
{ name: "04 - " + i18nService.t("april"), value: "4" },
|
||||
{ name: "05 - " + i18nService.t("may"), value: "5" },
|
||||
{ name: "06 - " + i18nService.t("june"), value: "6" },
|
||||
{ name: "07 - " + i18nService.t("july"), value: "7" },
|
||||
{ name: "08 - " + i18nService.t("august"), value: "8" },
|
||||
{ name: "09 - " + i18nService.t("september"), value: "9" },
|
||||
{ name: "10 - " + i18nService.t("october"), value: "10" },
|
||||
{ name: "11 - " + i18nService.t("november"), value: "11" },
|
||||
{ name: "12 - " + i18nService.t("december"), value: "12" },
|
||||
];
|
||||
this.identityTitleOptions = [
|
||||
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
||||
{ name: i18nService.t("mr"), value: i18nService.t("mr") },
|
||||
{ name: i18nService.t("mrs"), value: i18nService.t("mrs") },
|
||||
{ name: i18nService.t("ms"), value: i18nService.t("ms") },
|
||||
{ name: i18nService.t("mx"), value: i18nService.t("mx") },
|
||||
{ name: i18nService.t("dr"), value: i18nService.t("dr") },
|
||||
];
|
||||
this.uriMatchOptions = [
|
||||
{ name: i18nService.t("defaultMatchDetection"), value: null },
|
||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
||||
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||
];
|
||||
this.autofillOnPageLoadOptions = [
|
||||
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },
|
||||
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
|
||||
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||
),
|
||||
concatMap(async (policyAppliesToActiveUser) => {
|
||||
this.organizationDataOwnershipAppliesToUser = policyAppliesToActiveUser;
|
||||
await this.init();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.ownershipOptions.length) {
|
||||
this.ownershipOptions = [];
|
||||
}
|
||||
if (this.organizationDataOwnershipAppliesToUser) {
|
||||
this.allowPersonal = false;
|
||||
} else {
|
||||
const myEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
this.ownershipOptions.push({ name: myEmail, value: null });
|
||||
}
|
||||
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
const orgs = await firstValueFrom(this.organizationService.organizations$(userId));
|
||||
orgs
|
||||
.filter((org) => org.isMember)
|
||||
.sort(Utils.getSortFunction(this.i18nService, "name"))
|
||||
.forEach((o) => {
|
||||
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
|
||||
this.ownershipOptions.push({ name: o.name, value: o.id });
|
||||
}
|
||||
});
|
||||
if (!this.allowPersonal && this.organizationId == undefined) {
|
||||
this.organizationId = this.defaultOwnerId;
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.editMode = this.cipherId != null;
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
if (this.cloneMode) {
|
||||
this.cloneMode = true;
|
||||
this.title = this.i18nService.t("addItem");
|
||||
} else {
|
||||
this.title = this.i18nService.t("editItem");
|
||||
}
|
||||
} else {
|
||||
this.title = this.i18nService.t("addItem");
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(activeUserId);
|
||||
|
||||
if (this.cipher == null) {
|
||||
if (this.editMode) {
|
||||
const cipher = await this.loadCipher(activeUserId);
|
||||
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
// Adjust Cipher Name if Cloning
|
||||
if (this.cloneMode) {
|
||||
this.cipher.name += " - " + this.i18nService.t("clone");
|
||||
// If not allowing personal ownership, update cipher's org Id to prompt downstream changes
|
||||
if (this.cipher.organizationId == null && !this.allowPersonal) {
|
||||
this.cipher.organizationId = this.organizationId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.cipher = new CipherView();
|
||||
this.cipher.organizationId = this.organizationId == null ? null : this.organizationId;
|
||||
this.cipher.folderId = this.folderId;
|
||||
this.cipher.type = this.type == null ? CipherType.Login : this.type;
|
||||
this.cipher.login = new LoginView();
|
||||
this.cipher.login.uris = [new LoginUriView()];
|
||||
this.cipher.card = new CardView();
|
||||
this.cipher.identity = new IdentityView();
|
||||
this.cipher.secureNote = new SecureNoteView();
|
||||
this.cipher.secureNote.type = SecureNoteType.Generic;
|
||||
this.cipher.sshKey = new SshKeyView();
|
||||
this.cipher.reprompt = CipherRepromptType.None;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cipher != null && (!this.editMode || loadedAddEditCipherInfo || this.cloneMode)) {
|
||||
await this.organizationChanged();
|
||||
if (
|
||||
this.collectionIds != null &&
|
||||
this.collectionIds.length > 0 &&
|
||||
this.collections.length > 0
|
||||
) {
|
||||
this.collections.forEach((c) => {
|
||||
if (this.collectionIds.indexOf(c.id) > -1) {
|
||||
(c as any).checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only Admins can clone a cipher to different owner
|
||||
if (this.cloneMode && this.cipher.organizationId != null) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const cipherOrg = (
|
||||
await firstValueFrom(this.organizationService.memberOrganizations$(activeUserId))
|
||||
).find((o) => o.id === this.cipher.organizationId);
|
||||
|
||||
if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) {
|
||||
this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }];
|
||||
}
|
||||
}
|
||||
|
||||
// We don't want to copy passkeys when we clone a cipher
|
||||
if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) {
|
||||
this.cipher.login.fido2Credentials = null;
|
||||
}
|
||||
|
||||
this.folders$ = this.folderService.folderViews$(activeUserId);
|
||||
|
||||
if (this.editMode && this.previousCipherId !== this.cipherId) {
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, [this.cipher]);
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
|
||||
if (this.reprompt) {
|
||||
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
|
||||
}
|
||||
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
|
||||
this.cipher,
|
||||
this.isAdminConsoleAction,
|
||||
);
|
||||
|
||||
if (!this.editMode || this.cloneMode) {
|
||||
// Creating an ssh key directly while filtering to the ssh key category
|
||||
// must force a key to be set. SSH keys must never be created with an empty private key field
|
||||
if (
|
||||
this.cipher.type === CipherType.SshKey &&
|
||||
(this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "")
|
||||
) {
|
||||
await this.generateSshKey(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (this.cipher.isDeleted) {
|
||||
return this.restore();
|
||||
}
|
||||
|
||||
// normalize card expiry year on save
|
||||
if (this.cipher.type === this.cipherType.Card) {
|
||||
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
|
||||
}
|
||||
|
||||
// trim whitespace from the TOTP field
|
||||
if (this.cipher.type === this.cipherType.Login && this.cipher.login.totp) {
|
||||
this.cipher.login.totp = this.cipher.login.totp.trim();
|
||||
}
|
||||
|
||||
if (this.cipher.name == null || this.cipher.name === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("nameRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(!this.editMode || this.cloneMode) &&
|
||||
!this.allowPersonal &&
|
||||
this.cipher.organizationId == null
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("personalOwnershipSubmitError"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(!this.editMode || this.cloneMode) &&
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.uris != null &&
|
||||
this.cipher.login.uris.length === 1 &&
|
||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||
) {
|
||||
this.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
// Allows saving of selected collections during "Add" and "Clone" flows
|
||||
if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) {
|
||||
this.cipher.collectionIds =
|
||||
this.collections == null
|
||||
? []
|
||||
: this.collections.filter((c) => (c as any).checked).map((c) => c.id);
|
||||
}
|
||||
|
||||
// Clear current Cipher Id if exists to trigger "Add" cipher flow
|
||||
if (this.cloneMode) {
|
||||
this.cipher.id = null;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.encryptCipher(activeUserId);
|
||||
|
||||
try {
|
||||
this.formPromise = this.saveCipher(cipher);
|
||||
const savedCipher = await this.formPromise;
|
||||
|
||||
// Reset local cipher from the saved cipher returned from the server
|
||||
this.cipher = await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId),
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"),
|
||||
});
|
||||
this.onSavedCipher.emit(this.cipher);
|
||||
this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher");
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
addUri() {
|
||||
if (this.cipher.type !== CipherType.Login) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cipher.login.uris == null) {
|
||||
this.cipher.login.uris = [];
|
||||
}
|
||||
|
||||
this.cipher.login.uris.push(new LoginUriView());
|
||||
}
|
||||
|
||||
removeUri(uri: LoginUriView) {
|
||||
if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i = this.cipher.login.uris.indexOf(uri);
|
||||
if (i > -1) {
|
||||
this.cipher.login.uris.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
removePasskey() {
|
||||
if (this.cipher.type !== CipherType.Login || this.cipher.login.fido2Credentials == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cipher.login.fido2Credentials = null;
|
||||
}
|
||||
|
||||
onCardNumberChange(): void {
|
||||
this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number);
|
||||
}
|
||||
|
||||
getCardExpMonthDisplay() {
|
||||
return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name;
|
||||
}
|
||||
|
||||
trackByFunction(index: number, item: any) {
|
||||
return index;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCancelled.emit(this.cipher);
|
||||
}
|
||||
|
||||
attachments() {
|
||||
this.onEditAttachments.emit(this.cipher);
|
||||
}
|
||||
|
||||
share() {
|
||||
this.onShareCipher.emit(this.cipher);
|
||||
}
|
||||
|
||||
editCollections() {
|
||||
this.onEditCollections.emit(this.cipher);
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.deletePromise = this.deleteCipher(activeUserId);
|
||||
await this.deletePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
|
||||
),
|
||||
});
|
||||
this.onDeletedCipher.emit(this.cipher);
|
||||
this.messagingService.send(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher",
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async restore(): Promise<boolean> {
|
||||
if (!this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.restorePromise = this.restoreCipher(activeUserId);
|
||||
await this.restorePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("restoredItem"),
|
||||
});
|
||||
this.onRestoredCipher.emit(this.cipher);
|
||||
this.messagingService.send("restoredCipher");
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<boolean> {
|
||||
if (this.cipher.login?.username?.length) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwriteUsername" },
|
||||
content: { key: "overwriteUsernameConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.onGenerateUsername.emit();
|
||||
return true;
|
||||
}
|
||||
|
||||
async generatePassword(): Promise<boolean> {
|
||||
if (this.cipher.login?.password?.length) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwritePassword" },
|
||||
content: { key: "overwritePasswordConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.onGeneratePassword.emit();
|
||||
return true;
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
|
||||
if (this.editMode && this.showPassword) {
|
||||
document.getElementById("loginPassword")?.focus();
|
||||
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledPasswordVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTotpSeed() {
|
||||
this.showTotpSeed = !this.showTotpSeed;
|
||||
|
||||
if (this.editMode && this.showTotpSeed) {
|
||||
document.getElementById("loginTotp")?.focus();
|
||||
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledTOTPSeedVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCardNumber() {
|
||||
this.showCardNumber = !this.showCardNumber;
|
||||
if (this.showCardNumber) {
|
||||
void this.eventCollectionService.collectMany(
|
||||
EventType.Cipher_ClientToggledCardNumberVisible,
|
||||
[this.cipher],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCardCode() {
|
||||
this.showCardCode = !this.showCardCode;
|
||||
document.getElementById("cardCode").focus();
|
||||
if (this.editMode && this.showCardCode) {
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledCardCodeVisible, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
togglePrivateKey() {
|
||||
this.showPrivateKey = !this.showPrivateKey;
|
||||
}
|
||||
|
||||
toggleUriOptions(uri: LoginUriView) {
|
||||
const u = uri as any;
|
||||
u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions;
|
||||
}
|
||||
|
||||
loginUriMatchChanged(uri: LoginUriView) {
|
||||
const u = uri as any;
|
||||
u.showOptions = u.showOptions == null ? true : u.showOptions;
|
||||
}
|
||||
|
||||
async organizationChanged() {
|
||||
if (this.writeableCollections != null) {
|
||||
this.writeableCollections.forEach((c) => ((c as any).checked = false));
|
||||
}
|
||||
if (this.cipher.organizationId != null) {
|
||||
this.collections = this.writeableCollections?.filter(
|
||||
(c) => c.organizationId === this.cipher.organizationId,
|
||||
);
|
||||
// If there's only one collection, check it by default
|
||||
if (this.collections.length === 1) {
|
||||
(this.collections[0] as any).checked = true;
|
||||
}
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const org = (
|
||||
await firstValueFrom(this.organizationService.organizations$(activeUserId))
|
||||
).find((org) => org.id === this.cipher.organizationId);
|
||||
if (org != null) {
|
||||
this.cipher.organizationUseTotp = org.useTotp;
|
||||
}
|
||||
} else {
|
||||
this.collections = [];
|
||||
}
|
||||
}
|
||||
|
||||
async checkPassword() {
|
||||
if (this.checkPasswordPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.cipher.login == null ||
|
||||
this.cipher.login.password == null ||
|
||||
this.cipher.login.password === ""
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
|
||||
const matches = await this.checkPasswordPromise;
|
||||
this.checkPasswordPromise = null;
|
||||
|
||||
if (matches > 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: null,
|
||||
message: this.i18nService.t("passwordExposed", matches.toString()),
|
||||
});
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("passwordSafe"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
repromptChanged() {
|
||||
this.reprompt = !this.reprompt;
|
||||
if (this.reprompt) {
|
||||
this.cipher.reprompt = CipherRepromptType.Password;
|
||||
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
|
||||
} else {
|
||||
this.cipher.reprompt = CipherRepromptType.None;
|
||||
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadCollections() {
|
||||
const allCollections = await this.collectionService.getAllDecrypted();
|
||||
return allCollections.filter((c) => !c.readOnly);
|
||||
}
|
||||
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected encryptCipher(userId: UserId) {
|
||||
return this.cipherService.encrypt(this.cipher, userId);
|
||||
}
|
||||
|
||||
protected saveCipher(data: EncryptionContext) {
|
||||
let orgAdmin = this.organization?.canEditAllCiphers;
|
||||
|
||||
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
|
||||
if (!data.cipher.collectionIds) {
|
||||
orgAdmin = this.organization?.canEditUnassignedCiphers;
|
||||
}
|
||||
|
||||
return this.cipher.id == null
|
||||
? this.cipherService.createWithServer(data, orgAdmin)
|
||||
: this.cipherService.updateWithServer(data, orgAdmin);
|
||||
}
|
||||
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a cipher must be deleted as an admin by belonging to an organization and being unassigned to a collection.
|
||||
*/
|
||||
get asAdmin(): boolean {
|
||||
return (
|
||||
this.cipher.organizationId !== null &&
|
||||
this.cipher.organizationId.length > 0 &&
|
||||
(this.organization?.canEditAllCiphers ||
|
||||
!this.cipher.collectionIds ||
|
||||
this.cipher.collectionIds.length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
get defaultOwnerId(): string | null {
|
||||
return this.ownershipOptions[0].value;
|
||||
}
|
||||
|
||||
async loadAddEditCipherInfo(userId: UserId): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await firstValueFrom(
|
||||
this.cipherService.addEditCipherInfo$(userId),
|
||||
);
|
||||
const loadedSavedInfo = addEditCipherInfo != null;
|
||||
|
||||
if (loadedSavedInfo) {
|
||||
this.cipher = addEditCipherInfo.cipher;
|
||||
this.collectionIds = addEditCipherInfo.collectionIds;
|
||||
|
||||
if (!this.editMode && !this.allowPersonal && this.cipher.organizationId == null) {
|
||||
// This is a new cipher and personal ownership isn't allowed, so we need to set the default owner
|
||||
this.cipher.organizationId = this.defaultOwnerId;
|
||||
}
|
||||
}
|
||||
|
||||
await this.cipherService.setAddEditCipherInfo(null, userId);
|
||||
|
||||
return loadedSavedInfo;
|
||||
}
|
||||
|
||||
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
this.platformUtilsService.copyToClipboard(value, copyOptions);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
|
||||
});
|
||||
|
||||
if (typeI18nKey === "password") {
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [
|
||||
this.cipher,
|
||||
]);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedCardCode, [
|
||||
this.cipher,
|
||||
]);
|
||||
} else if (aType === "H_Field") {
|
||||
void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedHiddenField, [
|
||||
this.cipher,
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async importSshKeyFromClipboard() {
|
||||
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
|
||||
if (key != null) {
|
||||
this.cipher.sshKey.privateKey = key.privateKey;
|
||||
this.cipher.sshKey.publicKey = key.publicKey;
|
||||
this.cipher.sshKey.keyFingerprint = key.keyFingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSshKey(showNotification: boolean = true) {
|
||||
await firstValueFrom(this.sdkService.client$);
|
||||
const sshKey = generate_ssh_key("Ed25519");
|
||||
this.cipher.sshKey.privateKey = sshKey.privateKey;
|
||||
this.cipher.sshKey.publicKey = sshKey.publicKey;
|
||||
this.cipher.sshKey.keyFingerprint = sshKey.fingerprint;
|
||||
|
||||
if (showNotification) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("sshKeyGenerated"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async typeChange() {
|
||||
if (this.cipher.type === CipherType.SshKey) {
|
||||
await this.generateSshKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Directive()
|
||||
export class AttachmentsComponent implements OnInit {
|
||||
@Input() cipherId: string;
|
||||
@Input() viewOnly: boolean;
|
||||
@Output() onUploadedAttachment = new EventEmitter<CipherView>();
|
||||
@Output() onDeletedAttachment = new EventEmitter();
|
||||
@Output() onReuploadedAttachment = new EventEmitter();
|
||||
|
||||
cipher: CipherView;
|
||||
cipherDomain: Cipher;
|
||||
canAccessAttachments: boolean;
|
||||
formPromise: Promise<any>;
|
||||
deletePromises: { [id: string]: Promise<CipherData> } = {};
|
||||
reuploadPromises: { [id: string]: Promise<any> } = {};
|
||||
emergencyAccessId?: string = null;
|
||||
protected componentName = "";
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected i18nService: I18nService,
|
||||
protected keyService: KeyService,
|
||||
protected encryptService: EncryptService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected apiService: ApiService,
|
||||
protected win: Window,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogService,
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (files[0].size > 524288000) {
|
||||
// 500 MB
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("maxFileSize"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||
this.cipherDomain = await this.formPromise;
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("attachmentSaved"),
|
||||
});
|
||||
this.onUploadedAttachment.emit(this.cipher);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
// reset file input
|
||||
// ref: https://stackoverflow.com/a/20552042
|
||||
fileEl.type = "";
|
||||
fileEl.type = "file";
|
||||
fileEl.value = "";
|
||||
}
|
||||
|
||||
async delete(attachment: AttachmentView) {
|
||||
if (this.deletePromises[attachment.id] != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteAttachment" },
|
||||
content: { key: "deleteAttachmentConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id, activeUserId);
|
||||
const updatedCipher = await this.deletePromises[attachment.id];
|
||||
|
||||
const cipher = new Cipher(updatedCipher);
|
||||
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedAttachment"),
|
||||
});
|
||||
const i = this.cipher.attachments.indexOf(attachment);
|
||||
if (i > -1) {
|
||||
this.cipher.attachments.splice(i, 1);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.deletePromises[attachment.id] = null;
|
||||
this.onDeletedAttachment.emit(this.cipher);
|
||||
}
|
||||
|
||||
async download(attachment: AttachmentView) {
|
||||
const a = attachment as any;
|
||||
if (a.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canAccessAttachments) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("premiumRequired"),
|
||||
message: this.i18nService.t("premiumRequiredDesc"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
try {
|
||||
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||
this.cipher.id,
|
||||
attachment.id,
|
||||
this.emergencyAccessId,
|
||||
);
|
||||
url = attachmentDownloadResponse.url;
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||
url = attachment.url;
|
||||
} else if (e instanceof ErrorResponse) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
a.downloading = true;
|
||||
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||
if (response.status !== 200) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
a.downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipherDomain.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
});
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("fileSavedToDevice"),
|
||||
});
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
|
||||
a.downloading = false;
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||
);
|
||||
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
|
||||
|
||||
if (!this.canAccessAttachments) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "premiumRequired" },
|
||||
content: { key: "premiumRequiredDesc" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
type: "success",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri(
|
||||
"https://vault.bitwarden.com/#/settings/subscription/premium",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) {
|
||||
const a = attachment as any;
|
||||
if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => {
|
||||
// 1. Download
|
||||
a.downloading = true;
|
||||
const response = await fetch(new Request(attachment.url, { cache: "no-store" }));
|
||||
if (response.status !== 200) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
a.downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Resave
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipherDomain.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
attachment.fileName,
|
||||
decBuf,
|
||||
activeUserId,
|
||||
admin,
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
|
||||
// 3. Delete old
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
||||
attachment.id,
|
||||
activeUserId,
|
||||
);
|
||||
await this.deletePromises[attachment.id];
|
||||
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
|
||||
if (foundAttachment.length > 0) {
|
||||
const i = this.cipher.attachments.indexOf(foundAttachment[0]);
|
||||
if (i > -1) {
|
||||
this.cipher.attachments.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("attachmentSaved"),
|
||||
});
|
||||
this.onReuploadedAttachment.emit();
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
|
||||
a.downloading = false;
|
||||
});
|
||||
await this.reuploadPromises[attachment.id];
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected saveCipherAttachment(file: File, userId: UserId) {
|
||||
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
|
||||
return this.cipherService.deleteAttachmentWithServer(
|
||||
this.cipher.id,
|
||||
attachmentId,
|
||||
userId,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
// TODO: This should be removed but is needed since we re-use the same template
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class PasswordHistoryComponent implements OnInit {
|
||||
cipherId: string;
|
||||
history: PasswordHistoryView[] = [];
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected accountService: AccountService,
|
||||
private win: Window,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
copy(password: string) {
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
this.platformUtilsService.copyToClipboard(password, copyOptions);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
|
||||
});
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const decCipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Input } from "@angular/core";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
|
||||
@Directive()
|
||||
export class ViewCustomFieldsComponent {
|
||||
@Input() cipher: CipherView;
|
||||
@Input() promptPassword: () => Promise<boolean>;
|
||||
@Input() copy: (value: string, typeI18nKey: string, aType: string) => void;
|
||||
|
||||
fieldType = FieldType;
|
||||
|
||||
constructor(private eventCollectionService: EventCollectionService) {}
|
||||
|
||||
async toggleFieldValue(field: FieldView) {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const f = field as any;
|
||||
f.showValue = !f.showValue;
|
||||
f.showCount = false;
|
||||
if (f.showValue) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||
this.cipher.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleFieldCount(field: FieldView) {
|
||||
if (!field.showValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.showCount = !field.showCount;
|
||||
}
|
||||
|
||||
setTextDataOnDrag(event: DragEvent, data: string) {
|
||||
event.dataTransfer.setData("text", data);
|
||||
}
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { TotpInfo } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "BaseViewComponent";
|
||||
|
||||
@Directive()
|
||||
export class ViewComponent implements OnDestroy, OnInit {
|
||||
/** Observable of cipherId$ that will update each time the `Input` updates */
|
||||
private _cipherId$ = new BehaviorSubject<string>(null);
|
||||
|
||||
@Input()
|
||||
set cipherId(value: string) {
|
||||
this._cipherId$.next(value);
|
||||
}
|
||||
|
||||
get cipherId(): string {
|
||||
return this._cipherId$.getValue();
|
||||
}
|
||||
|
||||
@Input() collectionId: string;
|
||||
@Output() onEditCipher = new EventEmitter<CipherView>();
|
||||
@Output() onCloneCipher = new EventEmitter<CipherView>();
|
||||
@Output() onShareCipher = new EventEmitter<CipherView>();
|
||||
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
||||
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
||||
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
canRestoreCipher$: Observable<boolean>;
|
||||
cipher: CipherView;
|
||||
showPassword: boolean;
|
||||
showPasswordCount: boolean;
|
||||
showCardNumber: boolean;
|
||||
showCardCode: boolean;
|
||||
showPrivateKey: boolean;
|
||||
canAccessPremium: boolean;
|
||||
showPremiumRequiredTotp: boolean;
|
||||
fieldType = FieldType;
|
||||
checkPasswordPromise: Promise<number>;
|
||||
folder: FolderView;
|
||||
cipherType = CipherType;
|
||||
|
||||
private previousCipherId: string;
|
||||
protected passwordReprompted = false;
|
||||
|
||||
/**
|
||||
* Represents TOTP information including display formatting and timing
|
||||
*/
|
||||
protected totpInfo$: Observable<TotpInfo> | undefined;
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
this.cipher?.login?.fido2Credentials?.[0]?.creationDate,
|
||||
"short",
|
||||
);
|
||||
return `${dateCreated} ${creationDate}`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected folderService: FolderService,
|
||||
protected totpService: TotpService,
|
||||
protected tokenService: TokenService,
|
||||
protected i18nService: I18nService,
|
||||
protected keyService: KeyService,
|
||||
protected encryptService: EncryptService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected auditService: AuditService,
|
||||
protected win: Window,
|
||||
protected broadcasterService: BroadcasterService,
|
||||
protected ngZone: NgZone,
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
protected eventCollectionService: EventCollectionService,
|
||||
protected apiService: ApiService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogService,
|
||||
protected datePipe: DatePipe,
|
||||
protected accountService: AccountService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected toastService: ToastService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set up the subscription to the activeAccount$ and cipherId$ observables
|
||||
combineLatest([this.accountService.activeAccount$.pipe(getUserId), this._cipherId$])
|
||||
.pipe(
|
||||
tap(() => this.cleanUp()),
|
||||
switchMap(([userId, cipherId]) => {
|
||||
const cipher$ = this.cipherService.cipherViews$(userId).pipe(
|
||||
map((ciphers) => ciphers?.find((c) => c.id === cipherId)),
|
||||
filter((cipher) => !!cipher),
|
||||
);
|
||||
return combineLatest([of(userId), cipher$]);
|
||||
}),
|
||||
)
|
||||
.subscribe(([userId, cipher]) => {
|
||||
this.cipher = cipher;
|
||||
|
||||
void this.constructCipherDetails(userId);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.cleanUp();
|
||||
}
|
||||
|
||||
async edit() {
|
||||
this.onEditCipher.emit(this.cipher);
|
||||
}
|
||||
|
||||
async clone() {
|
||||
if (this.cipher.login?.hasFido2Credentials) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (await this.promptPassword()) {
|
||||
this.onCloneCipher.emit(this.cipher);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async share() {
|
||||
if (await this.promptPassword()) {
|
||||
this.onShareCipher.emit(this.cipher);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.deleteCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem",
|
||||
),
|
||||
});
|
||||
this.onDeletedCipher.emit(this.cipher);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async restore(): Promise<boolean> {
|
||||
if (!this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.restoreCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("restoredItem"),
|
||||
});
|
||||
this.onRestoredCipher.emit(this.cipher);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async togglePassword() {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPassword = !this.showPassword;
|
||||
this.showPasswordCount = false;
|
||||
if (this.showPassword) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledPasswordVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async togglePasswordCount() {
|
||||
if (!this.showPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPasswordCount = !this.showPasswordCount;
|
||||
}
|
||||
|
||||
async toggleCardNumber() {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showCardNumber = !this.showCardNumber;
|
||||
if (this.showCardNumber) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledCardNumberVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCardCode() {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showCardCode = !this.showCardCode;
|
||||
if (this.showCardCode) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientToggledCardCodeVisible,
|
||||
this.cipherId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
togglePrivateKey() {
|
||||
this.showPrivateKey = !this.showPrivateKey;
|
||||
}
|
||||
|
||||
async checkPassword() {
|
||||
if (
|
||||
this.cipher.login == null ||
|
||||
this.cipher.login.password == null ||
|
||||
this.cipher.login.password === ""
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
|
||||
const matches = await this.checkPasswordPromise;
|
||||
|
||||
if (matches > 0) {
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: null,
|
||||
message: this.i18nService.t("passwordExposed", matches.toString()),
|
||||
});
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("passwordSafe"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async launch(uri: Launchable, cipherId?: string) {
|
||||
if (!uri.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipherId) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId, activeUserId);
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
}
|
||||
|
||||
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.promptPassword())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copyOptions = this.win != null ? { window: this.win } : null;
|
||||
this.platformUtilsService.copyToClipboard(value, copyOptions);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
|
||||
});
|
||||
|
||||
if (typeI18nKey === "password") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId);
|
||||
} else if (aType === "H_Field") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setTextDataOnDrag(event: DragEvent, data: string) {
|
||||
event.dataTransfer.setData("text", data);
|
||||
}
|
||||
|
||||
async downloadAttachment(attachment: AttachmentView) {
|
||||
if (!(await this.promptPassword())) {
|
||||
return;
|
||||
}
|
||||
const a = attachment as any;
|
||||
if (a.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cipher.organizationId == null && !this.canAccessPremium) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("premiumRequired"),
|
||||
message: this.i18nService.t("premiumRequiredDesc"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
try {
|
||||
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||
this.cipher.id,
|
||||
attachment.id,
|
||||
);
|
||||
url = attachmentDownloadResponse.url;
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||
url = attachment.url;
|
||||
} else if (e instanceof ErrorResponse) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
a.downloading = true;
|
||||
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||
if (response.status !== 200) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
a.downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipher.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
|
||||
a.downloading = false;
|
||||
}
|
||||
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected async promptPassword() {
|
||||
if (this.cipher.reprompt === CipherRepromptType.None || this.passwordReprompted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (this.passwordReprompted = await this.passwordRepromptService.showPasswordPrompt());
|
||||
}
|
||||
|
||||
private cleanUp() {
|
||||
this.cipher = null;
|
||||
this.folder = null;
|
||||
this.showPassword = false;
|
||||
this.showCardNumber = false;
|
||||
this.showCardCode = false;
|
||||
this.passwordReprompted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a cipher is viewed, construct all details for the view that are not directly
|
||||
* available from the cipher object itself.
|
||||
*/
|
||||
private async constructCipherDetails(userId: UserId) {
|
||||
this.canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
);
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher);
|
||||
this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher);
|
||||
|
||||
if (this.cipher.folderId) {
|
||||
this.folder = await (
|
||||
await firstValueFrom(this.folderService.folderViews$(userId))
|
||||
).find((f) => f.id == this.cipher.folderId);
|
||||
}
|
||||
|
||||
const canGenerateTotp =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium);
|
||||
|
||||
this.totpInfo$ = canGenerateTotp
|
||||
? this.totpService.getCode$(this.cipher.login.totp).pipe(
|
||||
map((response) => {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % response.period;
|
||||
|
||||
// Format code
|
||||
const totpCodeFormatted =
|
||||
response.code.length > 4
|
||||
? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}`
|
||||
: response.code;
|
||||
|
||||
return {
|
||||
totpCode: response.code,
|
||||
totpCodeFormatted,
|
||||
totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"),
|
||||
totpSec: response.period - mod,
|
||||
totpLow: response.period - mod <= 7,
|
||||
} as TotpInfo;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this.previousCipherId !== this.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
||||
}
|
||||
this.previousCipherId = this.cipherId;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ export enum FeatureFlag {
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
||||
EndUserNotifications = "pm-10609-end-user-notifications",
|
||||
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
||||
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
|
||||
@@ -97,7 +96,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
|
||||
[FeatureFlag.EndUserNotifications]: FALSE,
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user