1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +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:
Jason Ng
2025-07-10 15:30:23 -04:00
committed by GitHub
parent 7cf10ba421
commit bf50160a47
34 changed files with 5 additions and 6000 deletions

View File

@@ -17,7 +17,6 @@ import {
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import {
LoginComponent,
LoginSecondaryContentComponent,
@@ -50,7 +49,6 @@ import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault/vault.component";
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
import { SendComponent } from "./tools/send/send.component";
@@ -102,15 +100,11 @@ const routes: Routes = [
},
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
...featureFlaggedRoute({
defaultComponent: VaultComponent,
flaggedComponent: VaultV2Component,
featureFlag: FeatureFlag.PM18520_UpdateDesktopCipherForm,
routeOptions: {
path: "vault",
canActivate: [authGuard],
},
}),
{
path: "vault",
component: VaultV2Component,
canActivate: [authGuard],
},
{ path: "set-password", component: SetPasswordComponent },
{
path: "send",

View File

@@ -18,19 +18,8 @@ import { UpdateTempPasswordComponent } from "../auth/update-temp-password.compon
import { SshAgentService } from "../autofill/services/ssh-agent.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";
import { CollectionsComponent } from "../vault/app/vault/collections.component";
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
import { PasswordHistoryComponent } from "../vault/app/vault/password-history.component";
import { ShareComponent } from "../vault/app/vault/share.component";
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
import { VaultItemsComponent } from "../vault/app/vault/vault-items.component";
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
import { VaultComponent } from "../vault/app/vault/vault.component";
import { ViewCustomFieldsComponent } from "../vault/app/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/app/vault/view.component";
import { SettingsComponent } from "./accounts/settings.component";
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
@@ -61,28 +50,17 @@ import { SharedModule } from "./shared/shared.module";
],
declarations: [
AccountSwitcherComponent,
AddEditComponent,
AddEditCustomFieldsComponent,
AppComponent,
AttachmentsComponent,
CollectionsComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
FolderAddEditComponent,
HeaderComponent,
PasswordHistoryComponent,
PremiumComponent,
RemovePasswordComponent,
SearchComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
UpdateTempPasswordComponent,
VaultComponent,
VaultItemsComponent,
VaultTimeoutInputComponent,
ViewCustomFieldsComponent,
ViewComponent,
],
providers: [SshAgentService],
bootstrap: [AppComponent],

View File

@@ -441,10 +441,6 @@ img,
user-select: none;
}
app-vault-view .box-footer {
user-select: auto;
}
/* override for vault icon in desktop */
app-vault-icon > div {
display: flex;

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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,
);
}
}

View File

@@ -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>

View File

@@ -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,
);
}
}

View File

@@ -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>

View File

@@ -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,
);
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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 || "&nbsp;" }}
</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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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");
}
}
}