mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-06 18:57:56 +01:00
c70a5aa024
* Use account service to track accounts and active account * Remove state service active account Observables. * Add email verified to account service * Do not store account info on logged out accounts * Add account activity tracking to account service * Use last account activity from account service * migrate or replicate account service data * Add `AccountActivityService` that handles storing account last active data * Move active and next active user to account service * Remove authenticated accounts from state object * Fold account activity into account service * Fix builds * Fix desktop app switch * Fix logging out non active user * Expand helper to handle new authenticated accounts location * Prefer view observable to tons of async pipes * Fix `npm run test:types` * Correct user activity sorting test * Be more precise about log out messaging * Fix dev compare errors All stored values are serializable, the next step wasn't necessary and was erroring on some types that lack `toString`. * If the account in unlocked on load of lock component, navigate away from lock screen * Handle no users case for auth service statuses * Specify account to switch to * Filter active account out of inactive accounts * Prefer constructor init * Improve comparator * Use helper methods internally * Fixup component tests * Clarify name * Ensure accounts object has only valid userIds * Capitalize const values * Prefer descriptive, single-responsibility guards * Update libs/common/src/state-migrations/migrate.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Fix merge * Add user Id validation activity for undefined was being set, which was resulting in requests for the auth status of `"undefined"` (string) userId, due to key enumeration. These changes stop that at both locations, as well as account add for good measure. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
244 lines
10 KiB
TypeScript
244 lines
10 KiB
TypeScript
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
|
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
|
|
|
import { BrowserApi } from "../platform/browser/browser-api";
|
|
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
|
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
|
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
|
|
|
import { routerTransition } from "./app-routing.animations";
|
|
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
|
|
|
|
@Component({
|
|
selector: "app-root",
|
|
styles: [],
|
|
animations: [routerTransition],
|
|
template: ` <div [@routerTransition]="getState(o)">
|
|
<router-outlet #o="outlet"></router-outlet>
|
|
</div>`,
|
|
})
|
|
export class AppComponent implements OnInit, OnDestroy {
|
|
private lastActivity: Date;
|
|
private activeUserId: UserId;
|
|
private recordActivitySubject = new Subject<void>();
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
constructor(
|
|
private authService: AuthService,
|
|
private i18nService: I18nService,
|
|
private router: Router,
|
|
private stateService: BrowserStateService,
|
|
private browserSendStateService: BrowserSendStateService,
|
|
private vaultBrowserStateService: VaultBrowserStateService,
|
|
private cipherService: CipherService,
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
private ngZone: NgZone,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private dialogService: DialogService,
|
|
private messageListener: MessageListener,
|
|
private toastService: ToastService,
|
|
private accountService: AccountService,
|
|
) {}
|
|
|
|
async ngOnInit() {
|
|
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
|
// Clear them aggressively to make sure this doesn't occur
|
|
await this.clearComponentStates();
|
|
|
|
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
|
this.activeUserId = account?.id;
|
|
});
|
|
|
|
this.authService.activeAccountStatus$
|
|
.pipe(
|
|
filter((status) => status === AuthenticationStatus.Unlocked),
|
|
concatMap(async () => {
|
|
await this.recordActivity();
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
|
|
this.ngZone.runOutsideAngular(() => {
|
|
window.onmousedown = () => this.recordActivity();
|
|
window.ontouchstart = () => this.recordActivity();
|
|
window.onclick = () => this.recordActivity();
|
|
window.onscroll = () => this.recordActivity();
|
|
window.onkeypress = () => this.recordActivity();
|
|
});
|
|
|
|
this.messageListener.allMessages$
|
|
.pipe(
|
|
tap((msg: any) => {
|
|
if (msg.command === "doneLoggingOut") {
|
|
this.authService.logOut(async () => {
|
|
if (msg.expired) {
|
|
this.toastService.showToast({
|
|
variant: "warning",
|
|
title: this.i18nService.t("loggedOut"),
|
|
message: this.i18nService.t("loginExpired"),
|
|
});
|
|
}
|
|
|
|
// 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(["home"]);
|
|
});
|
|
this.changeDetectorRef.detectChanges();
|
|
} else if (msg.command === "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(["home"]);
|
|
} else if (
|
|
msg.command === "locked" &&
|
|
(msg.userId == null || msg.userId == this.activeUserId)
|
|
) {
|
|
// 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"]);
|
|
} else if (msg.command === "showDialog") {
|
|
// 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.showDialog(msg);
|
|
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
|
|
// TODO: Should be refactored to live in another service.
|
|
// 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.showNativeMessagingFingerprintDialog(msg);
|
|
} else if (msg.command === "showToast") {
|
|
this.toastService._showToast(msg);
|
|
} else if (msg.command === "reloadProcess") {
|
|
const forceWindowReload =
|
|
this.platformUtilsService.isSafari() ||
|
|
this.platformUtilsService.isFirefox() ||
|
|
this.platformUtilsService.isOpera();
|
|
// Wait to make sure background has reloaded first.
|
|
window.setTimeout(
|
|
() => BrowserApi.reloadExtension(forceWindowReload ? window : null),
|
|
2000,
|
|
);
|
|
} else if (msg.command === "reloadPopup") {
|
|
// 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(["/"]);
|
|
} else if (msg.command === "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"]);
|
|
} else if (msg.command === "switchAccountFinish") {
|
|
// TODO: unset loading?
|
|
// this.loading = false;
|
|
} else if (msg.command == "update-temp-password") {
|
|
// 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"]);
|
|
}
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
|
|
// eslint-disable-next-line rxjs/no-async-subscribe
|
|
this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => {
|
|
if (event instanceof NavigationEnd) {
|
|
const url = event.urlAfterRedirects || event.url || "";
|
|
if (
|
|
url.startsWith("/tabs/") &&
|
|
(window as any).previousPopupUrl != null &&
|
|
(window as any).previousPopupUrl.startsWith("/tabs/")
|
|
) {
|
|
await this.clearComponentStates();
|
|
}
|
|
if (url.startsWith("/tabs/")) {
|
|
await this.cipherService.setAddEditCipherInfo(null);
|
|
}
|
|
(window as any).previousPopupUrl = url;
|
|
|
|
// Clear route direction after animation (400ms)
|
|
if ((window as any).routeDirection != null) {
|
|
window.setTimeout(() => {
|
|
(window as any).routeDirection = null;
|
|
}, 400);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
getState(outlet: RouterOutlet) {
|
|
if (outlet.activatedRouteData.state === "ciphers") {
|
|
const routeDirection =
|
|
(window as any).routeDirection != null ? (window as any).routeDirection : "";
|
|
return (
|
|
"ciphers_direction=" +
|
|
routeDirection +
|
|
"_" +
|
|
(outlet.activatedRoute.queryParams as any).value.folderId +
|
|
"_" +
|
|
(outlet.activatedRoute.queryParams as any).value.collectionId
|
|
);
|
|
} else {
|
|
return outlet.activatedRouteData.state;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private showToast(msg: any) {
|
|
this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options);
|
|
}
|
|
|
|
private async showDialog(msg: SimpleDialogOptions) {
|
|
await this.dialogService.openSimpleDialog(msg);
|
|
}
|
|
|
|
private async showNativeMessagingFingerprintDialog(msg: any) {
|
|
const dialogRef = DesktopSyncVerificationDialogComponent.open(this.dialogService, {
|
|
fingerprint: msg.fingerprint,
|
|
});
|
|
|
|
return firstValueFrom(dialogRef.closed);
|
|
}
|
|
|
|
private async clearComponentStates() {
|
|
if (!(await this.stateService.getIsAuthenticated())) {
|
|
return;
|
|
}
|
|
|
|
await Promise.all([
|
|
this.vaultBrowserStateService.setBrowserGroupingsComponentState(null),
|
|
this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null),
|
|
this.browserSendStateService.setBrowserSendComponentState(null),
|
|
this.browserSendStateService.setBrowserSendTypeComponentState(null),
|
|
]);
|
|
}
|
|
}
|