1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 22:44:11 +00:00
Files
browser/apps/desktop/src/app/app.component.ts
Oscar Hinton 9ea9c3a932 [PM-11290] Enable SDK (#11378)
Follow up PR to #10974, flips the compile time flags to enabled and includes some debug logic to detect if users encounter issues with the WASM bundle in preparation for active consumption of the SDK.
2024-10-07 13:56:02 +02:00

866 lines
36 KiB
TypeScript

import { DialogRef } from "@angular/cdk/dialog";
import {
Component,
NgZone,
OnDestroy,
OnInit,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router";
import { catchError, filter, firstValueFrom, map, of, Subject, takeUntil, timeout } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { LogoutReason } from "@bitwarden/auth/common";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricStateService } from "@bitwarden/key-management";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginApprovalComponent } from "../auth/login/login-approval.component";
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
import { flagEnabled } from "../platform/flags";
import { PremiumComponent } from "../vault/app/accounts/premium.component";
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
import { SettingsComponent } from "./accounts/settings.component";
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
import { GeneratorComponent } from "./tools/generator.component";
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
@Component({
selector: "app-root",
styles: [],
template: `
<ng-template #settings></ng-template>
<ng-template #premium></ng-template>
<ng-template #passwordHistory></ng-template>
<ng-template #appFolderAddEdit></ng-template>
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
<app-header></app-header>
<div id="container">
<div class="loading" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<router-outlet *ngIf="!loading"></router-outlet>
</div>
`,
})
export class AppComponent implements OnInit, OnDestroy {
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
passwordHistoryRef: ViewContainerRef;
@ViewChild("exportVault", { read: ViewContainerRef, static: true })
exportVaultModalRef: ViewContainerRef;
@ViewChild("appFolderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
loginApprovalModalRef: ViewContainerRef;
loading = false;
private lastActivity: Date = null;
private modal: ModalRef = null;
private idleTimer: number = null;
private isIdle = false;
private activeUserId: UserId = null;
private activeSimpleDialog: DialogRef<boolean> = null;
private destroy$ = new Subject<void>();
private accountCleanUpInProgress: { [userId: string]: boolean } = {};
constructor(
private masterPasswordService: MasterPasswordServiceAbstraction,
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
private toastService: ToastService,
private i18nService: I18nService,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private cryptoService: CryptoService,
private logService: LogService,
private messagingService: MessagingService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private platformUtilsService: PlatformUtilsService,
private systemService: SystemService,
private stateService: StateService,
private eventUploadService: EventUploadService,
private policyService: InternalPolicyService,
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService,
private configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private accountService: AccountService,
private sdkService: SdkService,
) {
if (flagEnabled("sdk")) {
// Warn if the SDK for some reason can't be initialized
this.sdkService.supported$
.pipe(
takeUntilDestroyed(),
catchError(() => {
return of(false);
}),
)
.subscribe((supported) => {
if (!supported) {
this.logService.debug("SDK is not supported");
this.sdkService.failedToInitialize().catch(this.logService.error);
} else {
this.logService.debug("SDK is supported");
}
});
}
}
ngOnInit() {
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
this.activeUserId = account?.id;
});
this.ngZone.runOutsideAngular(() => {
setTimeout(async () => {
await this.updateAppMenu();
}, 1000);
window.ontouchstart = () => this.recordActivity();
window.onmousedown = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
/// ############ DEPRECATED ############
/// Please do not use the AppComponent to send events between services.
///
/// Services that depends on other services, should do so through Dependency Injection
/// and subscribe to events through that service observable.
///
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "loggedIn":
case "unlocked":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.recordActivity();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.updateConnection();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateAppMenu();
this.systemService.cancelProcessReload();
break;
case "loggedOut":
this.modalService.closeAll();
if (message.userId == null || message.userId === this.activeUserId) {
await this.notificationsService.updateConnection();
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.systemService.startProcessReload(this.authService);
break;
case "authBlocked":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["login"]);
break;
case "logout":
this.loading = message.userId == null || message.userId === this.activeUserId;
await this.logOut(message.logoutReason, message.userId);
this.loading = false;
break;
case "lockVault":
await this.vaultTimeoutService.lock(message.userId);
break;
case "lockAllVaults": {
const currentUser = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a.id)),
);
const accounts = await firstValueFrom(this.accountService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
continue;
}
await this.vaultTimeoutService.lock(account);
}
break;
}
case "locked":
this.modalService.closeAll();
if (
message.userId == null ||
message.userId ===
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))))
) {
await this.router.navigate(["lock"]);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.updateConnection();
await this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.systemService.startProcessReload(this.authService);
break;
case "startProcessReload":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.systemService.startProcessReload(this.authService);
break;
case "cancelProcessReload":
this.systemService.cancelProcessReload();
break;
case "reloadProcess":
ipc.platform.reloadProcess();
break;
case "syncStarted":
break;
case "syncCompleted":
if (message.successfully) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateAppMenu();
await this.configService.ensureConfigFetched();
}
break;
case "openSettings":
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
break;
case "openPremium":
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
break;
case "showFingerprintPhrase": {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const publicKey = await firstValueFrom(this.cryptoService.userPublicKey$(activeUserId));
const fingerprint = await this.cryptoService.getFingerprint(activeUserId, publicKey);
const dialogRef = FingerprintDialogComponent.open(this.dialogService, { fingerprint });
await firstValueFrom(dialogRef.closed);
break;
}
case "deleteAccount":
DeleteAccountComponent.open(this.dialogService);
break;
case "openPasswordHistory":
await this.openModal<PasswordGeneratorHistoryComponent>(
PasswordGeneratorHistoryComponent,
this.passwordHistoryRef,
);
break;
case "showToast":
this.toastService._showToast(message);
break;
case "copiedToClipboard":
if (!message.clearing) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.systemService.clearClipboard(message.clipboardValue, message.clearMs);
}
break;
case "ssoCallback": {
const queryParams = {
code: message.code,
state: message.state,
redirectUri: message.redirectUri ?? "bitwarden://sso-callback",
};
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["sso"], {
queryParams: queryParams,
});
break;
}
case "premiumRequired": {
const premiumConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "premiumRequired" },
content: { key: "premiumRequiredDesc" },
acceptButtonText: { key: "learnMore" },
type: "success",
});
if (premiumConfirmed) {
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
}
break;
}
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "emailVerificationRequired" },
content: { key: "emailVerificationRequiredDesc" },
acceptButtonText: { key: "learnMore" },
type: "info",
});
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/create-bitwarden-account/",
);
}
break;
}
case "syncVault":
try {
await this.syncService.fullSync(true, true);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("syncingComplete"),
);
} catch {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("syncingFailed"),
);
}
break;
case "checkSyncVault":
try {
const lastSync = await this.syncService.getLastSync();
let lastSyncAgo = SyncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= SyncInterval) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send("scheduleNextSync");
break;
case "importVault":
await this.dialogService.open(ImportDesktopComponent);
break;
case "exportVault":
await this.dialogService.open(ExportDesktopComponent);
break;
case "newLogin":
this.routeToVault("add", CipherType.Login);
break;
case "newCard":
this.routeToVault("add", CipherType.Card);
break;
case "newIdentity":
this.routeToVault("add", CipherType.Identity);
break;
case "newSecureNote":
this.routeToVault("add", CipherType.SecureNote);
break;
default:
break;
case "newFolder":
await this.addFolder();
break;
case "openGenerator":
await this.openGenerator();
break;
case "convertAccountToKeyConnector":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/remove-password"]);
break;
case "switchAccount": {
// Clear sequentialized caches
clearCaches();
if (message.userId != null) {
await this.accountService.switchAccount(message.userId);
}
const locked =
(await this.authService.getAuthStatus(message.userId)) ===
AuthenticationStatus.Locked;
const forcedPasswordReset =
(await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(message.userId),
)) != ForceSetPasswordReason.None;
if (locked) {
this.modalService.closeAll();
await this.router.navigate(["lock"]);
} else if (forcedPasswordReset) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["update-temp-password"]);
} else {
this.messagingService.send("unlocked");
this.loading = true;
await this.syncService.fullSync(false);
this.loading = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["vault"]);
}
this.messagingService.send("finishSwitchAccount");
break;
}
case "systemSuspended":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnSleep);
break;
case "systemLocked":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnLocked);
break;
case "systemIdle":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle);
break;
case "openLoginApproval":
if (message.notificationId != null) {
this.dialogService.closeAll();
const dialogRef = LoginApprovalComponent.open(this.dialogService, {
notificationId: message.notificationId,
});
await firstValueFrom(dialogRef.closed);
}
break;
case "redrawMenu":
await this.updateAppMenu();
break;
case "deepLink":
this.processDeepLink(message.urlString);
break;
case "launchUri":
this.platformUtilsService.launchUri(message.url);
break;
}
});
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async addFolder() {
this.modalService.closeAll();
const [modal, childComponent] = await this.modalService.openViewRef(
FolderAddEditComponent,
this.folderAddEditModalRef,
(comp) => (comp.folderId = null),
);
this.modal = modal;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
childComponent.onSavedFolder.subscribe(async () => {
this.modal.close();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(false);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
async openGenerator() {
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
FeatureFlag.GeneratorToolsModernization,
);
if (isGeneratorSwapEnabled) {
await this.dialogService.open(CredentialGeneratorComponent);
return;
}
this.modalService.closeAll();
[this.modal] = await this.modalService.openViewRef(
GeneratorComponent,
this.generatorModalRef,
(comp) => (comp.comingFromAddEdit = false),
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private async updateAppMenu() {
let updateRequest: MenuUpdateRequest;
const stateAccounts = await firstValueFrom(this.accountService.accounts$);
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
updateRequest = {
accounts: null,
activeUserId: null,
};
} else {
const accounts: { [userId: string]: MenuAccount } = {};
for (const i in stateAccounts) {
const userId = i as UserId;
if (
i != null &&
userId != null &&
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
) {
const availableTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
);
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
accounts[userId] = {
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
isLocked: authStatus === AuthenticationStatus.Locked,
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
email: stateAccounts[userId].email,
userId: userId,
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
};
}
}
updateRequest = {
accounts: accounts,
activeUserId: await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
),
};
}
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
}
private async displayLogoutReason(logoutReason: LogoutReason) {
let toastOptions: ToastOptions;
switch (logoutReason) {
case "invalidSecurityStamp":
case "sessionExpired": {
toastOptions = {
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
};
break;
}
// We don't expect these scenarios to be common, but we want the user to
// understand why they are being logged out before a process reload.
case "accessTokenUnableToBeDecrypted": {
// Don't create multiple dialogs if this fires multiple times
if (this.activeSimpleDialog) {
// Let the caller of this function listen for the dialog to close
return firstValueFrom(this.activeSimpleDialog.closed);
}
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
title: { key: "loggedOut" },
content: { key: "accessTokenUnableToBeDecrypted" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
await firstValueFrom(this.activeSimpleDialog.closed);
this.activeSimpleDialog = null;
break;
}
case "refreshTokenSecureStorageRetrievalFailure": {
// Don't create multiple dialogs if this fires multiple times
if (this.activeSimpleDialog) {
// Let the caller of this function listen for the dialog to close
return firstValueFrom(this.activeSimpleDialog.closed);
}
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
title: { key: "loggedOut" },
content: { key: "refreshTokenSecureStorageRetrievalFailure" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
await firstValueFrom(this.activeSimpleDialog.closed);
this.activeSimpleDialog = null;
break;
}
}
if (toastOptions) {
this.toastService.showToast(toastOptions);
}
}
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
// passing null-ish values to us.
private async logOut(logoutReason: LogoutReason, userId: UserId) {
await this.displayLogoutReason(logoutReason);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userBeingLoggedOut = userId ?? activeUserId;
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
// doesn't attempt to update a user that is being logged out as we will manually
// call updateAppMenu when the logout is complete.
this.startAccountCleanUp(userBeingLoggedOut);
const nextUpAccount =
activeUserId === userBeingLoggedOut
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
: null;
try {
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error(
"The logout process did not complete in a reasonable amount of time.",
);
},
}),
),
);
// Provide the userId of the user to upload events for
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut);
await this.folderService.clear(userBeingLoggedOut);
await this.collectionService.clear(userBeingLoggedOut);
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
await this.biometricStateService.logout(userBeingLoggedOut);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.accountService.clean(userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
} finally {
this.finishAccountCleanUp(userBeingLoggedOut);
}
// We only need to change the display at all if the account being looked at is the one
// being logged out. If it was a background account, no need to do anything.
if (userBeingLoggedOut === activeUserId) {
if (nextUpAccount != null) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
} else {
// We don't have another user to switch to, bring them to the login page so they
// can sign into a user.
await this.accountService.switchAccount(null);
void this.router.navigate(["login"]);
}
}
await this.updateAppMenu();
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
this.authService.logOut(async () => {}, userBeingLoggedOut);
}
private async recordActivity() {
if (this.activeUserId == null) {
return;
}
const now = new Date();
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
await this.accountService.setAccountActivity(this.activeUserId, now);
// Idle states
if (this.isIdle) {
this.isIdle = false;
this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
}
}, IdleTimeout);
}
private idleStateChanged() {
if (this.isIdle) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.disconnectFromInactivity();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.reconnectFromActivity();
}
}
private async openModal<T>(type: Type<T>, ref: ViewContainerRef) {
this.modalService.closeAll();
[this.modal] = await this.modalService.openViewRef(type, ref);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private routeToVault(action: string, cipherType: CipherType) {
if (!this.router.url.includes("vault")) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/vault"], {
queryParams: {
action: action,
addType: cipherType,
},
replaceUrl: true,
});
}
}
private async checkForSystemTimeout(timeout: VaultTimeout): Promise<void> {
const accounts = await firstValueFrom(this.accountService.accounts$);
for (const userId in accounts) {
if (userId == null) {
continue;
}
const options = await this.getVaultTimeoutOptions(userId);
if (options[0] === timeout) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
options[1] === "logOut"
? this.logOut("vaultTimeout", userId as UserId)
: await this.vaultTimeoutService.lock(userId);
}
}
}
private async getVaultTimeoutOptions(userId: string): Promise<[VaultTimeout, string]> {
const timeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const action = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
return [timeout, action];
}
// Mark an account's clean up as started
private startAccountCleanUp(userId: string): void {
this.accountCleanUpInProgress[userId] = true;
}
// Mark an account's clean up as finished
private finishAccountCleanUp(userId: string): void {
this.accountCleanUpInProgress[userId] = false;
}
// Check if an account's clean up is in progress
private isAccountCleanUpInProgress(userId: string): boolean {
return this.accountCleanUpInProgress[userId] === true;
}
// Process the sso callback links
private processDeepLink(urlString: string) {
const url = new URL(urlString);
const code = url.searchParams.get("code");
const receivedState = url.searchParams.get("state");
let message = "";
if (code === null) {
return;
}
if (urlString.indexOf("bitwarden://duo-callback") === 0) {
message = "duoCallback";
} else if (receivedState === null) {
return;
}
if (urlString.indexOf("bitwarden://import-callback-lp") === 0) {
message = "importCallbackLastPass";
} else if (urlString.indexOf("bitwarden://sso-callback") === 0) {
message = "ssoCallback";
}
this.messagingService.send(message, { code: code, state: receivedState });
}
}