bitwarden-desktop/src/app/app.component.ts

469 lines
20 KiB
TypeScript
Raw Normal View History

import {
BodyOutputType,
Toast,
ToasterConfig,
ToasterService,
} from 'angular2-toaster';
2018-01-26 16:50:06 +01:00
2018-02-08 18:24:17 +01:00
import {
Component,
2018-02-08 21:58:47 +01:00
NgZone,
2018-02-08 18:24:17 +01:00
OnInit,
SecurityContext,
2018-02-18 04:37:43 +01:00
Type,
2018-02-10 05:41:29 +01:00
ViewChild,
2021-02-22 19:25:57 +01:00
ViewContainerRef,
2018-02-08 18:24:17 +01:00
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
2018-02-08 18:24:17 +01:00
import { Router } from '@angular/router';
2018-02-16 21:03:29 +01:00
import { PremiumComponent } from './accounts/premium.component';
2018-02-10 05:41:29 +01:00
import { SettingsComponent } from './accounts/settings.component';
2018-02-18 04:37:43 +01:00
import { PasswordGeneratorHistoryComponent } from './vault/password-generator-history.component';
2018-02-10 05:41:29 +01:00
import { AuthService } from 'jslib-common/abstractions/auth.service';
2021-12-06 12:03:02 +01:00
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 { StorageService } from 'jslib-common/abstractions/storage.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 { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { ConstantsService } from 'jslib-common/services/constants.service';
import { CipherType } from 'jslib-common/enums/cipherType';
2021-02-22 19:17:02 +01:00
import { ModalRef } from 'jslib-angular/components/modal/modal.ref';
import { ModalService } from 'jslib-angular/services/modal.service';
2021-02-22 19:17:02 +01:00
import { ExportComponent } from './vault/export.component';
import { FolderAddEditComponent } from './vault/folder-add-edit.component';
import { PasswordGeneratorComponent } from './vault/password-generator.component';
2018-02-10 05:25:18 +01:00
2018-02-10 21:22:07 +01:00
const BroadcasterSubscriptionId = 'AppComponent';
2018-08-24 21:30:26 +02:00
const IdleTimeout = 60000 * 10; // 10 minutes
2021-02-22 19:17:02 +01:00
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
2018-02-10 21:22:07 +01:00
2018-01-16 20:48:34 +01:00
@Component({
2018-01-16 21:58:17 +01:00
selector: 'app-root',
styles: [],
2018-01-26 20:12:41 +01:00
template: `
<toaster-container [toasterconfig]="toasterConfig" aria-live="polite"></toaster-container>
2018-02-10 05:41:29 +01:00
<ng-template #settings></ng-template>
2018-02-16 21:03:29 +01:00
<ng-template #premium></ng-template>
2018-02-18 04:37:43 +01:00
<ng-template #passwordHistory></ng-template>
2021-02-22 19:17:02 +01:00
<ng-template #appFolderAddEdit></ng-template>
<ng-template #exportVault></ng-template>
<ng-template #appPasswordGenerator></ng-template>
2018-01-26 20:12:41 +01:00
<router-outlet></router-outlet>`,
2018-01-16 20:48:34 +01:00
})
2018-02-08 18:24:17 +01:00
export class AppComponent implements OnInit {
2020-08-18 22:13:15 +02:00
@ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild('premium', { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
@ViewChild('passwordHistory', { read: ViewContainerRef, static: true }) passwordHistoryRef: ViewContainerRef;
2021-02-22 19:17:02 +01:00
@ViewChild('exportVault', { read: ViewContainerRef, static: true }) exportVaultModalRef: ViewContainerRef;
2021-02-22 19:25:57 +01:00
@ViewChild('appFolderAddEdit', { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild('appPasswordGenerator', { read: ViewContainerRef, static: true })
passwordGeneratorModalRef: ViewContainerRef;
2018-02-10 05:41:29 +01:00
2018-01-26 20:12:41 +01:00
toasterConfig: ToasterConfig = new ToasterConfig({
showCloseButton: true,
mouseoverTimerStop: true,
animation: 'flyRight',
limit: 5,
});
2018-02-10 05:25:18 +01:00
private lastActivity: number = null;
private modal: ModalRef = null;
2018-08-24 21:30:26 +02:00
private idleTimer: number = null;
private isIdle = false;
2018-02-10 05:25:18 +01:00
2021-02-22 19:17:02 +01:00
constructor(private broadcasterService: BroadcasterService, private userService: UserService,
2018-02-10 05:25:18 +01:00
private tokenService: TokenService, private folderService: FolderService,
2018-02-08 18:24:17 +01:00
private settingsService: SettingsService, private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService, private cipherService: CipherService,
private authService: AuthService, private router: Router,
2018-02-08 21:58:47 +01:00
private toasterService: ToasterService, private i18nService: I18nService,
private sanitizer: DomSanitizer, private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
private cryptoService: CryptoService, private logService: LogService,
2018-08-13 20:09:22 +02:00
private messagingService: MessagingService, private collectionService: CollectionService,
2018-11-16 16:34:31 +01:00
private searchService: SearchService, private notificationsService: NotificationsService,
2019-07-02 14:18:42 +02:00
private platformUtilsService: PlatformUtilsService, private systemService: SystemService,
private stateService: StateService, private eventService: EventService,
private policyService: PolicyService, private modalService: ModalService,
private keyConnectorService: KeyConnectorService) { }
2018-02-08 18:24:17 +01:00
ngOnInit() {
2018-04-12 20:38:41 +02:00
this.ngZone.runOutsideAngular(() => {
setTimeout(async () => {
await this.updateAppMenu('auth');
2018-04-12 20:38:41 +02:00
}, 1000);
2018-02-14 06:26:32 +01:00
2018-04-12 20:38:41 +02:00
window.onmousemove = () => this.recordActivity();
window.onmousedown = () => this.recordActivity();
window.ontouchstart = () => this.recordActivity();
window.onclick = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
2018-02-10 05:25:18 +01:00
2018-02-14 06:26:32 +01:00
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
2018-02-08 21:58:47 +01:00
this.ngZone.run(async () => {
switch (message.command) {
case 'loggedIn':
2018-08-20 22:23:55 +02:00
case 'unlocked':
2018-08-24 21:30:26 +02:00
this.notificationsService.updateConnection();
this.updateAppMenu('auth');
2019-02-27 17:29:31 +01:00
this.systemService.cancelProcessReload();
2019-02-25 21:07:44 +01:00
break;
case 'loggedOut':
if (this.modal != null) {
this.modal.close();
}
2019-02-25 21:07:44 +01:00
this.notificationsService.updateConnection();
this.updateAppMenu('auth');
2019-02-27 17:29:31 +01:00
this.systemService.startProcessReload();
2019-02-27 17:56:30 +01:00
await this.systemService.clearPendingClipboard();
2018-02-08 21:58:47 +01:00
break;
case 'authBlocked':
this.router.navigate(['login']);
break;
2018-02-08 21:58:47 +01:00
case 'logout':
2018-02-10 04:47:53 +01:00
this.logOut(!!message.expired);
break;
case 'lockVault':
await this.vaultTimeoutService.lock(true);
2018-02-08 21:58:47 +01:00
break;
case 'locked':
if (this.modal != null) {
this.modal.close();
}
2019-07-02 14:18:42 +02:00
this.stateService.purge();
2019-02-25 21:07:44 +01:00
this.router.navigate(['lock']);
2018-08-24 21:30:26 +02:00
this.notificationsService.updateConnection();
this.updateAppMenu('auth');
2019-02-27 17:29:31 +01:00
this.systemService.startProcessReload();
2019-02-27 17:56:30 +01:00
await this.systemService.clearPendingClipboard();
2019-02-25 21:07:44 +01:00
break;
case 'reloadProcess':
window.location.reload(true);
2018-02-08 21:58:47 +01:00
break;
case 'syncStarted':
break;
case 'syncCompleted':
await this.updateAppMenu('sync');
2018-02-08 21:58:47 +01:00
break;
2018-02-10 05:41:29 +01:00
case 'openSettings':
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
2018-02-10 05:41:29 +01:00
break;
2018-02-16 21:03:29 +01:00
case 'openPremium':
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
2018-02-18 04:37:43 +01:00
break;
2018-11-16 16:34:31 +01:00
case 'showFingerprintPhrase':
const fingerprint = await this.cryptoService.getFingerprint(
await this.userService.getUserId());
const result = await this.platformUtilsService.showDialog(
2018-11-16 17:15:16 +01:00
this.i18nService.t('yourAccountsFingerprint') + ':\n' + fingerprint.join('-'),
2018-11-16 16:34:31 +01:00
this.i18nService.t('fingerprintPhrase'), this.i18nService.t('learnMore'),
this.i18nService.t('close'));
if (result) {
2021-02-22 19:17:02 +01:00
this.platformUtilsService.launchUri(
2018-11-16 17:15:16 +01:00
'https://help.bitwarden.com/article/fingerprint-phrase/');
2018-11-16 16:34:31 +01:00
}
break;
2018-02-18 04:37:43 +01:00
case 'openPasswordHistory':
await this.openModal<PasswordGeneratorHistoryComponent>(
2018-02-18 04:37:43 +01:00
PasswordGeneratorHistoryComponent, this.passwordHistoryRef);
2018-02-16 21:03:29 +01:00
break;
case 'showToast':
2018-10-03 15:51:12 +02:00
this.showToast(message);
break;
2019-02-27 17:29:31 +01:00
case 'copiedToClipboard':
2019-05-30 15:37:06 +02:00
if (!message.clearing) {
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
}
2019-02-27 17:29:31 +01:00
break;
merge sso feature branch (#523) * Update jslib (101c568 -> 14b01f2) (#506) * Update jslib (14b01f2 -> 1513b25) (#510) * [jslib] Update (1513b25 -> 7c3a9d6) (#516) * update jslib (1513b25 -> 7c3a9d6) * Updated call to constructor super * [SSO] Added SSO flows & functionality (#513) * update jslib * bump version * Added sso button (wip) * Added sso & change password // Added modules/routes // Added strings for localization * Added password strength comp // reverted login route * Updated sso component to send client id // added routing for sso // added crypto function to services module provider list * Added deep linking * First round of UI updates // Added sso browser launching // Added missing strings * Updated UI and added missing strings * Removed extra change password style * Let constructor for WindowMain handle default width/height * Prepared for jslib update * Update jslib (1513b25 -> 7c3a9d6) * Update login super * Added params for launchSsoBrowser function * Update jslib (7c3a9d6 -> 4203937) * Added missing strings, removed unnecessary class param * Upgrade TypeScript (#517) * Updated password score // Update styles * Removed password-strength component files * Cleaned up module class // Fixed UL/LI formatting issues * Use exisiting loading string // removed new string * Update jslib (4203937 -> 9957125) * Updated class to perform new submit actions * Upgrade Angular (#520) * di resolution for CryptoFunctionServiceAbstraction * Update jslib (9957125 -> 5d874d0) (#521) * Updated change password flow to match web * Updated callout style Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> Co-authored-by: Oscar Hinton <hinton.oscar@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Oscar Hinton <hinton.oscar@gmail.com>
2020-08-21 15:50:36 +02:00
case 'ssoCallback':
this.router.navigate(['sso'], { queryParams: { code: message.code, state: message.state } });
break;
2021-02-18 19:03:20 +01:00
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);
2021-02-18 19:03:20 +01:00
}
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/article/create-bitwarden-account/');
}
break;
2021-02-22 19:17:02 +01:00
case 'syncVault':
try {
await this.syncService.fullSync(true, true);
this.toasterService.popAsync('success', null, this.i18nService.t('syncingComplete'));
} catch {
this.toasterService.popAsync('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);
}
2021-02-22 19:17:02 +01:00
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;
2018-02-08 21:58:47 +01:00
default:
2021-02-22 19:17:02 +01:00
break;
case 'newFolder':
await this.addFolder();
break;
case 'openPasswordGenerator':
// openPasswordGenerator has extended functionality if called in the vault
if (!this.router.url.includes('vault')) {
await this.openPasswordGenerator();
}
break;
case 'convertAccountToKeyConnector':
await this.keyConnectorService.setConvertAccountRequired(true);
this.router.navigate(['/remove-password']);
break;
2018-02-08 21:58:47 +01:00
}
});
});
}
2018-02-08 18:24:17 +01:00
2018-02-10 21:22:07 +01:00
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
2021-02-22 19:17:02 +01:00
async openExportVault() {
if (this.modal != null) {
this.modal.close();
}
const [modal, childComponent] = await this.modalService.openViewRef(ExportComponent, this.exportVaultModalRef);
this.modal = modal;
2021-02-22 19:17:02 +01:00
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;
2021-02-22 19:17:02 +01:00
childComponent.onSavedFolder.subscribe(async () => {
this.modal.close();
this.syncService.fullSync(false);
});
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async openPasswordGenerator() {
if (this.modal != null) {
this.modal.close();
}
[this.modal] = await this.modalService.openViewRef(PasswordGeneratorComponent, this.folderAddEditModalRef,
comp => comp.showSelect = false);
2021-02-22 19:17:02 +01:00
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private async updateAppMenu(type: 'auth' | 'sync') {
let data: any;
if (type === 'sync') {
data = {
hideChangeMasterPass: await this.keyConnectorService.getUsesKeyConnector(),
};
} else {
data = {
isAuthenticated: await this.userService.isAuthenticated(),
isLocked: await this.vaultTimeoutService.isLocked(),
};
}
this.messagingService.send('updateAppMenu', data);
2018-02-14 06:26:32 +01:00
}
2018-02-08 21:58:47 +01:00
private async logOut(expired: boolean) {
2019-07-09 19:11:10 +02:00
await this.eventService.uploadEvents();
2018-02-08 21:58:47 +01:00
const userId = await this.userService.getUserId();
2018-02-08 18:24:17 +01:00
2018-02-08 21:58:47 +01:00
await Promise.all([
2019-07-09 19:11:10 +02:00
this.eventService.clearEvents(),
2018-02-08 21:58:47 +01:00
this.syncService.setLastSync(new Date(0)),
this.tokenService.clearToken(),
this.cryptoService.clearKeys(),
this.userService.clear(),
this.settingsService.clear(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
2018-05-16 05:26:53 +02:00
this.collectionService.clear(userId),
2018-02-08 21:58:47 +01:00
this.passwordGenerationService.clear(),
this.vaultTimeoutService.clear(),
2019-07-02 14:18:42 +02:00
this.stateService.purge(),
this.policyService.clear(userId),
this.keyConnectorService.clear(),
2018-02-08 21:58:47 +01:00
]);
this.vaultTimeoutService.biometricLocked = true;
2018-08-13 20:09:22 +02:00
this.searchService.clearIndex();
2018-02-14 06:26:32 +01:00
this.authService.logOut(async () => {
2018-02-08 18:24:17 +01:00
if (expired) {
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
}
2018-02-14 14:54:27 +01:00
this.router.navigate(['login']);
2018-02-08 18:24:17 +01:00
});
2018-01-26 16:50:06 +01:00
}
2018-02-10 05:25:18 +01:00
private async recordActivity() {
const now = (new Date()).getTime();
if (this.lastActivity != null && now - this.lastActivity < 250) {
return;
}
this.lastActivity = now;
this.storageService.save(ConstantsService.lastActiveKey, now);
2018-08-24 21:30:26 +02:00
// 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();
}
2018-02-10 05:25:18 +01:00
}
2018-02-10 05:41:29 +01:00
private async openModal<T>(type: Type<T>, ref: ViewContainerRef) {
2018-02-16 21:03:29 +01:00
if (this.modal != null) {
this.modal.close();
}
[this.modal] = await this.modalService.openViewRef(type, ref);
2018-02-16 21:03:29 +01:00
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
2018-10-03 15:51:12 +02:00
private showToast(msg: any) {
const toast: Toast = {
type: msg.type,
title: msg.title,
};
if (typeof (msg.text) === 'string') {
toast.body = msg.text;
} else if (msg.text.length === 1) {
toast.body = msg.text[0];
} else {
let message = '';
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
toast.body = message;
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
toast.bodyOutputType = BodyOutputType.TrustedHtml;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
toast.timeout = msg.options.timeout;
}
}
this.toasterService.popAsync(toast);
}
2021-02-22 19:17:02 +01:00
private routeToVault(action: string, cipherType: CipherType) {
if (!this.router.url.includes('vault')) {
this.router.navigate(['/vault'], {
queryParams: {
action: action,
addType: cipherType,
},
replaceUrl: true,
});
}
}
2018-01-16 20:48:34 +01:00
}