diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 75fccbd400d..40cfff10563 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -47,6 +47,7 @@ import { LockComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { SetPasswordComponent } from "../auth/set-password.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; +import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard"; import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component"; import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component"; import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component"; @@ -296,7 +297,7 @@ const routes: Routes = [ }, { path: "lock", - canActivate: [lockGuard()], + canActivate: [lockGuard(), reactiveUnlockVaultGuard], data: { pageIcon: Icons.LockIcon, pageTitle: { diff --git a/apps/desktop/src/autofill/guards/reactive-vault-guard.ts b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts new file mode 100644 index 00000000000..d16787ef46a --- /dev/null +++ b/apps/desktop/src/autofill/guards/reactive-vault-guard.ts @@ -0,0 +1,42 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { combineLatest, map, switchMap, distinctUntilChanged } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; + +/** + * Reactive route guard that redirects to the unlocked vault. + * Redirects to vault when unlocked in main window. + */ +export const reactiveUnlockVaultGuard: CanActivateFn = () => { + const router = inject(Router); + const authService = inject(AuthService); + const accountService = inject(AccountService); + const desktopSettingsService = inject(DesktopSettingsService); + + return combineLatest([accountService.activeAccount$, desktopSettingsService.modalMode$]).pipe( + switchMap(([account, modalMode]) => { + if (!account) { + return [true]; + } + + // Monitor when the vault has been unlocked. + return authService.authStatusFor$(account.id).pipe( + distinctUntilChanged(), + map((authStatus) => { + // If vault is unlocked and we're not in modal mode, redirect to vault + if (authStatus === AuthenticationStatus.Unlocked && !modalMode?.isModalModeActive) { + return router.createUrlTree(["/vault"]); + } + + // Otherwise keep user on the lock screen + return true; + }), + ); + }), + ); +}; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index f3342d6bc24..14d31cca99e 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -145,12 +145,15 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { } async closeModal(): Promise { - await this.router.navigate(["/"]); + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); } + + await this.router.navigate(["/"]); } private initializeCiphersObservable(rpid: string): void { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts index b2deef2ce1e..6a465136458 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.spec.ts @@ -69,8 +69,10 @@ describe("Fido2ExcludedCiphersComponent", () => { await component.closeModal(); + expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); - expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); + expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]); }); }); }); diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 70c45f3585a..450e2dc186b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -64,11 +64,17 @@ export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy { } async closeModal(): Promise { - await this.router.navigate(["/"]); + // Clean up modal state + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + // Clean up session state if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); } + + // Navigate away + await this.router.navigate(["/"]); } } diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index a91ee15b021..fc582798043 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -100,6 +100,9 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { } async closeModal(): Promise { + await this.desktopSettingsService.setModalMode(false); + await this.accountService.setShowHeader(true); + if (this.session) { this.session.notifyConfirmCreateCredential(false); this.session.confirmChosenCipher(null); @@ -127,8 +130,6 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { this.logService.error("Failed to load ciphers", error); }); }); - - await this.closeModal(); } private async validateCipherAccess(cipher: CipherView): Promise { diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index b087816746f..48cec5a764e 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -338,8 +338,8 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(existingCipherIds); + await this.accountService.setShowHeader(false); await this.showUi("/fido2-excluded", this.windowObject.windowXy, false); - await this.accountService.setShowHeader(true); } async ensureUnlockedVault(): Promise { @@ -362,6 +362,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.logService.warning("Error while waiting for vault to unlock", error); } + if (status2 === AuthenticationStatus.Unlocked) { + await this.router.navigate(["/"]); + } + if (status2 !== AuthenticationStatus.Unlocked) { await this.hideUi(); throw new Error("Vault is not unlocked");