mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-6688] Use AccountService as account source (#8893)
* 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>
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -18,9 +18,9 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio
|
||||
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 { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
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";
|
||||
@@ -107,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
loading = false;
|
||||
|
||||
private lastActivity: number = null;
|
||||
private lastActivity: Date = null;
|
||||
private modal: ModalRef = null;
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
private activeUserId: string = null;
|
||||
private activeUserId: UserId = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -150,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private providerService: ProviderService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
|
||||
this.activeUserId = userId;
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
@@ -400,7 +400,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
break;
|
||||
case "switchAccount": {
|
||||
if (message.userId != null) {
|
||||
await this.stateService.setActiveUser(message.userId);
|
||||
await this.stateService.clearDecryptedData(message.userId);
|
||||
await this.accountService.switchAccount(message.userId);
|
||||
}
|
||||
const locked =
|
||||
(await this.authService.getAuthStatus(message.userId)) ===
|
||||
@@ -522,7 +523,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async updateAppMenu() {
|
||||
let updateRequest: MenuUpdateRequest;
|
||||
const stateAccounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const stateAccounts = await firstValueFrom(this.accountService.accounts$);
|
||||
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
|
||||
updateRequest = {
|
||||
accounts: null,
|
||||
@@ -531,32 +532,32 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
const accounts: { [userId: string]: MenuAccount } = {};
|
||||
for (const i in stateAccounts) {
|
||||
const userId = i as UserId;
|
||||
if (
|
||||
i != null &&
|
||||
stateAccounts[i]?.profile?.userId != null &&
|
||||
!this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up
|
||||
userId != null &&
|
||||
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
|
||||
) {
|
||||
const userId = stateAccounts[i].profile.userId;
|
||||
const availableTimeoutActions = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
|
||||
);
|
||||
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
accounts[userId] = {
|
||||
isAuthenticated: await this.stateService.getIsAuthenticated({
|
||||
userId: userId,
|
||||
}),
|
||||
isLocked:
|
||||
(await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
|
||||
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
|
||||
isLocked: authStatus === AuthenticationStatus.Locked,
|
||||
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
|
||||
email: stateAccounts[i].profile.email,
|
||||
userId: stateAccounts[i].profile.userId,
|
||||
email: stateAccounts[userId].email,
|
||||
userId: userId,
|
||||
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
|
||||
};
|
||||
}
|
||||
}
|
||||
updateRequest = {
|
||||
accounts: accounts,
|
||||
activeUserId: await this.stateService.getUserId(),
|
||||
activeUserId: await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -564,7 +565,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async logOut(expired: boolean, userId?: string) {
|
||||
const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId });
|
||||
const userBeingLoggedOut =
|
||||
(userId as UserId) ??
|
||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
|
||||
// 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
|
||||
@@ -572,9 +575,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.startAccountCleanUp(userBeingLoggedOut);
|
||||
|
||||
let preLogoutActiveUserId;
|
||||
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||
try {
|
||||
// Provide the userId of the user to upload events for
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId);
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||
await this.cipherService.clear(userBeingLoggedOut);
|
||||
@@ -582,22 +586,23 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.collectionService.clear(userBeingLoggedOut);
|
||||
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||
|
||||
preLogoutActiveUserId = this.activeUserId;
|
||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||
await this.accountService.clean(userBeingLoggedOut);
|
||||
} finally {
|
||||
this.finishAccountCleanUp(userBeingLoggedOut);
|
||||
}
|
||||
|
||||
if (this.activeUserId == null) {
|
||||
if (nextUpAccount == null) {
|
||||
// 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"]);
|
||||
} else if (preLogoutActiveUserId !== this.activeUserId) {
|
||||
this.messagingService.send("switchAccount");
|
||||
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
|
||||
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
||||
}
|
||||
|
||||
await this.updateAppMenu();
|
||||
@@ -622,13 +627,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
||||
const now = new Date();
|
||||
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.stateService.setLastActive(now, { userId: this.activeUserId });
|
||||
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
|
||||
Reference in New Issue
Block a user