From 0b306ca1a7d9af47be9b9575e2b615e77b0fd013 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 15 Dec 2021 17:32:00 -0500 Subject: [PATCH] [Account Switching] [Feature] Add the ability to maintain state for up to 5 accounts at once (#1079) * [refactor] Remove references to deprecated services * [feature] Implement account switching * [bug] Fix state handling for authentication dependent system menu items * [bug] Enable the account switcher to fucntion properly when switching to a locked accounts * [feature] Enable locking any account from the menu * [bug] Ensure the avatar instance used in the account switcher updates on account change * [style] Fix lint complaints * [bug] Ensure the logout command callback can handle any user in state * [style] Fix lint complaints * rollup * [style] Fix lint complaints * [bug] Don't clean up state until everything else is done on logout * [bug] Navigate to vault on a succesful account switch * [bug] Init the state service on start * [feature] Limit account switching to 5 account maximum * [bug] Resolve app lock state with 5 logged out accounts * [chore] Update account refrences to match recent jslib restructuring * [bug] Add missing awaits * [bug] Update app menu on logout * [bug] Hide the switcher if there are no authed accounts * [bug] Move authenticationStatus display information out of jslib * [bug] Remove unused active style from scss * [refactor] Rewrite the menu bar * [style] Fix lint complaints * [bug] Clean state of loggout out user after redirect * [bug] Redirect on logout if not explicity provided a userId that isn't active * [bug] Relocated several settings items to persistant storage * [bug] Correct account switcher styles on all themes * [chore] Include state migration service in services * [bug] Swap to next account on logout * [bug] Correct DI service * [bug] fix loginGuard deps in services.module * [chore] update jslib * [bug] Remove badly merged scss * [chore] update jslib * [review] Code review cleanup * [review] Code review cleanup Co-authored-by: Hinton --- jslib | 2 +- src/app/accounts/lock.component.ts | 13 +- src/app/accounts/login.component.html | 10 +- src/app/accounts/login.component.ts | 9 +- src/app/accounts/premium.component.ts | 8 +- src/app/accounts/set-password.component.ts | 14 +- src/app/accounts/settings.component.ts | 99 ++-- src/app/accounts/sso.component.ts | 11 +- src/app/accounts/two-factor.component.ts | 5 +- .../update-temp-password.component.ts | 10 +- src/app/app-routing.module.ts | 5 +- src/app/app.component.ts | 140 +++-- src/app/app.module.ts | 17 +- .../layout/account-switcher.component.html | 22 + src/app/layout/account-switcher.component.ts | 78 +++ src/app/layout/header.component.html | 4 + src/app/layout/header.component.ts | 8 + src/app/layout/search/search-bar.service.ts | 39 ++ src/app/layout/search/search.component.html | 4 + src/app/layout/search/search.component.ts | 24 + src/app/send/add-edit.component.ts | 8 +- src/app/send/efflux-dates.component.ts | 1 - src/app/send/send.component.html | 7 - src/app/send/send.component.ts | 14 +- src/app/services.module.ts | 42 +- src/app/vault/add-edit.component.ts | 16 +- src/app/vault/attachments.component.ts | 12 +- src/app/vault/ciphers.component.html | 7 - src/app/vault/ciphers.component.ts | 13 + src/app/vault/groupings.component.ts | 7 +- src/app/vault/share.component.ts | 10 +- src/app/vault/vault.component.ts | 26 +- src/app/vault/view.component.ts | 10 +- src/locales/en/messages.json | 13 +- src/main.ts | 43 +- src/main/menu.about.ts | 96 ++++ src/main/menu.account.ts | 121 ++++ src/main/menu.bitwarden.ts | 237 ++++++++ src/main/menu.edit.ts | 134 +++++ src/main/menu.file.ts | 134 +++++ src/main/menu.help.ts | 223 ++++++++ src/main/menu.main.ts | 519 +----------------- src/main/menu.updater.ts | 12 + src/main/menu.view.ts | 140 +++++ src/main/menu.window.ts | 111 ++++ src/main/menubar.ts | 105 ++++ src/main/messaging.main.ts | 26 +- src/main/powerMonitor.main.ts | 6 +- src/scss/header.scss | 171 ++++++ src/scss/misc.scss | 4 + src/scss/pages.scss | 25 +- src/scss/styles.scss | 4 +- src/scss/variables.scss | 6 + src/scss/vault.scss | 72 +-- src/services/loginGuard.service.ts | 25 + src/services/nativeMessaging.service.ts | 21 +- 56 files changed, 2106 insertions(+), 837 deletions(-) create mode 100644 src/app/layout/account-switcher.component.html create mode 100644 src/app/layout/account-switcher.component.ts create mode 100644 src/app/layout/header.component.html create mode 100644 src/app/layout/header.component.ts create mode 100644 src/app/layout/search/search-bar.service.ts create mode 100644 src/app/layout/search/search.component.html create mode 100644 src/app/layout/search/search.component.ts create mode 100644 src/main/menu.about.ts create mode 100644 src/main/menu.account.ts create mode 100644 src/main/menu.bitwarden.ts create mode 100644 src/main/menu.edit.ts create mode 100644 src/main/menu.file.ts create mode 100644 src/main/menu.help.ts create mode 100644 src/main/menu.updater.ts create mode 100644 src/main/menu.view.ts create mode 100644 src/main/menu.window.ts create mode 100644 src/main/menubar.ts create mode 100644 src/scss/header.scss create mode 100644 src/services/loginGuard.service.ts diff --git a/jslib b/jslib index 8fc3cf50..512c5c28 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 8fc3cf50d2967212ffbbf0d57cac71d0774aa2a8 +Subproject commit 512c5c283745c0e2aedaa17fc5d386c60d622064 diff --git a/src/app/accounts/lock.component.ts b/src/app/accounts/lock.component.ts index fd9b6088..53c795d8 100644 --- a/src/app/accounts/lock.component.ts +++ b/src/app/accounts/lock.component.ts @@ -19,14 +19,10 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { LockComponent as BaseLockComponent } from 'jslib-angular/components/lock.component'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - const BroadcasterSubscriptionId = 'LockComponent'; @Component({ @@ -38,20 +34,19 @@ export class LockComponent extends BaseLockComponent implements OnDestroy { constructor(router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, messagingService: MessagingService, - userService: UserService, cryptoService: CryptoService, - storageService: StorageService, vaultTimeoutService: VaultTimeoutService, + 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, userService, cryptoService, - storageService, vaultTimeoutService, environmentService, stateService, apiService, logService, + super(router, i18nService, platformUtilsService, messagingService, cryptoService, + vaultTimeoutService, environmentService, stateService, apiService, logService, keyConnectorService, ngZone); } async ngOnInit() { await super.ngOnInit(); - const autoPromptBiometric = !await this.storageService.get(ConstantsService.disableAutoBiometricsPromptKey); + const autoPromptBiometric = !await this.stateService.getNoAutoPromptBiometrics(); this.route.queryParams.subscribe(params => { if (this.supportsBiometric && params.promptBiometric && autoPromptBiometric) { diff --git a/src/app/accounts/login.component.html b/src/app/accounts/login.component.html index 8b3d5028..989043d3 100644 --- a/src/app/accounts/login.component.html +++ b/src/app/accounts/login.component.html @@ -1,3 +1,9 @@ +
diff --git a/src/app/accounts/login.component.ts b/src/app/accounts/login.component.ts index fff0ab03..9e991af0 100644 --- a/src/app/accounts/login.component.ts +++ b/src/app/accounts/login.component.ts @@ -20,7 +20,6 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; import { ModalService } from 'jslib-angular/services/modal.service'; @@ -44,11 +43,11 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { syncService: SyncService, private modalService: ModalService, platformUtilsService: PlatformUtilsService, stateService: StateService, environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationService, - cryptoFunctionService: CryptoFunctionService, storageService: StorageService, - private broadcasterService: BroadcasterService, ngZone: NgZone, - private messagingService: MessagingService, logService: LogService) { + cryptoFunctionService: CryptoFunctionService, private broadcasterService: BroadcasterService, + ngZone: NgZone, private messagingService: MessagingService, + logService: LogService) { super(authService, router, platformUtilsService, i18nService, stateService, environmentService, - passwordGenerationService, cryptoFunctionService, storageService, logService, ngZone); + passwordGenerationService, cryptoFunctionService, logService, ngZone); super.onSuccessfulLogin = () => { return syncService.fullSync(true); }; diff --git a/src/app/accounts/premium.component.ts b/src/app/accounts/premium.component.ts index 045651a7..26681fdb 100644 --- a/src/app/accounts/premium.component.ts +++ b/src/app/accounts/premium.component.ts @@ -4,7 +4,7 @@ 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 { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { PremiumComponent as BasePremiumComponent } from 'jslib-angular/components/premium.component'; @@ -14,8 +14,8 @@ import { PremiumComponent as BasePremiumComponent } from 'jslib-angular/componen }) export class PremiumComponent extends BasePremiumComponent { constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, - apiService: ApiService, userService: UserService, - logService: LogService) { - super(i18nService, platformUtilsService, apiService, userService, logService); + apiService: ApiService, logService: LogService, + stateService: StateService) { + super(i18nService, platformUtilsService, apiService, logService, stateService); } } diff --git a/src/app/accounts/set-password.component.ts b/src/app/accounts/set-password.component.ts index fc1595b9..134146d4 100644 --- a/src/app/accounts/set-password.component.ts +++ b/src/app/accounts/set-password.component.ts @@ -17,8 +17,8 @@ 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'; -import { UserService } from 'jslib-common/abstractions/user.service'; const BroadcasterSubscriptionId = 'SetPasswordComponent'; @@ -33,12 +33,14 @@ import { export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { constructor(apiService: ApiService, i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, - userService: UserService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router, + passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, + policyService: PolicyService, router: Router, syncService: SyncService, route: ActivatedRoute, - private broadcasterService: BroadcasterService, private ngZone: NgZone) { - super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, policyService, router, apiService, syncService, route); + private broadcasterService: BroadcasterService, private ngZone: NgZone, + stateService: StateService) { + super(i18nService, cryptoService, messagingService, passwordGenerationService, + platformUtilsService, policyService, router, apiService, syncService, route, + stateService); } get masterPasswordScoreWidth() { diff --git a/src/app/accounts/settings.component.ts b/src/app/accounts/settings.component.ts index a86e2764..f707772e 100644 --- a/src/app/accounts/settings.component.ts +++ b/src/app/accounts/settings.component.ts @@ -13,19 +13,16 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { ModalService } from 'jslib-angular/services/modal.service'; -import { ElectronConstants } from 'jslib-electron/electronConstants'; +import { SetPinComponent } from '../components/set-pin.component'; import { Utils } from 'jslib-common/misc/utils'; import { isWindowsStore } from 'jslib-electron/utils'; -import { SetPinComponent } from '../components/set-pin.component'; +import { StorageLocation } from 'jslib-common/enums/storageLocation'; @Component({ selector: 'app-settings', @@ -72,9 +69,9 @@ export class SettingsComponent implements OnInit { vaultTimeout: FormControl = new FormControl(null); constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, - private stateService: StateService, private messagingService: MessagingService, - private cryptoService: CryptoService, private modalService: ModalService) { + 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 @@ -153,31 +150,29 @@ export class SettingsComponent implements OnInit { async ngOnInit() { this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; - this.vaultTimeout.setValue(await this.vaultTimeoutService.getVaultTimeout()); - this.vaultTimeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + this.vaultTimeout.setValue(await this.stateService.getVaultTimeout()); + this.vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); const pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pin = pinSet[0] || pinSet[1]; - this.disableFavicons = await this.storageService.get(ConstantsService.disableFaviconKey); - this.enableBrowserIntegration = await this.storageService.get( - ElectronConstants.enableBrowserIntegration); - this.enableBrowserIntegrationFingerprint = await this.storageService.get(ElectronConstants.enableBrowserIntegrationFingerprint); - this.enableMinToTray = await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey); - this.enableCloseToTray = await this.storageService.get(ElectronConstants.enableCloseToTrayKey); - this.enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); - this.startToTray = await this.storageService.get(ElectronConstants.enableStartToTrayKey); - this.locale = await this.storageService.get(ConstantsService.localeKey); - this.theme = await this.storageService.get(ConstantsService.themeKey); - this.clearClipboard = await this.storageService.get(ConstantsService.clearClipboardKey); - this.minimizeOnCopyToClipboard = await this.storageService.get( - ElectronConstants.minimizeOnCopyToClipboardKey); + this.disableFavicons = await this.stateService.getDisableFavicon(); + this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration(); + this.enableBrowserIntegrationFingerprint = await this.stateService.getEnableBrowserIntegrationFingerprint(); + 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.locale = await this.stateService.getLocale(); + this.theme = await this.stateService.getTheme(); + 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.storageService.get(ConstantsService.biometricText); - this.noAutoPromptBiometrics = await this.storageService.get(ConstantsService.disableAutoBiometricsPromptKey); - this.noAutoPromptBiometricsText = await this.storageService.get(ElectronConstants.noAutoPromptBiometricsText); - this.alwaysShowDock = await this.storageService.get(ElectronConstants.alwaysShowDock); + this.biometricText = await this.stateService.getBiometricText(); + this.noAutoPromptBiometrics = await this.stateService.getNoAutoPromptBiometrics(); + this.noAutoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText(); + this.alwaysShowDock = await this.stateService.getAlwaysShowDock(); this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; - this.openAtLogin = await this.storageService.get(ElectronConstants.openAtLogin); + this.openAtLogin = await this.stateService.getOpenAtLogin(); } async saveVaultTimeoutOptions() { @@ -233,13 +228,13 @@ export class SettingsComponent implements OnInit { return; } if (this.biometric) { - await this.storageService.save(ConstantsService.biometricUnlockKey, true); + await this.stateService.setBiometricUnlock(true); } else { - await this.storageService.remove(ConstantsService.biometricUnlockKey); - await this.storageService.remove(ConstantsService.disableAutoBiometricsPromptKey); + await this.stateService.setBiometricUnlock(null); + await this.stateService.setNoAutoPromptBiometrics(null); this.noAutoPromptBiometrics = false; } - this.vaultTimeoutService.biometricLocked = false; + await this.stateService.setBiometricLocked(false); await this.cryptoService.toggleKey(); } @@ -249,29 +244,29 @@ export class SettingsComponent implements OnInit { } if (this.noAutoPromptBiometrics) { - await this.storageService.save(ConstantsService.disableAutoBiometricsPromptKey, true); + await this.stateService.setNoAutoPromptBiometrics(true); } else { - await this.storageService.remove(ConstantsService.disableAutoBiometricsPromptKey); + await this.stateService.setNoAutoPromptBiometrics(null); } } async saveFavicons() { - await this.storageService.save(ConstantsService.disableFaviconKey, this.disableFavicons); - await this.stateService.save(ConstantsService.disableFaviconKey, this.disableFavicons); + await this.stateService.setDisableFavicon(this.disableFavicons); + await this.stateService.setDisableFavicon(this.disableFavicons, { storageLocation: StorageLocation.Disk }); this.messagingService.send('refreshCiphers'); } async saveMinToTray() { - await this.storageService.save(ElectronConstants.enableMinimizeToTrayKey, this.enableMinToTray); + await this.stateService.setEnableMinimizeToTray(this.enableMinToTray); } async saveCloseToTray() { if (this.requireEnableTray) { this.enableTray = true; - await this.storageService.save(ElectronConstants.enableTrayKey, this.enableTray); + await this.stateService.setEnableTray(this.enableTray); } - await this.storageService.save(ElectronConstants.enableCloseToTrayKey, this.enableCloseToTray); + await this.stateService.setEnableCloseToTray(this.enableCloseToTray); } async saveTray() { @@ -282,9 +277,9 @@ export class SettingsComponent implements OnInit { if (confirm) { this.startToTray = false; - await this.storageService.save(ElectronConstants.enableStartToTrayKey, this.startToTray); + await this.stateService.setEnableStartToTray(this.startToTray); this.enableCloseToTray = false; - await this.storageService.save(ElectronConstants.enableCloseToTrayKey, this.enableCloseToTray); + await this.stateService.setEnableCloseToTray(this.enableCloseToTray); } else { this.enableTray = true; } @@ -292,42 +287,42 @@ export class SettingsComponent implements OnInit { return; } - await this.storageService.save(ElectronConstants.enableTrayKey, this.enableTray); + await this.stateService.setEnableTray(this.enableTray); this.messagingService.send(this.enableTray ? 'showTray' : 'removeTray'); } async saveStartToTray() { if (this.requireEnableTray) { this.enableTray = true; - await this.storageService.save(ElectronConstants.enableTrayKey, this.enableTray); + await this.stateService.setEnableTray(this.enableTray); } - await this.storageService.save(ElectronConstants.enableStartToTrayKey, this.startToTray); + await this.stateService.setEnableStartToTray(this.startToTray); } async saveLocale() { - await this.storageService.save(ConstantsService.localeKey, this.locale); + await this.stateService.setLocale(this.locale); } async saveTheme() { - await this.storageService.save(ConstantsService.themeKey, this.theme); + await this.stateService.setTheme(this.theme); window.setTimeout(() => window.location.reload(), 200); } async saveMinOnCopyToClipboard() { - await this.storageService.save(ElectronConstants.minimizeOnCopyToClipboardKey, this.minimizeOnCopyToClipboard); + await this.stateService.setMinimizeOnCopyToClipboard(this.minimizeOnCopyToClipboard); } async saveClearClipboard() { - await this.storageService.save(ConstantsService.clearClipboardKey, this.clearClipboard); + await this.stateService.setClearClipboard(this.clearClipboard); } async saveAlwaysShowDock() { - await this.storageService.save(ElectronConstants.alwaysShowDock, this.alwaysShowDock); + await this.stateService.setAlwaysShowDock(this.alwaysShowDock); } async saveOpenAtLogin() { - this.storageService.save(ElectronConstants.openAtLogin, this.openAtLogin); + this.stateService.setOpenAtLogin(this.openAtLogin); this.messagingService.send(this.openAtLogin ? 'addOpenAtLogin' : 'removeOpenAtLogin'); } @@ -350,7 +345,7 @@ export class SettingsComponent implements OnInit { return; } - await this.storageService.save(ElectronConstants.enableBrowserIntegration, this.enableBrowserIntegration); + await this.stateService.setEnableBrowserIntegration(this.enableBrowserIntegration); this.messagingService.send(this.enableBrowserIntegration ? 'enableBrowserIntegration' : 'disableBrowserIntegration'); if (!this.enableBrowserIntegration) { @@ -360,6 +355,6 @@ export class SettingsComponent implements OnInit { } async saveBrowserIntegrationFingerprint() { - await this.storageService.save(ElectronConstants.enableBrowserIntegrationFingerprint, this.enableBrowserIntegrationFingerprint); + await this.stateService.setEnableBrowserIntegrationFingerprint(this.enableBrowserIntegrationFingerprint); } } diff --git a/src/app/accounts/sso.component.ts b/src/app/accounts/sso.component.ts index 3c02acbf..615ba612 100644 --- a/src/app/accounts/sso.component.ts +++ b/src/app/accounts/sso.component.ts @@ -14,7 +14,6 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.component'; @@ -26,11 +25,11 @@ import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.c export class SsoComponent extends BaseSsoComponent { constructor(authService: AuthService, router: Router, i18nService: I18nService, syncService: SyncService, route: ActivatedRoute, - storageService: StorageService, stateService: StateService, - platformUtilsService: PlatformUtilsService, apiService: ApiService, - cryptoFunctionService: CryptoFunctionService, environmentService: EnvironmentService, - passwordGenerationService: PasswordGenerationService, logService: LogService) { - super(authService, router, i18nService, route, storageService, stateService, platformUtilsService, + 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); diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts index d783470c..2bacb24f 100644 --- a/src/app/accounts/two-factor.component.ts +++ b/src/app/accounts/two-factor.component.ts @@ -20,7 +20,6 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; import { SyncService } from 'jslib-common/abstractions/sync.service'; import { ModalService } from 'jslib-angular/services/modal.service'; @@ -40,10 +39,10 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { i18nService: I18nService, apiService: ApiService, platformUtilsService: PlatformUtilsService, syncService: SyncService, environmentService: EnvironmentService, private modalService: ModalService, - stateService: StateService, storageService: StorageService, route: ActivatedRoute, + stateService: StateService, route: ActivatedRoute, logService: LogService) { super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, - stateService, storageService, route, logService); + stateService, route, logService); super.onSuccessfulLogin = () => { return syncService.fullSync(true); }; diff --git a/src/app/accounts/update-temp-password.component.ts b/src/app/accounts/update-temp-password.component.ts index 071541d5..8a33e70f 100644 --- a/src/app/accounts/update-temp-password.component.ts +++ b/src/app/accounts/update-temp-password.component.ts @@ -8,8 +8,8 @@ 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'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from 'jslib-angular/components/update-temp-password.component'; @@ -56,10 +56,10 @@ export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent } constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, passwordGenerationService: PasswordGenerationService, policyService: PolicyService, - cryptoService: CryptoService, userService: UserService, - messagingService: MessagingService, apiService: ApiService, - syncService: SyncService, logService: LogService) { + cryptoService: CryptoService, messagingService: MessagingService, + apiService: ApiService, syncService: SyncService, + logService: LogService, stateService: StateService) { super(i18nService, platformUtilsService, passwordGenerationService, policyService, cryptoService, - userService, messagingService, apiService, syncService, logService); + messagingService, apiService, stateService, syncService, logService); } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2b10fd18..1513146a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -6,7 +6,7 @@ import { import { AuthGuardService } from 'jslib-angular/services/auth-guard.service'; import { LockGuardService } from 'jslib-angular/services/lock-guard.service'; -import { UnauthGuardService } from 'jslib-angular/services/unauth-guard.service'; +import { LoginGuardService } from '../services/loginGuard.service'; import { HintComponent } from './accounts/hint.component'; import { LockComponent } from './accounts/lock.component'; @@ -32,8 +32,7 @@ const routes: Routes = [ { path: 'login', component: LoginComponent, - canActivate: [UnauthGuardService], - + canActivate: [LoginGuardService], }, { path: '2fa', component: TwoFactorComponent }, { path: 'register', component: RegisterComponent }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a284a63e..6a35e4f3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -36,23 +36,22 @@ 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'; -import { ModalRef } from 'jslib-angular/components/modal/modal.ref'; -import { ModalService } from 'jslib-angular/services/modal.service'; import { ExportComponent } from './vault/export.component'; import { FolderAddEditComponent } from './vault/folder-add-edit.component'; import { PasswordGeneratorComponent } from './vault/password-generator.component'; +import { ModalRef } from 'jslib-angular/components/modal/modal.ref'; + +import { ModalService } from 'jslib-angular/services/modal.service'; +import { MenuUpdateRequest } from 'src/main/menu.updater'; + const BroadcasterSubscriptionId = 'AppComponent'; const IdleTimeout = 60000 * 10; // 10 minutes const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours @@ -67,7 +66,8 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours - `, + +
`, }) export class AppComponent implements OnInit { @ViewChild('settings', { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; @@ -84,14 +84,14 @@ export class AppComponent implements OnInit { private idleTimer: number = null; private isIdle = false; - constructor(private broadcasterService: BroadcasterService, private userService: UserService, + 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 storageService: StorageService, + private vaultTimeoutService: VaultTimeoutService, private cryptoService: CryptoService, private logService: LogService, private messagingService: MessagingService, private collectionService: CollectionService, private searchService: SearchService, private notificationsService: NotificationsService, @@ -103,7 +103,7 @@ export class AppComponent implements OnInit { ngOnInit() { this.ngZone.runOutsideAngular(() => { setTimeout(async () => { - await this.updateAppMenu('auth'); + await this.updateAppMenu(); }, 1000); window.onmousemove = () => this.recordActivity(); @@ -120,7 +120,7 @@ export class AppComponent implements OnInit { case 'loggedIn': case 'unlocked': this.notificationsService.updateConnection(); - this.updateAppMenu('auth'); + this.updateAppMenu(); this.systemService.cancelProcessReload(); break; case 'loggedOut': @@ -128,7 +128,7 @@ export class AppComponent implements OnInit { this.modal.close(); } this.notificationsService.updateConnection(); - this.updateAppMenu('auth'); + this.updateAppMenu(); this.systemService.startProcessReload(); await this.systemService.clearPendingClipboard(); break; @@ -136,21 +136,29 @@ export class AppComponent implements OnInit { this.router.navigate(['login']); break; case 'logout': - this.logOut(!!message.expired); + await this.logOut(!!message.expired, message.userId); break; case 'lockVault': - await this.vaultTimeoutService.lock(true); + 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(); } - this.stateService.purge(); - this.router.navigate(['lock']); + this.updateAppMenu(); + if (message.userId == null || message.userId === await this.stateService.getUserId()) { + this.router.navigate(['lock']); + } this.notificationsService.updateConnection(); - this.updateAppMenu('auth'); - this.systemService.startProcessReload(); await this.systemService.clearPendingClipboard(); + this.systemService.startProcessReload(); break; case 'reloadProcess': window.location.reload(true); @@ -158,7 +166,7 @@ export class AppComponent implements OnInit { case 'syncStarted': break; case 'syncCompleted': - await this.updateAppMenu('sync'); + await this.updateAppMenu(); break; case 'openSettings': await this.openModal(SettingsComponent, this.settingsRef); @@ -168,7 +176,7 @@ export class AppComponent implements OnInit { break; case 'showFingerprintPhrase': const fingerprint = await this.cryptoService.getFingerprint( - await this.userService.getUserId()); + 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'), @@ -322,52 +330,84 @@ export class AppComponent implements OnInit { }); } - private async updateAppMenu(type: 'auth' | 'sync') { - let data: any; - if (type === 'sync') { - data = { - hideChangeMasterPass: await this.keyConnectorService.getUsesKeyConnector(), + 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 { - data = { - isAuthenticated: await this.userService.isAuthenticated(), - isLocked: await this.vaultTimeoutService.isLocked(), + 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.vaultTimeoutService.isLocked(userId), + 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', data); + this.messagingService.send('updateAppMenu', { updateRequest: updateRequest }); } - private async logOut(expired: boolean) { - await this.eventService.uploadEvents(); - const userId = await this.userService.getUserId(); - + private async logOut(expired: boolean, userId?: string) { await Promise.all([ - this.eventService.clearEvents(), - this.syncService.setLastSync(new Date(0)), - this.tokenService.clearToken(), - this.cryptoService.clearKeys(), - this.userService.clear(), + this.eventService.uploadEvents(userId), + this.syncService.setLastSync(new Date(0), userId), + this.tokenService.clearToken(userId), + this.cryptoService.clearKeys(userId), this.settingsService.clear(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), - this.passwordGenerationService.clear(), - this.vaultTimeoutService.clear(), - this.stateService.purge(), + this.passwordGenerationService.clear(userId), + this.vaultTimeoutService.clear(userId), this.policyService.clear(userId), this.keyConnectorService.clear(), ]); - this.vaultTimeoutService.biometricLocked = true; - this.searchService.clearIndex(); - this.authService.logOut(async () => { - if (expired) { - this.platformUtilsService.showToast('warning', this.i18nService.t('loggedOut'), - this.i18nService.t('loginExpired')); - } + await this.stateService.setBiometricLocked(true, { userId: userId }); + + if (userId == null || userId === await this.stateService.getUserId()) { + this.searchService.clearIndex(); + this.authService.logOut(async () => { + if (expired) { + this.platformUtilsService.showToast('warning', this.i18nService.t('loggedOut'), + this.i18nService.t('loginExpired')); + } + }); + } + + await this.stateService.clean({ userId: userId }); + + if (this.stateService.activeAccount.getValue() == null) { this.router.navigate(['login']); - }); + } else { + const locked = await this.vaultTimeoutService.isLocked(); + if (locked) { + this.messagingService.send('locked'); + } else { + this.messagingService.send('unlocked'); + this.messagingService.send('syncVault'); + this.router.navigate(['vault']); + } + } + + await this.updateAppMenu(); } private async recordActivity() { @@ -377,7 +417,7 @@ export class AppComponent implements OnInit { } this.lastActivity = now; - this.storageService.save(ConstantsService.lastActiveKey, now); + await this.stateService.setLastActive(now); // Idle states if (this.isIdle) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3bbb5248..b6019281 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,16 +1,18 @@ import 'zone.js/dist/zone'; -import { AppRoutingModule } from './app-routing.module'; -import { ServicesModule } from './services.module'; - 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 } from '@angular/common'; 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 'zone.js/dist/zone'; + +import { AppRoutingModule } from './app-routing.module'; +import { ServicesModule } from './services.module'; import { AppComponent } from './app.component'; @@ -29,6 +31,7 @@ import { TwoFactorComponent } from './accounts/two-factor.component'; import { UpdateTempPasswordComponent } from './accounts/update-temp-password.component'; import { VaultTimeoutInputComponent } from './accounts/vault-timeout-input.component'; +import { AvatarComponent } from 'jslib-angular/components/avatar.component'; import { CalloutComponent } from 'jslib-angular/components/callout.component'; import { IconComponent } from 'jslib-angular/components/icon.component'; import { BitwardenToastModule } from 'jslib-angular/components/toastr.component'; @@ -70,7 +73,10 @@ import { AddEditComponent as SendAddEditComponent } from './send/add-edit.compon import { EffluxDatesComponent as SendEffluxDatesComponent } from './send/efflux-dates.component'; import { SendComponent } from './send/send.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 { PasswordRepromptComponent } from './components/password-reprompt.component'; import { SetPinComponent } from './components/set-pin.component'; @@ -183,6 +189,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); }), ScrollingModule, A11yModule, + OverlayModule, ], declarations: [ A11yTitleDirective, @@ -239,6 +246,10 @@ registerLocaleData(localeZhTw, 'zh-TW'); VerifyMasterPasswordComponent, ViewComponent, ViewCustomFieldsComponent, + HeaderComponent, + AccountSwitcherComponent, + AvatarComponent, + SearchComponent, ], providers: [DatePipe], bootstrap: [AppComponent], diff --git a/src/app/layout/account-switcher.component.html b/src/app/layout/account-switcher.component.html new file mode 100644 index 00000000..b81618f7 --- /dev/null +++ b/src/app/layout/account-switcher.component.html @@ -0,0 +1,22 @@ + + + + + diff --git a/src/app/layout/account-switcher.component.ts b/src/app/layout/account-switcher.component.ts new file mode 100644 index 00000000..e61b986b --- /dev/null +++ b/src/app/layout/account-switcher.component.ts @@ -0,0 +1,78 @@ +import { + animate, + state, + style, + transition, + trigger, + } from '@angular/animations'; +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; +import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; + +import { AuthenticationStatus } from 'jslib-common/enums/authenticationStatus'; + +import { Account } from 'jslib-common/models/domain/account'; + +@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: boolean = false; + accounts: { [userId: string]: Account } = {}; + activeAccountEmail: string; + + get showSwitcher() { + return this.accounts != null && Object.keys(this.accounts).length > 0; + } + + constructor(private stateService: StateService, private vaultTimeoutService: VaultTimeoutService, + private messagingService: MessagingService, private router: Router) {} + + async ngOnInit(): Promise { + this.stateService.accounts.subscribe(async accounts => { + for (const userId in accounts) { + if (userId === await this.stateService.getUserId()) { + accounts[userId].profile.authenticationStatus = AuthenticationStatus.Active; + } else { + accounts[userId].profile.authenticationStatus = await this.vaultTimeoutService.isLocked(userId) ? + AuthenticationStatus.Locked : + AuthenticationStatus.Unlocked; + } + } + + this.accounts = accounts; + this.activeAccountEmail = await this.stateService.getEmail(); + }); + } + + toggle() { + this.isOpen = !this.isOpen; + } + + async switch(userId: string) { + await this.stateService.setActiveUser(userId); + const locked = await this.vaultTimeoutService.isLocked(userId); + if (locked) { + this.messagingService.send('locked', { userId: userId }); + } else { + this.messagingService.send('unlocked'); + this.messagingService.send('syncVault'); + this.router.navigate(['vault']); + } + } +} diff --git a/src/app/layout/header.component.html b/src/app/layout/header.component.html new file mode 100644 index 00000000..e1639ef0 --- /dev/null +++ b/src/app/layout/header.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/app/layout/header.component.ts b/src/app/layout/header.component.ts new file mode 100644 index 00000000..90852dce --- /dev/null +++ b/src/app/layout/header.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-header', + templateUrl: 'header.component.html', +}) +export class HeaderComponent { +} diff --git a/src/app/layout/search/search-bar.service.ts b/src/app/layout/search/search-bar.service.ts new file mode 100644 index 00000000..94caeadf --- /dev/null +++ b/src/app/layout/search/search-bar.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export type SearchBarState = { + enabled: boolean; + placeholderText: string; +}; + +@Injectable() +export class SearchBarService { + + searchText = new BehaviorSubject(null); + + private _state = { + enabled: false, + placeholderText: '', + }; + + // tslint:disable-next-line:member-ordering + state = new BehaviorSubject(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); + } +} diff --git a/src/app/layout/search/search.component.html b/src/app/layout/search/search.component.html new file mode 100644 index 00000000..1ea3353b --- /dev/null +++ b/src/app/layout/search/search.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/layout/search/search.component.ts b/src/app/layout/search/search.component.ts new file mode 100644 index 00000000..6c82d273 --- /dev/null +++ b/src/app/layout/search/search.component.ts @@ -0,0 +1,24 @@ +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); + }); + } +} diff --git a/src/app/send/add-edit.component.ts b/src/app/send/add-edit.component.ts index bfe2a879..f1e57c1f 100644 --- a/src/app/send/add-edit.component.ts +++ b/src/app/send/add-edit.component.ts @@ -9,7 +9,7 @@ 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 { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/components/send/add-edit.component'; @@ -20,12 +20,12 @@ import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/componen export class AddEditComponent extends BaseAddEditComponent { constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, datePipe: DatePipe, - sendService: SendService, userService: UserService, + sendService: SendService, stateService: StateService, messagingService: MessagingService, policyService: PolicyService, logService: LogService) { super(i18nService, platformUtilsService, environmentService, - datePipe, sendService, userService, messagingService, policyService, - logService); + datePipe, sendService, messagingService, policyService, + logService, stateService); } async refresh() { diff --git a/src/app/send/efflux-dates.component.ts b/src/app/send/efflux-dates.component.ts index f6f14570..efce265b 100644 --- a/src/app/send/efflux-dates.component.ts +++ b/src/app/send/efflux-dates.component.ts @@ -3,7 +3,6 @@ import { DatePipe } from '@angular/common'; import { Component, OnChanges, - SimpleChanges, } from '@angular/core'; import { ControlContainer, NgForm } from '@angular/forms'; diff --git a/src/app/send/send.component.html b/src/app/send/send.component.html index e4740c90..6a94c8a0 100644 --- a/src/app/send/send.component.html +++ b/src/app/send/send.component.html @@ -31,13 +31,6 @@
-
{ + 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 () => { @@ -68,6 +75,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro ngOnDestroy() { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.searchBarService.setEnabled(false); } addSend() { diff --git a/src/app/services.module.ts b/src/app/services.module.ts index b4b6e239..d9b12676 100644 --- a/src/app/services.module.ts +++ b/src/app/services.module.ts @@ -10,13 +10,14 @@ import { ElectronRendererSecureStorageService } from 'jslib-electron/services/el import { ElectronRendererStorageService } from 'jslib-electron/services/electronRendererStorage.service'; 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 { SearchBarService } from './layout/search/search-bar.service'; import { JslibServicesModule } from 'jslib-angular/services/jslib-services.module'; import { AuthService } from 'jslib-common/services/auth.service'; -import { ConstantsService } from 'jslib-common/services/constants.service'; import { ContainerService } from 'jslib-common/services/container.service'; import { EventService } from 'jslib-common/services/event.service'; import { SystemService } from 'jslib-common/services/system.service'; @@ -46,16 +47,17 @@ import { ThemeType } from 'jslib-common/enums/themeType'; export function initFactory(window: Window, environmentService: EnvironmentServiceAbstraction, syncService: SyncServiceAbstraction, vaultTimeoutService: VaultTimeoutService, - storageService: StorageServiceAbstraction, i18nService: I18nService, eventService: EventService, + i18nService: I18nService, eventService: EventService, authService: AuthService, notificationsService: NotificationsServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction, stateService: StateServiceAbstraction, cryptoService: CryptoServiceAbstraction): Function { return async () => { + await stateService.init(); await environmentService.setUrlsFromStorage(); syncService.fullSync(true); - vaultTimeoutService.init(true); - const locale = await storageService.get(ConstantsService.localeKey); + await vaultTimeoutService.init(true); + const locale = await stateService.getLocale(); await i18nService.init(locale); eventService.init(true); authService.init(); @@ -63,22 +65,18 @@ export function initFactory(window: Window, environmentService: EnvironmentServi const htmlEl = window.document.documentElement; htmlEl.classList.add('os_' + platformUtilsService.getDeviceString()); htmlEl.classList.add('locale_' + i18nService.translationLocale); - const theme = await platformUtilsService.getEffectiveTheme(); htmlEl.classList.add('theme_' + theme); platformUtilsService.onDefaultSystemThemeChange(async sysTheme => { - const bwTheme = await storageService.get(ConstantsService.themeKey); + const bwTheme = await stateService.getTheme(); if (bwTheme == null || bwTheme === ThemeType.System) { htmlEl.classList.remove('theme_' + ThemeType.Light, 'theme_' + ThemeType.Dark); htmlEl.classList.add('theme_' + sysTheme); } }); - stateService.save(ConstantsService.disableFaviconKey, - await storageService.get(ConstantsService.disableFaviconKey)); - let installAction = null; - const installedVersion = await storageService.get(ConstantsService.installedVersionKey); + const installedVersion = await stateService.getInstalledVersion(); const currentVersion = await platformUtilsService.getApplicationVersion(); if (installedVersion == null) { installAction = 'install'; @@ -87,7 +85,7 @@ export function initFactory(window: Window, environmentService: EnvironmentServi } if (installAction != null) { - await storageService.save(ConstantsService.installedVersionKey, currentVersion); + await stateService.setInstalledVersion(currentVersion); } const containerService = new ContainerService(cryptoService); @@ -109,7 +107,6 @@ export function initFactory(window: Window, environmentService: EnvironmentServi EnvironmentServiceAbstraction, SyncServiceAbstraction, VaultTimeoutServiceAbstraction, - StorageServiceAbstraction, I18nServiceAbstraction, EventServiceAbstraction, AuthServiceAbstraction, @@ -124,12 +121,12 @@ export function initFactory(window: Window, environmentService: EnvironmentServi { provide: PlatformUtilsServiceAbstraction, useFactory: (i18nService: I18nServiceAbstraction, messagingService: MessagingServiceAbstraction, - storageService: StorageServiceAbstraction) => new ElectronPlatformUtilsService(i18nService, - messagingService, true, storageService), + stateService: StateServiceAbstraction) => new ElectronPlatformUtilsService(i18nService, + messagingService, true, stateService), deps: [ I18nServiceAbstraction, MessagingServiceAbstraction, - StorageServiceAbstraction, + StateServiceAbstraction, ], }, { @@ -148,25 +145,34 @@ export function initFactory(window: Window, environmentService: EnvironmentServi provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ - StorageServiceAbstraction, - 'SECURE_STORAGE', CryptoFunctionServiceAbstraction, PlatformUtilsServiceAbstraction, LogServiceAbstraction, + StateServiceAbstraction, ], }, { provide: SystemServiceAbstraction, useClass: SystemService, deps: [ - StorageServiceAbstraction, VaultTimeoutServiceAbstraction, MessagingServiceAbstraction, PlatformUtilsServiceAbstraction, + StateServiceAbstraction, ], }, { provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService }, NativeMessagingService, + SearchBarService, + { + provide: LoginGuardService, + useClass: LoginGuardService, + deps: [ + StateServiceAbstraction, + PlatformUtilsServiceAbstraction, + I18nServiceAbstraction, + ], + }, ], }) export class ServicesModule { diff --git a/src/app/vault/add-edit.component.ts b/src/app/vault/add-edit.component.ts index e0ae3a08..2a0009dd 100644 --- a/src/app/vault/add-edit.component.ts +++ b/src/app/vault/add-edit.component.ts @@ -16,15 +16,14 @@ 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'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { AddEditComponent as BaseAddEditComponent } from 'jslib-angular/components/add-edit.component'; - const BroadcasterSubscriptionId = 'AddEditComponent'; @Component({ @@ -37,13 +36,14 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, constructor(cipherService: CipherService, folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, auditService: AuditService, stateService: StateService, - userService: UserService, collectionService: CollectionService, - messagingService: MessagingService, eventService: EventService, - policyService: PolicyService, passwordRepromptService: PasswordRepromptService, - private broadcasterService: BroadcasterService, private ngZone: NgZone, logService: LogService) { + 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, - userService, collectionService, messagingService, eventService, policyService, passwordRepromptService, - logService); + collectionService, messagingService, eventService, policyService, logService, + passwordRepromptService, organizationService); } async ngOnInit() { diff --git a/src/app/vault/attachments.component.ts b/src/app/vault/attachments.component.ts index bb9b6bd9..dafe18a5 100644 --- a/src/app/vault/attachments.component.ts +++ b/src/app/vault/attachments.component.ts @@ -6,7 +6,7 @@ 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 { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/components/attachments.component'; @@ -16,10 +16,10 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from 'jslib-angular/ }) export class AttachmentsComponent extends BaseAttachmentsComponent { constructor(cipherService: CipherService, i18nService: I18nService, - cryptoService: CryptoService, userService: UserService, - platformUtilsService: PlatformUtilsService, apiService: ApiService, - logService: LogService) { - super(cipherService, i18nService, cryptoService, userService, platformUtilsService, - apiService, window, logService); + cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, + apiService: ApiService, logService: LogService, + stateService: StateService) { + super(cipherService, i18nService, cryptoService, platformUtilsService, + apiService, window, logService, stateService); } } diff --git a/src/app/vault/ciphers.component.html b/src/app/vault/ciphers.component.html index c9210d1a..c6e785ce 100644 --- a/src/app/vault/ciphers.component.html +++ b/src/app/vault/ciphers.component.html @@ -1,10 +1,3 @@ -
diff --git a/src/app/vault/ciphers.component.ts b/src/app/vault/ciphers.component.ts index c9c1cff4..ee9da403 100644 --- a/src/app/vault/ciphers.component.ts +++ b/src/app/vault/ciphers.component.ts @@ -1,14 +1,27 @@ 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; } diff --git a/src/app/vault/groupings.component.ts b/src/app/vault/groupings.component.ts index 3d87d42c..3f6d5e0a 100644 --- a/src/app/vault/groupings.component.ts +++ b/src/app/vault/groupings.component.ts @@ -2,8 +2,7 @@ import { Component } from '@angular/core'; import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { FolderService } from 'jslib-common/abstractions/folder.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { GroupingsComponent as BaseGroupingsComponent } from 'jslib-angular/components/groupings.component'; @@ -13,7 +12,7 @@ import { GroupingsComponent as BaseGroupingsComponent } from 'jslib-angular/comp }) export class GroupingsComponent extends BaseGroupingsComponent { constructor(collectionService: CollectionService, folderService: FolderService, - storageService: StorageService, userService: UserService) { - super(collectionService, folderService, storageService, userService); + stateService: StateService) { + super(collectionService, folderService, stateService); } } diff --git a/src/app/vault/share.component.ts b/src/app/vault/share.component.ts index 0a60c79c..afb0a045 100644 --- a/src/app/vault/share.component.ts +++ b/src/app/vault/share.component.ts @@ -4,8 +4,8 @@ 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'; -import { UserService } from 'jslib-common/abstractions/user.service'; import { ShareComponent as BaseShareComponent } from 'jslib-angular/components/share.component'; @@ -15,9 +15,9 @@ import { ShareComponent as BaseShareComponent } from 'jslib-angular/components/s }) export class ShareComponent extends BaseShareComponent { constructor(cipherService: CipherService, i18nService: I18nService, - collectionService: CollectionService, userService: UserService, - platformUtilsService: PlatformUtilsService, logService: LogService) { - super(collectionService, platformUtilsService, i18nService, userService, cipherService, - logService); + collectionService: CollectionService, platformUtilsService: PlatformUtilsService, + logService: LogService, organizationService: OrganizationService) { + super(collectionService, platformUtilsService, i18nService, cipherService, + logService, organizationService); } } diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index be6fc541..33aaef21 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -14,6 +14,7 @@ import { import { first } from 'rxjs/operators'; +import { SearchBarService } from '../layout/search/search-bar.service'; import { AddEditComponent } from './add-edit.component'; import { AttachmentsComponent } from './attachments.component'; import { CiphersComponent } from './ciphers.component'; @@ -35,15 +36,16 @@ import { FolderView } from 'jslib-common/models/view/folderView'; 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 { UserService } from 'jslib-common/abstractions/user.service'; import { invokeMenu, RendererMenuItem } from 'jslib-electron/utils'; @@ -86,11 +88,11 @@ export class VaultComponent implements OnInit, OnDestroy { private ngZone: NgZone, private syncService: SyncService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, private eventService: EventService, - private totpService: TotpService, private userService: UserService, - private passwordRepromptService: PasswordRepromptService) { } + private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, + private stateService: StateService, private searchBarService: SearchBarService) { } async ngOnInit() { - this.userHasPremiumAccess = await this.userService.canAccessPremium(); + this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone.run(async () => { let detectChanges = true; @@ -166,9 +168,13 @@ export class VaultComponent implements OnInit, OnDestroy { 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'); } @@ -494,14 +500,14 @@ export class VaultComponent implements OnInit, OnDestroy { } async clearGroupingFilters() { - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchVault'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchVault')); await this.ciphersComponent.reload(); this.clearFilters(); this.go(); } async filterFavorites() { - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFavorites'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchFavorites')); await this.ciphersComponent.reload(c => c.favorite); this.clearFilters(); this.favorites = true; @@ -509,7 +515,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async filterDeleted() { - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchTrash'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchTrash')); this.ciphersComponent.deleted = true; await this.ciphersComponent.reload(null, true); this.clearFilters(); @@ -518,7 +524,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async filterCipherType(type: CipherType) { - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchType'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchType')); await this.ciphersComponent.reload(c => c.type === type); this.clearFilters(); this.type = type; @@ -527,7 +533,7 @@ export class VaultComponent implements OnInit, OnDestroy { async filterFolder(folderId: string) { folderId = folderId === 'none' ? null : folderId; - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchFolder'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchFolder')); await this.ciphersComponent.reload(c => c.folderId === folderId); this.clearFilters(); this.folderId = folderId == null ? 'none' : folderId; @@ -535,7 +541,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async filterCollection(collectionId: string) { - this.ciphersComponent.searchPlaceholder = this.i18nService.t('searchCollection'); + this.searchBarService.setPlaceholderText(this.i18nService.t('searchCollection')); await this.ciphersComponent.reload(c => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1); this.clearFilters(); diff --git a/src/app/vault/view.component.ts b/src/app/vault/view.component.ts index 93340192..c711c495 100644 --- a/src/app/vault/view.component.ts +++ b/src/app/vault/view.component.ts @@ -18,9 +18,9 @@ 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 { UserService } from 'jslib-common/abstractions/user.service'; import { ViewComponent as BaseViewComponent } from 'jslib-angular/components/view.component'; @@ -40,12 +40,12 @@ export class ViewComponent extends BaseViewComponent implements OnChanges { cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, auditService: AuditService, broadcasterService: BroadcasterService, ngZone: NgZone, changeDetectorRef: ChangeDetectorRef, - userService: UserService, eventService: EventService, apiService: ApiService, + eventService: EventService, apiService: ApiService, private messagingService: MessagingService, passwordRepromptService: PasswordRepromptService, - logService: LogService) { + logService: LogService, stateService: StateService) { super(cipherService, totpService, tokenService, i18nService, cryptoService, platformUtilsService, - auditService, window, broadcasterService, ngZone, changeDetectorRef, userService, eventService, - apiService, passwordRepromptService, logService); + auditService, window, broadcasterService, ngZone, changeDetectorRef, eventService, + apiService, passwordRepromptService, logService, stateService); } ngOnInit() { super.ngOnInit(); diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index d9eee019..00d50a46 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -733,8 +733,8 @@ "loading": { "message": "Loading..." }, - "lockNow": { - "message": "Lock Now" + "lockVault": { + "message": "Lock Vault" }, "passwordGenerator": { "message": "Password Generator" @@ -1757,6 +1757,9 @@ "personalVaultExportPolicyInEffect": { "message": "One or more organization policies prevents you from exporting your personal vault." }, + "addAccount": { + "message": "Add Account" + }, "removeMasterPassword": { "message": "Remove Master Password" }, @@ -1783,5 +1786,11 @@ }, "ssoKeyConnectorUnavailable": { "message": "Unable to reach the key connector, try again later." + }, + "lockAllVaults": { + "message": "Lock All Vaults" + }, + "accountLimitReached": { + "message": "No more than 5 accounts may be logged in at the same time." } } diff --git a/src/main.ts b/src/main.ts index 071be867..e6f34007 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,24 +7,27 @@ import { MenuMain } from './main/menu.main'; import { MessagingMain } from './main/messaging.main'; import { PowerMonitorMain } from './main/powerMonitor.main'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { BiometricMain } from 'jslib-common/abstractions/biometric.main'; -import { ElectronConstants } from 'jslib-electron/electronConstants'; + import { KeytarStorageListener } from 'jslib-electron/keytarStorageListener'; + import { ElectronLogService } from 'jslib-electron/services/electronLog.service'; import { ElectronMainMessagingService } from 'jslib-electron/services/electronMainMessaging.service'; import { ElectronStorageService } from 'jslib-electron/services/electronStorage.service'; + import { TrayMain } from 'jslib-electron/tray.main'; import { UpdaterMain } from 'jslib-electron/updater.main'; import { WindowMain } from 'jslib-electron/window.main'; import { NativeMessagingMain } from './main/nativeMessaging.main'; +import { StateService } from 'jslib-common/services/state.service'; + export class Main { logService: ElectronLogService; i18nService: I18nService; storageService: ElectronStorageService; messagingService: ElectronMainMessagingService; + stateService: StateService; keytarStorageListener: KeytarStorageListener; windowMain: WindowMain; @@ -69,23 +72,23 @@ export class Main { const storageDefaults: any = {}; // Default vault timeout to "on restart", and action to "lock" - storageDefaults[ConstantsService.vaultTimeoutKey] = -1; - storageDefaults[ConstantsService.vaultTimeoutActionKey] = 'lock'; + storageDefaults['global.vaultTimeout'] = -1; + storageDefaults['global.vaultTimeoutAction'] = 'lock'; this.storageService = new ElectronStorageService(app.getPath('userData'), storageDefaults); - this.windowMain = new WindowMain(this.storageService, this.logService, true, undefined, undefined, + // TODO: this state service will have access to on disk storage, but not in memory storage. + // If we could get this to work using the stateService singleton that the rest of the app uses we could save + // ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events. + this.stateService = new StateService(this.storageService, null, this.logService, null); + + this.windowMain = new WindowMain(this.stateService, this.logService, true, undefined, undefined, arg => this.processDeepLink(arg), win => this.trayMain.setupWindowListeners(win)); - this.messagingMain = new MessagingMain(this, this.storageService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'desktop', () => { - this.menuMain.updateMenuItem.enabled = false; - }, () => { - this.menuMain.updateMenuItem.enabled = true; - }, () => { - this.menuMain.updateMenuItem.label = this.i18nService.t('restartToUpdate'); - }, 'bitwarden'); + this.messagingMain = new MessagingMain(this, this.stateService); + this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'desktop', + null, null, null, 'bitwarden'); this.menuMain = new MenuMain(this); this.powerMonitorMain = new PowerMonitorMain(this); - this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.storageService); + this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); this.messagingService = new ElectronMainMessagingService(this.windowMain, message => { this.messagingMain.onMessage(message); @@ -94,10 +97,10 @@ export class Main { if (process.platform === 'win32') { const BiometricWindowsMain = require('jslib-electron/biometric.windows.main').default; - this.biometricMain = new BiometricWindowsMain(this.storageService, this.i18nService, this.windowMain); + this.biometricMain = new BiometricWindowsMain(this.i18nService, this.windowMain, this.stateService, this.logService); } else if (process.platform === 'darwin') { const BiometricDarwinMain = require('jslib-electron/biometric.darwin.main').default; - this.biometricMain = new BiometricDarwinMain(this.storageService, this.i18nService); + this.biometricMain = new BiometricDarwinMain(this.i18nService, this.stateService); } this.keytarStorageListener = new KeytarStorageListener('Bitwarden', this.biometricMain); @@ -108,7 +111,7 @@ export class Main { bootstrap() { this.keytarStorageListener.init(); this.windowMain.init().then(async () => { - const locale = await this.storageService.get(ConstantsService.localeKey); + const locale = await this.stateService.getLocale(); await this.i18nService.init(locale != null ? locale : app.getLocale()); this.messagingMain.init(); this.menuMain.init(); @@ -118,7 +121,7 @@ export class Main { id: 'lockNow', click: () => this.messagingService.send('lockVault'), }]); - if (await this.storageService.get(ElectronConstants.enableStartToTrayKey)) { + if (await this.stateService.getEnableStartToTray()) { this.trayMain.hideToTray(); } this.powerMonitorMain.init(); @@ -127,7 +130,7 @@ export class Main { await this.biometricMain.init(); } - if (await this.storageService.get(ElectronConstants.enableBrowserIntegration)) { + if (await this.stateService.getEnableBrowserIntegration()) { this.nativeMessagingMain.listen(); } diff --git a/src/main/menu.about.ts b/src/main/menu.about.ts new file mode 100644 index 00000000..1839e889 --- /dev/null +++ b/src/main/menu.about.ts @@ -0,0 +1,96 @@ +import { + BrowserWindow, + clipboard, + dialog, + MenuItemConstructorOptions, +} from 'electron'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; + +import { UpdaterMain } from 'jslib-electron/updater.main'; +import { isMac, isSnapStore, isWindowsStore } from 'jslib-electron/utils'; + +import { IMenubarMenu } from './menubar'; + +export class AboutMenu implements IMenubarMenu { + readonly id: string = 'about'; + + get visible(): boolean { + return !isMac(); + } + + get label(): string { + return this.localize('about'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.separator, + this.checkForUpdates, + this.aboutBitwarden, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _updater: UpdaterMain; + private readonly _window: BrowserWindow; + private readonly _version: string; + + constructor( + i18nService: I18nService, + version: string, + window: BrowserWindow, + updater: UpdaterMain, + ) { + this._i18nService = i18nService; + this._updater = updater; + this._version = version; + this._window = window; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get checkForUpdates(): MenuItemConstructorOptions { + return { + id: 'checkForUpdates', + label: this.localize('checkForUpdates'), + visible: !isWindowsStore() && !isSnapStore(), + click: () => this.checkForUpdate(), + }; + } + + private get aboutBitwarden(): MenuItemConstructorOptions { + return { + id: 'aboutBitwarden', + label: this.localize('aboutBitwarden'), + click: async () => { + const aboutInformation = this.localize('version', this._version) + + '\nShell ' + process.versions.electron + + '\nRenderer ' + process.versions.chrome + + '\nNode ' + process.versions.node + + '\nArchitecture ' + process.arch; + const result = await dialog.showMessageBox(this._window, { + title: 'Bitwarden', + message: 'Bitwarden', + detail: aboutInformation, + type: 'info', + noLink: true, + buttons: [this.localize('ok'), this.localize('copy')], + }); + if (result.response === 1) { + clipboard.writeText(aboutInformation); + } + }, + }; + } + + private localize(s: string, p?: string) { + return this._i18nService.t(s, p); + } + + private async checkForUpdate() { + this._updater.checkForUpdate(true); + } +} diff --git a/src/main/menu.account.ts b/src/main/menu.account.ts new file mode 100644 index 00000000..98892100 --- /dev/null +++ b/src/main/menu.account.ts @@ -0,0 +1,121 @@ +import { + BrowserWindow, + dialog, + MenuItemConstructorOptions, + shell, +} from 'electron'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { isMacAppStore, isWindowsStore } from 'jslib-electron/utils'; + +import { IMenubarMenu } from './menubar'; + +export class AccountMenu implements IMenubarMenu { + readonly id: string = 'accountMenu'; + + get label(): string { + return this.localize('account'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.premiumMembership, + this.changeMasterPassword, + this.twoStepLogin, + this.fingerprintPhrase, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _webVaultUrl: string; + private readonly _window: BrowserWindow; + private readonly _isAuthenticated: boolean; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + webVaultUrl: string, + window: BrowserWindow, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._webVaultUrl = webVaultUrl; + this._window = window; + this._isAuthenticated = isAuthenticated; + } + + private get premiumMembership(): MenuItemConstructorOptions { + return { + label: this.localize('premiumMembership'), + click: () => this.sendMessage('openPremium'), + id: 'premiumMembership', + visible: !isWindowsStore() && !isMacAppStore(), + enabled: this._isAuthenticated, + }; + } + + private get changeMasterPassword(): MenuItemConstructorOptions { + return { + label: this.localize('changeMasterPass'), + id: 'changeMasterPass', + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('changeMasterPass'), + message: this.localize('changeMasterPass'), + detail: this.localize('changeMasterPasswordConfirmation'), + buttons: [this.localize('yes'), this.localize('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + shell.openExternal(this._webVaultUrl); + } + }, + enabled: this._isAuthenticated, + }; + } + + private get twoStepLogin(): MenuItemConstructorOptions { + return { + label: this.localize('twoStepLogin'), + id: 'twoStepLogin', + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('twoStepLogin'), + message: this.localize('twoStepLogin'), + detail: this.localize('twoStepLoginConfirmation'), + buttons: [this.localize('yes'), this.localize('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + shell.openExternal(this._webVaultUrl); + } + }, + enabled: this._isAuthenticated, + }; + } + + private get fingerprintPhrase(): MenuItemConstructorOptions { + return { + label: this.localize('fingerprintPhrase'), + id: 'fingerprintPhrase', + click: () => this.sendMessage('showFingerprintPhrase'), + enabled: this._isAuthenticated, + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menu.bitwarden.ts b/src/main/menu.bitwarden.ts new file mode 100644 index 00000000..3b74558a --- /dev/null +++ b/src/main/menu.bitwarden.ts @@ -0,0 +1,237 @@ +import { + BrowserWindow, + dialog, + MenuItem, + MenuItemConstructorOptions, +} from 'electron'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { UpdaterMain } from 'jslib-electron/updater.main'; +import { isMacAppStore, isSnapStore, isWindowsStore } from 'jslib-electron/utils'; + +import { IMenubarMenu } from './menubar'; + +import { MenuAccount } from './menu.updater'; + +// AKA: "FirstMenu" or "MacMenu" - the first menu that shows on all macOs apps +export class BitwardenMenu implements IMenubarMenu { + readonly id: string = 'bitwarden'; + readonly label: string = 'Bitwarden'; + + get items(): MenuItemConstructorOptions[] { + return [ + this.aboutBitwarden, + this.checkForUpdates, + this.separator, + this.settings, + this.lock, + this.lockAll, + this.logOut, + this.services, + this.separator, + this.hideBitwarden, + this.hideOthers, + this.showAll, + this.separator, + this.quitBitwarden, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _updater: UpdaterMain; + private readonly _messagingService: MessagingService; + private readonly _accounts: { [userId: string]: MenuAccount }; + private readonly _window: BrowserWindow; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + updater: UpdaterMain, + window: BrowserWindow, + accounts: { [userId: string]: MenuAccount }, + ) { + this._i18nService = i18nService; + this._updater = updater; + this._messagingService = messagingService; + this._window = window; + this._accounts = accounts; + } + + private get hasAccounts(): boolean { + return this._accounts != null && Object.keys(this._accounts).length > 0; + } + + private get aboutBitwarden(): MenuItemConstructorOptions { + return { + id: 'aboutBitwarden', + label: this.localize('aboutBitwarden'), + role: 'about', + visible: isMacAppStore(), + }; + } + + private get checkForUpdates(): MenuItemConstructorOptions { + return { + id: 'checkForUpdates', + label: this.localize('checkForUpdates'), + click: menuItem => this.checkForUpdate(menuItem), + visible: !isMacAppStore() && !isWindowsStore() && !isSnapStore(), + }; + } + + private get separator(): MenuItemConstructorOptions { + return { + type: 'separator', + }; + } + + private get settings(): MenuItemConstructorOptions { + return { + id: 'settings', + label: this.localize(process.platform === 'darwin' ? + 'preferences' : + 'settings' + ), + click: () => this.sendMessage('openSettings'), + accelerator: 'CmdOrCtrl+,', + }; + } + + private get lock(): MenuItemConstructorOptions { + return { + id: 'lock', + label: this.localize('lockVault'), + submenu: this.lockSubmenu, + enabled: this.hasAccounts, + }; + } + + private get lockSubmenu(): MenuItemConstructorOptions[] { + const value: MenuItemConstructorOptions[] = []; + for (const userId in this._accounts) { + if (userId == null) { + continue; + } + + value.push({ + label: this._accounts[userId].email, + id: `lockNow_${this._accounts[userId].userId}`, + click: () => this.sendMessage('lockVault', { userId: this._accounts[userId].userId }), + enabled: !this._accounts[userId].isLocked, + visible: this._accounts[userId].isAuthenticated, + }); + } + return value; + } + + private get lockAll(): MenuItemConstructorOptions { + return { + id: 'lockAllNow', + label: this.localize('lockAllVaults'), + click: () => this.sendMessage('lockAllVaults'), + accelerator: 'CmdOrCtrl+L', + enabled: this.hasAccounts, + }; + } + + private get logOut(): MenuItemConstructorOptions { + return { + id: 'logOut', + label: this.localize('logOut'), + submenu: this.logOutSubmenu, + enabled: this.hasAccounts, + }; + } + + private get logOutSubmenu(): MenuItemConstructorOptions[] { + const value: MenuItemConstructorOptions[] = []; + for (const userId in this._accounts) { + if (userId == null) { + continue; + } + + value.push({ + label: this._accounts[userId].email, + id: `logOut_${this._accounts[userId].userId}`, + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('logOut'), + message: this.localize('logOut'), + detail: this.localize('logOutConfirmation'), + buttons: [this.localize('logOut'), this.localize('cancel')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + this.sendMessage('logout', { userId: this._accounts[userId].userId }); + } + }, + visible: this._accounts[userId].isAuthenticated, + }); + } + return value; + } + + private get services(): MenuItemConstructorOptions { + return { + id: 'services', + label: this.localize('services'), + role: 'services', + submenu: [], + visible: isMacAppStore(), + }; + } + + private get hideBitwarden(): MenuItemConstructorOptions { + return { + id: 'hideBitwarden', + label: this.localize('hideBitwarden'), + role: 'hide', + visible: isMacAppStore(), + }; + } + + private get hideOthers(): MenuItemConstructorOptions { + return { + id: 'hideOthers', + label: this.localize('hideOthers'), + role: 'hideOthers', + visible: isMacAppStore(), + }; + } + + private get showAll(): MenuItemConstructorOptions { + return { + id: 'showAll', + label: this.localize('showAll'), + role: 'unhide', + visible: isMacAppStore(), + }; + } + + private get quitBitwarden(): MenuItemConstructorOptions { + return { + id: 'quitBitwarden', + label: this.localize('quitBitwarden'), + role: 'quit', + visible: isMacAppStore(), + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private async checkForUpdate(menuItem: MenuItem) { + menuItem.enabled = false; + this._updater.checkForUpdate(true); + menuItem.enabled = true; + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menu.edit.ts b/src/main/menu.edit.ts new file mode 100644 index 00000000..e63f116b --- /dev/null +++ b/src/main/menu.edit.ts @@ -0,0 +1,134 @@ +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { IMenubarMenu } from './menubar'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class EditMenu implements IMenubarMenu { + readonly id: string = 'editMenu'; + + get label(): string { + return this.localize('edit'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.undo, + this.redo, + this.separator, + this.cut, + this.copy, + this.paste, + this.separator, + this.selectAll, + this.separator, + this.copyUsername, + this.copyPassword, + this.copyVerificationCodeTotp, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated; + } + + private get undo(): MenuItemConstructorOptions { + return { + id: 'undo', + label: this.localize('undo'), + role: 'undo', + }; + } + + private get redo(): MenuItemConstructorOptions { + return { + id: 'redo', + label: this.localize('redo'), + role: 'redo', + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get cut(): MenuItemConstructorOptions { + return { + id: 'cut', + label: this.localize('cut'), + role: 'cut', + }; + } + + private get copy(): MenuItemConstructorOptions { + return { + id: 'copy', + label: this.localize('copy'), + role: 'copy', + }; + } + + private get paste(): MenuItemConstructorOptions { + return { + id: 'paste', + label: this.localize('paste'), + role: 'paste', + }; + } + + private get selectAll(): MenuItemConstructorOptions { + return { + id: 'selectAll', + label: this.localize('selectAll'), + role: 'selectAll', + }; + } + + private get copyUsername(): MenuItemConstructorOptions { + return { + label: this.localize('copyUsername'), + id: 'copyUsername', + click: () => this.sendMessage('copyUsername'), + accelerator: 'CmdOrCtrl+U', + enabled: this._isAuthenticated, + }; + } + + private get copyPassword(): MenuItemConstructorOptions { + return { + label: this.localize('copyPassword'), + id: 'copyPassword', + click: () => this.sendMessage('copyPassword'), + accelerator: 'CmdOrCtrl+P', + enabled: this._isAuthenticated, + }; + } + + private get copyVerificationCodeTotp(): MenuItemConstructorOptions { + return { + label: this.localize('copyVerificationCodeTotp'), + id: 'copyTotp', + click: () => this.sendMessage('copyTotp'), + accelerator: 'CmdOrCtrl+T', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.file.ts b/src/main/menu.file.ts new file mode 100644 index 00000000..0cd6577c --- /dev/null +++ b/src/main/menu.file.ts @@ -0,0 +1,134 @@ +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { isMacAppStore } from 'jslib-electron/utils'; + +import { IMenubarMenu } from './menubar'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class FileMenu implements IMenubarMenu { + readonly id: string = 'fileMenu'; + + get label(): string { + return this.localize('file'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.addNewLogin, + this.addNewItem, + this.addNewFolder, + this.separator, + this.syncVault, + this.exportVault, + this.quitBitwarden, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated; + } + + private get addNewLogin(): MenuItemConstructorOptions { + return { + label: this.localize('addNewLogin'), + click: () => this.sendMessage('newLogin'), + accelerator: 'CmdOrCtrl+N', + id: 'addNewLogin', + }; + } + + private get addNewItem(): MenuItemConstructorOptions { + return { + label: this.localize('addNewItem'), + id: 'addNewItem', + submenu: this.addNewItemSubmenu, + enabled: this._isAuthenticated, + }; + } + + private get addNewItemSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: 'typeLogin', + label: this.localize('typeLogin'), + click: () => this.sendMessage('newLogin'), + accelerator: 'CmdOrCtrl+Shift+L', + }, + { + id: 'typeCard', + label: this.localize('typeCard'), + click: () => this.sendMessage('newCard'), + accelerator: 'CmdOrCtrl+Shift+C', + }, + { + id: 'typeIdentity', + label: this.localize('typeIdentity'), + click: () => this.sendMessage('newIdentity'), + accelerator: 'CmdOrCtrl+Shift+I', + }, + { + id: 'typeSecureNote', + label: this.localize('typeSecureNote'), + click: () => this.sendMessage('newSecureNote'), + accelerator: 'CmdOrCtrl+Shift+S', + }, + ]; + } + + private get addNewFolder(): MenuItemConstructorOptions { + return { + id: 'addNewFolder', + label: this.localize('addNewFolder'), + click: () => this.sendMessage('newFolder'), + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get syncVault(): MenuItemConstructorOptions { + return { + id: 'syncVault', + label: this.localize('syncVault'), + click: () => this.sendMessage('syncVault'), + }; + } + + private get exportVault(): MenuItemConstructorOptions { + return { + id: 'exportVault', + label: this.localize('exportVault'), + click: () => this.sendMessage('exportVault'), + }; + } + + private get quitBitwarden(): MenuItemConstructorOptions { + return { + id: 'quitBitwarden', + label: this.localize('quitBitwarden'), + visible: !isMacAppStore(), + role: 'quit', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.help.ts b/src/main/menu.help.ts new file mode 100644 index 00000000..73151900 --- /dev/null +++ b/src/main/menu.help.ts @@ -0,0 +1,223 @@ +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { IMenubarMenu } from './menubar'; + +import { shell } from 'electron'; + +import { isMacAppStore, isWindowsStore } from 'jslib-electron/utils'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class HelpMenu implements IMenubarMenu { + readonly id: string = 'help'; + + get label(): string { + return this.localize('help'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.emailUs, + this.visitOurWebsite, + this.fileBugReport, + this.legal, + this.separator, + this.followUs, + this.separator, + this.goToWebVault, + this.separator, + this.getMobileApp, + this.getBrowserExtension, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _webVaultUrl: string; + + constructor( + i18nService: I18nService, + webVaultUrl: string + ) { + this._i18nService = i18nService; + this._webVaultUrl = webVaultUrl; + } + + private get emailUs(): MenuItemConstructorOptions { + return { + id: 'emailUs', + label: this.localize('emailUs'), + click: () => shell.openExternal('mailTo:hello@bitwarden.com'), + }; + } + + private get visitOurWebsite(): MenuItemConstructorOptions { + return { + id: 'visitOurWebsite', + label: this.localize('visitOurWebsite'), + click: () => shell.openExternal('https://bitwarden.com/contact'), + }; + } + + private get fileBugReport(): MenuItemConstructorOptions { + return { + id: 'fileBugReport', + label: this.localize('fileBugReport'), + click: () => shell.openExternal('https://github.com/bitwarden/desktop/issues'), + }; + } + + private get legal(): MenuItemConstructorOptions { + return { + id: 'legal', + label: this.localize('legal'), + visible: !isMacAppStore(), + submenu: this.legalSubmenu, + }; + } + + private get legalSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: 'termsOfService', + label: this.localize('termsOfService'), + click: () => shell.openExternal('https://bitwarden.com/terms/'), + }, + { + id: 'privacyPolicy', + label: this.localize('privacyPolicy'), + click: () => shell.openExternal('https://bitwarden.com/privacy/'), + }, + ]; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get followUs(): MenuItemConstructorOptions { + return { + id: 'followUs', + label: this.localize('followUs'), + submenu: this.followUsSubmenu, + }; + } + + private get followUsSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: 'blog', + label: this.localize('blog'), + click: () => shell.openExternal('https://blog.bitwarden.com'), + }, + { + id: 'twitter', + label: 'Twitter', + click: () => shell.openExternal('https://twitter.com/bitwarden'), + }, + { + id: 'facebook', + label: 'Facebook', + click: () => shell.openExternal('https://www.facebook.com/bitwarden/'), + }, + { + id: 'github', + label: 'GitHub', + click: () => shell.openExternal('https://github.com/bitwarden'), + }, + ]; + } + + private get goToWebVault(): MenuItemConstructorOptions { + return { + id: 'goToWebVault', + label: this.localize('goToWebVault'), + click: () => shell.openExternal(this._webVaultUrl), + }; + } + + private get getMobileApp(): MenuItemConstructorOptions { + return { + id: 'getMobileApp', + label: this.localize('getMobileApp'), + visible: !isWindowsStore(), + submenu: this.getMobileAppSubmenu, + }; + } + + private get getMobileAppSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: 'iOS', + label: 'iOS', + click: () => { + shell.openExternal('https://itunes.apple.com/app/' + + 'bitwarden-free-password-manager/id1137397744?mt=8'); + }, + }, + { + id: 'android', + label: 'Android', + click: () => { + shell.openExternal('https://play.google.com/store/apps/' + + 'details?id=com.x8bit.bitwarden'); + }, + }, + ]; + } + + private get getBrowserExtension(): MenuItemConstructorOptions { + return { + id: 'getBrowserExtension', + label: this.localize('getBrowserExtension'), + visible: !isWindowsStore(), + submenu: this.getBrowserExtensionSubmenu, + }; + } + + private get getBrowserExtensionSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: 'chrome', + label: 'Chrome', + click: () => { + shell.openExternal('https://chrome.google.com/webstore/detail/' + + 'bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb'); + }, + }, + { + id: 'firefox', + label: 'Firefox', + click: () => { + shell.openExternal('https://addons.mozilla.org/firefox/addon/' + + 'bitwarden-password-manager/'); + }, + }, + { + id: 'firefox', + label: 'Opera', + click: () => { + shell.openExternal('https://addons.opera.com/extensions/details/' + + 'bitwarden-free-password-manager/'); + }, + }, + { + id: 'firefox', + label: 'Edge', + click: () => { + shell.openExternal('https://microsoftedge.microsoft.com/addons/' + + 'detail/jbkfoedolllekgbhcbcoahefnbanhhlh'); + }, + }, + { + id: 'safari', + label: 'Safari', + click: () => { + shell.openExternal('https://bitwarden.com/download/'); + }, + }, + ]; + } + + private localize(s: string) { + return this._i18nService.t(s); + } +} diff --git a/src/main/menu.main.ts b/src/main/menu.main.ts index e81fc0d4..9e2f4d56 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu.main.ts @@ -1,525 +1,54 @@ import { app, - BrowserWindow, - clipboard, - dialog, - ipcMain, Menu, - MenuItem, - MenuItemConstructorOptions, - shell, } from 'electron'; import { Main } from '../main'; import { BaseMenu } from 'jslib-electron/baseMenu'; -import { isMacAppStore, isSnapStore, isWindowsStore } from 'jslib-electron/utils'; +import { MenuUpdateRequest } from './menu.updater'; +import { Menubar } from './menubar'; -import { ConstantsService } from 'jslib-common/services/constants.service'; +const cloudWebVaultUrl: string = 'https://vault.bitwarden.com'; export class MenuMain extends BaseMenu { - menu: Menu; - updateMenuItem: MenuItem; - addNewLogin: MenuItem; - addNewItem: MenuItem; - addNewFolder: MenuItem; - syncVault: MenuItem; - exportVault: MenuItem; - settings: MenuItem; - lockNow: MenuItem; - logOut: MenuItem; - twoStepLogin: MenuItem; - fingerprintPhrase: MenuItem; - changeMasterPass: MenuItem; - premiumMembership: MenuItem; - passwordGenerator: MenuItem; - passwordHistory: MenuItem; - searchVault: MenuItem; - copyUsername: MenuItem; - copyPassword: MenuItem; - copyTotp: MenuItem; - unlockedRequiredMenuItems: MenuItem[] = []; - constructor(private main: Main) { super(main.i18nService, main.windowMain); } - init() { - this.initProperties(); + async init() { this.initContextMenu(); - this.initApplicationMenu(); - - this.updateMenuItem = this.menu.getMenuItemById('checkForUpdates'); - this.addNewLogin = this.menu.getMenuItemById('addNewLogin'); - this.addNewItem = this.menu.getMenuItemById('addNewItem'); - this.addNewFolder = this.menu.getMenuItemById('addNewFolder'); - this.syncVault = this.menu.getMenuItemById('syncVault'); - this.exportVault = this.menu.getMenuItemById('exportVault'); - this.settings = this.menu.getMenuItemById('settings'); - this.lockNow = this.menu.getMenuItemById('lockNow'); - this.logOut = this.menu.getMenuItemById('logOut'); - this.twoStepLogin = this.menu.getMenuItemById('twoStepLogin'); - this.fingerprintPhrase = this.menu.getMenuItemById('fingerprintPhrase'); - this.changeMasterPass = this.menu.getMenuItemById('changeMasterPass'); - this.premiumMembership = this.menu.getMenuItemById('premiumMembership'); - this.passwordGenerator = this.menu.getMenuItemById('passwordGenerator'); - this.passwordHistory = this.menu.getMenuItemById('passwordHistory'); - this.searchVault = this.menu.getMenuItemById('searchVault'); - this.copyUsername = this.menu.getMenuItemById('copyUsername'); - this.copyPassword = this.menu.getMenuItemById('copyPassword'); - this.copyTotp = this.menu.getMenuItemById('copyTotp'); - - this.unlockedRequiredMenuItems = [ - this.addNewLogin, this.addNewItem, this.addNewFolder, - this.syncVault, this.exportVault, this.settings, this.lockNow, this.twoStepLogin, this.fingerprintPhrase, - this.changeMasterPass, this.premiumMembership, this.passwordGenerator, this.passwordHistory, - this.searchVault, this.copyUsername, this.copyPassword]; - this.updateApplicationMenuState(false, true, false); + await this.setMenu(); } - updateApplicationMenuState(isAuthenticated: boolean, isLocked: boolean, hideChangeMasterPass: boolean) { - if (isAuthenticated != null && isLocked != null) { - this.unlockedRequiredMenuItems.forEach((mi: MenuItem) => { - if (mi != null) { - mi.enabled = isAuthenticated && !isLocked; - } - }); - - if (this.logOut != null) { - this.logOut.enabled = isAuthenticated; - } - } - - if (this.changeMasterPass != null) { - this.changeMasterPass.visible = !(hideChangeMasterPass ?? false); - } - - if (this.menu != null) { - Menu.setApplicationMenu(this.menu); - } + async updateApplicationMenuState(updateRequest: MenuUpdateRequest) { + await this.setMenu(updateRequest); } - private initApplicationMenu() { - const accountSubmenu: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('changeMasterPass'), - id: 'changeMasterPass', - click: async () => { - const result = await dialog.showMessageBox(this.main.windowMain.win, { - title: this.main.i18nService.t('changeMasterPass'), - message: this.main.i18nService.t('changeMasterPass'), - detail: this.main.i18nService.t('changeMasterPasswordConfirmation'), - buttons: [this.main.i18nService.t('yes'), this.main.i18nService.t('no')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - await this.openWebVault(); - } - }, - }, - { - label: this.main.i18nService.t('twoStepLogin'), - id: 'twoStepLogin', - click: async () => { - const result = await dialog.showMessageBox(this.main.windowMain.win, { - title: this.main.i18nService.t('twoStepLogin'), - message: this.main.i18nService.t('twoStepLogin'), - detail: this.main.i18nService.t('twoStepLoginConfirmation'), - buttons: [this.main.i18nService.t('yes'), this.main.i18nService.t('no')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - await this.openWebVault(); - } - }, - }, - { - label: this.main.i18nService.t('fingerprintPhrase'), - id: 'fingerprintPhrase', - click: () => this.main.messagingService.send('showFingerprintPhrase'), - }, - { type: 'separator' }, - { - label: this.i18nService.t('logOut'), - id: 'logOut', - click: async () => { - const result = await dialog.showMessageBox(this.windowMain.win, { - title: this.i18nService.t('logOut'), - message: this.i18nService.t('logOut'), - detail: this.i18nService.t('logOutConfirmation'), - buttons: [this.i18nService.t('logOut'), this.i18nService.t('cancel')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - this.main.messagingService.send('logout'); - } - }, - }, - ]; - - this.editMenuItemOptions.submenu = (this.editMenuItemOptions.submenu as MenuItemConstructorOptions[]).concat([ - { type: 'separator' }, - { - label: this.main.i18nService.t('copyUsername'), - id: 'copyUsername', - click: () => this.main.messagingService.send('copyUsername'), - accelerator: 'CmdOrCtrl+U', - }, - { - label: this.main.i18nService.t('copyPassword'), - id: 'copyPassword', - click: () => this.main.messagingService.send('copyPassword'), - accelerator: 'CmdOrCtrl+P', - }, - { - label: this.main.i18nService.t('copyVerificationCodeTotp'), - id: 'copyTotp', - click: () => this.main.messagingService.send('copyTotp'), - accelerator: 'CmdOrCtrl+T', - }, - ]); - - if (!isWindowsStore() && !isMacAppStore()) { - accountSubmenu.unshift({ - label: this.main.i18nService.t('premiumMembership'), - click: () => this.main.messagingService.send('openPremium'), - id: 'premiumMembership', - }); - } - - let helpSubmenu: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('emailUs'), - click: () => shell.openExternal('mailTo:hello@bitwarden.com'), - }, - { - label: this.main.i18nService.t('visitOurWebsite'), - click: () => shell.openExternal('https://bitwarden.com/contact'), - }, - { - label: this.main.i18nService.t('fileBugReport'), - click: () => shell.openExternal('https://github.com/bitwarden/desktop/issues'), - }, - ]; - - if (isMacAppStore()) { - helpSubmenu.push({ - label: this.main.i18nService.t('legal'), - submenu: [ - { - label: this.main.i18nService.t('termsOfService'), - click: () => shell.openExternal('https://bitwarden.com/terms/'), - }, - { - label: this.main.i18nService.t('privacyPolicy'), - click: () => shell.openExternal('https://bitwarden.com/privacy/'), - }, - ], - }); - } - - helpSubmenu = helpSubmenu.concat([ - { type: 'separator' }, - { - label: this.main.i18nService.t('followUs'), - submenu: [ - { - label: this.main.i18nService.t('blog'), - click: () => shell.openExternal('https://blog.bitwarden.com'), - }, - { - label: 'Twitter', - click: () => shell.openExternal('https://twitter.com/bitwarden'), - }, - { - label: 'Facebook', - click: () => shell.openExternal('https://www.facebook.com/bitwarden/'), - }, - { - label: 'GitHub', - click: () => shell.openExternal('https://github.com/bitwarden'), - }, - ], - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('goToWebVault'), - click: async () => await this.openWebVault(), - }, - ]); - - if (!isWindowsStore()) { - helpSubmenu.push({ - label: this.main.i18nService.t('getMobileApp'), - submenu: [ - { - label: 'iOS', - click: () => { - shell.openExternal('https://itunes.apple.com/app/' + - 'bitwarden-free-password-manager/id1137397744?mt=8'); - }, - }, - { - label: 'Android', - click: () => { - shell.openExternal('https://play.google.com/store/apps/' + - 'details?id=com.x8bit.bitwarden'); - }, - }, - ], - }); - helpSubmenu.push({ - label: this.main.i18nService.t('getBrowserExtension'), - submenu: [ - { - label: 'Chrome', - click: () => { - shell.openExternal('https://chrome.google.com/webstore/detail/' + - 'bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb'); - }, - }, - { - label: 'Firefox', - click: () => { - shell.openExternal('https://addons.mozilla.org/firefox/addon/' + - 'bitwarden-password-manager/'); - }, - }, - { - label: 'Opera', - click: () => { - shell.openExternal('https://addons.opera.com/extensions/details/' + - 'bitwarden-free-password-manager/'); - }, - }, - { - label: 'Edge', - click: () => { - shell.openExternal('https://microsoftedge.microsoft.com/addons/' + - 'detail/jbkfoedolllekgbhcbcoahefnbanhhlh'); - }, - }, - { - label: 'Safari', - click: () => { - shell.openExternal('https://bitwarden.com/download/'); - }, - }, - ], - }); - } - - const template: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('file'), - submenu: [ - { - label: this.main.i18nService.t('addNewLogin'), - click: () => this.main.messagingService.send('newLogin'), - accelerator: 'CmdOrCtrl+N', - id: 'addNewLogin', - }, - { - label: this.main.i18nService.t('addNewItem'), - id: 'addNewItem', - submenu: [ - { - label: this.main.i18nService.t('typeLogin'), - click: () => this.main.messagingService.send('newLogin'), - accelerator: 'CmdOrCtrl+Shift+L', - }, - { - label: this.main.i18nService.t('typeCard'), - click: () => this.main.messagingService.send('newCard'), - accelerator: 'CmdOrCtrl+Shift+C', - }, - { - label: this.main.i18nService.t('typeIdentity'), - click: () => this.main.messagingService.send('newIdentity'), - accelerator: 'CmdOrCtrl+Shift+I', - }, - { - label: this.main.i18nService.t('typeSecureNote'), - click: () => this.main.messagingService.send('newSecureNote'), - accelerator: 'CmdOrCtrl+Shift+S', - }, - ], - }, - { - label: this.main.i18nService.t('addNewFolder'), - id: 'addNewFolder', - click: () => this.main.messagingService.send('newFolder'), - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('syncVault'), - id: 'syncVault', - click: () => this.main.messagingService.send('syncVault'), - }, - { - label: this.main.i18nService.t('exportVault'), - id: 'exportVault', - click: () => this.main.messagingService.send('exportVault'), - }, - ], - }, - this.editMenuItemOptions, - { - label: this.main.i18nService.t('view'), - submenu: ([ - { - label: this.main.i18nService.t('searchVault'), - id: 'searchVault', - click: () => this.main.messagingService.send('focusSearch'), - accelerator: 'CmdOrCtrl+F', - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('passwordGenerator'), - id: 'passwordGenerator', - click: () => this.main.messagingService.send('openPasswordGenerator'), - accelerator: 'CmdOrCtrl+G', - }, - { - label: this.main.i18nService.t('passwordHistory'), - id: 'passwordHistory', - click: () => this.main.messagingService.send('openPasswordHistory'), - }, - { type: 'separator' }, - ] as MenuItemConstructorOptions[]).concat(this.viewSubMenuItemOptions), - }, - { - label: this.main.i18nService.t('account'), - submenu: accountSubmenu, - }, - this.windowMenuItemOptions, - { - label: this.main.i18nService.t('help'), - role: 'help', - submenu: helpSubmenu, - }, - ]; - - const firstMenuOptions: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - { - label: this.main.i18nService.t(process.platform === 'darwin' ? 'preferences' : 'settings'), - id: 'settings', - click: () => this.main.messagingService.send('openSettings'), - accelerator: 'CmdOrCtrl+,', - }, - { - label: this.main.i18nService.t('lockNow'), - id: 'lockNow', - click: () => this.main.messagingService.send('lockVault'), - accelerator: 'CmdOrCtrl+L', - }, - ]; - - const updateMenuItem = { - label: this.main.i18nService.t('checkForUpdates'), - click: () => this.main.updaterMain.checkForUpdate(true), - id: 'checkForUpdates', - }; - - if (process.platform === 'darwin') { - const firstMenuPart: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('aboutBitwarden'), - role: 'about', - }, - ]; - - if (!isMacAppStore()) { - firstMenuPart.push(updateMenuItem); - } - - template.unshift({ - label: 'Bitwarden', - submenu: firstMenuPart.concat(firstMenuOptions, [ - { type: 'separator' }, - ], this.macAppMenuItemOptions), - }); - - // Window menu - template[template.length - 2].submenu = this.macWindowSubmenuOptions; - } else { - // File menu - template[0].submenu = (template[0].submenu as MenuItemConstructorOptions[]).concat( - firstMenuOptions, { - label: this.i18nService.t('quitBitwarden'), - role: 'quit', - }); - - // About menu - const aboutMenuAdditions: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - ]; - - if (!isWindowsStore() && !isSnapStore()) { - aboutMenuAdditions.push(updateMenuItem); - } - - aboutMenuAdditions.push({ - label: this.i18nService.t('aboutBitwarden'), - click: async () => { - const aboutInformation = this.i18nService.t('version', app.getVersion()) + - '\nShell ' + process.versions.electron + - '\nRenderer ' + process.versions.chrome + - '\nNode ' + process.versions.node + - '\nArchitecture ' + process.arch; - const result = await dialog.showMessageBox(this.windowMain.win, { - title: 'Bitwarden', - message: 'Bitwarden', - detail: aboutInformation, - type: 'info', - noLink: true, - buttons: [this.i18nService.t('ok'), this.i18nService.t('copy')], - }); - if (result.response === 1) { - clipboard.writeText(aboutInformation); - } - }, - }); - - template[template.length - 1].submenu = - (template[template.length - 1].submenu as MenuItemConstructorOptions[]).concat(aboutMenuAdditions); - } - - (template[template.length - 2].submenu as MenuItemConstructorOptions[]).splice(1, 0, - { - label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'), - click: () => this.main.messagingService.send('hideToTray'), - accelerator: 'CmdOrCtrl+Shift+M', - }, - { - type: 'checkbox', - label: this.main.i18nService.t('alwaysOnTop'), - checked: this.windowMain.win.isAlwaysOnTop(), - click: () => this.main.windowMain.toggleAlwaysOnTop(), - accelerator: 'CmdOrCtrl+Shift+T', - }); - this.menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(this.menu); + private async setMenu(updateRequest?: MenuUpdateRequest) { + Menu.setApplicationMenu(new Menubar( + this.main.i18nService, + this.main.messagingService, + this.main.updaterMain, + this.windowMain, + await this.getWebVaultUrl(), + app.getVersion(), + updateRequest, + ).menu); } - private async openWebVault() { - let webUrl = 'https://vault.bitwarden.com'; - const urlsObj: any = await this.main.storageService.get(ConstantsService.environmentUrlsKey); + private async getWebVaultUrl() { + let webVaultUrl = cloudWebVaultUrl; + const urlsObj: any = await this.main.stateService.getEnvironmentUrls(); if (urlsObj != null) { if (urlsObj.base != null) { - webUrl = urlsObj.base; + webVaultUrl = urlsObj.base; } else if (urlsObj.webVault != null) { - webUrl = urlsObj.webVault; + webVaultUrl = urlsObj.webVault; } } - shell.openExternal(webUrl); + return webVaultUrl; } + } diff --git a/src/main/menu.updater.ts b/src/main/menu.updater.ts new file mode 100644 index 00000000..f21495c4 --- /dev/null +++ b/src/main/menu.updater.ts @@ -0,0 +1,12 @@ +export class MenuUpdateRequest { + hideChangeMasterPassword: boolean; + activeUserId: string; + accounts: { [userId: string]: MenuAccount }; +} + +export class MenuAccount { + isAuthenticated: boolean; + isLocked: boolean; + userId: string; + email: string; +} diff --git a/src/main/menu.view.ts b/src/main/menu.view.ts new file mode 100644 index 00000000..55c731a6 --- /dev/null +++ b/src/main/menu.view.ts @@ -0,0 +1,140 @@ +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { IMenubarMenu } from './menubar'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class ViewMenu implements IMenubarMenu { + readonly id: 'viewMenu'; + + get label(): string { + return this.localize('view'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.searchVault, + this.separator, + this.passwordGenerator, + this.passwordHistory, + this.separator, + this.zoomIn, + this.zoomOut, + this.resetZoom, + this.separator, + this.toggleFullscreen, + this.separator, + this.reload, + this.toggleDevTools, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) + { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated; + } + + private get searchVault(): MenuItemConstructorOptions { + return { + id: 'searchVault', + label: this.localize('searchVault'), + click: () => this.sendMessage('focusSearch'), + accelerator: 'CmdOrCtrl+F', + enabled: this._isAuthenticated, + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get passwordGenerator(): MenuItemConstructorOptions { + return { + id: 'passwordGenerator', + label: this.localize('passwordGenerator'), + click: () => this.sendMessage('openPasswordGenerator'), + accelerator: 'CmdOrCtrl+G', + enabled: this._isAuthenticated, + }; + } + + private get passwordHistory(): MenuItemConstructorOptions { + return { + id: 'passwordHistory', + label: this.localize('passwordHistory'), + click: () => this.sendMessage('openPasswordHistory'), + enabled: this._isAuthenticated, + }; + } + + private get zoomIn(): MenuItemConstructorOptions { + return { + id: 'zoomIn', + label: this.localize('zoomIn'), + role: 'zoomIn', + accelerator: 'CmdOrCtrl+=', + }; + } + + private get zoomOut(): MenuItemConstructorOptions { + return { + id: 'zoomOut', + label: this.localize('zoomOut'), + role: 'zoomOut', + accelerator: 'CmdOrCtrl+-', + }; + } + + private get resetZoom(): MenuItemConstructorOptions { + return { + id: 'resetZoom', + label: this.localize('resetZoom'), + role: 'resetZoom', + accelerator: 'CmdOrCtrl+0', + }; + } + + private get toggleFullscreen(): MenuItemConstructorOptions { + return { + id: 'toggleFullScreen', + label: this.localize('toggleFullScreen'), + role: 'togglefullscreen', + }; + } + + private get reload(): MenuItemConstructorOptions { + return { + id: 'reload', + label: this.localize('reload'), + role: 'forceReload', + }; + } + + private get toggleDevTools(): MenuItemConstructorOptions { + return { + id: 'toggleDevTools', + label: this.localize('toggleDevTools'), + role: 'toggleDevTools', + accelerator: 'F12', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.window.ts b/src/main/menu.window.ts new file mode 100644 index 00000000..d27c78dd --- /dev/null +++ b/src/main/menu.window.ts @@ -0,0 +1,111 @@ +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { isMacAppStore } from 'jslib-electron/utils'; +import { WindowMain } from 'jslib-electron/window.main'; + +import { IMenubarMenu } from './menubar'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class WindowMenu implements IMenubarMenu { + readonly id: string; + + get label(): string { + return this.localize('window'); + } + + get items(): MenuItemConstructorOptions[] { + return [ + this.minimize, + this.hideToMenu, + this.alwaysOnTop, + this.zoom, + this.separator, + this.bringAllToFront, + this.close, + ]; + } + + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _window: WindowMain; + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + windowMain: WindowMain, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._window = windowMain; + } + + private get minimize(): MenuItemConstructorOptions { + return { + id: 'minimize', + label: this.localize('minimize'), + role: 'minimize', + visible: isMacAppStore(), + }; + } + + private get hideToMenu(): MenuItemConstructorOptions { + return { + id: 'hideToMenu', + label: this.localize(isMacAppStore() ? 'hideToMenuBar' : 'hideToTray'), + click: () => this.sendMessage('hideToTray'), + accelerator: 'CmdOrCtrl+Shift+M', + }; + } + + private get alwaysOnTop(): MenuItemConstructorOptions { + return { + id: 'alwaysOnTop', + label: this.localize('alwaysOnTop'), + type: 'checkbox', + checked: this._window.win.isAlwaysOnTop(), + click: () => this._window.toggleAlwaysOnTop(), + accelerator: 'CmdOrCtrl+Shift+T', + }; + } + + private get zoom(): MenuItemConstructorOptions { + return { + id: 'zoom', + label: this.localize('zoom'), + role: 'zoom', + visible: isMacAppStore(), + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get bringAllToFront(): MenuItemConstructorOptions { + return { + id: 'bringAllToFront', + label: this.localize('bringAllToFront'), + role: 'front', + visible: isMacAppStore(), + }; + } + + private get close(): MenuItemConstructorOptions { + return { + id: 'close', + label: this.localize('close'), + role: 'close', + visible: isMacAppStore(), + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menubar.ts b/src/main/menubar.ts new file mode 100644 index 00000000..e59b6213 --- /dev/null +++ b/src/main/menubar.ts @@ -0,0 +1,105 @@ +import { + Menu, + MenuItemConstructorOptions, +} from 'electron'; + +import { AboutMenu } from './menu.about'; +import { AccountMenu } from './menu.account'; +import { BitwardenMenu } from './menu.bitwarden'; +import { EditMenu } from './menu.edit'; +import { FileMenu } from './menu.file'; +import { HelpMenu } from './menu.help'; +import { MenuUpdateRequest } from './menu.updater'; +import { ViewMenu } from './menu.view'; +import { WindowMenu } from './menu.window'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; + +import { UpdaterMain } from 'jslib-electron/updater.main'; +import { WindowMain } from 'jslib-electron/window.main'; + +export interface IMenubarMenu { + id: string; + label: string; + visible?: boolean; // Assumes true if null + items: MenuItemConstructorOptions[]; +} + +export class Menubar { + private readonly items: IMenubarMenu[]; + + get menu(): Menu { + const template: MenuItemConstructorOptions[] = []; + if (this.items != null) { + this.items.forEach((item: IMenubarMenu) => { + if (item != null) { + template.push({ + id: item.id, + label: item.label, + submenu: item.items, + visible: item.visible ?? true, + }); + } + }); + } + return Menu.buildFromTemplate(template); + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + updaterMain: UpdaterMain, + windowMain: WindowMain, + webVaultUrl: string, + appVersion: string, + updateRequest?: MenuUpdateRequest, + ) { + this.items = [ + new BitwardenMenu( + i18nService, + messagingService, + updaterMain, + windowMain.win, + updateRequest?.accounts + ), + new FileMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId]?.isLocked ?? true, + ), + new EditMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId]?.isLocked ?? true, + ), + new ViewMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId]?.isLocked ?? true, + ), + new AccountMenu( + i18nService, + messagingService, + webVaultUrl, + windowMain.win, + updateRequest?.accounts[updateRequest?.activeUserId]?.isLocked ?? true, + ), + new WindowMenu( + i18nService, + messagingService, + windowMain, + ), + new AboutMenu( + i18nService, + appVersion, + windowMain.win, + updaterMain, + ), + new HelpMenu( + i18nService, + webVaultUrl, + ), + ]; + } +} diff --git a/src/main/messaging.main.ts b/src/main/messaging.main.ts index ef597019..cb8d7c64 100644 --- a/src/main/messaging.main.ts +++ b/src/main/messaging.main.ts @@ -4,24 +4,24 @@ import * as path from 'path'; import { Main } from '../main'; -import { ElectronConstants } from 'jslib-electron/electronConstants'; +import { StateService } from 'jslib-common/abstractions/state.service'; -import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { MenuUpdateRequest } from './menu.updater'; const SyncInterval = 5 * 60 * 1000; // 5 minutes export class MessagingMain { private syncTimeout: NodeJS.Timer; - constructor(private main: Main, private storageService: StorageService) { } + constructor(private main: Main, private stateService: StateService) { } init() { this.scheduleNextSync(); if (process.platform === 'linux') { - this.storageService.save(ElectronConstants.openAtLogin, fs.existsSync(this.linuxStartupFile())); + this.stateService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile())); } else { const loginSettings = app.getLoginItemSettings(); - this.storageService.save(ElectronConstants.openAtLogin, loginSettings.openAtLogin); + this.stateService.setOpenAtLogin(loginSettings.openAtLogin); } ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message)); } @@ -32,12 +32,11 @@ export class MessagingMain { this.scheduleNextSync(); break; case 'updateAppMenu': - this.main.menuMain.updateApplicationMenuState(message.isAuthenticated, message.isLocked, - message.hideChangeMasterPass); - this.updateTrayMenu(message.isAuthenticated, message.isLocked); + this.main.menuMain.updateApplicationMenuState(message.updateRequest); + this.updateTrayMenu(message.updateRequest); break; case 'minimizeOnCopy': - this.storageService.get(ElectronConstants.minimizeOnCopyToClipboardKey).then( + this.stateService.getMinimizeOnCopyToClipboard().then( shouldMinimize => { if (shouldMinimize && this.main.windowMain.win !== null) { this.main.windowMain.win.minimize(); @@ -93,13 +92,14 @@ export class MessagingMain { }, SyncInterval); } - private updateTrayMenu(isAuthenticated: boolean, isLocked: boolean) { - if (this.main.trayMain == null || this.main.trayMain.contextMenu == null) { + private updateTrayMenu(updateRequest: MenuUpdateRequest) { + if (this.main.trayMain == null || this.main.trayMain.contextMenu == null || updateRequest?.activeUserId == null) { return; } const lockNowTrayMenuItem = this.main.trayMain.contextMenu.getMenuItemById('lockNow'); - if (lockNowTrayMenuItem != null) { - lockNowTrayMenuItem.enabled = isAuthenticated && !isLocked; + const activeAccount = updateRequest.accounts[updateRequest.activeUserId]; + if (lockNowTrayMenuItem != null && activeAccount != null) { + lockNowTrayMenuItem.enabled = activeAccount.isAuthenticated && !activeAccount.isLocked; } this.main.trayMain.updateContextMenu(); } diff --git a/src/main/powerMonitor.main.ts b/src/main/powerMonitor.main.ts index 50f247cf..78f79134 100644 --- a/src/main/powerMonitor.main.ts +++ b/src/main/powerMonitor.main.ts @@ -1,7 +1,5 @@ import { powerMonitor } from 'electron'; -import { ConstantsService } from 'jslib-common/services/constants.service'; - import { isSnapStore } from 'jslib-electron/utils'; import { Main } from '../main'; @@ -60,8 +58,8 @@ export class PowerMonitorMain { } private async getVaultTimeoutOptions(): Promise<[number, string]> { - const timeout = await this.main.storageService.get(ConstantsService.vaultTimeoutKey); - const action = await this.main.storageService.get(ConstantsService.vaultTimeoutActionKey); + const timeout = await this.main.stateService.getVaultTimeout(); + const action = await this.main.stateService.getVaultTimeoutAction(); return [timeout, action]; } } diff --git a/src/scss/header.scss b/src/scss/header.scss new file mode 100644 index 00000000..7a2b8690 --- /dev/null +++ b/src/scss/header.scss @@ -0,0 +1,171 @@ +.header { + -webkit-app-region: drag; + min-height: 44px; + max-height: 44px; + border-bottom: 1px solid #000000; + display: grid; + grid-template-columns: 25% 1fr 25%; + grid-column-gap: 5px; + justify-items: center; + align-items: center; + + @include themify($themes) { + background-color: themed('headerBackgroundColor'); + border-bottom-color: themed('headerBorderColor'); + } + + app-search { + grid-column-start: 2; + width: 100%; + } + + app-account-switcher { + justify-self: end; + height: 100%; + } + + .search { + padding: 0 7px; + width: 100%; + text-align: left; + position: relative; + + .fa { + position: absolute; + top: 7px; + left: 15px; + + @include themify($themes) { + color: themed('headerInputPlaceholderColor'); + } + } + + input { + width: 100%; + margin: 0; + border: none; + padding: 5px 10px 5px 30px; + border-radius: $border-radius; + + @include themify($themes) { + background-color: themed('headerInputBackgroundColor'); + color: themed('headerInputColor'); + } + + &:focus { + border-radius: $border-radius; + outline: none; + + @include themify($themes) { + background-color: themed('headerInputBackgroundFocusColor'); + } + } + + &::-webkit-input-placeholder { + @include themify($themes) { + color: themed('headerInputPlaceholderColor'); + } + } + } + } +} + +.account-switcher { + display: grid; + grid-template-columns: auto 1fr auto; + grid-column-gap: 5px; + align-items: center; + justify-items: center; + padding: 0 10px; + height: 100%; + user-select: none; + + @include themify($themes) { + color: themed('accountSwitcherTextColor'); + } + + img { + display: block; + } + + span { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &:hover { + @include themify($themes) { + background-color: themed('headerBorderColor'); + color: themed('accountSwitcherTextColor'); + } + } +} + +.account-switcher-dropdown { + @include themify($themes) { + background-color: themed('accountSwitcherBackgroundColor'); + } + margin-right: 5px; + margin-top: 1px; // Fix for border-bottom in header + width: 100%; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2); + border-radius: $border-radius; + + a { + padding: 5px 10px; + display: block; + + @include themify($themes) { + color: themed('textColor'); + } + + &:hover { + @include themify($themes) { + background-color: themed('backgroundColorAlt2'); + } + } + } + + .accounts { + padding: 4px 0; + + .account { + display: grid; + grid-column-gap: 5px; + grid-template: + [row1-start] "email status" [row1-end] + [row2-start] "server server" [row2-end] + / 1fr auto; + align-items: baseline; + + .server { + font-size: $font-size-small; + } + + .email { + font-size: $font-size-large + } + + .status { + font-style: italic; + grid-area: status; + } + } + } + + .border { + @include themify($themes) { + background: themed('borderColor'); + } + left: 10px; + width: calc(100% - 20px); + height: 1px; + position: relative; + } + + .add { + margin: 4px 0; + } +} diff --git a/src/scss/misc.scss b/src/scss/misc.scss index 9e9b2ab9..854bea90 100644 --- a/src/scss/misc.scss +++ b/src/scss/misc.scss @@ -435,3 +435,7 @@ app-root > #loading { margin-bottom: 15px; } } + +.rounded-circle { + border-radius:50% !important; +} diff --git a/src/scss/pages.scss b/src/scss/pages.scss index 79250801..627a73fe 100644 --- a/src/scss/pages.scss +++ b/src/scss/pages.scss @@ -8,12 +8,12 @@ @media (min-height: 500px) { height: calc(100% + 50px); - margin-top: -50px; + padding-bottom: 50px; } @media (min-height: 800px) { height: calc(100% + 300px); - margin-top: -300px; + padding-bottom: 300px; } img { @@ -124,6 +124,27 @@ } } +.login-header { + padding: 1em; + font-size: 1.2em; + .environment-urls-settings-icon { + @include themify($themes) { + color: themed('mutedColor'); + } + + span { + visibility: hidden; + } + + &:hover, &:focus { + text-decoration: none; + + @include themify($themes) { + color: themed('primaryColor'); + } + } + } +} #sso-page { .content { width: 300px; diff --git a/src/scss/styles.scss b/src/scss/styles.scss index ec5ac20d..8c4bc203 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -1,4 +1,5 @@ -@import "../../jslib/angular/src/scss/webfonts.css"; +@import "../../jslib/angular/src/scss/webfonts.css"; +@import '~@angular/cdk/overlay-prebuilt.css'; @import "variables.scss"; @import "base.scss"; @import "grid.scss"; @@ -11,3 +12,4 @@ @import "modal.scss"; @import "plugins.scss"; @import "environment.scss"; +@import "header.scss"; diff --git a/src/scss/variables.scss b/src/scss/variables.scss index b5415db9..c2df7bf9 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -88,6 +88,8 @@ $themes: ( passwordSpecialColor: #c40800, calloutBorderColor: $border-color-dark, calloutBackgroundColor: $background-color, + acccountSwitcherBackgroundColor: $background-color, + accountSwitcherTextColor: #ffffff, ), dark: ( textColor: #ffffff, @@ -138,6 +140,8 @@ $themes: ( passwordSpecialColor: #ff7c70, calloutBorderColor: #2f2f2f, calloutBackgroundColor: #363636, + accountSwitcherBackgroundColor: #2f2f2f, + accountSwitcherTextColor: #ffffff, ), nord: ( textColor: $nord5, @@ -188,6 +192,8 @@ $themes: ( passwordSpecialColor: $nord12, calloutBorderColor: $nord1, calloutBackgroundColor: $nord2, + accountSwitcherBackgroundColor: $nord0, + accountSwitcherTextColor: $nord5, ), ); diff --git a/src/scss/vault.scss b/src/scss/vault.scss index ccbe7441..c9f8f539 100644 --- a/src/scss/vault.scss +++ b/src/scss/vault.scss @@ -1,5 +1,16 @@ @import "variables.scss"; +app-root { + display: flex; + flex-flow: column; + height: 100%; +} + +#container { + height: 100%; + min-height: 0; +} + .vault { height: 100%; display: flex; @@ -356,67 +367,6 @@ } } - .header { - min-height: 44px; - max-height: 44px; - flex: 0 0 auto; - border-bottom: 1px solid #000000; - display: flex; - align-items: center; - - @include themify($themes) { - background-color: themed('headerBackgroundColor'); - border-bottom-color: themed('headerBorderColor'); - } - - &.header-search { - .search { - padding: 0 7px; - width: 100%; - text-align: left; - position: relative; - - .fa { - position: absolute; - top: 7px; - left: 15px; - - @include themify($themes) { - color: themed('headerInputPlaceholderColor'); - } - } - - input { - width: 100%; - margin: 0; - border: none; - padding: 5px 10px 5px 30px; - border-radius: $border-radius; - - @include themify($themes) { - background-color: themed('headerInputBackgroundColor'); - color: themed('headerInputColor'); - } - - &:focus { - border-radius: $border-radius; - outline: none; - - @include themify($themes) { - background-color: themed('headerInputBackgroundFocusColor'); - } - } - - &::-webkit-input-placeholder { - @include themify($themes) { - color: themed('headerInputPlaceholderColor'); - } - } - } - } - } - } - .content { flex: 1 1 auto; position: relative; diff --git a/src/services/loginGuard.service.ts b/src/services/loginGuard.service.ts new file mode 100644 index 00000000..8cd380bb --- /dev/null +++ b/src/services/loginGuard.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; + +const maxAllowedAccounts = 5; + +@Injectable() +export class LoginGuardService implements CanActivate { + protected homepage = 'vault'; + constructor(private stateService: StateService, private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService) { } + + async canActivate() { + const accounts = this.stateService.accounts.getValue(); + if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { + this.platformUtilsService.showToast('error', null, this.i18nService.t('accountLimitReached')); + return false; + } + + return true; + } +} diff --git a/src/services/nativeMessaging.service.ts b/src/services/nativeMessaging.service.ts index 66808517..50879121 100644 --- a/src/services/nativeMessaging.service.ts +++ b/src/services/nativeMessaging.service.ts @@ -8,13 +8,14 @@ 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 { StorageService } from 'jslib-common/abstractions/storage.service'; -import { UserService } from 'jslib-common/abstractions/user.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { Utils } from 'jslib-common/misc/utils'; + import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey'; -import { ElectronConstants } from 'jslib-electron/electronConstants'; + +import { KeySuffixOptions } from 'jslib-common/enums/keySuffixOptions'; const MessageValidTimeout = 10 * 1000; const EncryptionAlgorithm = 'sha1'; @@ -25,9 +26,9 @@ export class NativeMessagingService { constructor(private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, private logService: LogService, - private i18nService: I18nService, private userService: UserService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService) { - ipcRenderer.on('nativeMessaging', async (event: any, message: any) => { + private i18nService: I18nService, private messagingService: MessagingService, + private vaultTimeoutService: VaultTimeoutService, private stateService: StateService) { + ipcRenderer.on('nativeMessaging', async (_event: any, message: any) => { this.messageHandler(message); }); } @@ -41,15 +42,15 @@ export class NativeMessagingService { const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey).buffer; // Valudate the UserId to ensure we are logged into the same account. - if (rawMessage.userId !== await this.userService.getUserId()) { + if (rawMessage.userId !== await this.stateService.getUserId()) { ipcRenderer.send('nativeMessagingReply', {command: 'wrongUserId', appId: appId}); return; } - if (await this.storageService.get(ElectronConstants.enableBrowserIntegrationFingerprint)) { + if (await this.stateService.getEnableBrowserIntegrationFingerprint()) { ipcRenderer.send('nativeMessagingReply', {command: 'verifyFingerprint', appId: appId}); - const fingerprint = (await this.cryptoService.getFingerprint(await this.userService.getUserId(), remotePublicKey)).join(' '); + const fingerprint = (await this.cryptoService.getFingerprint(await this.stateService.getUserId(), remotePublicKey)).join(' '); this.messagingService.send('setFocus'); @@ -104,7 +105,7 @@ export class NativeMessagingService { }); } - const keyB64 = await (await this.cryptoService.getKeyFromStorage('biometric')).keyB64; + const keyB64 = (await this.cryptoService.getKeyFromStorage(KeySuffixOptions.Biometric)).keyB64; if (keyB64 != null) { this.send({ command: 'biometricUnlock', response: 'unlocked', keyB64: keyB64 }, appId);