1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-19 17:03:33 +00:00

Move desktop into apps/desktop

This commit is contained in:
Hinton
2022-05-05 17:16:23 +02:00
parent 9852f2ec22
commit 28bc4113b9
331 changed files with 2 additions and 2 deletions

View File

@@ -0,0 +1,93 @@
<div class="modal fade" role="dialog" aria-modal="true" attr.aria-label="{{ 'settings' | i18n }}">
<div class="modal-dialog" role="document">
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-body">
<div class="box">
<div class="box-header">
{{ "selfHostedEnvironment" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
name="BaseUrl"
[(ngModel)]="baseUrl"
placeholder="{{ 'ex' | i18n }} https://bitwarden.company.com"
appInputVerbatim
/>
</div>
</div>
<div class="box-footer">
{{ "selfHostedEnvironmentFooter" | i18n }}
</div>
</div>
<div class="box">
<div class="box-header">
<button type="button" (click)="toggleCustom()" [attr.aria-expanded]="showCustom">
<i class="bwi bwi-plus-square" [hidden]="showCustom" aria-hidden="true"></i>
<i class="bwi bwi-minus-square" [hidden]="!showCustom" aria-hidden="true"></i>
{{ "customEnvironment" | i18n }}
</button>
</div>
<div class="box-content" [hidden]="!showCustom">
<div class="box-content-row" appBoxRow>
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
name="WebVaultUrl"
[(ngModel)]="webVaultUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" appInputVerbatim />
</div>
<div class="box-content-row" appBoxRow>
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"
name="IdentityUrl"
[(ngModel)]="identityUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="notificationsUrl">{{ "notificationsUrl" | i18n }}</label>
<input
id="notificationsUrl"
type="text"
name="NotificationsUrl"
[(ngModel)]="notificationsUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="iconsUrl">{{ "iconsUrl" | i18n }}</label>
<input
id="iconsUrl"
type="text"
name="IconsUrl"
[(ngModel)]="iconsUrl"
appInputVerbatim
/>
</div>
</div>
<div class="box-footer" [hidden]="!showCustom">
{{ "customEnvironmentFooter" | i18n }}
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="primary" appA11yTitle="{{ 'save' | i18n }}">
<i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { EnvironmentComponent as BaseEnvironmentComponent } from "jslib-angular/components/environment.component";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-environment",
templateUrl: "environment.component.html",
})
export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
i18nService: I18nService
) {
super(platformUtilsService, environmentService, i18nService);
}
}

View File

@@ -0,0 +1,31 @@
<form id="hint-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<h1>{{ "passwordHint" | i18n }}</h1>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
appAutofocus
appInputVerbatim
/>
</div>
</div>
<div class="box-footer">
{{ "enterEmailToGetHint" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/login" class="btn block">{{ "cancel" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-hint",
templateUrl: "hint.component.html",
})
export class HintComponent extends BaseHintComponent {
constructor(
router: Router,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
apiService: ApiService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
}

View File

@@ -0,0 +1,75 @@
<form id="lock-page" (ngSubmit)="submit()">
<div class="content">
<p aria-hidden="true"><i class="bwi bwi-lock bwi-4x text-muted"></i></p>
<p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput">
<div class="row-main" *ngIf="pinLock">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="!pinLock">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<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>
</div>
</div>
</div>
<div class="box-footer">
{{ "loggedInAsOn" | i18n: email:webVaultHostname }}
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock">
<button
type="button"
class="btn block"
[ngClass]="{ 'primary font-weight-bold': hideInput }"
(click)="unlockBiometric()"
>
{{ biometricText | i18n }}
</button>
</div>
<div class="buttons-row">
<button type="submit" class="btn primary block" *ngIf="!hideInput">
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
</button>
<button type="button" class="btn block" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,107 @@
import { Component, NgZone, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ipcRenderer } from "electron";
import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
const BroadcasterSubscriptionId = "LockComponent";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
})
export class LockComponent extends BaseLockComponent implements OnDestroy {
private deferFocus: boolean = null;
constructor(
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
cryptoService: CryptoService,
vaultTimeoutService: VaultTimeoutService,
environmentService: EnvironmentService,
stateService: StateService,
apiService: ApiService,
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
logService: LogService,
keyConnectorService: KeyConnectorService
) {
super(
router,
i18nService,
platformUtilsService,
messagingService,
cryptoService,
vaultTimeoutService,
environmentService,
stateService,
apiService,
logService,
keyConnectorService,
ngZone
);
}
async ngOnInit() {
await super.ngOnInit();
const autoPromptBiometric = !(await this.stateService.getNoAutoPromptBiometrics());
this.route.queryParams.subscribe((params) => {
if (this.supportsBiometric && params.promptBiometric && autoPromptBiometric) {
setTimeout(async () => {
if (await ipcRenderer.invoke("windowVisible")) {
this.unlockBiometric();
}
}, 1000);
}
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
private focusInput() {
document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
}
}

View File

@@ -0,0 +1,104 @@
<div id="login-page">
<div class="login-header">
<button
type="button"
appStopClick
(click)="settings()"
class="environment-urls-settings-icon"
attr.aria-label="{{ 'settings' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
{{ "settings" | i18n }}
</button>
</div>
<form
id="login-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
attr.aria-hidden="{{ showingModal }}"
>
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
appInputVerbatim="false"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<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>
</div>
</div>
</div>
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
<div class="box-content-row">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading"
><i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }}</b
>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/register" class="btn block">
<i class="bwi bwi-pencil-square" aria-hidden="true"></i> {{ "createAccount" | i18n }}
</button>
</div>
<div class="buttons-row">
<button
type="button"
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')"
class="btn block"
>
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</button>
</div>
</div>
<div class="sub-options">
<button type="button" routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</button>
</div>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

@@ -0,0 +1,128 @@
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
import { Router } from "@angular/router";
import { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component";
import { ModalService } from "jslib-angular/services/modal.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { EnvironmentComponent } from "./environment.component";
const BroadcasterSubscriptionId = "LoginComponent";
@Component({
selector: "app-login",
templateUrl: "login.component.html",
})
export class LoginComponent extends BaseLoginComponent implements OnDestroy {
@ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef;
showingModal = false;
protected alwaysRememberEmail = true;
private deferFocus: boolean = null;
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
syncService: SyncService,
private modalService: ModalService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
private messagingService: MessagingService,
logService: LogService
) {
super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}
async ngOnInit() {
await super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async settings() {
const [modal, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent,
this.environmentModal
);
modal.onShown.subscribe(() => {
this.showingModal = true;
});
modal.onClosed.subscribe(() => {
this.showingModal = false;
});
childComponent.onSaved.subscribe(() => {
modal.close();
});
}
onWindowHidden() {
this.showPassword = false;
}
async submit() {
await super.submit();
if (this.captchaSiteKey) {
const content = document.getElementById("content") as HTMLDivElement;
content.setAttribute("style", "width:335px");
}
}
}

View File

@@ -0,0 +1,89 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="premiumTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="box">
<div class="box-header" id="premiumTitle">
{{ "premiumMembership" | i18n }}
</div>
<div class="box-content box-content-padded">
<div *ngIf="!isPremium">
<p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p>
<p>{{ "premiumSignUpAndGet" | i18n }}</p>
<ul class="bwi-ul">
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpTwoStep" | i18n }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-li bwi-check text-success" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<p class="text-center lead no-margin">
{{ "premiumPrice" | i18n: (price | currency: "$") }}
</p>
</div>
<div *ngIf="isPremium">
<p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p>
<p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="primary" (click)="manage()" *ngIf="isPremium">
<b>{{ "premiumManage" | i18n }}</b>
</button>
<button
#purchaseBtn
type="button"
class="primary"
(click)="purchase()"
*ngIf="!isPremium"
[disabled]="purchaseBtn.loading"
>
<b>{{ "premiumPurchase" | i18n }}</b>
</button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
<div class="right" *ngIf="!isPremium">
<button
#refreshBtn
type="button"
(click)="refresh()"
[disabled]="refreshBtn.loading"
appA11yTitle="{{ 'premiumRefresh' | i18n }}"
[appApiAction]="refreshPromise"
>
<i
class="bwi bwi-refresh bwi-lg bwi-fw"
[hidden]="refreshBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!refreshBtn.loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "jslib-angular/components/premium.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-premium",
templateUrl: "premium.component.html",
})
export class PremiumComponent extends BasePremiumComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService,
stateService: StateService
) {
super(i18nService, platformUtilsService, apiService, logService, stateService);
}
}

View File

@@ -0,0 +1,148 @@
<form id="register-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<h1>{{ "createAccount" | i18n }}</h1>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ masterPasswordScoreColor }}"
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="email !== ''"
(input)="updatePasswordStrength()"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="progress">
<div
class="progress-bar bg-{{ masterPasswordScoreColor }}"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div>
</div>
<div class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
</div>
<div class="box-content-row" [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
</div>
<div class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="box last" *ngIf="showTerms">
<div class="box-footer checkbox">
<input
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<label for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/login" class="btn block">{{ "cancel" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,73 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "jslib-angular/components/register.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
const BroadcasterSubscriptionId = "RegisterComponent";
@Component({
selector: "app-register",
templateUrl: "register.component.html",
})
export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy {
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
cryptoService: CryptoService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
environmentService: EnvironmentService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
logService: LogService
) {
super(
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
super.ngOnInit();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
}

View File

@@ -0,0 +1,26 @@
<div id="remove-password-page" *ngIf="!loading">
<div class="content">
<h1>{{ "removeMasterPassword" | i18n }}</h1>
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
<div class="buttons">
<button
type="submit"
class="btn primary block"
[disabled]="actionPromise"
(click)="convert()"
>
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!continuing" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn secondary block"
[disabled]="actionPromise"
(click)="leave()"
>
<b [hidden]="leaving">{{ "leaveOrganization" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!leaving" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "jslib-angular/components/remove-password.component";
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -0,0 +1,157 @@
<form id="set-password-page" #form>
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "setMasterPassword" | i18n }}</p>
<div class="box text-center" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div *ngIf="!syncLoading">
<div class="box">
<app-callout type="tip">{{ "ssoCompleteRegistration" | i18n }}</app-callout>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword"
>{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ masterPasswordScoreColor }}"
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
(input)="updatePasswordStrength()"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="progress">
<div
class="progress-bar bg-{{ masterPasswordScoreColor }}"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div>
</div>
<div class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
</div>
</div>
<div class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<i
*ngIf="form.loading"
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button class="btn block" (click)="logOut()">
<span>{{ "logOut" | i18n }}</span>
</button>
</div>
</form>
</div>
</div>
</form>

View File

@@ -0,0 +1,104 @@
import { Component, NgZone, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
const BroadcasterSubscriptionId = "SetPasswordComponent";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
})
export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy {
constructor(
apiService: ApiService,
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyService: PolicyService,
router: Router,
syncService: SyncService,
route: ActivatedRoute,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
stateService: StateService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
router,
apiService,
syncService,
route,
stateService
);
}
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return "success";
case 3:
return "primary";
case 2:
return "warning";
default:
return "danger";
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t("strong");
case 3:
return this.i18nService.t("good");
case 2:
return this.i18nService.t("weak");
default:
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
}
async ngOnInit() {
await super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
}

View File

@@ -0,0 +1,354 @@
<div class="modal fade" role="dialog" aria-modal="true" attr.aria-label="{{ 'settings' | i18n }}">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body form">
<div class="box">
<div class="box-header">
{{ "settingsTitle" | i18n: currentUserEmail }}
</div>
<div class="box-content box-content-padded">
<h2>
<button
id="app-settings"
type="button"
class="box-header-expandable"
(click)="showSecurity = !showSecurity"
[attr.aria-expanded]="showSecurity"
appAutofocus
>
{{ "security" | i18n }}
<i
*ngIf="!showSecurity"
class="bwi bwi-angle-down bwi-sm icon"
aria-hidden="true"
></i>
<i
*ngIf="showSecurity"
class="bwi bwi-chevron-up bwi-sm icon"
aria-hidden="true"
></i>
</button>
</h2>
<ng-container *ngIf="showSecurity">
<app-vault-timeout-input
[vaultTimeouts]="vaultTimeouts"
[formControl]="vaultTimeout"
ngDefaultControl
></app-vault-timeout-input>
<div class="form-group">
<label>{{ "vaultTimeoutAction" | i18n }}</label>
<div class="radio radio-mt-2">
<label for="vaultTimeoutActionLock">
<input
type="radio"
name="VaultTimeoutAction"
id="vaultTimeoutActionLock"
value="lock"
[(ngModel)]="vaultTimeoutAction"
(change)="saveVaultTimeoutOptions()"
/>
{{ "lock" | i18n }}
</label>
</div>
<small class="help-block">{{ "vaultTimeoutActionLockDesc" | i18n }}</small>
<div class="radio">
<label for="vaultTimeoutActionLogOut">
<input
type="radio"
name="VaultTimeoutAction"
id="vaultTimeoutActionLogOut"
value="logOut"
[(ngModel)]="vaultTimeoutAction"
(change)="saveVaultTimeoutOptions()"
/>
{{ "logOut" | i18n }}
</label>
</div>
<small class="help-block">{{ "vaultTimeoutActionLogOutDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="pin">
<input
id="pin"
type="checkbox"
name="PIN"
[(ngModel)]="pin"
(change)="updatePin()"
/>
{{ "unlockWithPin" | i18n }}
</label>
</div>
</div>
<div class="form-group" *ngIf="supportsBiometric">
<div class="checkbox">
<label for="biometric">
<input
id="biometric"
type="checkbox"
name="biometric"
[checked]="biometric"
(change)="updateBiometric()"
/>
{{ biometricText | i18n }}
</label>
</div>
</div>
<div class="form-group" *ngIf="supportsBiometric">
<div class="checkbox">
<label for="noAutoPromptBiometrics">
<input
id="noAutoPromptBiometrics"
type="checkbox"
name="noAutoPromptBiometrics"
[(ngModel)]="noAutoPromptBiometrics"
[disabled]="!biometric"
(change)="updateNoAutoPromptBiometrics()"
/>
{{ noAutoPromptBiometricsText | i18n }}
</label>
</div>
</div>
</ng-container>
</div>
</div>
<div class="box">
<div class="box-content box-content-padded">
<h2>
<button
type="button"
class="box-header-expandable"
(click)="showAccountPreferences = !showAccountPreferences"
[attr.aria-expanded]="showAccountPreferences"
>
{{ "accountPreferences" | i18n }}
<i
*ngIf="!showAccountPreferences"
class="bwi bwi-angle-down bwi-sm icon"
aria-hidden="true"
></i>
<i
*ngIf="showAccountPreferences"
class="bwi bwi-chevron-up bwi-sm icon"
aria-hidden="true"
></i>
</button>
</h2>
<ng-container *ngIf="showAccountPreferences">
<div class="form-group">
<label for="clearClipboard">{{ "clearClipboard" | i18n }}</label>
<select
id="clearClipboard"
name="ClearClipboard"
[(ngModel)]="clearClipboard"
(change)="saveClearClipboard()"
>
<option *ngFor="let o of clearClipboardOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
<small class="help-block">{{ "clearClipboardDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="minimizeOnCopyToClipboard">
<input
id="minimizeOnCopyToClipboard"
type="checkbox"
name="MinimizeOnCopyToClipboard"
[(ngModel)]="minimizeOnCopyToClipboard"
(change)="saveMinOnCopyToClipboard()"
/>
{{ "minimizeOnCopyToClipboard" | i18n }}
</label>
</div>
<small class="help-block">{{ "minimizeOnCopyToClipboardDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="disableFavicons">
<input
id="disableFavicons"
type="checkbox"
name="DisableFavicons"
[(ngModel)]="disableFavicons"
(change)="saveFavicons()"
/>
{{ "disableFavicon" | i18n }}
</label>
</div>
<small class="help-block">{{ "disableFaviconDesc" | i18n }}</small>
</div>
</ng-container>
</div>
</div>
<div class="box">
<div class="box-content box-content-padded">
<h2>
<button
type="button"
class="box-header-expandable"
(click)="showAppPreferences = !showAppPreferences"
[attr.aria-expanded]="showAppPreferences"
>
{{ "appPreferences" | i18n }}
<i
*ngIf="!showAppPreferences"
class="bwi bwi-angle-down bwi-sm icon"
aria-hidden="true"
></i>
<i
*ngIf="showAppPreferences"
class="bwi bwi-chevron-up bwi-sm icon"
aria-hidden="true"
></i>
</button>
</h2>
<ng-container *ngIf="showAppPreferences">
<div class="form-group">
<div class="checkbox">
<label for="enableTray">
<input
id="enableTray"
type="checkbox"
name="EnableTray"
[(ngModel)]="enableTray"
(change)="saveTray()"
/>
{{ enableTrayText }}
</label>
</div>
<small class="help-block">{{ enableTrayDescText }}</small>
</div>
<div class="form-group" *ngIf="showMinToTray">
<div class="checkbox">
<label for="enableMinToTray">
<input
id="enableMinToTray"
type="checkbox"
name="EnableMinToTray"
[(ngModel)]="enableMinToTray"
(change)="saveMinToTray()"
/>
{{ enableMinToTrayText }}
</label>
</div>
<small class="help-block">{{ enableMinToTrayDescText }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableCloseToTray">
<input
id="enableCloseToTray"
type="checkbox"
name="EnableCloseToTray"
[(ngModel)]="enableCloseToTray"
(change)="saveCloseToTray()"
/>
{{ enableCloseToTrayText }}
</label>
</div>
<small class="help-block">{{ enableCloseToTrayDescText }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="startToTray">
<input
id="startToTray"
type="checkbox"
name="StartToTray"
[(ngModel)]="startToTray"
(change)="saveStartToTray()"
/>
{{ startToTrayText }}
</label>
</div>
<small class="help-block">{{ startToTrayDescText }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="openAtLogin">
<input
id="openAtLogin"
type="checkbox"
name="OpenAtLogin"
[(ngModel)]="openAtLogin"
(change)="saveOpenAtLogin()"
/>
{{ "openAtLogin" | i18n }}
</label>
</div>
<small class="help-block">{{ "openAtLoginDesc" | i18n }}</small>
</div>
<div class="form-group" *ngIf="showAlwaysShowDock">
<div class="checkbox">
<label for="alwaysShowDock">
<input
id="alwaysShowDock"
type="checkbox"
name="AlwaysShowDock"
[(ngModel)]="alwaysShowDock"
(change)="saveAlwaysShowDock()"
/>
{{ "alwaysShowDock" | i18n }}
</label>
</div>
<small class="help-block">{{ "alwaysShowDockDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableBrowserIntegration">
<input
id="enableBrowserIntegration"
type="checkbox"
name="EnableBrowserIntegration"
[(ngModel)]="enableBrowserIntegration"
(change)="saveBrowserIntegration()"
/>
{{ "enableBrowserIntegration" | i18n }}
</label>
</div>
<small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableBrowserIntegrationFingerprint">
<input
id="enableBrowserIntegrationFingerprint"
type="checkbox"
name="EnableBrowserIntegrationFingerprint"
[(ngModel)]="enableBrowserIntegrationFingerprint"
(change)="saveBrowserIntegrationFingerprint()"
[disabled]="!enableBrowserIntegration"
/>
{{ "enableBrowserIntegrationFingerprint" | i18n }}
</label>
</div>
<small class="help-block">{{
"enableBrowserIntegrationFingerprintDesc" | i18n
}}</small>
</div>
<div class="form-group">
<label for="theme">{{ "theme" | i18n }}</label>
<select id="theme" name="Theme" [(ngModel)]="theme" (change)="saveTheme()">
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="help-block">{{ "themeDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="locale">{{ "language" | i18n }}</label>
<select id="locale" name="Locale" [(ngModel)]="locale" (change)="saveLocale()">
<option *ngFor="let o of localeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="help-block">{{ "languageDesc" | i18n }}</small>
</div>
</ng-container>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,407 @@
import { Component, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { debounceTime } from "rxjs/operators";
import { ModalService } from "jslib-angular/services/modal.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { DeviceType } from "jslib-common/enums/deviceType";
import { StorageLocation } from "jslib-common/enums/storageLocation";
import { ThemeType } from "jslib-common/enums/themeType";
import { Utils } from "jslib-common/misc/utils";
import { isWindowsStore } from "jslib-electron/utils";
import { SetPinComponent } from "../components/set-pin.component";
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
})
export class SettingsComponent implements OnInit {
vaultTimeoutAction: string;
pin: boolean = null;
disableFavicons = false;
enableBrowserIntegration = false;
enableBrowserIntegrationFingerprint = false;
enableMinToTray = false;
enableCloseToTray = false;
enableTray = false;
showMinToTray = false;
startToTray = false;
minimizeOnCopyToClipboard = false;
locale: string;
vaultTimeouts: any[];
localeOptions: any[];
theme: ThemeType;
themeOptions: any[];
clearClipboard: number;
clearClipboardOptions: any[];
supportsBiometric: boolean;
biometric: boolean;
biometricText: string;
noAutoPromptBiometrics: boolean;
noAutoPromptBiometricsText: string;
alwaysShowDock: boolean;
showAlwaysShowDock = false;
openAtLogin: boolean;
requireEnableTray = false;
enableTrayText: string;
enableTrayDescText: string;
enableMinToTrayText: string;
enableMinToTrayDescText: string;
enableCloseToTrayText: string;
enableCloseToTrayDescText: string;
startToTrayText: string;
startToTrayDescText: string;
vaultTimeout: FormControl = new FormControl(null);
showSecurity = true;
showAccountPreferences = true;
showAppPreferences = true;
currentUserEmail: string;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutService: VaultTimeoutService,
private stateService: StateService,
private messagingService: MessagingService,
private cryptoService: CryptoService,
private modalService: ModalService
) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
// Workaround to avoid ghosting trays https://github.com/electron/electron/issues/17622
this.requireEnableTray = this.platformUtilsService.getDevice() === DeviceType.LinuxDesktop;
const trayKey = isMac ? "enableMenuBar" : "enableTray";
this.enableTrayText = this.i18nService.t(trayKey);
this.enableTrayDescText = this.i18nService.t(trayKey + "Desc");
const minToTrayKey = isMac ? "enableMinToMenuBar" : "enableMinToTray";
this.enableMinToTrayText = this.i18nService.t(minToTrayKey);
this.enableMinToTrayDescText = this.i18nService.t(minToTrayKey + "Desc");
const closeToTrayKey = isMac ? "enableCloseToMenuBar" : "enableCloseToTray";
this.enableCloseToTrayText = this.i18nService.t(closeToTrayKey);
this.enableCloseToTrayDescText = this.i18nService.t(closeToTrayKey + "Desc");
const startToTrayKey = isMac ? "startToMenuBar" : "startToTray";
this.startToTrayText = this.i18nService.t(startToTrayKey);
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
this.vaultTimeouts = [
// { name: i18nService.t('immediately'), value: 0 },
{ name: i18nService.t("oneMinute"), value: 1 },
{ name: i18nService.t("fiveMinutes"), value: 5 },
{ name: i18nService.t("fifteenMinutes"), value: 15 },
{ name: i18nService.t("thirtyMinutes"), value: 30 },
{ name: i18nService.t("oneHour"), value: 60 },
{ name: i18nService.t("fourHours"), value: 240 },
{ name: i18nService.t("onIdle"), value: -4 },
{ name: i18nService.t("onSleep"), value: -3 },
];
if (this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop) {
this.vaultTimeouts.push({ name: i18nService.t("onLocked"), value: -2 });
}
this.vaultTimeouts = this.vaultTimeouts.concat([
{ name: i18nService.t("onRestart"), value: -1 },
{ name: i18nService.t("never"), value: null },
]);
const localeOptions: any[] = [];
i18nService.supportedTranslationLocales.forEach((locale) => {
let name = locale;
if (i18nService.localeNames.has(locale)) {
name += " - " + i18nService.localeNames.get(locale);
}
localeOptions.push({ name: name, value: locale });
});
localeOptions.sort(Utils.getSortFunction(i18nService, "name"));
localeOptions.splice(0, 0, { name: i18nService.t("default"), value: null });
this.localeOptions = localeOptions;
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
];
this.clearClipboardOptions = [
{ name: i18nService.t("never"), value: null },
{ name: i18nService.t("tenSeconds"), value: 10 },
{ name: i18nService.t("twentySeconds"), value: 20 },
{ name: i18nService.t("thirtySeconds"), value: 30 },
{ name: i18nService.t("oneMinute"), value: 60 },
{ name: i18nService.t("twoMinutes"), value: 120 },
{ name: i18nService.t("fiveMinutes"), value: 300 },
];
}
async ngOnInit() {
// App preferences
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
this.enableMinToTray = await this.stateService.getEnableMinimizeToTray();
this.enableCloseToTray = await this.stateService.getEnableCloseToTray();
this.enableTray = await this.stateService.getEnableTray();
this.startToTray = await this.stateService.getEnableStartToTray();
this.alwaysShowDock = await this.stateService.getAlwaysShowDock();
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
this.openAtLogin = await this.stateService.getOpenAtLogin();
this.locale = await this.stateService.getLocale();
this.theme = await this.stateService.getTheme();
if ((await this.stateService.getUserId()) == null) {
return;
}
this.currentUserEmail = await this.stateService.getEmail();
// Security
this.vaultTimeout.setValue(await this.stateService.getVaultTimeout());
this.vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
this.vaultTimeout.valueChanges.pipe(debounceTime(500)).subscribe(() => {
this.saveVaultTimeoutOptions();
});
const pinSet = await this.vaultTimeoutService.isPinLockSet();
this.pin = pinSet[0] || pinSet[1];
// Account preferences
this.disableFavicons = await this.stateService.getDisableFavicon();
this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration();
this.enableBrowserIntegrationFingerprint =
await this.stateService.getEnableBrowserIntegrationFingerprint();
this.clearClipboard = await this.stateService.getClearClipboard();
this.minimizeOnCopyToClipboard = await this.stateService.getMinimizeOnCopyToClipboard();
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.biometric = await this.vaultTimeoutService.isBiometricLockSet();
this.biometricText = await this.stateService.getBiometricText();
this.noAutoPromptBiometrics = await this.stateService.getNoAutoPromptBiometrics();
this.noAutoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText();
}
async saveVaultTimeoutOptions() {
if (this.vaultTimeoutAction === "logOut") {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("vaultTimeoutLogOutConfirmation"),
this.i18nService.t("vaultTimeoutLogOutConfirmationTitle"),
this.i18nService.t("yes"),
this.i18nService.t("cancel"),
"warning"
);
if (!confirmed) {
this.vaultTimeoutAction = "lock";
return;
}
}
// Avoid saving 0 since it's useless as a timeout value.
if (this.vaultTimeout.value === 0) {
return;
}
if (!this.vaultTimeout.valid) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge")
);
return;
}
await this.vaultTimeoutService.setVaultTimeoutOptions(
this.vaultTimeout.value,
this.vaultTimeoutAction
);
}
async updatePin() {
if (this.pin) {
const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true });
if (ref == null) {
this.pin = false;
return;
}
this.pin = await ref.onClosedPromise();
}
if (!this.pin) {
await this.cryptoService.clearPinProtectedKey();
await this.vaultTimeoutService.clear();
}
}
async updateBiometric() {
const current = this.biometric;
if (this.biometric) {
this.biometric = false;
} else if (this.supportsBiometric) {
this.biometric = await this.platformUtilsService.authenticateBiometric();
}
if (this.biometric === current) {
return;
}
if (this.biometric) {
await this.stateService.setBiometricUnlock(true);
} else {
await this.stateService.setBiometricUnlock(null);
await this.stateService.setNoAutoPromptBiometrics(null);
this.noAutoPromptBiometrics = false;
}
await this.stateService.setBiometricLocked(false);
await this.cryptoService.toggleKey();
}
async updateNoAutoPromptBiometrics() {
if (!this.biometric) {
this.noAutoPromptBiometrics = false;
}
if (this.noAutoPromptBiometrics) {
await this.stateService.setNoAutoPromptBiometrics(true);
} else {
await this.stateService.setNoAutoPromptBiometrics(null);
}
}
async saveFavicons() {
await this.stateService.setDisableFavicon(this.disableFavicons);
await this.stateService.setDisableFavicon(this.disableFavicons, {
storageLocation: StorageLocation.Disk,
});
this.messagingService.send("refreshCiphers");
}
async saveMinToTray() {
await this.stateService.setEnableMinimizeToTray(this.enableMinToTray);
}
async saveCloseToTray() {
if (this.requireEnableTray) {
this.enableTray = true;
await this.stateService.setEnableTray(this.enableTray);
}
await this.stateService.setEnableCloseToTray(this.enableCloseToTray);
}
async saveTray() {
if (
this.requireEnableTray &&
!this.enableTray &&
(this.startToTray || this.enableCloseToTray)
) {
const confirm = await this.platformUtilsService.showDialog(
this.i18nService.t("confirmTrayDesc"),
this.i18nService.t("confirmTrayTitle"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (confirm) {
this.startToTray = false;
await this.stateService.setEnableStartToTray(this.startToTray);
this.enableCloseToTray = false;
await this.stateService.setEnableCloseToTray(this.enableCloseToTray);
} else {
this.enableTray = true;
}
return;
}
await this.stateService.setEnableTray(this.enableTray);
this.messagingService.send(this.enableTray ? "showTray" : "removeTray");
}
async saveStartToTray() {
if (this.requireEnableTray) {
this.enableTray = true;
await this.stateService.setEnableTray(this.enableTray);
}
await this.stateService.setEnableStartToTray(this.startToTray);
}
async saveLocale() {
await this.stateService.setLocale(this.locale);
}
async saveTheme() {
await this.stateService.setTheme(this.theme);
window.setTimeout(() => window.location.reload(), 200);
}
async saveMinOnCopyToClipboard() {
await this.stateService.setMinimizeOnCopyToClipboard(this.minimizeOnCopyToClipboard);
}
async saveClearClipboard() {
await this.stateService.setClearClipboard(this.clearClipboard);
}
async saveAlwaysShowDock() {
await this.stateService.setAlwaysShowDock(this.alwaysShowDock);
}
async saveOpenAtLogin() {
this.stateService.setOpenAtLogin(this.openAtLogin);
this.messagingService.send(this.openAtLogin ? "addOpenAtLogin" : "removeOpenAtLogin");
}
async saveBrowserIntegration() {
if (process.platform === "darwin" && !this.platformUtilsService.isMacAppStore()) {
await this.platformUtilsService.showDialog(
this.i18nService.t("browserIntegrationMasOnlyDesc"),
this.i18nService.t("browserIntegrationMasOnlyTitle"),
this.i18nService.t("ok"),
null,
"warning"
);
this.enableBrowserIntegration = false;
return;
} else if (isWindowsStore()) {
await this.platformUtilsService.showDialog(
this.i18nService.t("browserIntegrationWindowsStoreDesc"),
this.i18nService.t("browserIntegrationWindowsStoreTitle"),
this.i18nService.t("ok"),
null,
"warning"
);
this.enableBrowserIntegration = false;
return;
}
await this.stateService.setEnableBrowserIntegration(this.enableBrowserIntegration);
this.messagingService.send(
this.enableBrowserIntegration ? "enableBrowserIntegration" : "disableBrowserIntegration"
);
if (!this.enableBrowserIntegration) {
this.enableBrowserIntegrationFingerprint = false;
this.saveBrowserIntegrationFingerprint();
}
}
async saveBrowserIntegrationFingerprint() {
await this.stateService.setEnableBrowserIntegrationFingerprint(
this.enableBrowserIntegrationFingerprint
);
}
}

View File

@@ -0,0 +1,9 @@
<form id="sso-page" (ngSubmit)="submit()">
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<div class="box">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
</div>
</form>

View File

@@ -0,0 +1,54 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
})
export class SsoComponent extends BaseSsoComponent {
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
syncService: SyncService,
route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
logService: LogService
) {
super(
authService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
this.redirectUri = "bitwarden://sso-callback";
this.clientId = "desktop";
}
}

View File

@@ -0,0 +1,33 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="twoStepTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="box">
<div class="box-header" id="twoStepTitle">
{{ "twoStepOptions" | i18n }}
</div>
<div class="box-content">
<button
type="button"
appStopClick
*ngFor="let p of providers"
class="box-content-row"
(click)="choose(p)"
>
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="img-right" />
<span class="text">{{ p.name }}</span>
<span class="detail">{{ p.description }}</span>
</button>
<button type="button" appStopClick class="box-content-row" (click)="recover()">
<span class="text">{{ "recoveryCodeTitle" | i18n }}</span>
<span class="detail">{{ "recoveryCodeDesc" | i18n }}</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "jslib-angular/components/two-factor-options.component";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
@Component({
selector: "app-two-factor-options",
templateUrl: "two-factor-options.component.html",
})
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(
twoFactorService: TwoFactorService,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
}
}

View File

@@ -0,0 +1,144 @@
<form
id="two-factor-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
attr.aria-hidden="{{ showingModal }}"
>
<div id="content" class="content">
<h1>{{ title }}</h1>
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{ "enterVerificationCodeApp" | i18n }}
</p>
<p *ngIf="selectedProviderType === providerType.Email">
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
</p>
<div
class="box last"
*ngIf="
selectedProviderType === providerType.Email ||
selectedProviderType === providerType.Authenticator
"
>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="text"
name="Code"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p>{{ "insertYubiKey" | i18n }}</p>
<img src="../../images/yubikey.jpg" alt="" />
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="password"
name="Code"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
<div id="web-authn-frame">
<iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe>
</div>
<div class="box first">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<ng-container
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<div class="box last" *ngIf="selectedProviderType == null">
<div class="box-content">
<div class="box-content-row">
<p>{{ "noTwoStepProviders" | i18n }}</p>
<p>{{ "noTwoStepProviders2" | i18n }}</p>
</div>
</div>
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
<div class="box-content-row">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
</div>
</div>
<div class="buttons">
<button
type="submit"
class="btn primary block"
[disabled]="form.loading"
*ngIf="
selectedProviderType != null &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo
"
>
<span [hidden]="form.loading"
><i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}</span
>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/login" class="btn block">{{ "cancel" | i18n }}</button>
</div>
<div class="sub-options">
<button type="button" appStopClick (click)="anotherMethod()">
{{ "useAnotherTwoStepMethod" | i18n }}
</button>
<button
type="button"
appStopClick
(click)="sendEmail(true)"
[appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email"
>
{{ "sendVerificationCodeEmailAgain" | i18n }}
</button>
</div>
</div>
</form>
<ng-template #twoFactorOptions></ng-template>

View File

@@ -0,0 +1,94 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
import { ModalService } from "jslib-angular/services/modal.service";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
@Component({
selector: "app-two-factor",
templateUrl: "two-factor.component.html",
})
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
twoFactorOptionsModal: ViewContainerRef;
showingModal = false;
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
syncService: SyncService,
environmentService: EnvironmentService,
private modalService: ModalService,
stateService: StateService,
route: ActivatedRoute,
logService: LogService,
twoFactorService: TwoFactorService,
appIdService: AppIdService
) {
super(
authService,
router,
i18nService,
apiService,
platformUtilsService,
window,
environmentService,
stateService,
route,
logService,
twoFactorService,
appIdService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}
async anotherMethod() {
const [modal, childComponent] = await this.modalService.openViewRef(
TwoFactorOptionsComponent,
this.twoFactorOptionsModal
);
modal.onShown.subscribe(() => {
this.showingModal = true;
});
modal.onClosed.subscribe(() => {
this.showingModal = false;
});
childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
modal.close();
this.selectedProviderType = provider;
await this.init();
});
childComponent.onRecoverSelected.subscribe(() => {
modal.close();
});
}
async submit() {
await super.submit();
if (this.captchaSiteKey) {
const content = document.getElementById("content") as HTMLDivElement;
content.setAttribute("style", "width:335px");
}
}
}

View File

@@ -0,0 +1,122 @@
<form id="update-temp-password-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ "updateMasterPasswordWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong
class="sub-label text-{{ masterPasswordScoreStyle.Color }}"
*ngIf="masterPasswordScoreStyle.Text"
>
{{ masterPasswordScoreStyle.Text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="masterPassword === ''"
(input)="updatePasswordStrength()"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="progress">
<div
class="progress-bar bg-{{ masterPasswordScoreStyle.Color }}"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
[ngStyle]="{ width: masterPasswordScoreStyle.Width + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreStyle.Width }}"
></div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" [(ngModel)]="hint" />
</div>
</div>
<div class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" (click)="logOut()" class="btn block">{{ "logOut" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,80 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
interface MasterPasswordScore {
Color: string;
Text: string;
Width: number;
}
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
get masterPasswordScoreStyle(): MasterPasswordScore {
const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) {
case 4:
return {
Color: "bg-success",
Text: "strong",
Width: scoreWidth,
};
case 3:
return {
Color: "bg-primary",
Text: "good",
Width: scoreWidth,
};
case 2:
return {
Color: "bg-warning",
Text: "weak",
Width: scoreWidth,
};
default:
return {
Color: "bg-danger",
Text: "weak",
Width: scoreWidth,
};
}
}
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
policyService: PolicyService,
cryptoService: CryptoService,
messagingService: MessagingService,
apiService: ApiService,
syncService: SyncService,
logService: LogService,
stateService: StateService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
}

View File

@@ -0,0 +1,45 @@
<app-callout type="info" *ngIf="vaultTimeoutPolicy">
{{ "vaultTimeoutPolicyInEffect" | i18n: vaultTimeoutPolicyHours:vaultTimeoutPolicyMinutes }}
</app-callout>
<div [formGroup]="form">
<div class="form-group">
<label for="vaultTimeout">{{ "vaultTimeout" | i18n }}</label>
<select
id="vaultTimeout"
name="VaultTimeout"
formControlName="vaultTimeout"
class="form-control"
>
<option *ngFor="let o of vaultTimeouts" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="form-text text-muted">{{ "vaultTimeoutDesc" | i18n }}</small>
</div>
<div class="form-group row" *ngIf="showCustom" formGroupName="custom">
<div class="col">
<label for="hours">{{ "hours" | i18n }}</label>
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
</div>
<div class="col">
<label for="minutes">{{ "minutes" | i18n }}</label>
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
</div>
</div>
<div class="form-group"></div>
<!-- Styling fix -->
</div>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms";
import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "jslib-angular/components/settings/vault-timeout-input.component";
@Component({
selector: "app-vault-timeout-input",
templateUrl: "vault-timeout-input.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: VaultTimeoutInputComponent,
},
],
})
export class VaultTimeoutInputComponent extends VaultTimeoutInputComponentBase {}

View File

@@ -0,0 +1,70 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
import { LockGuardService } from "jslib-angular/services/lock-guard.service";
import { LoginGuardService } from "../services/loginGuard.service";
import { HintComponent } from "./accounts/hint.component";
import { LockComponent } from "./accounts/lock.component";
import { LoginComponent } from "./accounts/login.component";
import { RegisterComponent } from "./accounts/register.component";
import { RemovePasswordComponent } from "./accounts/remove-password.component";
import { SetPasswordComponent } from "./accounts/set-password.component";
import { SsoComponent } from "./accounts/sso.component";
import { TwoFactorComponent } from "./accounts/two-factor.component";
import { UpdateTempPasswordComponent } from "./accounts/update-temp-password.component";
import { SendComponent } from "./send/send.component";
import { VaultComponent } from "./vault/vault.component";
const routes: Routes = [
{ path: "", redirectTo: "/vault", pathMatch: "full" },
{
path: "lock",
component: LockComponent,
canActivate: [LockGuardService],
},
{
path: "login",
component: LoginComponent,
canActivate: [LoginGuardService],
},
{ path: "2fa", component: TwoFactorComponent },
{ path: "register", component: RegisterComponent },
{
path: "vault",
component: VaultComponent,
canActivate: [AuthGuardService],
},
{ path: "hint", component: HintComponent },
{ path: "set-password", component: SetPasswordComponent },
{ path: "sso", component: SsoComponent },
{
path: "send",
component: SendComponent,
canActivate: [AuthGuardService],
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [AuthGuardService],
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [AuthGuardService],
data: { titleId: "removeMasterPassword" },
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
useHash: true,
/*enableTracing: true,*/
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,626 @@
import {
Component,
NgZone,
OnInit,
SecurityContext,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { ModalService } from "jslib-angular/services/modal.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SettingsService } from "jslib-common/abstractions/settings.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { SystemService } from "jslib-common/abstractions/system.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { CipherType } from "jslib-common/enums/cipherType";
import { MenuUpdateRequest } from "../main/menu/menu.updater";
import { PremiumComponent } from "./accounts/premium.component";
import { SettingsComponent } from "./accounts/settings.component";
import { ExportComponent } from "./vault/export.component";
import { FolderAddEditComponent } from "./vault/folder-add-edit.component";
import { GeneratorComponent } from "./vault/generator.component";
import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-history.component";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
const systemTimeoutOptions = {
onLock: -2,
onSuspend: -3,
onIdle: -4,
};
@Component({
selector: "app-root",
styles: [],
template: `
<ng-template #settings></ng-template>
<ng-template #premium></ng-template>
<ng-template #passwordHistory></ng-template>
<ng-template #appFolderAddEdit></ng-template>
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<app-header></app-header>
<div id="container">
<div class="loading" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<router-outlet *ngIf="!loading"></router-outlet>
</div>
`,
})
export class AppComponent implements OnInit {
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
passwordHistoryRef: ViewContainerRef;
@ViewChild("exportVault", { read: ViewContainerRef, static: true })
exportVaultModalRef: ViewContainerRef;
@ViewChild("appFolderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
loading = false;
private lastActivity: number = null;
private modal: ModalRef = null;
private idleTimer: number = null;
private isIdle = false;
private activeUserId: string = null;
constructor(
private broadcasterService: BroadcasterService,
private tokenService: TokenService,
private folderService: FolderService,
private settingsService: SettingsService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
private toastrService: ToastrService,
private i18nService: I18nService,
private sanitizer: DomSanitizer,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private cryptoService: CryptoService,
private logService: LogService,
private messagingService: MessagingService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private platformUtilsService: PlatformUtilsService,
private systemService: SystemService,
private stateService: StateService,
private eventService: EventService,
private policyService: PolicyService,
private modalService: ModalService,
private keyConnectorService: KeyConnectorService
) {}
ngOnInit() {
this.stateService.activeAccount.subscribe((userId) => {
this.activeUserId = userId;
});
this.ngZone.runOutsideAngular(() => {
setTimeout(async () => {
await this.updateAppMenu();
}, 1000);
window.ontouchstart = () => this.recordActivity();
window.onmousedown = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "loggedIn":
case "unlocked":
this.notificationsService.updateConnection();
this.updateAppMenu();
this.systemService.cancelProcessReload();
break;
case "loggedOut":
if (this.modal != null) {
this.modal.close();
}
this.notificationsService.updateConnection();
this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.reloadProcess();
break;
case "authBlocked":
this.router.navigate(["login"]);
break;
case "logout":
this.loading = message.userId == null || message.userId === this.activeUserId;
await this.logOut(!!message.expired, message.userId);
this.loading = false;
break;
case "lockVault":
await this.vaultTimeoutService.lock(true, message.userId);
break;
case "lockAllVaults":
for (const userId in this.stateService.accounts.getValue()) {
if (userId != null) {
await this.vaultTimeoutService.lock(true, userId);
}
}
break;
case "locked":
if (this.modal != null) {
this.modal.close();
}
if (
message.userId == null ||
message.userId === (await this.stateService.getUserId())
) {
await this.router.navigate(["lock"]);
}
this.notificationsService.updateConnection();
await this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.reloadProcess();
break;
case "reloadProcess":
window.location.reload(true);
break;
case "syncStarted":
break;
case "syncCompleted":
await this.updateAppMenu();
break;
case "openSettings":
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
break;
case "openPremium":
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
break;
case "showFingerprintPhrase": {
const fingerprint = await this.cryptoService.getFingerprint(
await this.stateService.getUserId()
);
const result = await this.platformUtilsService.showDialog(
this.i18nService.t("yourAccountsFingerprint") + ":\n" + fingerprint.join("-"),
this.i18nService.t("fingerprintPhrase"),
this.i18nService.t("learnMore"),
this.i18nService.t("close")
);
if (result) {
this.platformUtilsService.launchUri("https://bitwarden.com/help/fingerprint-phrase/");
}
break;
}
case "openPasswordHistory":
await this.openModal<PasswordGeneratorHistoryComponent>(
PasswordGeneratorHistoryComponent,
this.passwordHistoryRef
);
break;
case "showToast":
this.showToast(message);
break;
case "copiedToClipboard":
if (!message.clearing) {
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
}
break;
case "ssoCallback":
this.router.navigate(["sso"], {
queryParams: { code: message.code, state: message.state },
});
break;
case "premiumRequired": {
const premiumConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("premiumRequiredDesc"),
this.i18nService.t("premiumRequired"),
this.i18nService.t("learnMore"),
this.i18nService.t("cancel")
);
if (premiumConfirmed) {
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
}
break;
}
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("emailVerificationRequiredDesc"),
this.i18nService.t("emailVerificationRequired"),
this.i18nService.t("learnMore"),
this.i18nService.t("cancel")
);
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/create-bitwarden-account/"
);
}
break;
}
case "syncVault":
try {
await this.syncService.fullSync(true, true);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("syncingComplete")
);
} catch {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("syncingFailed")
);
}
break;
case "checkSyncVault":
try {
const lastSync = await this.syncService.getLastSync();
let lastSyncAgo = SyncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= SyncInterval) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send("scheduleNextSync");
break;
case "exportVault":
await this.openExportVault();
break;
case "newLogin":
this.routeToVault("add", CipherType.Login);
break;
case "newCard":
this.routeToVault("add", CipherType.Card);
break;
case "newIdentity":
this.routeToVault("add", CipherType.Identity);
break;
case "newSecureNote":
this.routeToVault("add", CipherType.SecureNote);
break;
default:
break;
case "newFolder":
await this.addFolder();
break;
case "openGenerator":
// openGenerator has extended functionality if called in the vault
if (!this.router.url.includes("vault")) {
await this.openGenerator();
}
break;
case "convertAccountToKeyConnector":
this.router.navigate(["/remove-password"]);
break;
case "switchAccount": {
if (message.userId != null) {
await this.stateService.setActiveUser(message.userId);
}
const locked =
(await this.authService.getAuthStatus(message.userId)) ===
AuthenticationStatus.Locked;
if (locked) {
this.messagingService.send("locked", { userId: message.userId });
} else {
this.messagingService.send("unlocked");
this.loading = true;
await this.syncService.fullSync(true);
this.loading = false;
this.router.navigate(["vault"]);
}
break;
}
case "systemSuspended":
await this.checkForSystemTimeout(systemTimeoutOptions.onSuspend);
break;
case "systemLocked":
await this.checkForSystemTimeout(systemTimeoutOptions.onLock);
break;
case "systemIdle":
await this.checkForSystemTimeout(systemTimeoutOptions.onIdle);
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async openExportVault() {
if (this.modal != null) {
this.modal.close();
}
const [modal, childComponent] = await this.modalService.openViewRef(
ExportComponent,
this.exportVaultModalRef
);
this.modal = modal;
childComponent.onSaved.subscribe(() => {
this.modal.close();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async addFolder() {
if (this.modal != null) {
this.modal.close();
}
const [modal, childComponent] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => (comp.folderId = null)
);
this.modal = modal;
childComponent.onSavedFolder.subscribe(async () => {
this.modal.close();
this.syncService.fullSync(false);
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async openGenerator() {
if (this.modal != null) {
this.modal.close();
}
[this.modal] = await this.modalService.openViewRef(
GeneratorComponent,
this.generatorModalRef,
(comp) => (comp.comingFromAddEdit = false)
);
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private async updateAppMenu() {
let updateRequest: MenuUpdateRequest;
const stateAccounts = this.stateService.accounts?.getValue();
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
updateRequest = {
accounts: null,
activeUserId: null,
hideChangeMasterPassword: true,
};
} else {
const accounts: { [userId: string]: any } = {};
for (const i in stateAccounts) {
if (i != null && stateAccounts[i]?.profile?.userId != null) {
const userId = stateAccounts[i].profile.userId;
accounts[userId] = {
isAuthenticated: await this.stateService.getIsAuthenticated({
userId: userId,
}),
isLocked:
(await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
email: stateAccounts[i].profile.email,
userId: stateAccounts[i].profile.userId,
};
}
}
updateRequest = {
accounts: accounts,
activeUserId: await this.stateService.getUserId(),
hideChangeMasterPassword: await this.keyConnectorService.getUsesKeyConnector(),
};
}
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
}
private async logOut(expired: boolean, userId?: string) {
const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId });
await Promise.all([
this.eventService.uploadEvents(userBeingLoggedOut),
this.syncService.setLastSync(new Date(0), userBeingLoggedOut),
this.cryptoService.clearKeys(userBeingLoggedOut),
this.settingsService.clear(userBeingLoggedOut),
this.cipherService.clear(userBeingLoggedOut),
this.folderService.clear(userBeingLoggedOut),
this.collectionService.clear(userBeingLoggedOut),
this.passwordGenerationService.clear(userBeingLoggedOut),
this.vaultTimeoutService.clear(userBeingLoggedOut),
this.policyService.clear(userBeingLoggedOut),
this.keyConnectorService.clear(),
]);
await this.stateService.setBiometricLocked(true, { userId: userBeingLoggedOut });
if (userBeingLoggedOut === this.activeUserId) {
this.searchService.clearIndex();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
"warning",
this.i18nService.t("loggedOut"),
this.i18nService.t("loginExpired")
);
}
});
}
const preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });
if (this.activeUserId == null) {
this.router.navigate(["login"]);
} else if (preLogoutActiveUserId !== this.activeUserId) {
this.messagingService.send("switchAccount");
}
await this.updateAppMenu();
}
private async recordActivity() {
if (this.activeUserId == null) {
return;
}
const now = new Date().getTime();
if (this.lastActivity != null && now - this.lastActivity < 250) {
return;
}
this.lastActivity = now;
await this.stateService.setLastActive(now, { userId: this.activeUserId });
// Idle states
if (this.isIdle) {
this.isIdle = false;
this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
}
}, IdleTimeout);
}
private idleStateChanged() {
if (this.isIdle) {
this.notificationsService.disconnectFromInactivity();
} else {
this.notificationsService.reconnectFromActivity();
}
}
private async openModal<T>(type: Type<T>, ref: ViewContainerRef) {
if (this.modal != null) {
this.modal.close();
}
[this.modal] = await this.modalService.openViewRef(type, ref);
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private showToast(msg: any) {
let message = "";
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
}
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
}
private routeToVault(action: string, cipherType: CipherType) {
if (!this.router.url.includes("vault")) {
this.router.navigate(["/vault"], {
queryParams: {
action: action,
addType: cipherType,
},
replaceUrl: true,
});
}
}
private async reloadProcess(): Promise<void> {
const accounts = this.stateService.accounts.getValue();
if (accounts != null) {
const keys = Object.keys(accounts);
if (keys.length > 0) {
for (const userId of keys) {
if ((await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Unlocked) {
return;
}
}
}
}
await this.systemService.startProcessReload();
}
private async checkForSystemTimeout(timeout: number): Promise<void> {
for (const userId in this.stateService.accounts.getValue()) {
if (userId == null) {
continue;
}
const options = await this.getVaultTimeoutOptions(userId);
if (options[0] === timeout) {
options[1] === "logOut"
? this.logOut(false, userId)
: await this.vaultTimeoutService.lock(true, userId);
}
}
}
private async getVaultTimeoutOptions(userId: string): Promise<[number, string]> {
const timeout = await this.stateService.getVaultTimeout({ userId: userId });
const action = await this.stateService.getVaultTimeoutAction({ userId: userId });
return [timeout, action];
}
}

View File

@@ -0,0 +1,224 @@
import "zone.js/dist/zone";
import { A11yModule } from "@angular/cdk/a11y";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { DatePipe, registerLocaleData } from "@angular/common";
import localeAf from "@angular/common/locales/af";
import localeAz from "@angular/common/locales/az";
import localeBe from "@angular/common/locales/be";
import localeBg from "@angular/common/locales/bg";
import localeBn from "@angular/common/locales/bn";
import localeBs from "@angular/common/locales/bs";
import localeCa from "@angular/common/locales/ca";
import localeCs from "@angular/common/locales/cs";
import localeDa from "@angular/common/locales/da";
import localeDe from "@angular/common/locales/de";
import localeEl from "@angular/common/locales/el";
import localeEnGb from "@angular/common/locales/en-GB";
import localeEnIn from "@angular/common/locales/en-IN";
import localeEo from "@angular/common/locales/eo";
import localeEs from "@angular/common/locales/es";
import localeEt from "@angular/common/locales/et";
import localeFa from "@angular/common/locales/fa";
import localeFi from "@angular/common/locales/fi";
import localeFil from "@angular/common/locales/fil";
import localeFr from "@angular/common/locales/fr";
import localeHe from "@angular/common/locales/he";
import localeHi from "@angular/common/locales/hi";
import localeHr from "@angular/common/locales/hr";
import localeHu from "@angular/common/locales/hu";
import localeId from "@angular/common/locales/id";
import localeIt from "@angular/common/locales/it";
import localeJa from "@angular/common/locales/ja";
import localeKa from "@angular/common/locales/ka";
import localeKm from "@angular/common/locales/km";
import localeKn from "@angular/common/locales/kn";
import localeKo from "@angular/common/locales/ko";
import localeLv from "@angular/common/locales/lv";
import localeMl from "@angular/common/locales/ml";
import localeNb from "@angular/common/locales/nb";
import localeNl from "@angular/common/locales/nl";
import localeNn from "@angular/common/locales/nn";
import localePl from "@angular/common/locales/pl";
import localePtBr from "@angular/common/locales/pt";
import localePtPt from "@angular/common/locales/pt-PT";
import localeRo from "@angular/common/locales/ro";
import localeRu from "@angular/common/locales/ru";
import localeSi from "@angular/common/locales/si";
import localeSk from "@angular/common/locales/sk";
import localeSl from "@angular/common/locales/sl";
import localeSr from "@angular/common/locales/sr";
import localeMe from "@angular/common/locales/sr-Latn-ME";
import localeSv from "@angular/common/locales/sv";
import localeTh from "@angular/common/locales/th";
import localeTr from "@angular/common/locales/tr";
import localeUk from "@angular/common/locales/uk";
import localeVi from "@angular/common/locales/vi";
import localeZhCn from "@angular/common/locales/zh-Hans";
import localeZhTw from "@angular/common/locales/zh-Hant";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { JslibModule } from "jslib-angular/jslib.module";
import { EnvironmentComponent } from "./accounts/environment.component";
import { HintComponent } from "./accounts/hint.component";
import { LockComponent } from "./accounts/lock.component";
import { LoginComponent } from "./accounts/login.component";
import { PremiumComponent } from "./accounts/premium.component";
import { RegisterComponent } from "./accounts/register.component";
import { RemovePasswordComponent } from "./accounts/remove-password.component";
import { SetPasswordComponent } from "./accounts/set-password.component";
import { SettingsComponent } from "./accounts/settings.component";
import { SsoComponent } from "./accounts/sso.component";
import { TwoFactorOptionsComponent } from "./accounts/two-factor-options.component";
import { TwoFactorComponent } from "./accounts/two-factor.component";
import { UpdateTempPasswordComponent } from "./accounts/update-temp-password.component";
import { VaultTimeoutInputComponent } from "./accounts/vault-timeout-input.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { PasswordRepromptComponent } from "./components/password-reprompt.component";
import { SetPinComponent } from "./components/set-pin.component";
import { UserVerificationComponent } from "./components/user-verification.component";
import { AccountSwitcherComponent } from "./layout/account-switcher.component";
import { HeaderComponent } from "./layout/header.component";
import { NavComponent } from "./layout/nav.component";
import { SearchComponent } from "./layout/search/search.component";
import { AddEditComponent as SendAddEditComponent } from "./send/add-edit.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "./send/efflux-dates.component";
import { SendComponent } from "./send/send.component";
import { ServicesModule } from "./services/services.module";
import { AddEditCustomFieldsComponent } from "./vault/add-edit-custom-fields.component";
import { AddEditComponent } from "./vault/add-edit.component";
import { AttachmentsComponent } from "./vault/attachments.component";
import { CiphersComponent } from "./vault/ciphers.component";
import { CollectionsComponent } from "./vault/collections.component";
import { ExportComponent } from "./vault/export.component";
import { FolderAddEditComponent } from "./vault/folder-add-edit.component";
import { GeneratorComponent } from "./vault/generator.component";
import { GroupingsComponent } from "./vault/groupings.component";
import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-history.component";
import { PasswordHistoryComponent } from "./vault/password-history.component";
import { ShareComponent } from "./vault/share.component";
import { VaultComponent } from "./vault/vault.component";
import { ViewCustomFieldsComponent } from "./vault/view-custom-fields.component";
import { ViewComponent } from "./vault/view.component";
registerLocaleData(localeAf, "af");
registerLocaleData(localeAz, "az");
registerLocaleData(localeBe, "be");
registerLocaleData(localeBg, "bg");
registerLocaleData(localeBn, "bn");
registerLocaleData(localeBs, "bs");
registerLocaleData(localeCa, "ca");
registerLocaleData(localeCs, "cs");
registerLocaleData(localeDa, "da");
registerLocaleData(localeDe, "de");
registerLocaleData(localeEl, "el");
registerLocaleData(localeEnGb, "en-GB");
registerLocaleData(localeEnIn, "en-IN");
registerLocaleData(localeEo, "eo");
registerLocaleData(localeEs, "es");
registerLocaleData(localeEt, "et");
registerLocaleData(localeFa, "fa");
registerLocaleData(localeFi, "fi");
registerLocaleData(localeFil, "fil");
registerLocaleData(localeFr, "fr");
registerLocaleData(localeHe, "he");
registerLocaleData(localeHi, "hi");
registerLocaleData(localeHr, "hr");
registerLocaleData(localeHu, "hu");
registerLocaleData(localeId, "id");
registerLocaleData(localeIt, "it");
registerLocaleData(localeJa, "ja");
registerLocaleData(localeKa, "ka");
registerLocaleData(localeKm, "km");
registerLocaleData(localeKn, "kn");
registerLocaleData(localeKo, "ko");
registerLocaleData(localeLv, "lv");
registerLocaleData(localeMe, "me");
registerLocaleData(localeMl, "ml");
registerLocaleData(localeNb, "nb");
registerLocaleData(localeNl, "nl");
registerLocaleData(localeNn, "nn");
registerLocaleData(localePl, "pl");
registerLocaleData(localePtBr, "pt-BR");
registerLocaleData(localePtPt, "pt-PT");
registerLocaleData(localeRo, "ro");
registerLocaleData(localeRu, "ru");
registerLocaleData(localeSi, "si");
registerLocaleData(localeSk, "sk");
registerLocaleData(localeSl, "sl");
registerLocaleData(localeSr, "sr");
registerLocaleData(localeSv, "sv");
registerLocaleData(localeTh, "th");
registerLocaleData(localeTr, "tr");
registerLocaleData(localeUk, "uk");
registerLocaleData(localeVi, "vi");
registerLocaleData(localeZhCn, "zh-CN");
registerLocaleData(localeZhTw, "zh-TW");
@NgModule({
imports: [
A11yModule,
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
DragDropModule,
FormsModule,
JslibModule,
OverlayModule,
ReactiveFormsModule,
ScrollingModule,
ServicesModule,
],
declarations: [
AccountSwitcherComponent,
AddEditComponent,
AddEditCustomFieldsComponent,
AppComponent,
AttachmentsComponent,
CiphersComponent,
CollectionsComponent,
EnvironmentComponent,
ExportComponent,
FolderAddEditComponent,
GroupingsComponent,
HeaderComponent,
HintComponent,
LockComponent,
LoginComponent,
NavComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordHistoryComponent,
PasswordRepromptComponent,
PremiumComponent,
RegisterComponent,
RemovePasswordComponent,
SearchComponent,
SendAddEditComponent,
SendComponent,
SendEffluxDatesComponent,
SetPasswordComponent,
SetPinComponent,
SettingsComponent,
ShareComponent,
SsoComponent,
TwoFactorComponent,
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,
UserVerificationComponent,
VaultComponent,
VaultTimeoutInputComponent,
ViewComponent,
ViewCustomFieldsComponent,
],
providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,54 @@
<div class="modal fade" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()">
<div class="modal-body">
<div class="box">
<div class="box-header">{{ "passwordConfirmation" | i18n }}</div>
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
appAutofocus
/>
</div>
<div class="action-buttons">
<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>
</div>
</div>
</div>
<div class="box-footer">
{{ "passwordConfirmationDesc" | i18n }}
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit">
<span>{{ "ok" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,8 @@
import { Component } from "@angular/core";
import { PasswordRepromptComponent as BasePasswordRepromptComponent } from "jslib-angular/components/password-reprompt.component";
@Component({
templateUrl: "password-reprompt.component.html",
})
export class PasswordRepromptComponent extends BasePasswordRepromptComponent {}

View File

@@ -0,0 +1,65 @@
<div class="modal fade" role="dialog" aria-modal="true">
<div class="modal-dialog modal-dialog-scrollable set-pin-modal" role="document">
<form class="modal-content" #form (ngSubmit)="submit()">
<div class="modal-body">
<div>
{{ "setYourPinCode" | i18n }}
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPin ? 'text' : 'password' }}"
name="Pin"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
appAutofocus
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPin"
(click)="toggleVisibility()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPin, 'bwi-eye-slash': showPin }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="checkbox" *ngIf="showMasterPassOnRestart">
<label for="masterPasswordOnRestart">
<input
type="checkbox"
id="masterPasswordOnRestart"
name="MasterPasswordOnRestart"
[(ngModel)]="masterPassOnRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" appBlurClick>
<span>{{ "ok" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,8 @@
import { Component } from "@angular/core";
import { SetPinComponent as BaseSetPinComponent } from "jslib-angular/components/set-pin.component";
@Component({
templateUrl: "set-pin.component.html",
})
export class SetPinComponent extends BaseSetPinComponent {}

View File

@@ -0,0 +1,46 @@
<ng-container *ngIf="!usesKeyConnector">
<div class="box-content-row" appBoxRow>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPasswordHash"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
</div>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<div class="box-content-row" appBoxRow>
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button
type="button"
class="btn btn-outline-secondary"
(click)="requestOTP()"
[disabled]="disableRequestOTP"
>
{{ "sendCode" | i18n }}
</button>
<span class="ml-2 text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</div>
<div class="box-content-row" appBoxRow>
<label for="verificationCode">{{ "verificationCode" | i18n }}</label>
<input
id="verificationCode"
type="input"
name="verificationCode"
class="form-control"
[formControl]="secret"
required
appAutofocus
appInputVerbatim
/>
</div>
</ng-container>

View File

@@ -0,0 +1,23 @@
import { animate, style, transition, trigger } from "@angular/animations";
import { Component } from "@angular/core";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { UserVerificationComponent as BaseComponent } from "jslib-angular/components/user-verification.component";
@Component({
selector: "app-user-verification",
templateUrl: "user-verification.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: UserVerificationComponent,
},
],
animations: [
trigger("sent", [
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
]),
],
})
export class UserVerificationComponent extends BaseComponent {}

View File

@@ -0,0 +1,100 @@
<button
class="account-switcher"
(click)="toggle()"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
[hidden]="!showSwitcher"
aria-haspopup="menu"
aria-controls="cdk-overlay-container"
[attr.aria-expanded]="isOpen"
>
<ng-container *ngIf="activeAccountEmail != null; else noActiveAccount">
<app-avatar
[data]="activeAccountEmail"
size="25"
[circle]="true"
[fontSize]="14"
[dynamic]="true"
*ngIf="activeAccountEmail != null"
aria-hidden="true"
></app-avatar>
<span>{{ activeAccountEmail }}</span>
</ng-container>
<ng-template #noActiveAccount>
<span>{{ "switchAccount" | i18n }}</span>
</ng-template>
<i
class="bwi"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }"
></i>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="close()"
(detach)="close()"
[cdkConnectedOverlayOpen]="showSwitcher && isOpen"
[cdkConnectedOverlayPositions]="overlayPostition"
cdkConnectedOverlayMinWidth="250px"
>
<div
class="account-switcher-dropdown"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<div class="accounts" *ngIf="numberOfAccounts > 0">
<button
*ngFor="let a of accounts | keyvalue"
class="account"
(click)="switch(a.key)"
appA11yTitle="{{ 'loggedInAsOn' | i18n: a.value.profile.email:a.value.serverUrl }}"
attr.aria-label="{{ 'switchAccount' | i18n }}"
>
<app-avatar
[data]="a.value.profile.email"
size="25"
[circle]="true"
[fontSize]="14"
[dynamic]="true"
*ngIf="a.value.profile.email != null"
aria-hidden="true"
></app-avatar>
<div class="accountInfo">
<span class="email" aria-hidden="true">{{ a.value.profile.email }}</span>
<span class="server" aria-hidden="true" *ngIf="a.value.serverUrl != 'bitwarden.com'">{{
a.value.serverUrl
}}</span>
<span class="status" aria-hidden="true">{{
(a.value.profile.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
| i18n
}}</span>
</div>
<i
class="bwi bwi-2x text-muted"
[ngClass]="
a.value.profile.authenticationStatus == authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
"
aria-hidden="true"
></i>
</button>
</div>
<ng-container *ngIf="activeAccountEmail != null">
<div class="border" *ngIf="numberOfAccounts > 0"></div>
<ng-container *ngIf="numberOfAccounts < 4">
<button class="add" routerLink="/login" (click)="addAccount()">
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="numberOfAccounts == 4">
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
</ng-container>
</ng-container>
</div>
</ng-template>

View File

@@ -0,0 +1,135 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, OnInit } from "@angular/core";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { Utils } from "jslib-common/misc/utils";
import { Account } from "jslib-common/models/domain/account";
export class SwitcherAccount extends Account {
get serverUrl() {
return this.removeWebProtocolFromString(
this.settings?.environmentUrls?.base ??
this.settings?.environmentUrls.api ??
"https://bitwarden.com"
);
}
private removeWebProtocolFromString(urlString: string) {
const regex = /http(s)?(:)?(\/\/)?|(\/\/)?(www\.)?/g;
return urlString.replace(regex, "");
}
}
@Component({
selector: "app-account-switcher",
templateUrl: "account-switcher.component.html",
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
})
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
})
)
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
})
export class AccountSwitcherComponent implements OnInit {
isOpen = false;
accounts: { [userId: string]: SwitcherAccount } = {};
activeAccountEmail: string;
serverUrl: string;
authStatus = AuthenticationStatus;
overlayPostition: ConnectedPosition[] = [
{
originX: "end",
originY: "bottom",
overlayX: "end",
overlayY: "top",
},
];
get showSwitcher() {
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccountEmail);
const userIsAddingAnAdditionalAccount = Object.keys(this.accounts).length > 0;
return userIsInAVault || userIsAddingAnAdditionalAccount;
}
get numberOfAccounts() {
if (this.accounts == null) {
this.isOpen = false;
return 0;
}
return Object.keys(this.accounts).length;
}
constructor(
private stateService: StateService,
private authService: AuthService,
private messagingService: MessagingService
) {}
async ngOnInit(): Promise<void> {
this.stateService.accounts.subscribe(async (accounts: { [userId: string]: Account }) => {
for (const userId in accounts) {
accounts[userId].profile.authenticationStatus = await this.authService.getAuthStatus(
userId
);
}
this.accounts = await this.createSwitcherAccounts(accounts);
this.activeAccountEmail = await this.stateService.getEmail();
});
}
toggle() {
this.isOpen = !this.isOpen;
}
close() {
this.isOpen = false;
}
async switch(userId: string) {
this.close();
this.messagingService.send("switchAccount", { userId: userId });
}
async addAccount() {
this.close();
await this.stateService.setActiveUser(null);
}
private async createSwitcherAccounts(baseAccounts: {
[userId: string]: Account;
}): Promise<{ [userId: string]: SwitcherAccount }> {
const switcherAccounts: { [userId: string]: SwitcherAccount } = {};
for (const userId in baseAccounts) {
if (userId == null || userId === (await this.stateService.getUserId())) {
continue;
}
// environmentUrls are stored on disk and must be retrieved seperatly from the in memory state offered from subscribing to accounts
baseAccounts[userId].settings.environmentUrls = await this.stateService.getEnvironmentUrls({
userId: userId,
});
switcherAccounts[userId] = new SwitcherAccount(baseAccounts[userId]);
}
return switcherAccounts;
}
}

View File

@@ -0,0 +1,4 @@
<div class="header">
<app-search></app-search>
<app-account-switcher></app-account-switcher>
</div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-header",
templateUrl: "header.component.html",
})
export class HeaderComponent {}

View File

@@ -0,0 +1,13 @@
<ng-container *ngFor="let item of items">
<button
type="button"
[routerLink]="item.link"
class="btn primary"
routerLinkActive="active"
[title]="item.label"
#rla="routerLinkActive"
[attr.aria-pressed]="rla.isActive"
>
<i class="bwi" [ngClass]="item.icon"></i>{{ item.label }}
</button>
</ng-container>

View File

@@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Component({
selector: "app-nav",
templateUrl: "nav.component.html",
})
export class NavComponent {
items: any[] = [
{
link: "/vault",
icon: "bwi-lock-f",
label: this.i18nService.translate("myVault"),
},
{
link: "/send",
icon: "bwi-send-f",
label: "Send",
},
];
constructor(private i18nService: I18nService) {}
}

View File

@@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs";
export type SearchBarState = {
enabled: boolean;
placeholderText: string;
};
@Injectable()
export class SearchBarService {
searchText = new BehaviorSubject<string>(null);
private _state = {
enabled: false,
placeholderText: "",
};
// tslint:disable-next-line:member-ordering
state = new BehaviorSubject<SearchBarState>(this._state);
setEnabled(enabled: boolean) {
this._state.enabled = enabled;
this.updateState();
}
setPlaceholderText(placeholderText: string) {
this._state.placeholderText = placeholderText;
this.updateState();
}
setSearchText(value: string) {
this.searchText.next(value);
}
private updateState() {
this.state.next(this._state);
}
}

View File

@@ -0,0 +1,11 @@
<div class="search" *ngIf="state.enabled">
<input
type="search"
[placeholder]="state.placeholderText"
id="search"
autocomplete="off"
[formControl]="searchText"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>

View File

@@ -0,0 +1,23 @@
import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";
import { SearchBarService, SearchBarState } from "./search-bar.service";
@Component({
selector: "app-search",
templateUrl: "search.component.html",
})
export class SearchComponent {
state: SearchBarState;
searchText: FormControl = new FormControl(null);
constructor(private searchBarService: SearchBarService) {
this.searchBarService.state.subscribe((state) => {
this.state = state;
});
this.searchText.valueChanges.subscribe((value) => {
this.searchBarService.setSearchText(value);
});
}
}

View File

@@ -0,0 +1,19 @@
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { isDev } from "jslib-electron/utils";
// tslint:disable-next-line
require("../scss/styles.scss");
import { AppModule } from "./app.module";
if (!isDev()) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
// Disable drag and drop to prevent malicious links from executing in the context of the app
document.addEventListener("dragover", (event) => event.preventDefault());
document.addEventListener("drop", (event) => event.preventDefault());

View File

@@ -0,0 +1,292 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<div class="inner-content" *ngIf="send">
<div class="box">
<app-callout *ngIf="disableSend">
<span>{{ "sendDisabledWarning" | i18n }}</span>
</app-callout>
<app-callout type="info" *ngIf="disableHideEmail && !disableSend">
{{ "sendOptionsPolicyInEffect" | i18n }}
</app-callout>
</div>
<div class="box">
<div class="box-header">
{{ title }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="send.name"
appAutofocus
[readOnly]="disableSend"
/>
</div>
<div class="box-content-row box-content-row-radio" *ngIf="!editMode">
<label class="radio-header">{{ "whatTypeOfSend" | i18n }}</label>
<div class="item" *ngFor="let o of typeOptions">
<input
type="radio"
class="radio"
[(ngModel)]="send.type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged(o)"
[checked]="send.type === o.value"
[disabled]="disableSend"
/>
<label class="unstyled" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBowRow *ngIf="!editMode && send.type === sendType.File">
<label for="file">{{ "file" | i18n }}</label>
<input
type="file"
id="file"
class="form-control-file"
name="file"
required
[disabled]="disableSend"
/>
</div>
<div class="box-content-row" appBowRow *ngIf="editMode && send.type === sendType.File">
<label for="file">{{ "file" | i18n }}</label>
<div class="row-main">{{ send.file.fileName }} ({{ send.file.sizeName }})</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="send.type === sendType.Text">
<label for="text">{{ "text" | i18n }}</label>
<textarea
id="text"
name="text"
[(ngModel)]="send.text.text"
rows="6"
[readOnly]="disableSend"
></textarea>
</div>
</div>
<div class="box-footer" *ngIf="!editMode && send.type === sendType.File">
{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}
</div>
<div class="box-footer" *ngIf="send.type === sendType.Text">
{{ "sendTextDesc" | i18n }}
</div>
</div>
<div class="box" *ngIf="send.type === sendType.Text">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideText">{{ "textHiddenByDefault" | i18n }}</label>
<input
id="hideText"
name="hideText"
type="checkbox"
[(ngModel)]="send.text.hidden"
[disabled]="disableSend"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
<button
type="button"
class="toggle"
appStopClick
(click)="toggleOptions()"
[attr.aria-expanded]="showOptions"
>
{{ "options" | i18n }}
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !showOptions, 'bwi-chevron-up': showOptions }"
></i>
</button>
</div>
</div>
<div [hidden]="!showOptions">
<app-send-efflux-dates
[initialDeletionDate]="send.deletionDate"
[initialExpirationDate]="send.expirationDate"
[editMode]="editMode"
[disabled]="disableSend"
(datesChanged)="setDates($event)"
>
</app-send-efflux-dates>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="maxAccessCount">{{ "maxAccessCount" | i18n }}</label>
<input
id="maxAccessCount"
type="number"
name="maxAccessCount"
[(ngModel)]="send.maxAccessCount"
[readOnly]="disableSend"
/>
</div>
</div>
<div class="box-footer" *ngIf="!editMode">
{{ "maxAccessCountDesc" | i18n }}
</div>
<div class="box-footer" *ngIf="editMode">
<p>{{ "maxAccessCountDesc" | i18n }}</p>
{{ "currentAccessCount" | i18n }}: <strong>{{ send.accessCount }}</strong>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="password">{{
(hasPassword ? "newPassword" : "password") | i18n
}}</label>
<input
id="password"
name="password"
type="{{ showPassword ? 'text' : 'password' }}"
[(ngModel)]="password"
[readOnly]="disableSend"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePasswordVisible()"
[disabled]="disableSend"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
<div class="box-footer">
{{ "sendPasswordDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-header">
{{ "notes" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea
id="notes"
name="notes"
[(ngModel)]="send.notes"
rows="6"
[readOnly]="disableSend"
></textarea>
</div>
</div>
<div class="box-footer">
{{ "sendNotesDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="hideEmail">{{ "hideEmail" | i18n }}</label>
<input
id="hideEmail"
type="checkbox"
name="HideEmail"
[(ngModel)]="send.hideEmail"
[disabled]="(disableHideEmail && !send.hideEmail) || disableSend"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="disabled">{{ "disableSend" | i18n }}</label>
<input
id="disabled"
type="checkbox"
name="disabled"
[(ngModel)]="send.disabled"
[disabled]="disableSend"
/>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
{{ "share" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow *ngIf="editMode">
<label for="link">{{ "sendLinkLabel" | i18n }}</label>
<input id="link" name="link" [ngModel]="link" readonly />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="copyLink">{{ "copySendLinkOnSave" | i18n }}</label>
<input
id="copyLink"
name="copyLink"
[(ngModel)]="copyLink"
type="checkbox"
[disabled]="disableSend"
/>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<button
type="submit"
class="primary btn-submit"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
*ngIf="!disableSend"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span><i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i></span>
</button>
<button type="button" (click)="cancel()" [disabled]="form.loading">
{{ "cancel" | i18n }}
</button>
<div class="right">
<button
type="button"
(click)="copyLinkToClipboard(link)"
appA11yTitle="{{ 'copySendLinkToClipboard' | i18n }}"
*ngIf="editMode"
>
<i class="bwi bwi-clone bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
<button
#deleteBtn
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,62 @@
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
import { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/send/add-edit.component";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-send-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
datePipe: DatePipe,
sendService: SendService,
stateService: StateService,
messagingService: MessagingService,
policyService: PolicyService,
logService: LogService
) {
super(
i18nService,
platformUtilsService,
environmentService,
datePipe,
sendService,
messagingService,
policyService,
logService,
stateService
);
}
async refresh() {
this.password = null;
const send = await this.loadSend();
this.send = await send.decrypt();
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
}
cancel() {
this.onCancelled.emit(this.send);
}
async copyLinkToClipboard(link: string) {
super.copyLinkToClipboard(link);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("valueCopied", this.i18nService.t("sendLink"))
);
}
}

View File

@@ -0,0 +1,55 @@
<ng-container [formGroup]="datesForm">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="deletionDate">{{ "deletionDate" | i18n }}</label>
<select
id="deletionDate"
name="DeletionDateSelect"
formControlName="selectedDeletionDatePreset"
required
>
<option *ngFor="let o of deletionDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="help-block">{{ "deletionDateDesc" | i18n }}</small>
</div>
<div class="box-content-row" *ngIf="selectedDeletionDatePreset.value === 0 || editMode">
<label *ngIf="editMode" for="deletionDateCustom">{{ "deletionDate" | i18n }}</label>
<input
id="deletionDateCustom"
type="datetime-local"
name="deletionDate"
formControlName="defaultDeletionDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
/>
<small class="help-block" *ngIf="editMode">{{ "deletionDateDesc" | i18n }}</small>
</div>
<div class="box-content-row" appBoxRow *ngIf="!editMode">
<label for="expirationDate">{{ "expirationDate" | i18n }}</label>
<select
id="expirationDate"
name="expirationDateSelect"
formControlName="selectedExpirationDatePreset"
required
>
<option *ngFor="let o of expirationDatePresets" [ngValue]="o.value">{{ o.name }}</option>
</select>
<small class="help-block">{{ "expirationDateDesc" | i18n }}</small>
</div>
<div class="box-content-row" *ngIf="selectedExpirationDatePreset.value === 0 || editMode">
<label *ngIf="editMode" for="expirationDateCustom">{{ "expirationDate" | i18n }}</label>
<input
id="expirationDateCustom"
type="datetime-local"
name="expirationDate"
formControlName="defaultExpirationDateTime"
required
placeholder="MM/DD/YYYY HH:MM AM/PM"
[readOnly]="disableSend"
/>
<small *ngIf="editMode" class="help-block">{{ "expirationDateDesc" | i18n }}</small>
</div>
</div>
</div>
</ng-container>

View File

@@ -0,0 +1,38 @@
import { DatePipe } from "@angular/common";
import { Component, OnChanges } from "@angular/core";
import { ControlContainer, NgForm } from "@angular/forms";
import { EffluxDatesComponent as BaseEffluxDatesComponent } from "jslib-angular/components/send/efflux-dates.component";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-send-efflux-dates",
templateUrl: "efflux-dates.component.html",
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class EffluxDatesComponent extends BaseEffluxDatesComponent implements OnChanges {
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected datePipe: DatePipe
) {
super(i18nService, platformUtilsService, datePipe);
}
// We reuse the same form on desktop and just swap content, so need to watch these to maintin proper values.
ngOnChanges() {
this.selectedExpirationDatePreset.setValue(0);
this.selectedDeletionDatePreset.setValue(0);
this.defaultDeletionDateTime.setValue(
this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm")
);
if (this.initialExpirationDate) {
this.defaultExpirationDateTime.setValue(
this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm")
);
} else {
this.defaultExpirationDateTime.setValue(null);
}
}
}

View File

@@ -0,0 +1,153 @@
<div id="sends" class="vault">
<div class="groupings">
<div class="content">
<div class="inner-content">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedAll }">
<button
type="button"
appStopClick
(click)="selectAll()"
[attr.aria-pressed]="selectedAll"
>
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{ "allSends" | i18n }}
</button>
</li>
</ul>
<h2>{{ "types" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedType === sendType.Text }">
<button
type="button"
appStopClick
(click)="selectType(sendType.Text)"
[attr.aria-pressed]="selectedType === sendType.Text"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>&nbsp;{{
"sendTypeText" | i18n
}}
</button>
</li>
<li [ngClass]="{ active: selectedType === sendType.File }">
<button
type="button"
appStopClick
(click)="selectType(sendType.File)"
[attr.aria-pressed]="selectedType === sendType.File"
>
<i class="bwi bwi-fw bwi-file" aria-hidden="true"></i>&nbsp;{{
"sendTypeFile" | i18n
}}
</button>
</li>
</ul>
</div>
<div class="footer">
<app-nav class="nav"></app-nav>
</div>
</div>
</div>
<div id="items" class="items">
<div class="content">
<div class="list" *ngIf="filteredSends.length">
<button
*ngFor="let s of filteredSends"
appStopClick
(click)="selectSend(s.id)"
title="{{ 'viewItem' | i18n }}"
(contextmenu)="viewSendMenu(s)"
[ngClass]="{ active: s.id === sendId }"
[attr.aria-pressed]="s.id === sendId"
class="flex-list-item"
>
<span class="item-icon" aria-hidden="true">
<i class="bwi bwi-fw bwi-lg" [ngClass]="s.type == 0 ? 'bwi-file-text' : 'bwi-file'"></i>
</span>
<span class="item-content">
<span class="item-title">
{{ s.name }}
<span class="title-badges">
<ng-container *ngIf="s.disabled">
<i
class="bwi bwi-exclamation-triangle"
appStopProp
title="{{ 'disabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "disabled" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i
class="bwi bwi-key"
appStopProp
title="{{ 'password' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "password" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i
class="bwi bwi-ban"
appStopProp
title="{{ 'maxAccessCountReached' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "maxAccessCountReached" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i
class="bwi bwi-clock"
appStopProp
title="{{ 'expired' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "expired" | i18n }}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i
class="bwi bwi-trash"
appStopProp
title="{{ 'pendingDeletion' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "pendingDeletion" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="item-details">{{ s.deletionDate | date }}</span>
</span>
</button>
</div>
<div class="no-items" *ngIf="!filteredSends.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
</ng-container>
</div>
</div>
<div class="footer">
<button (click)="addSend()" class="block primary" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
<app-send-add-edit
id="addEdit"
class="details"
*ngIf="action == 'add' || action == 'edit'"
[sendId]="sendId"
[type]="selectedSendType"
(onSavedSend)="savedSend($event)"
(onCancelled)="cancel($event)"
(onDeletedSend)="deletedSend($event)"
></app-send-add-edit>
<div class="logo" *ngIf="!action">
<div class="content">
<div class="inner-content">
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,144 @@
import { Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { SendComponent as BaseSendComponent } from "jslib-angular/components/send/send.component";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { SendService } from "jslib-common/abstractions/send.service";
import { SendView } from "jslib-common/models/view/sendView";
import { invokeMenu, RendererMenuItem } from "jslib-electron/utils";
import { SearchBarService } from "../layout/search/search-bar.service";
import { AddEditComponent } from "./add-edit.component";
enum Action {
None = "",
Add = "add",
Edit = "edit",
}
const BroadcasterSubscriptionId = "SendComponent";
@Component({
selector: "app-send",
templateUrl: "send.component.html",
})
export class SendComponent extends BaseSendComponent implements OnInit, OnDestroy {
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
sendId: string;
action: Action = Action.None;
constructor(
sendService: SendService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
searchService: SearchService,
policyService: PolicyService,
private searchBarService: SearchBarService,
logService: LogService
) {
super(
sendService,
i18nService,
platformUtilsService,
environmentService,
ngZone,
searchService,
policyService,
logService
);
this.searchBarService.searchText.subscribe((searchText) => {
this.searchText = searchText;
this.searchTextChanged();
});
}
async ngOnInit() {
this.searchBarService.setEnabled(true);
this.searchBarService.setPlaceholderText(this.i18nService.t("searchSends"));
super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
await this.load();
break;
}
});
});
await this.load();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.searchBarService.setEnabled(false);
}
addSend() {
this.action = Action.Add;
if (this.addEditComponent != null) {
this.addEditComponent.sendId = null;
this.addEditComponent.send = null;
this.addEditComponent.load();
}
}
cancel(s: SendView) {
this.action = Action.None;
this.sendId = null;
}
async deletedSend(s: SendView) {
await this.refresh();
this.action = Action.None;
this.sendId = null;
}
async savedSend(s: SendView) {
await this.refresh();
this.selectSend(s.id);
}
async selectSend(sendId: string) {
if (sendId === this.sendId && this.action === Action.Edit) {
return;
}
this.action = Action.Edit;
this.sendId = sendId;
if (this.addEditComponent != null) {
this.addEditComponent.sendId = sendId;
await this.addEditComponent.refresh();
}
}
get selectedSendType() {
return this.sends.find((s) => s.id === this.sendId)?.type;
}
viewSendMenu(send: SendView) {
const menu: RendererMenuItem[] = [];
menu.push({
label: this.i18nService.t("copyLink"),
click: () => this.copy(send),
});
menu.push({
label: this.i18nService.t("delete"),
click: async () => {
await this.delete(send);
await this.deletedSend(send);
},
});
invokeMenu(menu);
}
}

View File

@@ -0,0 +1,81 @@
import { Inject, Injectable } from "@angular/core";
import { WINDOW } from "jslib-angular/services/jslib-services.module";
import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "jslib-common/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "jslib-common/abstractions/event.service";
import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
import { NotificationsService as NotificationsServiceAbstraction } from "jslib-common/abstractions/notifications.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
import { ThemeType } from "jslib-common/enums/themeType";
import { ContainerService } from "jslib-common/services/container.service";
import { EventService } from "jslib-common/services/event.service";
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { I18nService } from "../../services/i18n.service";
import { NativeMessagingService } from "../../services/nativeMessaging.service";
@Injectable()
export class InitService {
constructor(
@Inject(WINDOW) private win: Window,
private environmentService: EnvironmentServiceAbstraction,
private syncService: SyncServiceAbstraction,
private vaultTimeoutService: VaultTimeoutServiceAbstraction,
private i18nService: I18nServiceAbstraction,
private eventService: EventServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private notificationsService: NotificationsServiceAbstraction,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,
private cryptoService: CryptoServiceAbstraction,
private nativeMessagingService: NativeMessagingService
) {}
init() {
return async () => {
this.nativeMessagingService.init();
await this.stateService.init();
await this.environmentService.setUrlsFromStorage();
this.syncService.fullSync(true);
(this.vaultTimeoutService as VaultTimeoutService).init(true);
const locale = await this.stateService.getLocale();
await (this.i18nService as I18nService).init(locale);
(this.eventService as EventService).init(true);
this.twoFactorService.init();
setTimeout(() => this.notificationsService.init(), 3000);
const htmlEl = this.win.document.documentElement;
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
const theme = await this.platformUtilsService.getEffectiveTheme();
htmlEl.classList.add("theme_" + theme);
this.platformUtilsService.onDefaultSystemThemeChange(async (sysTheme) => {
const bwTheme = await this.stateService.getTheme();
if (bwTheme == null || bwTheme === ThemeType.System) {
htmlEl.classList.remove("theme_" + ThemeType.Light, "theme_" + ThemeType.Dark);
htmlEl.classList.add("theme_" + sysTheme);
}
});
let installAction = null;
const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion();
if (installedVersion == null) {
installAction = "install";
} else if (installedVersion !== currentVersion) {
installAction = "update";
}
if (installAction != null) {
await this.stateService.setInstalledVersion(currentVersion);
}
const containerService = new ContainerService(this.cryptoService);
containerService.attachToGlobal(this.win);
};
}
}

View File

@@ -0,0 +1,135 @@
import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core";
import {
JslibServicesModule,
SECURE_STORAGE,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
WINDOW,
CLIENT_TYPE,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
} from "jslib-angular/services/jslib-services.module";
import { BroadcasterService as BroadcasterServiceAbstraction } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "jslib-common/abstractions/cryptoFunction.service";
import { I18nService as I18nServiceAbstraction } from "jslib-common/abstractions/i18n.service";
import {
LogService,
LogService as LogServiceAbstraction,
} from "jslib-common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "jslib-common/abstractions/messaging.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "jslib-common/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "jslib-common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "jslib-common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "jslib-common/abstractions/storage.service";
import { SystemService as SystemServiceAbstraction } from "jslib-common/abstractions/system.service";
import { ClientType } from "jslib-common/enums/clientType";
import { StateFactory } from "jslib-common/factories/stateFactory";
import { GlobalState } from "jslib-common/models/domain/globalState";
import { SystemService } from "jslib-common/services/system.service";
import { ElectronCryptoService } from "jslib-electron/services/electronCrypto.service";
import { ElectronLogService } from "jslib-electron/services/electronLog.service";
import { ElectronPlatformUtilsService } from "jslib-electron/services/electronPlatformUtils.service";
import { ElectronRendererMessagingService } from "jslib-electron/services/electronRendererMessaging.service";
import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service";
import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service";
import { Account } from "../../models/account";
import { I18nService } from "../../services/i18n.service";
import { LoginGuardService } from "../../services/loginGuard.service";
import { NativeMessagingService } from "../../services/nativeMessaging.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
import { SearchBarService } from "../layout/search/search-bar.service";
import { InitService } from "./init.service";
const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
@NgModule({
imports: [JslibServicesModule],
declarations: [],
providers: [
InitService,
NativeMessagingService,
SearchBarService,
LoginGuardService,
{
provide: APP_INITIALIZER,
useFactory: (initService: InitService) => initService.init(),
deps: [InitService],
multi: true,
},
{
provide: STATE_FACTORY,
useValue: new StateFactory(GlobalState, Account),
},
{
provide: CLIENT_TYPE,
useValue: ClientType.Desktop,
},
{
provide: RELOAD_CALLBACK,
useValue: null,
},
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
{
provide: PlatformUtilsServiceAbstraction,
useClass: ElectronPlatformUtilsService,
deps: [
I18nServiceAbstraction,
MessagingServiceAbstraction,
CLIENT_TYPE,
StateServiceAbstraction,
],
},
{
provide: I18nServiceAbstraction,
useClass: I18nService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
},
{
provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
},
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
{
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
CryptoFunctionServiceAbstraction,
PlatformUtilsServiceAbstraction,
LogServiceAbstraction,
StateServiceAbstraction,
],
},
{
provide: SystemServiceAbstraction,
useClass: SystemService,
deps: [
MessagingServiceAbstraction,
PlatformUtilsServiceAbstraction,
RELOAD_CALLBACK,
StateServiceAbstraction,
],
},
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: StateServiceAbstraction,
useClass: StateService,
deps: [
StorageServiceAbstraction,
SECURE_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,
STATE_SERVICE_USE_CACHE,
],
},
],
})
export class ServicesModule {}

View File

@@ -0,0 +1,118 @@
<div class="box">
<div class="box-header">
{{ "customFields" | i18n }}
</div>
<div class="box-content">
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div
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 }"
>
<button
type="button"
appStopClick
(click)="removeField(f)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<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
/>
<!-- Text -->
<input
id="fieldValue{{ i }}"
type="text"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Text"
placeholder="{{ 'value' | i18n }}"
appInputVerbatim
/>
<!-- 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
/>
<!-- Linked -->
<select
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
>
<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"
/>
<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)"
>
<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 }}" cdkDragHandle>
<i class="bwi bwi-hamburger" aria-hidden="true"></i>
</div>
</div>
</div>
<!-- Add new custom field -->
<div class="box-content-row" 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

@@ -0,0 +1,15 @@
import { Component } from "@angular/core";
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "jslib-angular/components/add-edit-custom-fields.component";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Component({
selector: "app-vault-add-edit-custom-fields",
templateUrl: "add-edit-custom-fields.component.html",
})
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
constructor(i18nService: I18nService, eventService: EventService) {
super(i18nService, eventService);
}
}

View File

@@ -0,0 +1,611 @@
<form #form="ngForm" (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<div class="inner-content" *ngIf="cipher">
<div class="box">
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
{{ "personalOwnershipPolicyInEffect" | i18n }}
</app-callout>
<div class="box-header">
{{ title }}
</div>
<div class="box-content">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="cipher.name"
[appAutofocus]="!editMode"
/>
</div>
<!-- Login -->
<div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginUsername">{{ "username" | i18n }}</label>
<input
id="loginUsername"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generateUsername' | i18n }}"
(click)="generateUsername()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginPassword">{{ "password" | i18n }}</label>
<input
id="loginPassword"
class="monospaced"
type="{{ showPassword ? 'text' : 'password' }}"
name="Login.Password"
[(ngModel)]="cipher.login.password"
[disabled]="!cipher.viewPassword"
appInputVerbatim
/>
</div>
<div class="action-buttons" *ngIf="cipher.viewPassword">
<button
type="button"
#checkPasswordBtn
class="row-btn btn"
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="checkPasswordBtn.loading"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="checkPasswordBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!checkPasswordBtn.loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generatePassword' | i18n }}"
(click)="generatePassword()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
<input
id="loginTotp"
type="{{ cipher.viewPassword ? 'text' : 'password' }}"
name="Login.Totp"
class="monospaced"
[(ngModel)]="cipher.login.totp"
[disabled]="!cipher.viewPassword"
appInputVerbatim
/>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.type === cipherType.Card">
<div class="box-content-row" appBoxRow>
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
<input
id="cardCardholderName"
type="text"
name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardNumber">{{ "number" | i18n }}</label>
<input
id="cardNumber"
class="monospaced"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
[(ngModel)]="cipher.card.number"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showCardNumber"
(click)="toggleCardNumber()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardBrand">{{ "brand" | i18n }}</label>
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
<input
id="cardExpYear"
type="text"
name="Card.ExpYear"
[(ngModel)]="cipher.card.expYear"
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardCode">{{ "securityCode" | i18n }}</label>
<input
id="cardCode"
class="monospaced"
type="{{ showCardCode ? 'text' : 'password' }}"
name="Card.Code"
[(ngModel)]="cipher.card.code"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showCardCode"
(click)="toggleCardCode()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
</div>
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.type === cipherType.Identity">
<div class="box-content-row" appBoxRow>
<label for="idTitle">{{ "title" | i18n }}</label>
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="idFirstName">{{ "firstName" | i18n }}</label>
<input
id="idFirstName"
type="text"
name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
<input
id="idMiddleName"
type="text"
name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLastName">{{ "lastName" | i18n }}</label>
<input
id="idLastName"
type="text"
name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idUsername">{{ "username" | i18n }}</label>
<input
id="idUsername"
type="text"
name="Identity.Username"
[(ngModel)]="cipher.identity.username"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCompany">{{ "company" | i18n }}</label>
<input
id="idCompany"
type="text"
name="Identity.Company"
[(ngModel)]="cipher.identity.company"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idSsn">{{ "ssn" | i18n }}</label>
<input
id="idSsn"
type="text"
name="Identity.SSN"
[(ngModel)]="cipher.identity.ssn"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
<input
id="idPassportNumber"
type="text"
name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
<input
id="idLicenseNumber"
type="text"
name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idEmail">{{ "email" | i18n }}</label>
<input
id="idEmail"
type="text"
name="Identity.Email"
[(ngModel)]="cipher.identity.email"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPhone">{{ "phone" | i18n }}</label>
<input
id="idPhone"
type="text"
name="Identity.Phone"
[(ngModel)]="cipher.identity.phone"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress1">{{ "address1" | i18n }}</label>
<input
id="idAddress1"
type="text"
name="Identity.Address1"
[(ngModel)]="cipher.identity.address1"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress2">{{ "address2" | i18n }}</label>
<input
id="idAddress2"
type="text"
name="Identity.Address2"
[(ngModel)]="cipher.identity.address2"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress3">{{ "address3" | i18n }}</label>
<input
id="idAddress3"
type="text"
name="Identity.Address3"
[(ngModel)]="cipher.identity.address3"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCity">{{ "cityTown" | i18n }}</label>
<input
id="idCity"
type="text"
name="Identity.City"
[(ngModel)]="cipher.identity.city"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idState">{{ "stateProvince" | i18n }}</label>
<input
id="idState"
type="text"
name="Identity.State"
[(ngModel)]="cipher.identity.state"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="idPostalCode"
type="text"
name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCountry">{{ "country" | i18n }}</label>
<input
id="idCountry"
type="text"
name="Identity.Country"
[(ngModel)]="cipher.identity.country"
/>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.type === cipherType.Login">
<div class="box-content">
<ng-container *ngIf="cipher.login.hasUris">
<div
class="box-content-row box-content-row-multi"
appBoxRow
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
>
<button
type="button"
appStopClick
(click)="removeUri(u)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
<input
id="loginUri{{ i }}"
type="text"
name="Login.Uris[{{ i }}].Uri"
[(ngModel)]="u.uri"
placeholder="{{ 'ex' | i18n }} https://google.com"
appInputVerbatim
/>
<label for="loginUriMatch{{ i }}" class="sr-only">
{{ "matchDetection" | i18n }} {{ i + 1 }}
</label>
<select
id="loginUriMatch{{ i }}"
name="Login.Uris[{{ i }}].Match"
[(ngModel)]="u.match"
[hidden]="u.showOptions === false || (u.showOptions == null && u.match == null)"
(change)="loginUriMatchChanged(u)"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleOptions' | i18n }}"
(click)="toggleUriOptions(u)"
[attr.aria-expanded]="
!(u.showOptions === false || (u.showOptions == null && u.match == null))
"
>
<i class="bwi bwi-lg bwi-cog" aria-hidden="true"></i>
</button>
</div>
</div>
</ng-container>
<button type="button" appStopClick (click)="addUri()" class="box-content-row">
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
{{ "newUri" | i18n }}
</button>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="folder">{{ "folder" | i18n }}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
<option *ngFor="let f of folders" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favorite">{{ "favorite" | i18n }}</label>
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
<label for="passwordPrompt"
>{{ "passwordPrompt" | i18n }}
<button
type="button"
appA11yTitle="{{ 'learnMore' | i18n }}"
(click)="openHelpReprompt()"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</button>
</label>
<input
id="passwordPrompt"
type="checkbox"
name="PasswordPrompt"
[ngModel]="reprompt"
(change)="repromptChanged()"
/>
</div>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="attachments()"
*ngIf="editMode && !cloneMode"
>
<div class="row-main">{{ "attachments" | i18n }}</div>
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="editCollections()"
*ngIf="editMode && !cloneMode && cipher.organizationId"
>
<div class="row-main">{{ "collections" | i18n }}</div>
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box">
<div class="box-header">
<label for="notes">{{ "notes" | i18n }}</label>
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"></textarea>
</div>
</div>
</div>
<app-vault-add-edit-custom-fields
[cipher]="cipher"
[thisCipherType]="cipher.type"
[editMode]="editMode"
>
</app-vault-add-edit-custom-fields>
<div class="box" *ngIf="allowOwnershipOptions()">
<div class="box-header">
{{ "ownership" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
<select
id="organizationId"
class="form-control"
name="OrganizationId"
[(ngModel)]="cipher.organizationId"
(change)="organizationChanged()"
>
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
</div>
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
<div class="box-header">
{{ "collections" | i18n }}
</div>
<div class="box-content" *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
>
<i class="bwi bwi-save-changes bwi-lg bwi-fw" [hidden]="form.loading" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
<button type="button" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
<div class="right">
<button
type="button"
(click)="share()"
appA11yTitle="{{ 'moveToOrganization' | i18n }}"
*ngIf="editMode && cipher && !cipher.organizationId && !cloneMode"
>
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
<button
#deleteBtn
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode && !cloneMode"
[disabled]="deleteBtn.loading"
[appApiAction]="deletePromise"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,124 @@
import { Component, NgZone, OnChanges, OnDestroy, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "jslib-angular/components/add-edit.component";
import { AuditService } from "jslib-common/abstractions/audit.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
const BroadcasterSubscriptionId = "AddEditComponent";
@Component({
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent implements OnChanges, OnDestroy {
@ViewChild("form")
private form: NgForm;
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
stateService: StateService,
collectionService: CollectionService,
messagingService: MessagingService,
eventService: EventService,
policyService: PolicyService,
passwordRepromptService: PasswordRepromptService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
logService: LogService,
organizationService: OrganizationService
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
stateService,
collectionService,
messagingService,
eventService,
policyService,
logService,
passwordRepromptService,
organizationService
);
}
async ngOnInit() {
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 super.init();
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;
}
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"
);
}
}

View File

@@ -0,0 +1,78 @@
<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">
<div class="box-header" id="attachmentsTitle">
{{ "attachments" | i18n }}
</div>
<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]="deleteBtn.loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="deleteBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
{{ "newAttachment" | i18n }}
</div>
<div class="box-content no-hover">
<div class="box-content-row">
<label for="file">{{ "file" | i18n }}</label>
<input type="file" id="file" name="file" required />
</div>
</div>
<div class="box-footer">
{{ "maxFileSize" | i18n }}
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
>
<i
class="bwi bwi-save-changes bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,37 @@
import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "jslib-angular/components/attachments.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-vault-attachments",
templateUrl: "attachments.component.html",
})
export class AttachmentsComponent extends BaseAttachmentsComponent {
constructor(
cipherService: CipherService,
i18nService: I18nService,
cryptoService: CryptoService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService,
stateService: StateService
) {
super(
cipherService,
i18nService,
cryptoService,
platformUtilsService,
apiService,
window,
logService,
stateService
);
}
}

View File

@@ -0,0 +1,65 @@
<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">
{{ c.name }}
<ng-container *ngIf="c.organizationId">
<i
class="bwi bwi-collection 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 *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
</div>
</button>
</div>
</cdk-virtual-scroll-viewport>
<div class="no-items" *ngIf="!ciphers.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button (click)="addCipher()" class="btn block primary link">{{ "addItem" | i18n }}</button>
</ng-container>
</div>
</div>
<div class="footer">
<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>

View File

@@ -0,0 +1,26 @@
import { Component } from "@angular/core";
import { CiphersComponent as BaseCiphersComponent } from "jslib-angular/components/ciphers.component";
import { SearchService } from "jslib-common/abstractions/search.service";
import { CipherView } from "jslib-common/models/view/cipherView";
import { SearchBarService } from "../layout/search/search-bar.service";
@Component({
selector: "app-vault-ciphers",
templateUrl: "ciphers.component.html",
})
export class CiphersComponent extends BaseCiphersComponent {
constructor(searchService: SearchService, searchBarService: SearchBarService) {
super(searchService);
searchBarService.searchText.subscribe((searchText) => {
this.searchText = searchText;
this.search(200);
});
}
trackByFn(index: number, c: CipherView) {
return c.id;
}
}

View File

@@ -0,0 +1,51 @@
<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">
<div class="box-header" id="collectionsTitle">
{{ "collections" | i18n }}
</div>
<div class="box-content" *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
>
<i
class="bwi bwi-save-changes bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "jslib-angular/components/collections.component";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-vault-collections",
templateUrl: "collections.component.html",
})
export class CollectionsComponent extends BaseCollectionsComponent {
constructor(
cipherService: CipherService,
i18nService: I18nService,
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(collectionService, platformUtilsService, i18nService, cipherService, logService);
}
}

View File

@@ -0,0 +1,45 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="exportTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [formGroup]="exportForm">
<div class="modal-body">
<app-callout
type="warning"
title="{{ 'vaultExportDisabled' | i18n }}"
*ngIf="disabledByPolicy"
>
{{ "personalVaultExportPolicyInEffect" | i18n }}
</app-callout>
<app-export-scope-callout *ngIf="!disabledByPolicy"></app-export-scope-callout>
<div class="box">
<div class="box-header" id="exportTitle">
{{ "exportVault" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="format">{{ "fileFormat" | i18n }}</label>
<select class="form-control" id="format" name="Format" formControlName="format">
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
</select>
</div>
<app-user-verification ngDefaultControl formControlName="secret" name="secret">
</app-user-verification>
</div>
<div class="box-footer">
<p>{{ "confirmIdentity" | i18n }}</p>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'submit' | i18n }}"
[disabled]="disabledByPolicy"
>
<i class="bwi bwi-download bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,77 @@
import * as os from "os";
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ExportComponent as BaseExportComponent } from "jslib-angular/components/export.component";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { ExportService } from "jslib-common/abstractions/export.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
const BroadcasterSubscriptionId = "ExportComponent";
@Component({
selector: "app-export",
templateUrl: "export.component.html",
})
export class ExportComponent extends BaseExportComponent implements OnInit {
constructor(
cryptoService: CryptoService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
exportService: ExportService,
eventService: EventService,
policyService: PolicyService,
userVerificationService: UserVerificationService,
formBuilder: FormBuilder,
private broadcasterService: BroadcasterService,
logService: LogService
) {
super(
cryptoService,
i18nService,
platformUtilsService,
exportService,
eventService,
policyService,
window,
logService,
userVerificationService,
formBuilder
);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async warningDialog() {
if (this.encryptedFormat) {
return await this.platformUtilsService.showDialog(
this.i18nService.t("encExportKeyWarningDesc") +
os.EOL +
os.EOL +
this.i18nService.t("encExportAccountWarningDesc"),
this.i18nService.t("confirmVaultExport"),
this.i18nService.t("exportVault"),
this.i18nService.t("cancel"),
"warning",
true
);
} else {
return await this.platformUtilsService.showDialog(
this.i18nService.t("exportWarningDesc"),
this.i18nService.t("confirmVaultExport"),
this.i18nService.t("exportVault"),
this.i18nService.t("cancel"),
"warning"
);
}
}
}

View File

@@ -0,0 +1,68 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="folderAddEditTitle">
<div class="modal-dialog modal-sm" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-body">
<div class="box">
<div class="box-header" id="folderAddEditTitle">
{{ title }}
</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="folder.name"
[appAutofocus]="!editMode"
/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading"
>
<i
class="bwi bwi-save-changes bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
<div class="right">
<button
#deleteBtn
type="button"
(click)="delete()"
class="danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="deleteBtn.loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="deleteBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

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

View File

@@ -0,0 +1,570 @@
<div
class="modal fade"
role="dialog"
aria-modal="true"
attr.aria-label="{{ 'generatePassword' | i18n }}"
>
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">
{{ "generator" | i18n }}
</div>
<app-callout
type="info"
*ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'"
>
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout>
<div class="generated-block" *ngIf="type === 'password'">
<div class="generated-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
<div class="action-buttons">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="generated-block" *ngIf="type === 'username'">
<div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="regenerate()"
[disabled]="form.loading"
>
<i
class="bwi bwi-lg bwi-generate"
[ngClass]="form.loading ? 'bwi-spin' : ''"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="box">
<div class="box-content condensed">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="typeHeading"
>
<label id="typeHeading" class="radio-header">{{
"whatWouldYouLikeToGenerate" | i18n
}}</label>
<div
class="radio-group text-default"
appBoxRow
name="TypeOptions"
*ngFor="let o of typeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="type"
name="Type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="type === o.value"
[disabled]="comingFromAddEdit && type !== o.value"
/>
<label class="unstyled" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<ng-container *ngIf="type === 'password'">
<div class="box">
<div class="box-header">
<button
type="button"
(click)="toggleOptions()"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-expanded]="showOptions"
>
<i class="bwi bwi-plus-square" aria-hidden="true" [hidden]="showOptions"></i>
<i class="bwi bwi-minus-square" aria-hidden="true" [hidden]="!showOptions"></i>
{{ "options" | i18n }}
</button>
</div>
<div class="box-content condensed" [hidden]="!showOptions">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="passwordTypeHeading"
>
<label id="passwordTypeHeading" class="radio-header">{{
"passwordType" | i18n
}}</label>
<div
class="radio-group text-default"
appBoxRow
name="PassTypeOptions"
*ngFor="let o of passTypeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="passwordOptions.type"
name="PasswordType"
id="passwordType_{{ o.value }}"
[value]="o.value"
(change)="savePasswordOptions()"
[checked]="passwordOptions.type === o.value"
/>
<label class="unstyled" for="passwordType_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<div class="box" [hidden]="!showOptions" *ngIf="passwordOptions.type === 'passphrase'">
<div class="box-content condensed">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
type="number"
min="3"
max="20"
(blur)="savePasswordOptions()"
[(ngModel)]="passwordOptions.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
type="text"
maxlength="1"
(input)="savePasswordOptions()"
[(ngModel)]="passwordOptions.wordSeparator"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</div>
</div>
</div>
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
type="number"
min="5"
max="128"
[(ngModel)]="passwordOptions.length"
(blur)="savePasswordOptions()"
/>
<input
id="lengthRange"
type="range"
min="5"
max="128"
step="1"
[(ngModel)]="passwordOptions.length"
(change)="sliderChanged()"
(input)="sliderInput()"
attr.aria-label="{{ 'length' | i18n }}"
tabindex="-1"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input
id="uppercase"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useUppercase"
[(ngModel)]="passwordOptions.uppercase"
attr.aria-label="{{ 'uppercase' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input
id="lowercase"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useLowercase"
[(ngModel)]="passwordOptions.lowercase"
attr.aria-label="{{ 'lowercase' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
[(ngModel)]="passwordOptions.number"
attr.aria-label="{{ 'numbers' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!@#$%^&*</label>
<input
id="special"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
[(ngModel)]="passwordOptions.special"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
/>
</div>
</div>
</div>
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<input
id="min-number"
type="number"
min="0"
max="9"
(blur)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minNumber"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
type="number"
min="0"
max="9"
(blur)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minSpecial"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "ambiguous" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div class="box">
<div class="box-header">
<button
type="button"
(click)="toggleOptions()"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-expanded]="showOptions"
>
<i class="bwi bwi-plus-square" aria-hidden="true" [hidden]="showOptions"></i>
<i class="bwi bwi-minus-square" aria-hidden="true" [hidden]="!showOptions"></i>
{{ "options" | i18n }}
</button>
</div>
<div class="box-content condensed" [hidden]="!showOptions">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="usernameTypeHeading"
>
<label id="usernameTypeHeading" class="radio-header">
{{ "usernameType" | i18n }}
<a
href="#"
appStopClick
(click)="usernameTypesLearnMore()"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</label>
<div
class="radio-group align-start text-default"
appBoxRow
name="UsernameTypeOptions"
*ngFor="let o of usernameTypeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="usernameOptions.type"
name="UsernameType"
id="usernameType_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label class="unstyled" for="usernameType_{{ o.value }}">
{{ o.name }}
<div class="small text-muted" *ngIf="o.desc">{{ o.desc }}</div>
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'forwarded'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" role="radiogroup" aria-labelledby="forwardTypeHeading">
<label id="forwardTypeHeading" class="radio-header">{{ "service" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of forwardOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.forwardedService"
name="ForwardType"
id="forwardtype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.forwardedService === o.value"
/>
<label for="forwardtype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<ng-container *ngIf="usernameOptions.forwardedService === 'simplelogin'">
<div class="box-content-row" appBoxRow>
<label for="simplelogin-apikey">{{ "apiKey" | i18n }}</label>
<input
id="simplelogin-apikey"
type="password"
name="SimpleLoginApiKey"
[(ngModel)]="usernameOptions.forwardedSimpleLoginApiKey"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="simplelogin-hostname">{{ "hostname" | i18n }}</label>
<input
id="simplelogin-hostname"
type="text"
name="SimpleLoginHostname"
[(ngModel)]="usernameOptions.forwardedSimpleLoginHostname"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="box-content-row" appBoxRow>
<label for="anonaddy-accessToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="anonaddy-accessToken"
type="password"
name="AnonAddyAccessToken"
[(ngModel)]="usernameOptions.forwardedAnonAddyApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="anonaddy-domain">{{ "domainName" | i18n }}</label>
<input
id="anonaddy-domain"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyDomain"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'firefoxrelay'">
<div class="box-content-row" appBoxRow>
<label for="firefox-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="firefox-apikey"
type="password"
name="FirefoxApiKey"
[(ngModel)]="usernameOptions.forwardedFirefoxApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'subaddress'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" appBoxRow>
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
<input
id="subaddress-email"
type="text"
name="SubaddressEmail"
[(ngModel)]="usernameOptions.subaddressEmail"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="subaddressTypeHeading"
*ngIf="subaddressOptions.length > 1"
>
<label id="subaddressTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of subaddressOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.subaddressType"
name="SubaddressType"
id="subaddresstype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.subaddressType === o.value"
/>
<label for="subaddresstype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="subaddress-website">{{ "website" | i18n }}</label>
<input
id="subaddress-website"
type="text"
name="SubaddressWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'catchall'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" appBoxRow>
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
<input
id="catchall-domain"
type="text"
name="CatchallDomain"
[(ngModel)]="usernameOptions.catchallDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="catchallTypeHeading"
*ngIf="catchallOptions.length > 1"
>
<label id="catchallTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of catchallOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.catchallType"
name="CatchallType"
id="catchalltype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.catchallType === o.value"
/>
<label for="catchalltype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="catchall-website">{{ "website" | i18n }}</label>
<input
id="catchall-website"
type="text"
name="CatchallWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'word'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordIncludeNumber"
/>
</div>
</div>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button
type="button"
class="primary"
*ngIf="comingFromAddEdit"
(click)="select()"
appA11yTitle="{{ 'select' | i18n }}"
>
<i class="bwi bwi-lg bwi-fw bwi-check" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal">
{{ (comingFromAddEdit ? "cancel" : "close") | i18n }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { GeneratorComponent as BaseGeneratorComponent } from "jslib-angular/components/generator.component";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { UsernameGenerationService } from "jslib-common/abstractions/usernameGeneration.service";
@Component({
selector: "app-generator",
templateUrl: "generator.component.html",
})
export class GeneratorComponent extends BaseGeneratorComponent {
constructor(
passwordGenerationService: PasswordGenerationService,
usernameGenerationService: UsernameGenerationService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
route: ActivatedRoute,
logService: LogService
) {
super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
stateService,
i18nService,
logService,
route,
window
);
}
usernameTypesLearnMore() {
this.platformUtilsService.launchUri("https://bitwarden.com/help/generator/#username-types");
}
}

View File

@@ -0,0 +1,190 @@
<div class="content">
<div class="inner-content">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedAll }">
<button type="button" [attr.aria-pressed]="selectedAll" appStopClick (click)="selectAll()">
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{ "allItems" | i18n }}
</button>
</li>
<li [ngClass]="{ active: selectedFavorites }">
<button
type="button"
[attr.aria-pressed]="selectedFavorites"
appStopClick
(click)="selectFavorites()"
>
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>&nbsp;{{ "favorites" | i18n }}
</button>
</li>
<li [ngClass]="{ active: selectedTrash }">
<button
type="button"
[attr.aria-pressed]="selectedTrash"
appStopClick
(click)="selectTrash()"
>
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>&nbsp;{{ "trash" | i18n }}
</button>
</li>
</ul>
<h2>{{ "types" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedType === cipherType.Login }">
<button
type="button"
[attr.aria-pressed]="selectedType === cipherType.Login"
appStopClick
(click)="selectType(cipherType.Login)"
>
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
</button>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Card }">
<button
type="button"
[attr.aria-pressed]="selectedType === cipherType.Card"
appStopClick
(click)="selectType(cipherType.Card)"
>
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</button>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Identity }">
<button
type="button"
[attr.aria-pressed]="selectedType === cipherType.Identity"
appStopClick
(click)="selectType(cipherType.Identity)"
>
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</button>
</li>
<li [ngClass]="{ active: selectedType === cipherType.SecureNote }">
<button
type="button"
[attr.aria-pressed]="selectedType === cipherType.SecureNote"
appStopClick
(click)="selectType(cipherType.SecureNote)"
>
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
</button>
</li>
</ul>
<p *ngIf="!loaded" class="text-muted">{{ "loading" | i18n }}</p>
<ng-container *ngIf="loaded">
<div class="heading">
<h2>{{ "folders" | i18n }}</h2>
<button (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</button>
</div>
<ul>
<ng-template #recursiveFolders let-folders>
<li
*ngFor="let f of folders"
[ngClass]="{ active: selectedFolder && f.node.id === selectedFolderId }"
>
<button
type="button"
[attr.aria-pressed]="selectedFolder && f.node.id === selectedFolderId"
appStopClick
(click)="selectFolder(f.node)"
>
<i
*ngIf="f.children.length"
class="bwi bwi-fw"
title="{{ 'toggleCollapse' | i18n }}"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node)
}"
(click)="collapse(f.node)"
appStopProp
></i>
<i
*ngIf="f.children.length === 0"
class="bwi bwi-fw bwi-folder"
aria-hidden="true"
></i>
&nbsp;{{ f.node.name }}
<span
appStopProp
appStopClick
(click)="editFolder(f.node)"
role="button"
appA11yTitle="{{ 'editFolder' | i18n }}"
*ngIf="f.node.id"
>
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
</span>
</button>
<ul class="bwi-ul" *ngIf="f.children.length && !isCollapsed(f.node)">
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
></ng-container>
</ul>
<div *ngIf="collections && collections.length">
<h2>{{ "collections" | i18n }}</h2>
<ul>
<ng-template #recursiveCollections let-collections>
<li
*ngFor="let c of collections"
[ngClass]="{ active: c.node.id === selectedCollectionId }"
>
<button
*ngIf="c.children.length == 0"
type="button"
[attr.aria-pressed]="c.node.id === selectedCollectionId"
appStopClick
(click)="selectCollection(c.node)"
>
<i
*ngIf="c.children.length"
class="bwi bwi-fw"
title="{{ 'toggleCollapse' | i18n }}"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(c.node),
'bwi-angle-down': !isCollapsed(c.node)
}"
(click)="collapse(c.node)"
appStopProp
></i>
<i
*ngIf="c.children.length === 0"
class="bwi bwi-fw bwi-collection"
aria-hidden="true"
></i>
&nbsp;{{ c.node.name }}
</button>
<ul class="bwi-ul" *ngIf="c.children.length && !isCollapsed(c.node)">
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
>
</ng-container>
</ul>
</div>
</ng-container>
</div>
<div class="footer">
<app-nav class="nav"></app-nav>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-vault-groupings",
templateUrl: "groupings.component.html",
})
export class GroupingsComponent extends BaseGroupingsComponent {
constructor(
collectionService: CollectionService,
folderService: FolderService,
stateService: StateService
) {
super(collectionService, folderService, stateService);
}
}

View File

@@ -0,0 +1,52 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="passwordGenHistoryTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="box">
<div class="box-header" id="passwordGenHistoryTitle">
{{ "passwordHistory" | i18n }}
</div>
<div class="box-content condensed">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main">
<div
class="generated-wrapper monospaced"
appSelectCopy
[innerHTML]="h.password | colorPassword"
></div>
<span class="detail">{{ h.date | 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 class="right">
<button
type="button"
(click)="clear()"
class="danger"
appA11yTitle="{{ 'clear' | i18n }}"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "jslib-angular/components/password-generator-history.component";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-password-generator-history",
templateUrl: "password-generator-history.component.html",
})
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
constructor(
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService
) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
}
}

View File

@@ -0,0 +1,40 @@
<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">
<div class="box-header" id="passwordHistoryTitle">
{{ "passwordHistory" | i18n }}
</div>
<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">
{{ h.password }}
</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

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "jslib-angular/components/password-history.component";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-password-history",
templateUrl: "password-history.component.html",
})
export class PasswordHistoryComponent extends BasePasswordHistoryComponent {
constructor(
cipherService: CipherService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService
) {
super(cipherService, platformUtilsService, i18nService, window);
}
}

View File

@@ -0,0 +1,78 @@
<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">
<div class="modal-body">
<div class="box">
<div class="box-header" id="moveToOrgTitle">
{{ "moveToOrganization" | i18n }}
</div>
<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"
[(ngModel)]="organizationId"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div>
</div>
<div class="box" *ngIf="organizations && organizations.length">
<div class="box-header">
{{ "collections" | i18n }}
</div>
<div class="box-content" *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="primary"
appA11yTitle="{{ 'save' | i18n }}"
[disabled]="form.loading || !canSave"
*ngIf="organizations && organizations.length"
>
<i
class="bwi bwi-save-changes bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,33 @@
import { Component } from "@angular/core";
import { ShareComponent as BaseShareComponent } from "jslib-angular/components/share.component";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
@Component({
selector: "app-vault-share",
templateUrl: "share.component.html",
})
export class ShareComponent extends BaseShareComponent {
constructor(
cipherService: CipherService,
i18nService: I18nService,
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
organizationService: OrganizationService
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService
);
}
}

View File

@@ -0,0 +1,74 @@
<div id="vault" class="vault" attr.aria-hidden="{{ showingModal }}">
<app-vault-ciphers
id="items"
class="items"
[activeCipherId]="cipherId"
(onCipherClicked)="viewCipher($event)"
(onCipherRightClicked)="viewCipherMenu($event)"
(onAddCipher)="addCipher($event)"
(onAddCipherOptions)="addCipherOptions()"
>
</app-vault-ciphers>
<app-vault-view
id="details"
class="details"
*ngIf="cipherId && action === 'view'"
[cipherId]="cipherId"
(onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)"
(onEditCipher)="editCipherWithoutPasswordPrompt($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"
[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, true)"
(onGenerateUsername)="openGenerator(true, 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>
<app-vault-groupings
id="groupings"
class="groupings"
(onAllClicked)="clearGroupingFilters()"
(onFavoritesClicked)="filterFavorites()"
(onCipherTypeClicked)="filterCipherType($event)"
(onFolderClicked)="filterFolder($event.id)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
(onCollectionClicked)="filterCollection($event.id)"
(onTrashClicked)="filterDeleted()"
>
</app-vault-groupings>
</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

@@ -0,0 +1,786 @@
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { ModalService } from "jslib-angular/services/modal.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from "jslib-common/enums/eventType";
import { CipherView } from "jslib-common/models/view/cipherView";
import { FolderView } from "jslib-common/models/view/folderView";
import { invokeMenu, RendererMenuItem } from "jslib-electron/utils";
import { SearchBarService } from "../layout/search/search-bar.service";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
import { CiphersComponent } from "./ciphers.component";
import { CollectionsComponent } from "./collections.component";
import { FolderAddEditComponent } from "./folder-add-edit.component";
import { GeneratorComponent } from "./generator.component";
import { GroupingsComponent } from "./groupings.component";
import { PasswordHistoryComponent } from "./password-history.component";
import { ShareComponent } from "./share.component";
import { ViewComponent } from "./view.component";
const BroadcasterSubscriptionId = "VaultComponent";
@Component({
selector: "app-vault",
templateUrl: "vault.component.html",
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(ViewComponent) viewComponent: ViewComponent;
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild("generator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
@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;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
action: string;
cipherId: string = null;
favorites = false;
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
addType: CipherType = null;
addOrganizationId: string = null;
addCollectionIds: string[] = null;
showingModal = false;
deleted = false;
userHasPremiumAccess = false;
private modal: ModalRef = null;
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 eventService: EventService,
private totpService: TotpService,
private passwordRepromptService: PasswordRepromptService,
private stateService: StateService,
private searchBarService: SearchBarService
) {}
async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
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 "focusSearch":
(document.querySelector("#search") as HTMLInputElement).select();
detectChanges = false;
break;
case "openGenerator":
await this.openGenerator(false);
break;
case "syncCompleted":
await this.load();
break;
case "refreshCiphers":
this.ciphersComponent.refresh();
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 this.totpService.getCode(tCipher.login.totp);
this.copyValue(tCipher, value, "verificationCodeTotp", "TOTP");
}
break;
}
default:
detectChanges = false;
break;
}
if (detectChanges) {
this.changeDetectorRef.detectChanges();
}
});
});
if (!this.syncService.syncInProgress) {
await this.load();
}
document.body.classList.remove("layout_frontend");
this.searchBarService.setEnabled(true);
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
}
ngOnDestroy() {
this.searchBarService.setEnabled(false);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
document.body.classList.add("layout_frontend");
}
async load() {
this.route.queryParams.pipe(first()).subscribe(async (params) => {
await this.groupingsComponent.load();
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
} else {
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 = Number(params.addType);
this.addCipher(this.addType);
}
if (params.deleted) {
this.groupingsComponent.selectedTrash = true;
await this.filterDeleted();
} else if (params.favorites) {
this.groupingsComponent.selectedFavorites = true;
await this.filterFavorites();
} else if (params.type && params.action !== "add") {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
} else {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
}
}
});
}
async viewCipher(cipher: CipherView) {
if (!(await this.canNavigateAway("view", 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(() => {
this.viewCipher(cipher);
}),
},
];
if (!cipher.isDeleted) {
menu.push({
label: this.i18nService.t("edit"),
click: () =>
this.functionWithChangeDetection(() => {
this.editCipher(cipher);
}),
});
menu.push({
label: this.i18nService.t("clone"),
click: () =>
this.functionWithChangeDetection(() => {
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");
this.eventService.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 this.totpService.getCode(cipher.login.totp);
this.copyValue(cipher, value, "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");
this.eventService.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.action = "add";
this.cipherId = null;
this.updateCollectionProperties();
this.go();
}
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 = cipher.id;
this.action = "view";
this.go();
await this.ciphersComponent.refresh();
}
async deletedCipher(cipher: CipherView) {
this.cipherId = null;
this.action = null;
this.go();
await this.ciphersComponent.refresh();
}
async restoredCipher(cipher: CipherView) {
this.cipherId = null;
this.action = null;
this.go();
await this.ciphersComponent.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;
childComponent.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true));
childComponent.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true));
this.modal.onClosed.subscribe(async () => {
this.modal = null;
if (madeAttachmentChanges) {
await this.ciphersComponent.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;
childComponent.onSharedCipher.subscribe(async () => {
this.modal.close();
this.viewCipher(cipher);
await this.ciphersComponent.refresh();
});
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;
childComponent.onSavedCollections.subscribe(() => {
this.modal.close();
this.viewCipher(cipher);
});
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)
);
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 clearGroupingFilters() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
await this.ciphersComponent.reload();
this.clearFilters();
this.go();
}
async filterFavorites() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchFavorites"));
await this.ciphersComponent.reload((c) => c.favorite);
this.clearFilters();
this.favorites = true;
this.go();
}
async filterDeleted() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchTrash"));
this.ciphersComponent.deleted = true;
await this.ciphersComponent.reload(null, true);
this.clearFilters();
this.deleted = true;
this.go();
}
async filterCipherType(type: CipherType) {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchType"));
await this.ciphersComponent.reload((c) => c.type === type);
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
folderId = folderId === "none" ? null : folderId;
this.searchBarService.setPlaceholderText(this.i18nService.t("searchFolder"));
await this.ciphersComponent.reload((c) => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? "none" : folderId;
this.go();
}
async filterCollection(collectionId: string) {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchCollection"));
await this.ciphersComponent.reload(
(c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1
);
this.clearFilters();
this.collectionId = collectionId;
this.updateCollectionProperties();
this.go();
}
async openGenerator(comingFromAddEdit: boolean, passwordType = true) {
if (this.modal != null) {
this.modal.close();
}
const cipher = this.addEditComponent?.cipher;
const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null;
const [modal, childComponent] = await this.modalService.openViewRef(
GeneratorComponent,
this.generatorModalRef,
(comp) => {
comp.comingFromAddEdit = comingFromAddEdit;
if (comingFromAddEdit) {
comp.type = passwordType ? "password" : "username";
if (loginType && cipher.login.hasUris && cipher.login.uris[0].hostname != null) {
comp.usernameWebsite = cipher.login.uris[0].hostname;
}
}
}
);
this.modal = modal;
childComponent.onSelected.subscribe((value: string) => {
this.modal.close();
if (loginType) {
this.addEditComponent.markPasswordAsDirty();
if (passwordType) {
this.addEditComponent.cipher.login.password = value;
} else {
this.addEditComponent.cipher.login.username = value;
}
}
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async addFolder() {
this.messagingService.send("newFolder");
}
async editFolder(folderId: string) {
if (this.modal != null) {
this.modal.close();
}
const [modal, childComponent] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => (comp.folderId = folderId)
);
this.modal = modal;
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
});
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
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.platformUtilsService.showDialog(
this.i18nService.t("unsavedChangesConfirmation"),
this.i18nService.t("unsavedChangesTitle"),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
return !confirmed;
}
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
this.addCollectionIds = null;
this.addType = null;
this.addOrganizationId = null;
this.deleted = false;
}
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,
};
}
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 (
cipher.reprompt !== CipherRepromptType.None &&
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
}
this.platformUtilsService.copyToClipboard(value);
this.platformUtilsService.showToast(
"info",
null,
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 updateCollectionProperties() {
if (this.collectionId != null) {
const collection = this.groupingsComponent.collections.filter(
(c) => c.id === this.collectionId
);
if (collection.length > 0) {
this.addOrganizationId = collection[0].organizationId;
this.addCollectionIds = [this.collectionId];
return;
}
}
this.addOrganizationId = null;
this.addCollectionIds = null;
}
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) {
return (
cipher.reprompt === CipherRepromptType.None ||
(await this.passwordRepromptService.showPasswordPrompt())
);
}
}

View File

@@ -0,0 +1,72 @@
<div class="box">
<div class="box-header">
{{ "customFields" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
<div class="row-main">
<span class="row-label">{{ 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 show-whitespace"
[innerHTML]="field.value | colorPassword"
></span>
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
</div>
<div *ngIf="field.type === fieldType.Boolean">
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
<span class="sr-only">{{ field.value }}</span>
</div>
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
<div class="icon icon-small">
<i
class="bwi bwi-link"
aria-hidden="true"
appA11yTitle="{{ 'linkedValue' | i18n }}"
></i>
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
</div>
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
</div>
</div>
<div class="action-buttons">
<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

@@ -0,0 +1,14 @@
import { Component } from "@angular/core";
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "jslib-angular/components/view-custom-fields.component";
import { EventService } from "jslib-common/abstractions/event.service";
@Component({
selector: "app-vault-view-custom-fields",
templateUrl: "view-custom-fields.component.html",
})
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
constructor(eventService: EventService) {
super(eventService);
}
}

View File

@@ -0,0 +1,412 @@
<div class="content">
<div class="inner-content" *ngIf="cipher">
<div class="box">
<div class="box-header">
{{ "itemInformation" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row">
<span class="row-label">{{ "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"
class="monospaced generated-wrapper"
appSelectCopy
[innerHTML]="cipher.login.password | colorPassword"
></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]="checkPasswordBtn.loading"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="checkPasswordBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!checkPasswordBtn.loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher.login.password, 'password', 'Password')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div
class="box-content-row box-content-row-flex totp"
[ngClass]="{ low: totpLow }"
*ngIf="cipher.login.totp && totpCode"
>
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, totpCode)"
>{{ "verificationCodeTotp" | i18n }}</span
>
<span class="totp-code">{{ totpCodeFormatted }}</span>
</div>
<span class="totp-countdown">
<span class="totp-sec">{{ totpSec }}</span>
<svg>
<g>
<circle
class="totp-circle inner"
r="12.6"
cy="16"
cx="16"
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
></circle>
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
</g>
</svg>
</span>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.card">
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
<span class="row-label">{{ "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">{{ "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">{{ "brand" | i18n }}</span>
{{ cipher.card.brand }}
</div>
<div class="box-content-row" *ngIf="cipher.card.expiration">
<span class="row-label">{{ "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">{{ "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">{{ "identityName" | i18n }}</span>
{{ cipher.identity.fullName }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.username">
<span class="row-label">{{ "username" | i18n }}</span>
{{ cipher.identity.username }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.company">
<span class="row-label">{{ "company" | i18n }}</span>
{{ cipher.identity.company }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.ssn">
<span class="row-label">{{ "ssn" | i18n }}</span>
{{ cipher.identity.ssn }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
<span class="row-label">{{ "passportNumber" | i18n }}</span>
{{ cipher.identity.passportNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
<span class="row-label">{{ "licenseNumber" | i18n }}</span>
{{ cipher.identity.licenseNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.email">
<span class="row-label">{{ "email" | i18n }}</span>
{{ cipher.identity.email }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.phone">
<span class="row-label">{{ "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">{{ "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>
</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" *ngIf="!u.isWebsite">{{ "uri" | i18n }}</span>
<span class="row-label" *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-share-square" 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.notes">
<div class="box-header">
{{ "notes" | i18n }}
</div>
<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)">
<div class="box-header">
{{ "attachments" | i18n }}
</div>
<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="!attachment.downloading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
*ngIf="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.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">
<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
class="primary"
(click)="restore()"
appA11yTitle="{{ 'restore' | i18n }}"
*ngIf="cipher.isDeleted"
>
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
<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>
<div class="right">
<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

@@ -0,0 +1,115 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
NgZone,
OnChanges,
Output,
} from "@angular/core";
import { ViewComponent as BaseViewComponent } from "jslib-angular/components/view.component";
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuditService } from "jslib-common/abstractions/audit.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { TokenService } from "jslib-common/abstractions/token.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { CipherView } from "jslib-common/models/view/cipherView";
const BroadcasterSubscriptionId = "ViewComponent";
@Component({
selector: "app-vault-view",
templateUrl: "view.component.html",
})
export class ViewComponent extends BaseViewComponent implements OnChanges {
@Output() onViewCipherPasswordHistory = new EventEmitter<CipherView>();
constructor(
cipherService: CipherService,
totpService: TotpService,
tokenService: TokenService,
i18nService: I18nService,
cryptoService: CryptoService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
broadcasterService: BroadcasterService,
ngZone: NgZone,
changeDetectorRef: ChangeDetectorRef,
eventService: EventService,
apiService: ApiService,
private messagingService: MessagingService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
stateService: StateService
) {
super(
cipherService,
totpService,
tokenService,
i18nService,
cryptoService,
platformUtilsService,
auditService,
window,
broadcasterService,
ngZone,
changeDetectorRef,
eventService,
apiService,
passwordRepromptService,
logService,
stateService
);
}
ngOnInit() {
super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async ngOnChanges() {
await super.load();
}
viewHistory() {
this.onViewCipherPasswordHistory.emit(this.cipher);
}
async copy(value: string, typeI18nKey: string, aType: string) {
super.copy(value, typeI18nKey, aType);
this.messagingService.send("minimizeOnCopy");
}
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;
});
}
}
}