mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 18:53:29 +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:
@@ -1,19 +1,17 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
type ActiveAccount = {
|
||||
@@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & {
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
activeAccount?: ActiveAccount;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
export class AccountSwitcherComponent {
|
||||
activeAccount$: Observable<ActiveAccount | null>;
|
||||
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
||||
authStatus = AuthenticationStatus;
|
||||
|
||||
view$: Observable<{
|
||||
activeAccount: ActiveAccount | null;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount };
|
||||
numberOfAccounts: number;
|
||||
showSwitcher: boolean;
|
||||
}>;
|
||||
|
||||
isOpen = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
@@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
showSwitcher$: Observable<boolean>;
|
||||
|
||||
get showSwitcher() {
|
||||
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email);
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0;
|
||||
return userIsInAVault || userIsAddingAnAdditionalAccount;
|
||||
}
|
||||
|
||||
get numberOfAccounts() {
|
||||
if (this.inactiveAccounts == null) {
|
||||
this.isOpen = false;
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(this.inactiveAccounts).length;
|
||||
}
|
||||
numberOfAccounts$: Observable<number>;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
private avatarService: AvatarService,
|
||||
private messagingService: MessagingService,
|
||||
private router: Router,
|
||||
private tokenService: TokenService,
|
||||
private environmentService: EnvironmentService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
) {}
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.activeAccount$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap(async (active) => {
|
||||
if (active == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.stateService.accounts$
|
||||
.pipe(
|
||||
concatMap(async (accounts: { [userId: string]: Account }) => {
|
||||
this.inactiveAccounts = await this.createInactiveAccounts(accounts);
|
||||
return {
|
||||
id: active.id,
|
||||
name: active.name,
|
||||
email: active.email,
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.inactiveAccounts$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.accountService.accounts$,
|
||||
this.authService.authStatuses$,
|
||||
]).pipe(
|
||||
switchMap(async ([activeAccount, accounts, accountStatuses]) => {
|
||||
// Filter out logged out accounts and active account
|
||||
accounts = Object.fromEntries(
|
||||
Object.entries(accounts).filter(
|
||||
([id]: [UserId, AccountInfo]) =>
|
||||
accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id,
|
||||
),
|
||||
);
|
||||
return this.createInactiveAccounts(accounts);
|
||||
}),
|
||||
);
|
||||
this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe(
|
||||
map(([activeAccount, inactiveAccounts]) => {
|
||||
const hasActiveUser = activeAccount != null;
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0;
|
||||
return hasActiveUser || userIsAddingAnAdditionalAccount;
|
||||
}),
|
||||
);
|
||||
this.numberOfAccounts$ = this.inactiveAccounts$.pipe(
|
||||
map((accounts) => Object.keys(accounts).length),
|
||||
);
|
||||
|
||||
try {
|
||||
this.activeAccount = {
|
||||
id: await this.tokenService.getUserId(),
|
||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||
email: await this.tokenService.getEmail(),
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||
};
|
||||
} catch {
|
||||
this.activeAccount = undefined;
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.view$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.inactiveAccounts$,
|
||||
this.numberOfAccounts$,
|
||||
this.showSwitcher$,
|
||||
]).pipe(
|
||||
map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({
|
||||
activeAccount,
|
||||
inactiveAccounts,
|
||||
numberOfAccounts,
|
||||
showSwitcher,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
|
||||
await this.router.navigate(["/login"]);
|
||||
await this.stateService.setActiveUser(null);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.stateService.clearDecryptedData(activeAccount?.id as UserId);
|
||||
await this.accountService.switchAccount(null);
|
||||
}
|
||||
|
||||
private async createInactiveAccounts(baseAccounts: {
|
||||
[userId: string]: Account;
|
||||
[userId: string]: AccountInfo;
|
||||
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
@@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
|
||||
inactiveAccounts[userId] = {
|
||||
id: userId,
|
||||
name: baseAccounts[userId].profile.name,
|
||||
email: baseAccounts[userId].profile.email,
|
||||
name: baseAccounts[userId].name,
|
||||
email: baseAccounts[userId].email,
|
||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
||||
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
|
||||
|
||||
Reference in New Issue
Block a user