diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 2f009c9c7a..67a761a32d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -156,6 +156,7 @@ export class AppComponent implements OnInit, OnDestroy { switch (message.command) { case "loggedIn": case "unlocked": + this.recordActivity(); this.notificationsService.updateConnection(); this.updateAppMenu(); this.systemService.cancelProcessReload(); @@ -198,6 +199,12 @@ export class AppComponent implements OnInit, OnDestroy { await this.systemService.clearPendingClipboard(); await this.systemService.startProcessReload(this.authService); break; + case "startProcessReload": + this.systemService.startProcessReload(this.authService); + break; + case "cancelProcessReload": + this.systemService.cancelProcessReload(); + break; case "reloadProcess": (window.location as any).reload(true); break; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f5a078cdcc..2be831073b 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -7,7 +7,7 @@ import { GlobalState } from "@bitwarden/common/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { StateService } from "@bitwarden/common/services/state.service"; -import { BiometricMain } from "./main/biometric/biometric.main"; +import { BiometricsService, BiometricsServiceAbstraction } from "./main/biometric/index"; import { DesktopCredentialStorageListener } from "./main/desktop-credential-storage-listener"; import { MenuMain } from "./main/menu/menu.main"; import { MessagingMain } from "./main/messaging.main"; @@ -37,7 +37,7 @@ export class Main { menuMain: MenuMain; powerMonitorMain: PowerMonitorMain; trayMain: TrayMain; - biometricMain: BiometricMain; + biometricsService: BiometricsServiceAbstraction; nativeMessagingMain: NativeMessagingMain; constructor() { @@ -98,40 +98,37 @@ export class Main { 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.stateService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, "bitwarden"); - this.menuMain = new MenuMain(this); - this.powerMonitorMain = new PowerMonitorMain(this); + this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { this.messagingMain.onMessage(message); }); + this.powerMonitorMain = new PowerMonitorMain(this.messagingService); + this.menuMain = new MenuMain( + this.i18nService, + this.messagingService, + this.stateService, + this.windowMain, + this.updaterMain + ); - if (process.platform === "win32") { - // eslint-disable-next-line - const BiometricWindowsMain = require("./main/biometric/biometric.windows.main").default; - this.biometricMain = new BiometricWindowsMain( - this.i18nService, - this.windowMain, - this.stateService, - this.logService - ); - } else if (process.platform === "darwin") { - // eslint-disable-next-line - const BiometricDarwinMain = require("./main/biometric/biometric.darwin.main").default; - this.biometricMain = new BiometricDarwinMain(this.i18nService, this.stateService); - } + this.biometricsService = new BiometricsService( + this.i18nService, + this.windowMain, + this.stateService, + this.logService, + this.messagingService, + process.platform + ); this.desktopCredentialStorageListener = new DesktopCredentialStorageListener( "Bitwarden", - this.biometricMain + this.biometricsService ); this.nativeMessagingMain = new NativeMessagingMain( @@ -163,8 +160,8 @@ export class Main { } this.powerMonitorMain.init(); await this.updaterMain.init(); - if (this.biometricMain != null) { - await this.biometricMain.init(); + if (this.biometricsService != null) { + await this.biometricsService.init(); } if ( diff --git a/apps/desktop/src/main/biometric/biometric.darwin.main.ts b/apps/desktop/src/main/biometric/biometric.darwin.main.ts index 49638425b7..2e360eb367 100644 --- a/apps/desktop/src/main/biometric/biometric.darwin.main.ts +++ b/apps/desktop/src/main/biometric/biometric.darwin.main.ts @@ -3,9 +3,9 @@ import { ipcMain, systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { BiometricMain } from "../biometric/biometric.main"; +import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction"; -export default class BiometricDarwinMain implements BiometricMain { +export default class BiometricDarwinMain implements BiometricsServiceAbstraction { constructor(private i18nservice: I18nService, private stateService: StateService) {} async init() { diff --git a/apps/desktop/src/main/biometric/biometric.windows.main.ts b/apps/desktop/src/main/biometric/biometric.windows.main.ts index 207e6c38b5..c2df381811 100644 --- a/apps/desktop/src/main/biometric/biometric.windows.main.ts +++ b/apps/desktop/src/main/biometric/biometric.windows.main.ts @@ -7,9 +7,9 @@ import { biometrics } from "@bitwarden/desktop-native"; import { WindowMain } from "../window.main"; -import { BiometricMain } from "./biometric.main"; +import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction"; -export default class BiometricWindowsMain implements BiometricMain { +export default class BiometricWindowsMain implements BiometricsServiceAbstraction { constructor( private i18nservice: I18nService, private windowMain: WindowMain, diff --git a/apps/desktop/src/main/biometric/biometric.main.ts b/apps/desktop/src/main/biometric/biometrics.service.abstraction.ts similarity index 70% rename from apps/desktop/src/main/biometric/biometric.main.ts rename to apps/desktop/src/main/biometric/biometrics.service.abstraction.ts index a2e3da4df8..8841185912 100644 --- a/apps/desktop/src/main/biometric/biometric.main.ts +++ b/apps/desktop/src/main/biometric/biometrics.service.abstraction.ts @@ -1,4 +1,4 @@ -export abstract class BiometricMain { +export abstract class BiometricsServiceAbstraction { init: () => Promise; supportsBiometric: () => Promise; authenticateBiometric: () => Promise; diff --git a/apps/desktop/src/main/biometric/biometrics.service.spec.ts b/apps/desktop/src/main/biometric/biometrics.service.spec.ts new file mode 100644 index 0000000000..80cc75318a --- /dev/null +++ b/apps/desktop/src/main/biometric/biometrics.service.spec.ts @@ -0,0 +1,81 @@ +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; + +import { WindowMain } from "../window.main"; + +import BiometricDarwinMain from "./biometric.darwin.main"; +import BiometricWindowsMain from "./biometric.windows.main"; +import { BiometricsService } from "./biometrics.service"; +import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction"; + +jest.mock("@bitwarden/desktop-native", () => { + return { + biometrics: jest.fn(), + passwords: jest.fn(), + }; +}); + +describe("biometrics tests", function () { + const i18nService = mock(); + const windowMain = mock(); + const stateService = mock(); + const logService = mock(); + const messagingService = mock(); + + it("Should call the platformspecific methods", () => { + const sut = new BiometricsService( + i18nService, + windowMain, + stateService, + logService, + messagingService, + process.platform + ); + + const mockService = mock(); + (sut as any).platformSpecificService = mockService; + sut.init(); + expect(mockService.init).toBeCalled(); + + sut.supportsBiometric(); + expect(mockService.supportsBiometric).toBeCalled(); + + sut.authenticateBiometric(); + expect(mockService.authenticateBiometric).toBeCalled(); + }); + + describe("Should create a platform specific service", function () { + it("Should create a biometrics service specific for Windows", () => { + const sut = new BiometricsService( + i18nService, + windowMain, + stateService, + logService, + messagingService, + "win32" + ); + + const internalService = (sut as any).platformSpecificService; + expect(internalService).not.toBeNull(); + expect(internalService).toBeInstanceOf(BiometricWindowsMain); + }); + + it("Should create a biometrics service specific for MacOs", () => { + const sut = new BiometricsService( + i18nService, + windowMain, + stateService, + logService, + messagingService, + "darwin" + ); + const internalService = (sut as any).platformSpecificService; + expect(internalService).not.toBeNull(); + expect(internalService).toBeInstanceOf(BiometricDarwinMain); + }); + }); +}); diff --git a/apps/desktop/src/main/biometric/biometrics.service.ts b/apps/desktop/src/main/biometric/biometrics.service.ts new file mode 100644 index 0000000000..b3179450ff --- /dev/null +++ b/apps/desktop/src/main/biometric/biometrics.service.ts @@ -0,0 +1,65 @@ +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; + +import { WindowMain } from "../window.main"; + +import { BiometricsServiceAbstraction } from "./biometrics.service.abstraction"; + +export class BiometricsService implements BiometricsServiceAbstraction { + private platformSpecificService: BiometricsServiceAbstraction; + + constructor( + private i18nService: I18nService, + private windowMain: WindowMain, + private stateService: StateService, + private logService: LogService, + private messagingService: MessagingService, + private platform: NodeJS.Platform + ) { + this.loadPlatformSpecificService(this.platform); + } + + private loadPlatformSpecificService(platform: NodeJS.Platform) { + if (platform === "win32") { + this.loadWindowsHelloService(); + } else if (platform === "darwin") { + this.loadMacOSService(); + } + } + + private loadWindowsHelloService() { + // eslint-disable-next-line + const BiometricWindowsMain = require("./biometric.windows.main").default; + this.platformSpecificService = new BiometricWindowsMain( + this.i18nService, + this.windowMain, + this.stateService, + this.logService + ); + } + + private loadMacOSService() { + // eslint-disable-next-line + const BiometricDarwinMain = require("./biometric.darwin.main").default; + this.platformSpecificService = new BiometricDarwinMain(this.i18nService, this.stateService); + } + + async init() { + return await this.platformSpecificService.init(); + } + + async supportsBiometric(): Promise { + return await this.platformSpecificService.supportsBiometric(); + } + + async authenticateBiometric(): Promise { + this.messagingService.send("cancelProcessReload"); + const response = await this.platformSpecificService.authenticateBiometric(); + if (!response) { + this.messagingService.send("startProcessReload"); + } + return response; + } +} diff --git a/apps/desktop/src/main/biometric/index.ts b/apps/desktop/src/main/biometric/index.ts new file mode 100644 index 0000000000..f5a594d966 --- /dev/null +++ b/apps/desktop/src/main/biometric/index.ts @@ -0,0 +1,2 @@ +export * from "./biometrics.service.abstraction"; +export * from "./biometrics.service"; diff --git a/apps/desktop/src/main/desktop-credential-storage-listener.ts b/apps/desktop/src/main/desktop-credential-storage-listener.ts index d31333f812..c3d923d58f 100644 --- a/apps/desktop/src/main/desktop-credential-storage-listener.ts +++ b/apps/desktop/src/main/desktop-credential-storage-listener.ts @@ -2,13 +2,16 @@ import { ipcMain } from "electron"; import { passwords } from "@bitwarden/desktop-native"; -import { BiometricMain } from "./biometric/biometric.main"; +import { BiometricsServiceAbstraction } from "./biometric/index"; const AuthRequiredSuffix = "_biometric"; const AuthenticatedActions = ["getPassword"]; export class DesktopCredentialStorageListener { - constructor(private serviceName: string, private biometricService: BiometricMain) {} + constructor( + private serviceName: string, + private biometricService: BiometricsServiceAbstraction + ) {} init() { ipcMain.handle("keytar", async (event: any, message: any) => { diff --git a/apps/desktop/src/main/menu/menu.main.ts b/apps/desktop/src/main/menu/menu.main.ts index 6a2bd3531b..4ae9669c48 100644 --- a/apps/desktop/src/main/menu/menu.main.ts +++ b/apps/desktop/src/main/menu/menu.main.ts @@ -1,8 +1,10 @@ import { app, Menu } from "electron"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { Main } from "../../main"; +import { UpdaterMain } from "../updater.main"; import { WindowMain } from "../window.main"; import { MenuUpdateRequest } from "./menu.updater"; @@ -11,13 +13,13 @@ import { Menubar } from "./menubar"; const cloudWebVaultUrl = "https://vault.bitwarden.com"; export class MenuMain { - private i18nService: I18nService; - private windowMain: WindowMain; - - constructor(private main: Main) { - this.i18nService = main.i18nService; - this.windowMain = main.windowMain; - } + constructor( + private i18nService: I18nService, + private messagingService: MessagingService, + private stateService: StateService, + private windowMain: WindowMain, + private updaterMain: UpdaterMain + ) {} async init() { this.initContextMenu(); @@ -31,9 +33,9 @@ export class MenuMain { private async setMenu(updateRequest?: MenuUpdateRequest) { Menu.setApplicationMenu( new Menubar( - this.main.i18nService, - this.main.messagingService, - this.main.updaterMain, + this.i18nService, + this.messagingService, + this.updaterMain, this.windowMain, await this.getWebVaultUrl(), app.getVersion(), @@ -44,7 +46,7 @@ export class MenuMain { private async getWebVaultUrl() { let webVaultUrl = cloudWebVaultUrl; - const urlsObj: any = await this.main.stateService.getEnvironmentUrls(); + const urlsObj = await this.stateService.getEnvironmentUrls(); if (urlsObj != null) { if (urlsObj.base != null) { webVaultUrl = urlsObj.base; diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 56e6139ccf..067a380ba0 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,6 @@ import { powerMonitor } from "electron"; -import { Main } from "../main"; +import { ElectronMainMessagingService } from "../services/electron-main-messaging.service"; import { isSnapStore } from "../utils"; // tslint:disable-next-line @@ -10,21 +10,21 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private main: Main) {} + constructor(private messagingService: ElectronMainMessagingService) {} init() { // ref: https://github.com/electron/electron/issues/13767 if (!isSnapStore()) { // System sleep powerMonitor.on("suspend", () => { - this.main.messagingService.send("systemSuspended"); + this.messagingService.send("systemSuspended"); }); } if (process.platform !== "linux") { // System locked powerMonitor.on("lock-screen", () => { - this.main.messagingService.send("systemLocked"); + this.messagingService.send("systemLocked"); }); } @@ -37,7 +37,7 @@ export class PowerMonitorMain { return; } - this.main.messagingService.send("systemIdle"); + this.messagingService.send("systemIdle"); } this.idle = idle; diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index 314ea076b2..088e76e2c5 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -16,11 +16,7 @@ export class UpdaterMain { private doingUpdateCheckWithFeedback = false; private canUpdate = false; - constructor( - private i18nService: I18nService, - private windowMain: WindowMain, - private projectName: string - ) { + constructor(private i18nService: I18nService, private windowMain: WindowMain) { autoUpdater.logger = log; const linuxCanUpdate = process.platform === "linux" && isAppImage(); @@ -49,8 +45,7 @@ export class UpdaterMain { const result = await dialog.showMessageBox(this.windowMain.win, { type: "info", - title: - this.i18nService.t(this.projectName) + " - " + this.i18nService.t("updateAvailable"), + title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("updateAvailable"), message: this.i18nService.t("updateAvailable"), detail: this.i18nService.t("updateAvailableDesc"), buttons: [this.i18nService.t("yes"), this.i18nService.t("no")], @@ -87,7 +82,7 @@ export class UpdaterMain { const result = await dialog.showMessageBox(this.windowMain.win, { type: "info", - title: this.i18nService.t(this.projectName) + " - " + this.i18nService.t("restartToUpdate"), + title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), message: this.i18nService.t("restartToUpdate"), detail: this.i18nService.t("restartToUpdateDesc", info.version), buttons: [this.i18nService.t("restart"), this.i18nService.t("later")], diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index b1d88a821f..e4489f7020 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -20,12 +20,12 @@ export class WindowMain { private windowStates: { [key: string]: WindowState } = {}; private enableAlwaysOnTop = false; + readonly defaultWidth = 950; + readonly defaultHeight = 600; + constructor( private stateService: StateService, private logService: LogService, - private hideTitleBar = false, - private defaultWidth = 950, - private defaultHeight = 600, private argvCallback: (argv: string[]) => void = null, private createWindowCallback: (win: BrowserWindow) => void ) {} @@ -118,7 +118,7 @@ export class WindowMain { y: this.windowStates[mainWindowSizeKey].y, title: app.name, icon: process.platform === "linux" ? path.join(__dirname, "/images/icon.png") : undefined, - titleBarStyle: this.hideTitleBar && process.platform === "darwin" ? "hiddenInset" : undefined, + titleBarStyle: process.platform === "darwin" ? "hiddenInset" : undefined, show: false, backgroundColor: "#fff", alwaysOnTop: this.enableAlwaysOnTop, diff --git a/libs/common/src/services/system.service.ts b/libs/common/src/services/system.service.ts index dc67a7f1e2..4e1dad9af5 100644 --- a/libs/common/src/services/system.service.ts +++ b/libs/common/src/services/system.service.ts @@ -45,34 +45,23 @@ export class SystemService implements SystemServiceAbstraction { } this.cancelProcessReload(); - this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 10000); - } - - private async inactiveMoreThanSeconds(seconds: number): Promise { - const lastActive = await this.stateService.getLastActive(); - if (lastActive != null) { - const diffMs = new Date().getTime() - lastActive; - return diffMs >= seconds * 1000; - } - return true; + await this.executeProcessReload(); } private async executeProcessReload() { - const accounts = await firstValueFrom(this.stateService.accounts$); - const doRefresh = - accounts == null || - Object.keys(accounts).length == 0 || - (await this.inactiveMoreThanSeconds(5)); - const biometricLockedFingerprintValidated = await this.stateService.getBiometricFingerprintValidated(); - if (doRefresh && !biometricLockedFingerprintValidated) { + if (!biometricLockedFingerprintValidated) { clearInterval(this.reloadInterval); this.reloadInterval = null; this.messagingService.send("reloadProcess"); if (this.reloadCallback != null) { await this.reloadCallback(); } + return; + } + if (this.reloadInterval == null) { + this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); } }