1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 23:13:36 +00:00
Files
browser/apps/web/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

385 lines
16 KiB
TypeScript

import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs";
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 { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { 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 { 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 { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
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 { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricStateService } from "@bitwarden/key-management";
import { flagEnabled } from "../utils/flags";
import { PolicyListService } from "./admin-console/core/policy-list.service";
import {
DisableSendPolicy,
MasterPasswordPolicy,
PasswordGeneratorPolicy,
PersonalOwnershipPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
} 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>();
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 cryptoService: CryptoService,
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 logService: LogService,
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.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 "loggedIn":
// 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(false);
break;
case "loggedOut":
if (
message.userId == null ||
message.userId === (await firstValueFrom(this.accountService.activeAccount$))
) {
await this.notificationsService.updateConnection(false);
}
break;
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.notificationsService.updateConnection(false);
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(["/"]);
break;
case "logout":
await this.logOut(message.logoutReason, message.redirect);
break;
case "lockVault":
await this.vaultTimeoutService.lock();
break;
case "locked":
// 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(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(["lock"]);
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;
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 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 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;
}
default: {
toastOptions = {
variant: "info",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loggedOutDesc"),
};
break;
}
}
this.toastService.showToast(toastOptions);
}
private async logOut(logoutReason: LogoutReason, redirect = true) {
await this.displayLogoutReason(logoutReason);
await this.eventUploadService.uploadEvents();
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
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.cryptoService.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) {
// 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(["/"]);
}
}, 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();
}
}
}