1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 22:33:35 +00:00
Files
browser/apps/web/src/app/app.component.ts
Jake Fink 43f5423e78 [PM-12606] Move Vault Timeout and Vault Timeout Settings to KM (#13405)
* move vault timeout and vault timeout settings to km

* move browser vault timeout service to km

* fix cli import

* fix imports

* fix some relative imports

* use relative imports within common

* fix imports

* fix new imports

* Fix new imports

* fix spec imports
2025-02-28 09:55:03 -05:00

372 lines
15 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { PolicyListService } from "./admin-console/core/policy-list.service";
import {
DisableSendPolicy,
MasterPasswordPolicy,
PasswordGeneratorPolicy,
PersonalOwnershipPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
RemoveUnlockWithPinPolicy,
} from "./admin-console/organizations/policies";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
@Component({
selector: "app-root",
templateUrl: "app.component.html",
})
export class AppComponent implements OnDestroy, OnInit {
private lastActivity: Date = null;
private idleTimer: number = null;
private isIdle = false;
private destroy$ = new Subject<void>();
loading = false;
constructor(
@Inject(DOCUMENT) private document: Document,
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 platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private keyService: KeyService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private stateService: StateService,
private eventUploadService: EventUploadService,
private policyService: InternalPolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService,
protected configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private organizationService: InternalOrganizationServiceAbstraction,
private accountService: AccountService,
private apiService: ApiService,
private appIdService: AppIdService,
private processReloadService: ProcessReloadServiceAbstraction,
) {}
ngOnInit() {
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
this.document.documentElement.lang = locale;
});
this.ngZone.runOutsideAngular(() => {
window.onmousemove = () => this.recordActivity();
window.onmousedown = () => this.recordActivity();
window.ontouchstart = () => this.recordActivity();
window.onclick = () => 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 "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(["/"]);
break;
case "logout":
// note: the message.logoutReason isn't consumed anymore because of the process reload clearing any toasts.
await this.logOut(message.redirect);
break;
case "lockVault":
await this.vaultTimeoutService.lock();
break;
case "locked":
await this.processReloadService.startProcessReload(this.authService);
break;
case "lockedUrl":
break;
case "syncStarted":
break;
case "syncCompleted":
if (message.successfully) {
await this.configService.ensureConfigFetched();
}
break;
case "upgradeOrganization": {
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "upgradeOrganization" },
content: { key: "upgradeOrganizationDesc" },
acceptButtonText: { key: "upgradeOrganization" },
type: "info",
});
if (upgradeConfirmed) {
// 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([
"organizations",
message.organizationId,
"billing",
"subscription",
]);
}
break;
}
case "premiumRequired": {
const premiumConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "premiumRequired" },
content: { key: "premiumRequiredDesc" },
acceptButtonText: { key: "upgrade" },
type: "success",
});
if (premiumConfirmed) {
// 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
await this.router.navigate(["settings/subscription/premium"]);
}
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 "showToast":
this.toastService._showToast(message);
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 "syncOrganizationStatusChanged": {
const { organizationId, enabled } = message;
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const organizations = await firstValueFrom(
this.organizationService.organizations$(userId),
);
const organization = organizations.find((org) => org.id === organizationId);
if (organization) {
const updatedOrganization = {
...organization,
enabled: enabled,
};
await this.organizationService.upsert(updatedOrganization, userId);
}
break;
}
case "syncOrganizationCollectionSettingChanged": {
const { organizationId, limitCollectionCreation, limitCollectionDeletion } = message;
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const organizations = await firstValueFrom(
this.organizationService.organizations$(userId),
);
const organization = organizations.find((org) => org.id === organizationId);
if (organization) {
await this.organizationService.upsert(
{
...organization,
limitCollectionCreation: limitCollectionCreation,
limitCollectionDeletion: limitCollectionDeletion,
},
userId,
);
}
break;
}
default:
break;
}
});
});
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll(".modal"));
for (const modal of modals) {
(jq(modal) as any).modal("hide");
}
}
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new RemoveUnlockWithPinPolicy(),
new ResetPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new PersonalOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
]);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.complete();
}
private async logOut(redirect = true) {
// Ensure the loading state is applied before proceeding to avoid a flash
// of the login screen before the process reload fires.
this.ngZone.run(() => {
this.loading = true;
document.body.classList.add("layout_frontend");
});
// Note: we don't display a toast logout reason anymore as the process reload
// will prevent any toasts from being displayed long enough to be read
await this.eventUploadService.uploadEvents();
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userId).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.");
},
}),
),
);
await Promise.all([
this.keyService.clearKeys(),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.biometricStateService.logout(userId),
]);
await this.stateEventRunnerService.handleEvent("logout", userId);
await this.searchService.clearIndex();
this.authService.logOut(async () => {
await this.stateService.clean({ userId: userId });
await this.accountService.clean(userId);
await this.accountService.switchAccount(null);
await logoutPromise;
if (redirect) {
await this.router.navigate(["/"]);
}
await this.processReloadService.startProcessReload(this.authService);
// Normally we would need to reset the loading state to false or remove the layout_frontend
// class from the body here, but the process reload completely reloads the app so
// it handles it.
}, userId);
}
private async recordActivity() {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const now = new Date();
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
await this.accountService.setAccountActivity(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();
}
}
}