mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-06 09:20:43 +01:00
[Bug] [Account Switching] Improve State Management Performance (#611)
* [bug] Improve state management performance Large vaults see a clear degrade in performance using the state service, especially when multiple vaults are authed and unlocked at the same time. Some changes made to address this: 1. Clearing in memory decrypted data for non active accounts. This really should have been something we were doing anyway, but letting go of that memory burden has a noticable performance boost. 2. Not loading a bunch of unecsassary data from disk accounts into memory on application startup. This was being done to initilize in memory accounts, but brought a lot of extra baggage with it like storing encrypted data in memory, even though it is never referenced that way. 3. Breaking the on disk state object up into seperate keys for accounts instead of storing everything together under a "state" key. This ensures there is less information fetched from disk each time we call for an account. There were some restructuring changes needed to facilitate these items: 1. We need to be able to construct an account in the StateService, but typescript doesn't allow for new() constraints on generics so a factory needs to be created and passed into the StateService for this to work. 2. Since we can't reference an all-knowing "accounts" object for on disk state anymore we have to maintain a list of authenticated accounts, and this has been added. 3. The StateMigration service needed to be updated to break up the accounts object, so current dev and QA state will be broken and need to be reset. Some other general refactorings that were helpful gettings this working: 1. Added a constant for keys to the StateService and StateMigrationService. 2. Bundling everything needed to deauthenticate a user into a dedicated method. 3. Bundling all the disk storage clear methods (that should be refactored later into client specific state services) into one helper method. 4. Bundling everything needed to dynamically select a new active user into a dedicated method. * [bug] Set environmentUrls appropriatly on account add * [bug] Stop tracking activity without an active user * [bug] Remove lastActive from globalState and globalState migration * [style] Ran prettier
This commit is contained in:
parent
cc285e5ea7
commit
ccd715d7b8
@ -76,6 +76,8 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||
import { UnauthGuardService } from "./unauth-guard.service";
|
||||
import { ValidationService } from "./validation.service";
|
||||
|
||||
import { Account, AccountFactory } from "jslib-common/models/domain/account";
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
providers: [
|
||||
@ -325,7 +327,19 @@ import { ValidationService } from "./validation.service";
|
||||
},
|
||||
{
|
||||
provide: StateServiceAbstraction,
|
||||
useClass: StateService,
|
||||
useFactory: (
|
||||
storageService: StorageServiceAbstraction,
|
||||
secureStorageService: StorageServiceAbstraction,
|
||||
logService: LogService,
|
||||
stateMigrationService: StateMigrationServiceAbstraction
|
||||
) =>
|
||||
new StateService(
|
||||
storageService,
|
||||
secureStorageService,
|
||||
logService,
|
||||
stateMigrationService,
|
||||
new AccountFactory(Account)
|
||||
),
|
||||
deps: [
|
||||
StorageServiceAbstraction,
|
||||
"SECURE_STORAGE",
|
||||
|
@ -183,3 +183,15 @@ export class Account {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountFactory<T extends Account = Account> {
|
||||
private accountConstructor: new (init: Partial<T>) => T;
|
||||
|
||||
constructor(accountConstructor: new (init: Partial<T>) => T) {
|
||||
this.accountConstructor = accountConstructor;
|
||||
}
|
||||
|
||||
create(args: Partial<T>) {
|
||||
return new this.accountConstructor(args);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { EnvironmentUrls } from "./environmentUrls";
|
||||
export class GlobalState {
|
||||
enableAlwaysOnTop?: boolean;
|
||||
installedVersion?: string;
|
||||
lastActive?: number;
|
||||
locale?: string = "en";
|
||||
openAtLogin?: boolean;
|
||||
organizationInvitation?: any;
|
||||
|
@ -5,4 +5,5 @@ export class State<TAccount extends Account = Account> {
|
||||
accounts: { [userId: string]: TAccount } = {};
|
||||
globals: GlobalState = new GlobalState();
|
||||
activeUserId: string;
|
||||
authenticatedAccounts: string[] = [];
|
||||
}
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
||||
|
||||
import {
|
||||
Account,
|
||||
AccountData,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../models/domain/account";
|
||||
import { Account, AccountData, AccountFactory } from "../models/domain/account";
|
||||
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
@ -43,6 +37,18 @@ import { BehaviorSubject } from "rxjs";
|
||||
import { StateMigrationService } from "../abstractions/stateMigration.service";
|
||||
import { EnvironmentUrls } from "../models/domain/environmentUrls";
|
||||
|
||||
const keys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
activeUserId: "activeUserId",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
autoKey: "_masterkey_auto",
|
||||
biometricKey: "_masterkey_biometric",
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
export class StateService<TAccount extends Account = Account>
|
||||
implements StateServiceAbstraction<TAccount>
|
||||
{
|
||||
@ -55,32 +61,47 @@ export class StateService<TAccount extends Account = Account>
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected logService: LogService,
|
||||
protected stateMigrationService: StateMigrationService
|
||||
protected stateMigrationService: StateMigrationService,
|
||||
protected accountFactory: AccountFactory<TAccount>
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (await this.stateMigrationService.needsMigration()) {
|
||||
await this.stateMigrationService.migrate();
|
||||
}
|
||||
if (this.state.activeUserId == null) {
|
||||
await this.loadStateFromDisk();
|
||||
}
|
||||
|
||||
await this.initAccountState();
|
||||
}
|
||||
|
||||
async loadStateFromDisk() {
|
||||
if ((await this.getActiveUserIdFromStorage()) != null) {
|
||||
const diskState = await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
this.state = diskState;
|
||||
await this.pruneInMemoryAccounts();
|
||||
await this.pushAccounts();
|
||||
async initAccountState() {
|
||||
this.state.authenticatedAccounts =
|
||||
(await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? [];
|
||||
for (const i in this.state.authenticatedAccounts) {
|
||||
if (i != null) {
|
||||
await this.syncAccountFromDisk(this.state.authenticatedAccounts[i]);
|
||||
}
|
||||
}
|
||||
const storedActiveUser = await this.storageService.get<string>(keys.activeUserId);
|
||||
if (storedActiveUser != null) {
|
||||
this.state.activeUserId = storedActiveUser;
|
||||
}
|
||||
await this.pushAccounts();
|
||||
this.activeAccount.next(this.state.activeUserId);
|
||||
}
|
||||
|
||||
async syncAccountFromDisk(userId: string) {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
this.state.accounts[userId] = this.createAccount();
|
||||
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
||||
this.state.accounts[userId].profile = diskAccount.profile;
|
||||
}
|
||||
|
||||
async addAccount(account: TAccount) {
|
||||
await this.setAccountEnvironmentUrls(account);
|
||||
account = await this.setAccountEnvironmentUrls(account);
|
||||
this.state.authenticatedAccounts.push(account.profile.userId);
|
||||
this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts);
|
||||
this.state.accounts[account.profile.userId] = account;
|
||||
await this.scaffoldNewAccountStorage(account);
|
||||
await this.setActiveUser(account.profile.userId);
|
||||
@ -88,36 +109,21 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
|
||||
async setActiveUser(userId: string): Promise<void> {
|
||||
this.clearDecryptedDataForActiveUser();
|
||||
this.state.activeUserId = userId;
|
||||
const storedState = await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
storedState.activeUserId = userId;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
|
||||
await this.pushAccounts();
|
||||
await this.storageService.save(keys.activeUserId, userId);
|
||||
this.activeAccount.next(this.state.activeUserId);
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
// Find and set the next active user if any exists
|
||||
await this.setAccessToken(null, { userId: options?.userId });
|
||||
if (options?.userId == null || options.userId === (await this.getUserId())) {
|
||||
for (const userId in this.state.accounts) {
|
||||
if (userId == null) {
|
||||
continue;
|
||||
}
|
||||
if (await this.getIsAuthenticated({ userId: userId })) {
|
||||
await this.setActiveUser(userId);
|
||||
break;
|
||||
}
|
||||
await this.setActiveUser(null);
|
||||
}
|
||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
||||
await this.deAuthenticateAccount(options.userId);
|
||||
if (options.userId === this.state.activeUserId) {
|
||||
await this.dynamicallySetActiveUser();
|
||||
}
|
||||
|
||||
await this.removeAccountFromSessionStorage(options?.userId);
|
||||
await this.removeAccountFromLocalStorage(options?.userId);
|
||||
await this.removeAccountFromSecureStorage(options?.userId);
|
||||
await this.removeAccountFromDisk(options?.userId);
|
||||
this.removeAccountFromMemory(options?.userId);
|
||||
await this.pushAccounts();
|
||||
}
|
||||
@ -425,7 +431,7 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get(`${options.userId}_masterkey_auto`, options);
|
||||
return await this.secureStorageService.get(`${options.userId}${partialKeys.autoKey}`, options);
|
||||
}
|
||||
|
||||
async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> {
|
||||
@ -436,7 +442,7 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.secureStorageService.save(`${options.userId}_masterkey_auto`, value, options);
|
||||
await this.secureStorageService.save(`${options.userId}${partialKeys.autoKey}`, value, options);
|
||||
}
|
||||
|
||||
async getCryptoMasterKeyB64(options?: StorageOptions): Promise<string> {
|
||||
@ -444,7 +450,10 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get(`${options?.userId}_masterkey`, options);
|
||||
return await this.secureStorageService.get(
|
||||
`${options?.userId}${partialKeys.masterKey}`,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise<void> {
|
||||
@ -452,7 +461,11 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.secureStorageService.save(`${options.userId}_masterkey`, value, options);
|
||||
await this.secureStorageService.save(
|
||||
`${options.userId}${partialKeys.masterKey}`,
|
||||
value,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise<string> {
|
||||
@ -463,7 +476,10 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get(`${options.userId}_masterkey_biometric`, options);
|
||||
return await this.secureStorageService.get(
|
||||
`${options.userId}${partialKeys.biometricKey}`,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise<boolean> {
|
||||
@ -474,7 +490,10 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return false;
|
||||
}
|
||||
return await this.secureStorageService.has(`${options.userId}_masterkey_biometric`, options);
|
||||
return await this.secureStorageService.has(
|
||||
`${options.userId}${partialKeys.biometricKey}`,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async setCryptoMasterKeyBiometric(value: string, options?: StorageOptions): Promise<void> {
|
||||
@ -485,7 +504,11 @@ export class StateService<TAccount extends Account = Account>
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
await this.secureStorageService.save(`${options.userId}_masterkey_biometric`, value, options);
|
||||
await this.secureStorageService.save(
|
||||
`${options.userId}${partialKeys.biometricKey}`,
|
||||
value,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
async getDecodedToken(options?: StorageOptions): Promise<any> {
|
||||
@ -866,20 +889,16 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.profile?.email;
|
||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||
?.profile?.email;
|
||||
}
|
||||
|
||||
async setEmail(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
||||
);
|
||||
account.profile.email = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
||||
}
|
||||
|
||||
async getEmailVerified(options?: StorageOptions): Promise<boolean> {
|
||||
@ -1526,14 +1545,9 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
|
||||
async getLastActive(options?: StorageOptions): Promise<number> {
|
||||
const lastActive = (
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.profile?.lastActive;
|
||||
return (
|
||||
lastActive ??
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.lastActive
|
||||
);
|
||||
}
|
||||
|
||||
async setLastActive(value: number, options?: StorageOptions): Promise<void> {
|
||||
@ -1547,15 +1561,6 @@ export class StateService<TAccount extends Account = Account>
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
globals.lastActive = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getLastSync(options?: StorageOptions): Promise<string> {
|
||||
@ -2091,7 +2096,7 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
|
||||
protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
|
||||
return (await this.storageService.get<State<TAccount>>("state", options))?.globals;
|
||||
return await this.storageService.get<GlobalState>(keys.global, options);
|
||||
}
|
||||
|
||||
protected saveGlobalsToMemory(globals: GlobalState): void {
|
||||
@ -2100,15 +2105,9 @@ export class StateService<TAccount extends Account = Account>
|
||||
|
||||
protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
|
||||
if (options.useSecureStorage) {
|
||||
const state =
|
||||
(await this.secureStorageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||
state.globals = globals;
|
||||
await this.secureStorageService.save("state", state, options);
|
||||
await this.secureStorageService.save(keys.global, globals, options);
|
||||
} else {
|
||||
const state =
|
||||
(await this.storageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||
state.globals = globals;
|
||||
await this.saveStateToStorage(state, options);
|
||||
await this.storageService.save(keys.global, globals, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2147,15 +2146,15 @@ export class StateService<TAccount extends Account = Account>
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = options?.useSecureStorage
|
||||
? (await this.secureStorageService.get<State<TAccount>>("state", options)) ??
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
const account = options?.useSecureStorage
|
||||
? (await this.secureStorageService.get<TAccount>(options.userId, options)) ??
|
||||
(await this.storageService.get<TAccount>(
|
||||
options.userId,
|
||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })
|
||||
))
|
||||
: await this.storageService.get<State<TAccount>>("state", options);
|
||||
: await this.storageService.get<TAccount>(options.userId, options);
|
||||
|
||||
return state?.accounts[options?.userId ?? this.state.activeUserId];
|
||||
return account;
|
||||
}
|
||||
|
||||
protected useMemory(storageLocation: StorageLocation) {
|
||||
@ -2183,11 +2182,7 @@ export class StateService<TAccount extends Account = Account>
|
||||
? this.secureStorageService
|
||||
: this.storageService;
|
||||
|
||||
const state =
|
||||
(await storageLocation.get<State<TAccount>>("state", options)) ?? new State<TAccount>();
|
||||
state.accounts[account.profile.userId] = account;
|
||||
|
||||
await storageLocation.save("state", state, options);
|
||||
await storageLocation.save(`${options.userId}`, account, options);
|
||||
}
|
||||
|
||||
protected async saveAccountToMemory(account: TAccount): Promise<void> {
|
||||
@ -2203,47 +2198,57 @@ export class StateService<TAccount extends Account = Account>
|
||||
await this.scaffoldNewAccountMemoryStorage(account);
|
||||
}
|
||||
|
||||
// TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService.
|
||||
// For now these methods exist with some redundancy to facilitate this special web requirement.
|
||||
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
const storedAccount = await this.storageService.get<TAccount>(
|
||||
account.profile.userId,
|
||||
await this.defaultOnDiskLocalOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
);
|
||||
if (storedAccount?.settings != null) {
|
||||
// EnvironmentUrls are set before authenticating and should override whatever is stored from last session
|
||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions());
|
||||
await this.storageService.save(
|
||||
account.profile.userId,
|
||||
account,
|
||||
await this.defaultOnDiskLocalOptions()
|
||||
);
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
const storedAccount = await this.storageService.get<TAccount>(
|
||||
account.profile.userId,
|
||||
await this.defaultOnDiskMemoryOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
);
|
||||
if (storedAccount?.settings != null) {
|
||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions());
|
||||
await this.storageService.save(
|
||||
account.profile.userId,
|
||||
account,
|
||||
await this.defaultOnDiskMemoryOptions()
|
||||
);
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
const storedAccount = await this.storageService.get<TAccount>(
|
||||
account.profile.userId,
|
||||
await this.defaultOnDiskOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
);
|
||||
if (storedAccount?.settings != null) {
|
||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
|
||||
await this.storageService.save(
|
||||
account.profile.userId,
|
||||
account,
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
}
|
||||
//
|
||||
|
||||
protected async pushAccounts(): Promise<void> {
|
||||
await this.pruneInMemoryAccounts();
|
||||
@ -2313,34 +2318,33 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
|
||||
protected async getActiveUserIdFromStorage(): Promise<string> {
|
||||
const state = await this.storageService.get<State<TAccount>>("state");
|
||||
return state?.activeUserId;
|
||||
return await this.storageService.get<string>(keys.activeUserId);
|
||||
}
|
||||
|
||||
protected async removeAccountFromLocalStorage(
|
||||
userId: string = this.state.activeUserId
|
||||
): Promise<void> {
|
||||
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||
const storedAccount = await this.storageService.get<TAccount>(userId, {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
});
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||
await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions());
|
||||
await this.storageService.save(
|
||||
userId,
|
||||
this.resetAccount(storedAccount),
|
||||
await this.defaultOnDiskLocalOptions()
|
||||
);
|
||||
}
|
||||
|
||||
protected async removeAccountFromSessionStorage(
|
||||
userId: string = this.state.activeUserId
|
||||
): Promise<void> {
|
||||
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||
const storedAccount = await this.storageService.get<TAccount>(userId, {
|
||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||
});
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||
await this.saveStateToStorage(state, await this.defaultOnDiskOptions());
|
||||
await this.storageService.save(
|
||||
userId,
|
||||
this.resetAccount(storedAccount),
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
}
|
||||
|
||||
protected async removeAccountFromSecureStorage(
|
||||
@ -2355,13 +2359,6 @@ export class StateService<TAccount extends Account = Account>
|
||||
delete this.state.accounts[userId];
|
||||
}
|
||||
|
||||
protected async saveStateToStorage(
|
||||
state: State<TAccount>,
|
||||
options: StorageOptions
|
||||
): Promise<void> {
|
||||
await this.storageService.save("state", state, options);
|
||||
}
|
||||
|
||||
protected async pruneInMemoryAccounts() {
|
||||
// We preserve settings for logged out accounts, but we don't want to consider them when thinking about active account state
|
||||
for (const userId in this.state.accounts) {
|
||||
@ -2371,13 +2368,10 @@ export class StateService<TAccount extends Account = Account>
|
||||
}
|
||||
}
|
||||
|
||||
// settings persist even on reset
|
||||
// settings persist even on reset, and are not effected by this method
|
||||
protected resetAccount(account: TAccount) {
|
||||
account.data = new AccountData();
|
||||
account.keys = new AccountKeys();
|
||||
account.profile = new AccountProfile();
|
||||
account.tokens = new AccountTokens();
|
||||
return account;
|
||||
const persistentAccountInformation = { settings: account.settings };
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
protected async setAccountEnvironmentUrls(account: TAccount): Promise<TAccount> {
|
||||
@ -2389,4 +2383,44 @@ export class StateService<TAccount extends Account = Account>
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
||||
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
|
||||
}
|
||||
|
||||
protected clearDecryptedDataForActiveUser() {
|
||||
const userId = this.state.activeUserId;
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
this.state.accounts[userId].data = new AccountData();
|
||||
}
|
||||
|
||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||
return this.accountFactory.create(init);
|
||||
}
|
||||
|
||||
protected async deAuthenticateAccount(userId: string) {
|
||||
await this.setAccessToken(null, { userId: userId });
|
||||
const index = this.state.authenticatedAccounts.indexOf(userId);
|
||||
if (index > -1) {
|
||||
this.state.authenticatedAccounts.splice(index, 1);
|
||||
await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts);
|
||||
}
|
||||
}
|
||||
|
||||
protected async removeAccountFromDisk(userId: string) {
|
||||
await this.removeAccountFromSessionStorage(userId);
|
||||
await this.removeAccountFromLocalStorage(userId);
|
||||
await this.removeAccountFromSecureStorage(userId);
|
||||
}
|
||||
|
||||
protected async dynamicallySetActiveUser() {
|
||||
for (const userId in this.state.accounts) {
|
||||
if (userId == null) {
|
||||
continue;
|
||||
}
|
||||
if (await this.getIsAuthenticated({ userId: userId })) {
|
||||
await this.setActiveUser(userId);
|
||||
break;
|
||||
}
|
||||
await this.setActiveUser(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,18 @@ const v1KeyPrefixes = {
|
||||
settings: "settings_",
|
||||
};
|
||||
|
||||
const keys = {
|
||||
global: "global",
|
||||
authenticatedAccounts: "authenticatedAccounts",
|
||||
activeUserId: "activeUserId",
|
||||
};
|
||||
|
||||
const partialKeys = {
|
||||
autoKey: "_masterkey_auto",
|
||||
biometricKey: "_masterkey_biometric",
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
export class StateMigrationService {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
@ -121,17 +133,16 @@ export class StateMigrationService {
|
||||
|
||||
async needsMigration(): Promise<boolean> {
|
||||
const currentStateVersion = (
|
||||
await this.storageService.get<State<Account>>("state", {
|
||||
await this.storageService.get<GlobalState>(keys.global, {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
})
|
||||
)?.globals?.stateVersion;
|
||||
)?.stateVersion;
|
||||
return currentStateVersion == null || currentStateVersion < StateVersion.Latest;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion =
|
||||
(await this.storageService.get<State<Account>>("state"))?.globals?.stateVersion ??
|
||||
StateVersion.One;
|
||||
(await this.storageService.get<GlobalState>(keys.global))?.stateVersion ?? StateVersion.One;
|
||||
while (currentStateVersion < StateVersion.Latest) {
|
||||
switch (currentStateVersion) {
|
||||
case StateVersion.One:
|
||||
@ -152,8 +163,10 @@ export class StateMigrationService {
|
||||
globals: new GlobalState(),
|
||||
accounts: {},
|
||||
activeUserId: null,
|
||||
authenticatedAccounts: [],
|
||||
}
|
||||
: {
|
||||
authenticatedAccounts: [userId],
|
||||
activeUserId: userId,
|
||||
globals: {
|
||||
biometricAwaitingAcceptance: await this.storageService.get<boolean>(
|
||||
@ -182,7 +195,6 @@ export class StateMigrationService {
|
||||
v1Keys.installedVersion,
|
||||
options
|
||||
),
|
||||
lastActive: await this.storageService.get<number>(v1Keys.lastActive, options),
|
||||
locale: await this.storageService.get<string>(v1Keys.locale, options),
|
||||
loginRedirect: null,
|
||||
mainWindowSize: null,
|
||||
@ -490,12 +502,19 @@ export class StateMigrationService {
|
||||
initialState.globals.environmentUrls =
|
||||
(await this.storageService.get<EnvironmentUrls>(v1Keys.environmentUrls, options)) ??
|
||||
new EnvironmentUrls();
|
||||
|
||||
await this.storageService.save("state", initialState, options);
|
||||
await this.storageService.save(keys.global, initialState.globals, options);
|
||||
await this.storageService.save(keys.activeUserId, initialState.activeUserId, options);
|
||||
if (initialState.activeUserId != null) {
|
||||
await this.storageService.save(
|
||||
initialState.activeUserId,
|
||||
initialState.accounts[initialState.activeUserId]
|
||||
);
|
||||
}
|
||||
await this.storageService.save(keys.authenticatedAccounts, initialState.authenticatedAccounts);
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}_masterkey_biometric`,
|
||||
`${userId}${partialKeys.biometricKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }),
|
||||
{ keySuffix: "biometric" }
|
||||
);
|
||||
@ -504,7 +523,7 @@ export class StateMigrationService {
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}_masterkey_auto`,
|
||||
`${userId}${partialKeys.autoKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }),
|
||||
{ keySuffix: "auto" }
|
||||
);
|
||||
@ -513,7 +532,7 @@ export class StateMigrationService {
|
||||
|
||||
if (await this.secureStorageService.has(v1Keys.key)) {
|
||||
await this.secureStorageService.save(
|
||||
`${userId}_masterkey`,
|
||||
`${userId}${partialKeys.masterKey}`,
|
||||
await this.secureStorageService.get(v1Keys.key)
|
||||
);
|
||||
await this.secureStorageService.remove(v1Keys.key);
|
||||
|
Loading…
Reference in New Issue
Block a user