612 lines
23 KiB
HTML
612 lines
23 KiB
HTML
<form #form="ngForm" (ngSubmit)="submit()" [appApiAction]="formPromise">
|
|
<div class="content">
|
|
<div class="inner-content" *ngIf="cipher">
|
|
<div class="box">
|
|
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
|
|
{{ "personalOwnershipPolicyInEffect" | i18n }}
|
|
</app-callout>
|
|
<div class="box-header">
|
|
{{ title }}
|
|
</div>
|
|
<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">
|
|
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</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"
|
|
/>
|
|
</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
|
|
/>
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button
|
|
type="button"
|
|
class="row-btn"
|
|
appStopClick
|
|
appA11yTitle="{{ 'generateUsername' | i18n }}"
|
|
(click)="generateUsername()"
|
|
>
|
|
<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"
|
|
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]="checkPasswordBtn.loading"
|
|
>
|
|
<i
|
|
class="bwi bwi-lg bwi-check-circle"
|
|
[hidden]="checkPasswordBtn.loading"
|
|
aria-hidden="true"
|
|
></i>
|
|
<i
|
|
class="bwi bwi-lg bwi-spinner bwi-spin"
|
|
[hidden]="!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()"
|
|
>
|
|
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
|
|
</button>
|
|
</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"
|
|
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"
|
|
/>
|
|
</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"
|
|
[(ngModel)]="cipher.card.number"
|
|
appInputVerbatim
|
|
/>
|
|
</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>
|
|
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
|
|
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="box-content-row" appBoxRow>
|
|
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
|
|
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
|
|
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
|
|
{{ o.name }}
|
|
</option>
|
|
</select>
|
|
</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' }}"
|
|
/>
|
|
</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
|
|
/>
|
|
</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>
|
|
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
|
|
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
|
|
{{ o.name }}
|
|
</option>
|
|
</select>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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
|
|
/>
|
|
</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"
|
|
/>
|
|
</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
|
|
/>
|
|
</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
|
|
/>
|
|
</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
|
|
/>
|
|
</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
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="box" *ngIf="cipher.type === cipherType.Login">
|
|
<div class="box-content">
|
|
<ng-container *ngIf="cipher.login.hasUris">
|
|
<div
|
|
class="box-content-row box-content-row-multi"
|
|
appBoxRow
|
|
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
|
|
>
|
|
<button
|
|
type="button"
|
|
appStopClick
|
|
(click)="removeUri(u)"
|
|
appA11yTitle="{{ 'remove' | i18n }}"
|
|
>
|
|
<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"
|
|
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]="u.showOptions === false || (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]="
|
|
!(u.showOptions === false || (u.showOptions == null && u.match == null))
|
|
"
|
|
>
|
|
<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">
|
|
<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" [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()"
|
|
/>
|
|
</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">
|
|
<div class="box-header">
|
|
<label for="notes">{{ "notes" | i18n }}</label>
|
|
</div>
|
|
<div class="box-content">
|
|
<div class="box-content-row" appBoxRow>
|
|
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<app-vault-add-edit-custom-fields
|
|
[cipher]="cipher"
|
|
[thisCipherType]="cipher.type"
|
|
[editMode]="editMode"
|
|
>
|
|
</app-vault-add-edit-custom-fields>
|
|
<div class="box" *ngIf="allowOwnershipOptions()">
|
|
<div class="box-header">
|
|
{{ "ownership" | i18n }}
|
|
</div>
|
|
<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">
|
|
<div class="box-header">
|
|
{{ "collections" | i18n }}
|
|
</div>
|
|
<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>
|
|
<div class="footer">
|
|
<button
|
|
type="submit"
|
|
class="primary"
|
|
appA11yTitle="{{ 'save' | i18n }}"
|
|
[disabled]="form.loading"
|
|
>
|
|
<i class="bwi bwi-save-changes bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
|
|
<i
|
|
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
|
[hidden]="!form.loading"
|
|
aria-hidden="true"
|
|
></i>
|
|
</button>
|
|
<button type="button" (click)="cancel()">
|
|
{{ "cancel" | i18n }}
|
|
</button>
|
|
<div class="right">
|
|
<button
|
|
type="button"
|
|
(click)="share()"
|
|
appA11yTitle="{{ 'moveToOrganization' | i18n }}"
|
|
*ngIf="editMode && cipher && !cipher.organizationId && !cloneMode"
|
|
>
|
|
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw" aria-hidden="true"></i>
|
|
</button>
|
|
<button
|
|
#deleteBtn
|
|
type="button"
|
|
(click)="delete()"
|
|
class="danger"
|
|
appA11yTitle="{{ 'delete' | i18n }}"
|
|
*ngIf="editMode && !cloneMode"
|
|
[disabled]="deleteBtn.loading"
|
|
[appApiAction]="deletePromise"
|
|
>
|
|
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
|
<i
|
|
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
|
[hidden]="!deleteBtn.loading"
|
|
aria-hidden="true"
|
|
></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|