1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 11:43:46 +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:
Matt Gibson
2024-04-30 09:13:02 -04:00
committed by GitHub
parent 61d079cc34
commit c70a5aa024
67 changed files with 1380 additions and 618 deletions

View File

@@ -1,4 +1,4 @@
import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs";
import { combineLatestWith, map, distinctUntilChanged, shareReplay, combineLatest } from "rxjs";
import {
AccountInfo,
@@ -7,8 +7,9 @@ import {
} from "../../auth/abstractions/account.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { Utils } from "../../platform/misc/utils";
import {
ACCOUNT_MEMORY,
ACCOUNT_DISK,
GlobalState,
GlobalStateProvider,
KeyDefinition,
@@ -16,25 +17,36 @@ import {
import { UserId } from "../../types/guid";
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
ACCOUNT_MEMORY,
ACCOUNT_DISK,
"accounts",
{
deserializer: (accountInfo) => accountInfo,
},
);
export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", {
export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_DISK, "activeAccountId", {
deserializer: (id: UserId) => id,
});
export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK, "activity", {
deserializer: (activity) => new Date(activity),
});
const LOGGED_OUT_INFO: AccountInfo = {
email: "",
emailVerified: false,
name: undefined,
};
export class AccountServiceImplementation implements InternalAccountService {
private lock = new Subject<UserId>();
private logout = new Subject<UserId>();
private accountsState: GlobalState<Record<UserId, AccountInfo>>;
private activeAccountIdState: GlobalState<UserId | undefined>;
accounts$;
activeAccount$;
accountActivity$;
sortedUserIds$;
nextUpAccount$;
constructor(
private messagingService: MessagingService,
@@ -53,14 +65,40 @@ export class AccountServiceImplementation implements InternalAccountService {
distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)),
shareReplay({ bufferSize: 1, refCount: false }),
);
this.accountActivity$ = this.globalStateProvider
.get(ACCOUNT_ACTIVITY)
.state$.pipe(map((activity) => activity ?? {}));
this.sortedUserIds$ = this.accountActivity$.pipe(
map((activity) => {
return Object.entries(activity)
.map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive }))
.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()) // later dates first
.map((a) => a.userId);
}),
);
this.nextUpAccount$ = combineLatest([
this.accounts$,
this.activeAccount$,
this.sortedUserIds$,
]).pipe(
map(([accounts, activeAccount, sortedUserIds]) => {
const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null);
return nextId ? { id: nextId, ...accounts[nextId] } : null;
}),
);
}
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
if (!Utils.isGuid(userId)) {
throw new Error("userId is required");
}
await this.accountsState.update((accounts) => {
accounts ||= {};
accounts[userId] = accountData;
return accounts;
});
await this.setAccountActivity(userId, new Date());
}
async setAccountName(userId: UserId, name: string): Promise<void> {
@@ -71,6 +109,15 @@ export class AccountServiceImplementation implements InternalAccountService {
await this.setAccountInfo(userId, { email });
}
async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void> {
await this.setAccountInfo(userId, { emailVerified });
}
async clean(userId: UserId) {
await this.setAccountInfo(userId, LOGGED_OUT_INFO);
await this.removeAccountActivity(userId);
}
async switchAccount(userId: UserId): Promise<void> {
await this.activeAccountIdState.update(
(_, accounts) => {
@@ -94,6 +141,37 @@ export class AccountServiceImplementation implements InternalAccountService {
);
}
async setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> {
if (!Utils.isGuid(userId)) {
// only store for valid userIds
return;
}
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
(activity) => {
activity ||= {};
activity[userId] = lastActivity;
return activity;
},
{
shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(),
},
);
}
async removeAccountActivity(userId: UserId): Promise<void> {
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
(activity) => {
if (activity == null) {
return activity;
}
delete activity[userId];
return activity;
},
{ shouldUpdate: (oldActivity) => oldActivity?.[userId] != null },
);
}
// TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow
async delete(): Promise<void> {
try {