mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +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 { 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 { 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 { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
|
||||||
import {
|
import {
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
@@ -50,7 +49,6 @@ import { SetPasswordComponent } from "../auth/set-password.component";
|
|||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.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 { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||||
import { SendComponent } from "./tools/send/send.component";
|
import { SendComponent } from "./tools/send/send.component";
|
||||||
@@ -102,15 +100,11 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||||
},
|
},
|
||||||
...featureFlaggedRoute({
|
{
|
||||||
defaultComponent: VaultComponent,
|
|
||||||
flaggedComponent: VaultV2Component,
|
|
||||||
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
|
|
||||||
routeOptions: {
|
|
||||||
path: "vault",
|
path: "vault",
|
||||||
|
component: VaultV2Component,
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
{ path: "set-password", component: SetPasswordComponent },
|
{ path: "set-password", component: SetPasswordComponent },
|
||||||
{
|
{
|
||||||
path: "send",
|
path: "send",
|
||||||
|
|||||||
@@ -18,19 +18,8 @@ import { UpdateTempPasswordComponent } from "../auth/update-temp-password.compon
|
|||||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.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 { 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 { 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 { SettingsComponent } from "./accounts/settings.component";
|
||||||
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
|
||||||
@@ -61,28 +50,17 @@ import { SharedModule } from "./shared/shared.module";
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AccountSwitcherComponent,
|
AccountSwitcherComponent,
|
||||||
AddEditComponent,
|
|
||||||
AddEditCustomFieldsComponent,
|
|
||||||
AppComponent,
|
AppComponent,
|
||||||
AttachmentsComponent,
|
|
||||||
CollectionsComponent,
|
|
||||||
ColorPasswordPipe,
|
ColorPasswordPipe,
|
||||||
ColorPasswordCountPipe,
|
ColorPasswordCountPipe,
|
||||||
FolderAddEditComponent,
|
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
PasswordHistoryComponent,
|
|
||||||
PremiumComponent,
|
PremiumComponent,
|
||||||
RemovePasswordComponent,
|
RemovePasswordComponent,
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
SetPasswordComponent,
|
SetPasswordComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
ShareComponent,
|
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
VaultComponent,
|
|
||||||
VaultItemsComponent,
|
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
ViewCustomFieldsComponent,
|
|
||||||
ViewComponent,
|
|
||||||
],
|
],
|
||||||
providers: [SshAgentService],
|
providers: [SshAgentService],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -441,10 +441,6 @@ img,
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
app-vault-view .box-footer {
|
|
||||||
user-select: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* override for vault icon in desktop */
|
/* override for vault icon in desktop */
|
||||||
app-vault-icon > div {
|
app-vault-icon > div {
|
||||||
display: flex;
|
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",
|
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||||
CipherKeyEncryption = "cipher-key-encryption",
|
CipherKeyEncryption = "cipher-key-encryption",
|
||||||
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
|
|
||||||
EndUserNotifications = "pm-10609-end-user-notifications",
|
EndUserNotifications = "pm-10609-end-user-notifications",
|
||||||
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
|
||||||
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
|
PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp",
|
||||||
@@ -97,7 +96,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||||
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
|
|
||||||
[FeatureFlag.EndUserNotifications]: FALSE,
|
[FeatureFlag.EndUserNotifications]: FALSE,
|
||||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||||
|
|||||||
Reference in New Issue
Block a user