Move desktop into apps/desktop
93
apps/desktop/src/app/accounts/environment.component.html
Normal 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>
|
||||
20
apps/desktop/src/app/accounts/environment.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
31
apps/desktop/src/app/accounts/hint.component.html
Normal 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>
|
||||
24
apps/desktop/src/app/accounts/hint.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
75
apps/desktop/src/app/accounts/lock.component.html
Normal 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>
|
||||
107
apps/desktop/src/app/accounts/lock.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
104
apps/desktop/src/app/accounts/login.component.html
Normal 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>
|
||||
128
apps/desktop/src/app/accounts/login.component.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
89
apps/desktop/src/app/accounts/premium.component.html
Normal 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>
|
||||
24
apps/desktop/src/app/accounts/premium.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
148
apps/desktop/src/app/accounts/register.component.html
Normal 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>
|
||||
73
apps/desktop/src/app/accounts/register.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
26
apps/desktop/src/app/accounts/remove-password.component.html
Normal 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>
|
||||
@@ -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 {}
|
||||
157
apps/desktop/src/app/accounts/set-password.component.html
Normal 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>
|
||||
104
apps/desktop/src/app/accounts/set-password.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
354
apps/desktop/src/app/accounts/settings.component.html
Normal 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>
|
||||
407
apps/desktop/src/app/accounts/settings.component.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
9
apps/desktop/src/app/accounts/sso.component.html
Normal 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>
|
||||
54
apps/desktop/src/app/accounts/sso.component.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
144
apps/desktop/src/app/accounts/two-factor.component.html
Normal 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>
|
||||
94
apps/desktop/src/app/accounts/two-factor.component.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
70
apps/desktop/src/app/app-routing.module.ts
Normal 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 {}
|
||||
626
apps/desktop/src/app/app.component.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
224
apps/desktop/src/app/app.module.ts
Normal 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 {}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
65
apps/desktop/src/app/components/set-pin.component.html
Normal 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>
|
||||
8
apps/desktop/src/app/components/set-pin.component.ts
Normal 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 {}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
100
apps/desktop/src/app/layout/account-switcher.component.html
Normal 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>
|
||||
135
apps/desktop/src/app/layout/account-switcher.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
4
apps/desktop/src/app/layout/header.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="header">
|
||||
<app-search></app-search>
|
||||
<app-account-switcher></app-account-switcher>
|
||||
</div>
|
||||
7
apps/desktop/src/app/layout/header.component.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
templateUrl: "header.component.html",
|
||||
})
|
||||
export class HeaderComponent {}
|
||||
13
apps/desktop/src/app/layout/nav.component.html
Normal 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>
|
||||
24
apps/desktop/src/app/layout/nav.component.ts
Normal 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) {}
|
||||
}
|
||||
38
apps/desktop/src/app/layout/search/search-bar.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
11
apps/desktop/src/app/layout/search/search.component.html
Normal 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>
|
||||
23
apps/desktop/src/app/layout/search/search.component.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
19
apps/desktop/src/app/main.ts
Normal 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());
|
||||
292
apps/desktop/src/app/send/add-edit.component.html
Normal 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>
|
||||
62
apps/desktop/src/app/send/add-edit.component.ts
Normal 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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
55
apps/desktop/src/app/send/efflux-dates.component.html
Normal 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>
|
||||
38
apps/desktop/src/app/send/efflux-dates.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
153
apps/desktop/src/app/send/send.component.html
Normal 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> {{ "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> {{
|
||||
"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> {{
|
||||
"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>
|
||||
144
apps/desktop/src/app/send/send.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
81
apps/desktop/src/app/services/init.service.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
135
apps/desktop/src/app/services/services.module.ts
Normal 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 {}
|
||||
118
apps/desktop/src/app/vault/add-edit-custom-fields.component.html
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
611
apps/desktop/src/app/vault/add-edit.component.html
Normal 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>
|
||||
124
apps/desktop/src/app/vault/add-edit.component.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
78
apps/desktop/src/app/vault/attachments.component.html
Normal 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>
|
||||
37
apps/desktop/src/app/vault/attachments.component.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
65
apps/desktop/src/app/vault/ciphers.component.html
Normal 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>
|
||||
26
apps/desktop/src/app/vault/ciphers.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
51
apps/desktop/src/app/vault/collections.component.html
Normal 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>
|
||||
24
apps/desktop/src/app/vault/collections.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
45
apps/desktop/src/app/vault/export.component.html
Normal 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>
|
||||
77
apps/desktop/src/app/vault/export.component.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
apps/desktop/src/app/vault/folder-add-edit.component.html
Normal 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>
|
||||
22
apps/desktop/src/app/vault/folder-add-edit.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
570
apps/desktop/src/app/vault/generator.component.html
Normal 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>
|
||||
41
apps/desktop/src/app/vault/generator.component.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
190
apps/desktop/src/app/vault/groupings.component.html
Normal 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> {{ "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> {{ "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> {{ "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> {{ "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> {{ "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> {{ "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> {{
|
||||
"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>
|
||||
{{ 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>
|
||||
{{ 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>
|
||||
20
apps/desktop/src/app/vault/groupings.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
40
apps/desktop/src/app/vault/password-history.component.html
Normal 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>
|
||||
20
apps/desktop/src/app/vault/password-history.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
78
apps/desktop/src/app/vault/share.component.html
Normal 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>
|
||||
33
apps/desktop/src/app/vault/share.component.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
74
apps/desktop/src/app/vault/vault.component.html
Normal 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>
|
||||
786
apps/desktop/src/app/vault/vault.component.ts
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
72
apps/desktop/src/app/vault/view-custom-fields.component.html
Normal 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 || " " }}
|
||||
</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>
|
||||
14
apps/desktop/src/app/vault/view-custom-fields.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
412
apps/desktop/src/app/vault/view.component.html
Normal 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>
|
||||
115
apps/desktop/src/app/vault/view.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
35
apps/desktop/src/entry.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NativeMessagingProxy } from "./proxy/native-messaging-proxy";
|
||||
|
||||
// We need to import the other dependencies using `require` since `import` will
|
||||
// generate `Error: Cannot find module 'electron'`. The cause of this error is
|
||||
// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows
|
||||
// which removes the electron module. This flag is needed for stdin/out to work
|
||||
// properly on Windows.
|
||||
|
||||
if (
|
||||
process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1)
|
||||
) {
|
||||
if (process.platform === "darwin") {
|
||||
// eslint-disable-next-line
|
||||
const app = require("electron").app;
|
||||
|
||||
app.on("ready", () => {
|
||||
app.dock.hide();
|
||||
});
|
||||
}
|
||||
|
||||
process.stdout.on("error", (e) => {
|
||||
if (e.code === "EPIPE") {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
const proxy = new NativeMessagingProxy();
|
||||
proxy.run();
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
const Main = require("./main").Main;
|
||||
|
||||
const main = new Main();
|
||||
main.bootstrap();
|
||||
}
|
||||
1
apps/desktop/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module "forcefocus";
|
||||
BIN
apps/desktop/src/images/bwi-globe.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
apps/desktop/src/images/icon-highlight.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/desktop/src/images/icon-highlight@2x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/desktop/src/images/icon-template.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
apps/desktop/src/images/icon-template@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
apps/desktop/src/images/icon.ico
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
apps/desktop/src/images/icon.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
6
apps/desktop/src/images/loading.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
|
||||
<text fill="%23333333" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
|
||||
font-size="18" text-anchor="middle">
|
||||
Loading...
|
||||
</text>
|
||||
</svg>
|
||||
BIN
apps/desktop/src/images/logo-dark@2x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/desktop/src/images/logo-white@2x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
34
apps/desktop/src/images/search-desktop-dark.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.7">
|
||||
<g opacity="0.7">
|
||||
<g clip-path="url(#clip0_44_9647)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3599 73.2564C43.579 74.4366 47.0654 75.0822 50.7059 75.0822C66.9882 75.0822 80.1876 62.1696 80.1876 46.2411C80.1876 45.8578 80.1804 45.4762 80.1648 45.0966H108.891V84.6672H40.3599V73.2564Z" fill="#4C525F"/>
|
||||
<path d="M21.5461 46.241C21.5461 62.1696 34.7456 75.0822 51.028 75.0822C67.3104 75.0822 80.5098 62.1696 80.5098 46.241C80.5098 30.3125 67.3104 17.4 51.028 17.4C34.7456 17.4 21.5461 30.3125 21.5461 46.241Z" stroke="#A4B0C6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35.3603 70.5954C35.3603 69.933 34.823 69.3954 34.1603 69.3954C33.4976 69.3954 32.9603 69.933 32.9603 70.5954H35.3603ZM112.835 40.2387C114.169 40.2387 115.2 41.3027 115.2 42.5698H117.6C117.6 39.9762 115.493 37.8387 112.835 37.8387V40.2387ZM115.2 42.5698V88.6158H117.6V42.5698H115.2ZM115.2 88.6158C115.2 89.9094 114.142 90.9468 112.835 90.9468V93.3468C115.425 93.3468 117.6 91.2774 117.6 88.6158H115.2ZM112.835 90.9468H37.7256V93.3468H112.835V90.9468ZM37.7256 90.9468C36.3913 90.9468 35.3603 89.883 35.3603 88.6158H32.9603C32.9603 91.2096 35.0667 93.3468 37.7256 93.3468V90.9468ZM35.3603 88.6158V70.5954H32.9603V88.6158H35.3603ZM79.8684 40.2387H112.835V37.8387H79.8684V40.2387Z" fill="#A4B0C6"/>
|
||||
<path d="M79.9068 45.2867H109.021V84.8574H40.4873V73.0512" stroke="#A4B0C6" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M57.3565 102.56H93.2046" stroke="#A4B0C6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M68.9544 92.1468V102.56" stroke="#A4B0C6" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M80.553 92.1468V102.56" stroke="#A4B0C6" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M27.4398 64.9452L22.9296 69.4554L5.72134 86.6634C4.54976 87.8352 4.54976 89.7342 5.72133 90.906L6.95929 92.1438C8.13085 93.3156 10.0304 93.3156 11.202 92.1438L28.4102 74.9358L32.9204 70.4256" stroke="#A4B0C6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M101.293 53.1537H85.1784" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 59.1966H90.2142" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M85.1784 59.1966H77.625" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 65.2392H94.2426" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M88.7034 65.2392H73.0926" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 71.2824H85.1784" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M79.6392 71.2824H71.0784" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 77.325H78.6318" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M73.0926 77.325H59.9997" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M54.4604 77.325H46.4032" stroke="#A4B0C6" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M29.1638 33.0108H70.6926C72.0181 33.0108 73.0926 34.0853 73.0926 35.4108V41.6894C73.0926 43.0149 72.0181 44.0894 70.6926 44.0894H29.1638C27.8383 44.0894 26.7638 43.0149 26.7638 41.6894V35.4108C26.7638 34.0853 27.8383 33.0108 29.1638 33.0108Z" stroke="#A4B0C6" stroke-width="4"/>
|
||||
<path d="M22.7354 54.1609H57.0962C58.4217 54.1609 59.4962 55.2354 59.4962 56.5609V62.8392C59.4962 64.1652 58.4217 65.2392 57.0962 65.2392H28.7783" stroke="#A4B0C6" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M79.1358 54.1609H72.975C71.6496 54.1609 70.575 55.2354 70.575 56.5609V62.9736C70.575 64.2252 71.5896 65.2392 72.8406 65.2392" stroke="#A4B0C6" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_44_9647">
|
||||
<rect width="120" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
34
apps/desktop/src/images/search-desktop-light.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.7">
|
||||
<g opacity="0.7">
|
||||
<g clip-path="url(#clip0_44_9647)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3599 73.2564C43.579 74.4366 47.0654 75.0822 50.7059 75.0822C66.9882 75.0822 80.1876 62.1696 80.1876 46.2411C80.1876 45.8578 80.1804 45.4762 80.1648 45.0966H108.891V84.6672H40.3599V73.2564Z" fill="#A4B0C6"/>
|
||||
<path d="M21.5461 46.241C21.5461 62.1696 34.7456 75.0822 51.028 75.0822C67.3104 75.0822 80.5098 62.1696 80.5098 46.241C80.5098 30.3125 67.3104 17.4 51.028 17.4C34.7456 17.4 21.5461 30.3125 21.5461 46.241Z" stroke="#4C525F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M35.3603 70.5954C35.3603 69.933 34.823 69.3954 34.1603 69.3954C33.4976 69.3954 32.9603 69.933 32.9603 70.5954H35.3603ZM112.835 40.2387C114.169 40.2387 115.2 41.3027 115.2 42.5698H117.6C117.6 39.9762 115.493 37.8387 112.835 37.8387V40.2387ZM115.2 42.5698V88.6158H117.6V42.5698H115.2ZM115.2 88.6158C115.2 89.9094 114.142 90.9468 112.835 90.9468V93.3468C115.425 93.3468 117.6 91.2774 117.6 88.6158H115.2ZM112.835 90.9468H37.7256V93.3468H112.835V90.9468ZM37.7256 90.9468C36.3913 90.9468 35.3603 89.883 35.3603 88.6158H32.9603C32.9603 91.2096 35.0667 93.3468 37.7256 93.3468V90.9468ZM35.3603 88.6158V70.5954H32.9603V88.6158H35.3603ZM79.8684 40.2387H112.835V37.8387H79.8684V40.2387Z" fill="#4C525F"/>
|
||||
<path d="M79.9068 45.2867H109.021V84.8574H40.4873V73.0512" stroke="#4C525F" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M57.3565 102.56H93.2046" stroke="#4C525F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M68.9544 92.1468V102.56" stroke="#4C525F" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M80.553 92.1468V102.56" stroke="#4C525F" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M27.4398 64.9452L22.9296 69.4554L5.72134 86.6634C4.54976 87.8352 4.54976 89.7342 5.72133 90.906L6.95929 92.1438C8.13085 93.3156 10.0304 93.3156 11.202 92.1438L28.4102 74.9358L32.9204 70.4256" stroke="#4C525F" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M101.293 53.1537H85.1784" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 59.1966H90.2142" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M85.1784 59.1966H77.625" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 65.2392H94.2426" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M88.7034 65.2392H73.0926" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 71.2824H85.1784" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M79.6392 71.2824H71.0784" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M101.293 77.325H78.6318" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M73.0926 77.325H59.9997" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M54.4604 77.325H46.4032" stroke="#4C525F" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M29.1638 33.0108H70.6926C72.0181 33.0108 73.0926 34.0853 73.0926 35.4108V41.6894C73.0926 43.0149 72.0181 44.0894 70.6926 44.0894H29.1638C27.8383 44.0894 26.7638 43.0149 26.7638 41.6894V35.4108C26.7638 34.0853 27.8383 33.0108 29.1638 33.0108Z" stroke="#4C525F" stroke-width="4"/>
|
||||
<path d="M22.7354 54.1609H57.0962C58.4217 54.1609 59.4962 55.2354 59.4962 56.5609V62.8392C59.4962 64.1652 58.4217 65.2392 57.0962 65.2392H28.7783" stroke="#4C525F" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M79.1358 54.1609H72.975C71.6496 54.1609 70.575 55.2354 70.575 56.5609V62.9736C70.575 64.2252 71.5896 65.2392 72.8406 65.2392" stroke="#4C525F" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_44_9647">
|
||||
<rect width="120" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
apps/desktop/src/images/two-factor/0.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |