mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +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:
@@ -25,11 +25,10 @@ export type InitOptions = {
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
accounts$: Observable<{ [userId: string]: T }>;
|
||||
activeAccount$: Observable<string>;
|
||||
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
setActiveUser: (userId: string) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<UserId>;
|
||||
clearDecryptedData: (userId: UserId) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
init: (initOptions?: InitOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
@@ -122,8 +121,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>;
|
||||
@@ -147,8 +144,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
*/
|
||||
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
||||
getLastActive: (options?: StorageOptions) => Promise<number>;
|
||||
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||
@@ -180,5 +175,4 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
nextUpActiveUser: () => Promise<UserId>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,33 @@ import * as path from "path";
|
||||
import { Utils } from "./utils";
|
||||
|
||||
describe("Utils Service", () => {
|
||||
describe("isGuid", () => {
|
||||
it("is false when null", () => {
|
||||
expect(Utils.isGuid(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when undefined", () => {
|
||||
expect(Utils.isGuid(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when empty", () => {
|
||||
expect(Utils.isGuid("")).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when not a string", () => {
|
||||
expect(Utils.isGuid(123 as any)).toBe(false);
|
||||
});
|
||||
|
||||
it("is false when not a guid", () => {
|
||||
expect(Utils.isGuid("not a guid")).toBe(false);
|
||||
});
|
||||
|
||||
it("is true when a guid", () => {
|
||||
// we use a limited guid scope in which all zeroes is invalid
|
||||
expect(Utils.isGuid("00000000-0000-1000-8000-000000000000")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDomain", () => {
|
||||
it("should fail for invalid urls", () => {
|
||||
expect(Utils.getDomain(null)).toBeNull();
|
||||
|
||||
@@ -9,9 +9,6 @@ export class State<
|
||||
> {
|
||||
accounts: { [userId: string]: TAccount } = {};
|
||||
globals: TGlobalState;
|
||||
activeUserId: string;
|
||||
authenticatedAccounts: string[] = [];
|
||||
accountActivity: { [userId: string]: number } = {};
|
||||
|
||||
constructor(globals: TGlobalState) {
|
||||
this.globals = globals;
|
||||
|
||||
@@ -31,10 +31,12 @@ describe("EnvironmentService", () => {
|
||||
[testUser]: {
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
[alternateTestUser]: {
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
});
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
@@ -47,6 +49,7 @@ describe("EnvironmentService", () => {
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
emailVerified: false,
|
||||
});
|
||||
await awaitAsync();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify, JsonValue } from "type-fest";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
@@ -33,10 +33,7 @@ const keys = {
|
||||
state: "state",
|
||||
stateVersion: "stateVersion",
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
activeUserId: "activeUserId",
|
||||
tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication
|
||||
accountActivity: "accountActivity",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
@@ -58,9 +55,6 @@ export class StateService<
|
||||
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
|
||||
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
|
||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||
|
||||
private hasBeenInited = false;
|
||||
protected isRecoveredSession = false;
|
||||
|
||||
@@ -112,36 +106,16 @@ export class StateService<
|
||||
}
|
||||
|
||||
// Get all likely authenticated accounts
|
||||
const authenticatedAccounts = (
|
||||
(await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? []
|
||||
).filter((account) => account != null);
|
||||
const authenticatedAccounts = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))),
|
||||
);
|
||||
|
||||
await this.updateState(async (state) => {
|
||||
for (const i in authenticatedAccounts) {
|
||||
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
|
||||
}
|
||||
|
||||
// After all individual accounts have been added
|
||||
state.authenticatedAccounts = authenticatedAccounts;
|
||||
|
||||
const storedActiveUser = await this.storageService.get<string>(keys.activeUserId);
|
||||
if (storedActiveUser != null) {
|
||||
state.activeUserId = storedActiveUser;
|
||||
}
|
||||
await this.pushAccounts();
|
||||
this.activeAccountSubject.next(state.activeUserId);
|
||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||
// account service tracks logged out accounts, but State service does not, so we need to add the active account
|
||||
// if it's not in the accounts list.
|
||||
if (state.activeUserId != null && this.accountsSubject.value[state.activeUserId] == null) {
|
||||
const activeDiskAccount = await this.getAccountFromDisk({ userId: state.activeUserId });
|
||||
await this.accountService.addAccount(state.activeUserId as UserId, {
|
||||
name: activeDiskAccount.profile.name,
|
||||
email: activeDiskAccount.profile.email,
|
||||
});
|
||||
}
|
||||
await this.accountService.switchAccount(state.activeUserId as UserId);
|
||||
// End TODO
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -161,61 +135,25 @@ export class StateService<
|
||||
return state;
|
||||
});
|
||||
|
||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||
// The determination of state should be handled by the various services that control those values.
|
||||
await this.accountService.addAccount(userId as UserId, {
|
||||
name: diskAccount.profile.name,
|
||||
email: diskAccount.profile.email,
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async addAccount(account: TAccount) {
|
||||
await this.environmentService.seedUserEnvironment(account.profile.userId as UserId);
|
||||
await this.updateState(async (state) => {
|
||||
state.authenticatedAccounts.push(account.profile.userId);
|
||||
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
|
||||
state.accounts[account.profile.userId] = account;
|
||||
return state;
|
||||
});
|
||||
await this.scaffoldNewAccountStorage(account);
|
||||
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
|
||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||
await this.accountService.addAccount(account.profile.userId as UserId, {
|
||||
name: account.profile.name,
|
||||
email: account.profile.email,
|
||||
});
|
||||
await this.setActiveUser(account.profile.userId);
|
||||
}
|
||||
|
||||
async setActiveUser(userId: string): Promise<void> {
|
||||
await this.clearDecryptedDataForActiveUser();
|
||||
await this.updateState(async (state) => {
|
||||
state.activeUserId = userId;
|
||||
await this.storageService.save(keys.activeUserId, userId);
|
||||
this.activeAccountSubject.next(state.activeUserId);
|
||||
// TODO: temporary update to avoid routing all account status changes through account service for now.
|
||||
await this.accountService.switchAccount(userId as UserId);
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<UserId> {
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
await this.deAuthenticateAccount(options.userId);
|
||||
let currentUser = (await this.state())?.activeUserId;
|
||||
if (options.userId === currentUser) {
|
||||
currentUser = await this.dynamicallySetActiveUser();
|
||||
}
|
||||
|
||||
await this.removeAccountFromDisk(options?.userId);
|
||||
await this.removeAccountFromMemory(options?.userId);
|
||||
await this.pushAccounts();
|
||||
return currentUser as UserId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,24 +453,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getEmailVerified(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.profile.emailVerified ?? false
|
||||
);
|
||||
}
|
||||
|
||||
async setEmailVerified(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.profile.emailVerified = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getEnableBrowserIntegration(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
@@ -642,35 +562,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getLastActive(options?: StorageOptions): Promise<number> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
|
||||
const accountActivity = await this.storageService.get<{ [userId: string]: number }>(
|
||||
keys.accountActivity,
|
||||
options,
|
||||
);
|
||||
|
||||
if (accountActivity == null || Object.keys(accountActivity).length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return accountActivity[options.userId];
|
||||
}
|
||||
|
||||
async setLastActive(value: number, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
if (options.userId == null) {
|
||||
return;
|
||||
}
|
||||
const accountActivity =
|
||||
(await this.storageService.get<{ [userId: string]: number }>(
|
||||
keys.accountActivity,
|
||||
options,
|
||||
)) ?? {};
|
||||
accountActivity[options.userId] = value;
|
||||
await this.storageService.save(keys.accountActivity, accountActivity, options);
|
||||
}
|
||||
|
||||
async getLastSync(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
|
||||
@@ -910,24 +801,28 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async getAccountFromMemory(options: StorageOptions): Promise<TAccount> {
|
||||
const userId =
|
||||
options.userId ??
|
||||
(await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
));
|
||||
|
||||
return await this.state().then(async (state) => {
|
||||
if (state.accounts == null) {
|
||||
return null;
|
||||
}
|
||||
return state.accounts[await this.getUserIdFromMemory(options)];
|
||||
});
|
||||
}
|
||||
|
||||
protected async getUserIdFromMemory(options: StorageOptions): Promise<string> {
|
||||
return await this.state().then((state) => {
|
||||
return options?.userId != null
|
||||
? state.accounts[options.userId]?.profile?.userId
|
||||
: state.activeUserId;
|
||||
return state.accounts[userId];
|
||||
});
|
||||
}
|
||||
|
||||
protected async getAccountFromDisk(options: StorageOptions): Promise<TAccount> {
|
||||
if (options?.userId == null && (await this.state())?.activeUserId == null) {
|
||||
const userId =
|
||||
options.userId ??
|
||||
(await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
));
|
||||
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1086,53 +981,76 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async defaultInMemoryOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Memory,
|
||||
userId: (await this.state()).activeUserId,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||
userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()),
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()),
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Memory,
|
||||
userId: (await this.state())?.activeUserId ?? (await this.getUserId()),
|
||||
userId,
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
useSecureStorage: true,
|
||||
userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()),
|
||||
userId,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getActiveUserIdFromStorage(): Promise<string> {
|
||||
return await this.storageService.get<string>(keys.activeUserId);
|
||||
return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
}
|
||||
|
||||
protected async removeAccountFromLocalStorage(userId: string = null): Promise<void> {
|
||||
userId = userId ?? (await this.state())?.activeUserId;
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
const storedAccount = await this.getAccount(
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
@@ -1143,7 +1061,10 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async removeAccountFromSessionStorage(userId: string = null): Promise<void> {
|
||||
userId = userId ?? (await this.state())?.activeUserId;
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
const storedAccount = await this.getAccount(
|
||||
this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
@@ -1154,7 +1075,10 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async removeAccountFromSecureStorage(userId: string = null): Promise<void> {
|
||||
userId = userId ?? (await this.state())?.activeUserId;
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
await this.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.setUserKeyBiometric(null, { userId: userId });
|
||||
await this.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
@@ -1163,8 +1087,11 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async removeAccountFromMemory(userId: string = null): Promise<void> {
|
||||
userId ??= await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
await this.updateState(async (state) => {
|
||||
userId = userId ?? state.activeUserId;
|
||||
delete state.accounts[userId];
|
||||
return state;
|
||||
});
|
||||
@@ -1178,15 +1105,16 @@ export class StateService<
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
protected async clearDecryptedDataForActiveUser(): Promise<void> {
|
||||
async clearDecryptedData(userId: UserId): Promise<void> {
|
||||
await this.updateState(async (state) => {
|
||||
const userId = state?.activeUserId;
|
||||
if (userId != null && state?.accounts[userId]?.data != null) {
|
||||
state.accounts[userId].data = new AccountData();
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||
@@ -1201,14 +1129,6 @@ export class StateService<
|
||||
// We must have a manual call to clear tokens as we can't leverage state provider to clean
|
||||
// up our data as we have secure storage in the mix.
|
||||
await this.tokenService.clearTokens(userId as UserId);
|
||||
await this.setLastActive(null, { userId: userId });
|
||||
await this.updateState(async (state) => {
|
||||
state.authenticatedAccounts = state.authenticatedAccounts.filter((id) => id !== userId);
|
||||
|
||||
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
protected async removeAccountFromDisk(userId: string) {
|
||||
@@ -1217,32 +1137,6 @@ export class StateService<
|
||||
await this.removeAccountFromSecureStorage(userId);
|
||||
}
|
||||
|
||||
async nextUpActiveUser() {
|
||||
const accounts = (await this.state())?.accounts;
|
||||
if (accounts == null || Object.keys(accounts).length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let newActiveUser;
|
||||
for (const userId in accounts) {
|
||||
if (userId == null) {
|
||||
continue;
|
||||
}
|
||||
if (await this.getIsAuthenticated({ userId: userId })) {
|
||||
newActiveUser = userId;
|
||||
break;
|
||||
}
|
||||
newActiveUser = null;
|
||||
}
|
||||
return newActiveUser as UserId;
|
||||
}
|
||||
|
||||
protected async dynamicallySetActiveUser() {
|
||||
const newActiveUser = await this.nextUpActiveUser();
|
||||
await this.setActiveUser(newActiveUser);
|
||||
return newActiveUser;
|
||||
}
|
||||
|
||||
protected async saveSecureStorageKey<T extends JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { firstValueFrom, timeout } from "rxjs";
|
||||
import { firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MessagingService } from "../abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
@@ -25,15 +27,18 @@ export class SystemService implements SystemServiceAbstraction {
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async startProcessReload(authService: AuthService): Promise<void> {
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
if (accounts != null) {
|
||||
const keys = Object.keys(accounts);
|
||||
if (keys.length > 0) {
|
||||
for (const userId of keys) {
|
||||
if ((await authService.getAuthStatus(userId)) === AuthenticationStatus.Unlocked) {
|
||||
let status = await firstValueFrom(authService.authStatusFor$(userId as UserId));
|
||||
status = await authService.getAuthStatus(userId);
|
||||
if (status === AuthenticationStatus.Unlocked) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -63,15 +68,24 @@ export class SystemService implements SystemServiceAbstraction {
|
||||
clearInterval(this.reloadInterval);
|
||||
this.reloadInterval = null;
|
||||
|
||||
const currentUser = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500)));
|
||||
const currentUser = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
timeout(500),
|
||||
),
|
||||
);
|
||||
// Replace current active user if they will be logged out on reload
|
||||
if (currentUser != null) {
|
||||
const timeoutAction = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)),
|
||||
);
|
||||
if (timeoutAction === VaultTimeoutAction.LogOut) {
|
||||
const nextUser = await this.stateService.nextUpActiveUser();
|
||||
await this.stateService.setActiveUser(nextUser);
|
||||
const nextUser = await firstValueFrom(
|
||||
this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)),
|
||||
);
|
||||
// Can be removed once we migrate password generation history to state providers
|
||||
await this.stateService.clearDecryptedData(currentUser);
|
||||
await this.accountService.switchAccount(nextUser);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { SingleUserStateProvider } from "../user-state.provider";
|
||||
|
||||
@@ -14,7 +13,7 @@ describe("DefaultActiveUserStateProvider", () => {
|
||||
id: userId,
|
||||
name: "name",
|
||||
email: "email",
|
||||
status: AuthenticationStatus.Locked,
|
||||
emailVerified: false,
|
||||
};
|
||||
const accountService = mockAccountServiceWith(userId, accountInfo);
|
||||
let sut: DefaultActiveUserStateProvider;
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("DefaultActiveUserState", () => {
|
||||
activeAccountSubject.next({
|
||||
id: userId,
|
||||
email: `test${id}@example.com`,
|
||||
emailVerified: false,
|
||||
name: `Test User ${id}`,
|
||||
});
|
||||
await awaitAsync();
|
||||
|
||||
@@ -69,7 +69,12 @@ describe("DefaultStateProvider", () => {
|
||||
userId?: UserId,
|
||||
) => Observable<string>,
|
||||
) => {
|
||||
const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut };
|
||||
const accountInfo = {
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
status: AuthenticationStatus.LoggedOut,
|
||||
};
|
||||
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
@@ -114,7 +119,12 @@ describe("DefaultStateProvider", () => {
|
||||
);
|
||||
|
||||
describe("getUserState$", () => {
|
||||
const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut };
|
||||
const accountInfo = {
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
status: AuthenticationStatus.LoggedOut,
|
||||
};
|
||||
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
|
||||
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
|
||||
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
||||
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
||||
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||
|
||||
Reference in New Issue
Block a user