[feat(Account Switching)] Allow for extending application state (#584)
* [feat(Account Switching)] Allow for extending application state * [bug(Account Switching)] Remove hardcoded dev urls * [bug(Account Switching)] Init Account when signing in * [bug(Account Switching)] Check for state migration version in local storage for web * [bug(Account Switching)] Fix never lock configurations * [chore] Prettier merge * [bug] Move environmentUrls to global state * [chore] Ran prettier * [bug]change storage location for enityId and type * [style] Ran prettier Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
This commit is contained in:
parent
59a5300458
commit
9e26336549
|
@ -24,11 +24,11 @@ import { CollectionView } from "../models/view/collectionView";
|
|||
import { FolderView } from "../models/view/folderView";
|
||||
import { SendView } from "../models/view/sendView";
|
||||
|
||||
export abstract class StateService {
|
||||
accounts: BehaviorSubject<{ [userId: string]: Account }>;
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
accounts: BehaviorSubject<{ [userId: string]: T }>;
|
||||
activeAccount: BehaviorSubject<string>;
|
||||
|
||||
addAccount: (account: Account) => Promise<void>;
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
setActiveUser: (userId: string) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
init: () => Promise<void>;
|
||||
|
|
|
@ -130,9 +130,6 @@ export class AccountSettings {
|
|||
enableMinimizeToTray?: boolean;
|
||||
enableStartToTray?: boolean;
|
||||
enableTray?: boolean;
|
||||
environmentUrls?: any = {
|
||||
server: "bitwarden.com",
|
||||
};
|
||||
equivalentDomains?: any;
|
||||
minimizeOnCopyToClipboard?: boolean;
|
||||
neverDomains?: { [id: string]: any };
|
||||
|
|
|
@ -24,4 +24,7 @@ export class GlobalState {
|
|||
noAutoPromptBiometrics?: boolean;
|
||||
noAutoPromptBiometricsText?: string;
|
||||
stateVersion: number;
|
||||
environmentUrls?: any = {
|
||||
server: "bitwarden.com",
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Account } from "./account";
|
||||
import { GlobalState } from "./globalState";
|
||||
|
||||
export class State {
|
||||
accounts: { [userId: string]: Account } = {};
|
||||
export class State<TAccount extends Account = Account> {
|
||||
accounts: { [userId: string]: TAccount } = {};
|
||||
globals: GlobalState = new GlobalState();
|
||||
activeUserId: string;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,13 @@ import { HashPurpose } from "../enums/hashPurpose";
|
|||
import { KdfType } from "../enums/kdfType";
|
||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
|
||||
import { Account, AccountData, AccountProfile, AccountTokens } from "../models/domain/account";
|
||||
import {
|
||||
Account,
|
||||
AccountData,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../models/domain/account";
|
||||
import { AuthResult } from "../models/domain/authResult";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
|
||||
|
@ -538,27 +544,34 @@ export class AuthService implements AuthServiceAbstraction {
|
|||
result.forcePasswordReset = tokenResponse.forcePasswordReset;
|
||||
|
||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||
await this.stateService.addAccount({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
apiKeyClientId: clientId,
|
||||
apiKeyClientSecret: clientSecret,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
apiKeyClientId: clientId,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
},
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...{
|
||||
apiKeyClientSecret: clientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (tokenResponse.twoFactorToken != null) {
|
||||
await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email);
|
||||
|
|
|
@ -761,13 +761,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
|
||||
// Helpers
|
||||
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
|
||||
if (
|
||||
(await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) ||
|
||||
(await this.shouldStoreKey(KeySuffixOptions.Biometric, userId))
|
||||
) {
|
||||
await this.stateService.setCryptoMasterKeyB64(key.keyB64, { userId: userId });
|
||||
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
|
||||
} else if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
|
||||
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
|
||||
} else {
|
||||
await this.stateService.setCryptoMasterKeyB64(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -109,18 +109,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const urlsObj: any = await this.stateService.getEnvironmentUrls();
|
||||
const urls = urlsObj || {
|
||||
base: null,
|
||||
api: null,
|
||||
identity: null,
|
||||
icons: null,
|
||||
notifications: null,
|
||||
events: null,
|
||||
webVault: null,
|
||||
keyConnector: null,
|
||||
};
|
||||
|
||||
const urls: any = await this.stateService.getEnvironmentUrls();
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
if (urls.base) {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
||||
|
||||
import { Account } from "../models/domain/account";
|
||||
import {
|
||||
Account,
|
||||
AccountData,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../models/domain/account";
|
||||
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
|
@ -34,19 +40,21 @@ import { SendData } from "../models/data/sendData";
|
|||
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { StateMigrationService } from "./stateMigration.service";
|
||||
import { StateMigrationService } from "../abstractions/stateMigration.service";
|
||||
|
||||
export class StateService implements StateServiceAbstraction {
|
||||
accounts = new BehaviorSubject<{ [userId: string]: Account }>({});
|
||||
export class StateService<TAccount extends Account = Account>
|
||||
implements StateServiceAbstraction<TAccount>
|
||||
{
|
||||
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||
activeAccount = new BehaviorSubject<string>(null);
|
||||
|
||||
private state: State = new State();
|
||||
protected state: State<TAccount> = new State<TAccount>();
|
||||
|
||||
constructor(
|
||||
private storageService: StorageService,
|
||||
private secureStorageService: StorageService,
|
||||
private logService: LogService,
|
||||
private stateMigrationService: StateMigrationService
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected logService: LogService,
|
||||
protected stateMigrationService: StateMigrationService
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
|
@ -60,7 +68,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
|
||||
async loadStateFromDisk() {
|
||||
if ((await this.getActiveUserIdFromStorage()) != null) {
|
||||
const diskState = await this.storageService.get<State>(
|
||||
const diskState = await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
|
@ -71,10 +79,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
}
|
||||
}
|
||||
|
||||
async addAccount(account: Account) {
|
||||
if (account?.profile?.userId == null) {
|
||||
return;
|
||||
}
|
||||
async addAccount(account: TAccount) {
|
||||
this.state.accounts[account.profile.userId] = account;
|
||||
await this.scaffoldNewAccountStorage(account);
|
||||
await this.setActiveUser(account.profile.userId);
|
||||
|
@ -83,7 +88,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
|
||||
async setActiveUser(userId: string): Promise<void> {
|
||||
this.state.activeUserId = userId;
|
||||
const storedState = await this.storageService.get<State>(
|
||||
const storedState = await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskOptions()
|
||||
);
|
||||
|
@ -1321,47 +1326,64 @@ export class StateService implements StateServiceAbstraction {
|
|||
}
|
||||
|
||||
async getEntityId(options?: StorageOptions): Promise<string> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||
?.profile?.entityId;
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.profile?.entityId;
|
||||
}
|
||||
|
||||
async setEntityId(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
account.profile.entityId = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEntityType(options?: StorageOptions): Promise<any> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
||||
?.profile?.entityType;
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.profile?.entityType;
|
||||
}
|
||||
|
||||
async setEntityType(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
account.profile.entityType = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEnvironmentUrls(options?: StorageOptions): Promise<any> {
|
||||
return (
|
||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.settings?.environmentUrls ?? {
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.environmentUrls ?? {
|
||||
base: null,
|
||||
api: null,
|
||||
identity: null,
|
||||
icons: null,
|
||||
notifications: null,
|
||||
events: null,
|
||||
webVault: null,
|
||||
keyConnector: null,
|
||||
// TODO: this is a bug and we should use base instead for the server detail in the account switcher, otherwise self hosted urls will not show correctly
|
||||
server: "bitwarden.com",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async setEnvironmentUrls(value: any, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
account.settings.environmentUrls = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
globals.environmentUrls = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
@ -1402,17 +1424,20 @@ export class StateService implements StateServiceAbstraction {
|
|||
|
||||
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))?.profile
|
||||
?.everBeenUnlocked ?? false
|
||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.profile?.everBeenUnlocked ?? false
|
||||
);
|
||||
}
|
||||
|
||||
async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
account.profile.everBeenUnlocked = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getForcePasswordReset(options?: StorageOptions): Promise<boolean> {
|
||||
|
@ -1981,10 +2006,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
const accountVaultTimeout = (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.settings?.vaultTimeout;
|
||||
const globalVaultTimeout = (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.vaultTimeout;
|
||||
return accountVaultTimeout ?? globalVaultTimeout ?? 15;
|
||||
return accountVaultTimeout;
|
||||
}
|
||||
|
||||
async setVaultTimeout(value: number, options?: StorageOptions): Promise<void> {
|
||||
|
@ -2047,7 +2069,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
private async getGlobals(options: StorageOptions): Promise<GlobalState> {
|
||||
protected async getGlobals(options: StorageOptions): Promise<GlobalState> {
|
||||
let globals: GlobalState;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
globals = this.getGlobalsFromMemory();
|
||||
|
@ -2060,39 +2082,41 @@ export class StateService implements StateServiceAbstraction {
|
|||
return globals ?? new GlobalState();
|
||||
}
|
||||
|
||||
private async saveGlobals(globals: GlobalState, options: StorageOptions) {
|
||||
protected async saveGlobals(globals: GlobalState, options: StorageOptions) {
|
||||
return this.useMemory(options.storageLocation)
|
||||
? this.saveGlobalsToMemory(globals)
|
||||
: await this.saveGlobalsToDisk(globals, options);
|
||||
}
|
||||
|
||||
private getGlobalsFromMemory(): GlobalState {
|
||||
protected getGlobalsFromMemory(): GlobalState {
|
||||
return this.state.globals;
|
||||
}
|
||||
|
||||
private async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
|
||||
return (await this.storageService.get<State>("state", options))?.globals;
|
||||
protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
|
||||
return (await this.storageService.get<State<TAccount>>("state", options))?.globals;
|
||||
}
|
||||
|
||||
private saveGlobalsToMemory(globals: GlobalState): void {
|
||||
protected saveGlobalsToMemory(globals: GlobalState): void {
|
||||
this.state.globals = globals;
|
||||
}
|
||||
|
||||
private async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
|
||||
protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
|
||||
if (options.useSecureStorage) {
|
||||
const state = (await this.secureStorageService.get<State>("state", options)) ?? new State();
|
||||
const state =
|
||||
(await this.secureStorageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||
state.globals = globals;
|
||||
await this.secureStorageService.save("state", state, options);
|
||||
} else {
|
||||
const state = (await this.storageService.get<State>("state", options)) ?? new State();
|
||||
const state =
|
||||
(await this.storageService.get<State<TAccount>>("state", options)) ?? new State();
|
||||
state.globals = globals;
|
||||
await this.saveStateToStorage(state, options);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAccount(options: StorageOptions): Promise<Account> {
|
||||
protected async getAccount(options: StorageOptions): Promise<TAccount> {
|
||||
try {
|
||||
let account: Account;
|
||||
let account: TAccount;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
account = this.getAccountFromMemory(options);
|
||||
}
|
||||
|
@ -2101,51 +2125,51 @@ export class StateService implements StateServiceAbstraction {
|
|||
account = await this.getAccountFromDisk(options);
|
||||
}
|
||||
|
||||
return account != null ? new Account(account) : null;
|
||||
return account;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private getAccountFromMemory(options: StorageOptions): Account {
|
||||
protected getAccountFromMemory(options: StorageOptions): TAccount {
|
||||
if (this.state.accounts == null) {
|
||||
return null;
|
||||
}
|
||||
return this.state.accounts[this.getUserIdFromMemory(options)];
|
||||
}
|
||||
|
||||
private getUserIdFromMemory(options: StorageOptions): string {
|
||||
protected getUserIdFromMemory(options: StorageOptions): string {
|
||||
return options?.userId != null
|
||||
? this.state.accounts[options.userId]?.profile?.userId
|
||||
: this.state.activeUserId;
|
||||
}
|
||||
|
||||
private async getAccountFromDisk(options: StorageOptions): Promise<Account> {
|
||||
protected async getAccountFromDisk(options: StorageOptions): Promise<TAccount> {
|
||||
if (options?.userId == null && this.state.activeUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = options?.useSecureStorage
|
||||
? (await this.secureStorageService.get<State>("state", options)) ??
|
||||
(await this.storageService.get<State>(
|
||||
? (await this.secureStorageService.get<State<TAccount>>("state", options)) ??
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local })
|
||||
))
|
||||
: await this.storageService.get<State>("state", options);
|
||||
: await this.storageService.get<State<TAccount>>("state", options);
|
||||
|
||||
return state?.accounts[options?.userId ?? this.state.activeUserId];
|
||||
}
|
||||
|
||||
private useMemory(storageLocation: StorageLocation) {
|
||||
protected useMemory(storageLocation: StorageLocation) {
|
||||
return storageLocation === StorageLocation.Memory || storageLocation === StorageLocation.Both;
|
||||
}
|
||||
|
||||
private useDisk(storageLocation: StorageLocation) {
|
||||
protected useDisk(storageLocation: StorageLocation) {
|
||||
return storageLocation === StorageLocation.Disk || storageLocation === StorageLocation.Both;
|
||||
}
|
||||
|
||||
private async saveAccount(
|
||||
account: Account,
|
||||
protected async saveAccount(
|
||||
account: TAccount,
|
||||
options: StorageOptions = {
|
||||
storageLocation: StorageLocation.Both,
|
||||
useSecureStorage: false,
|
||||
|
@ -2156,86 +2180,75 @@ export class StateService implements StateServiceAbstraction {
|
|||
: await this.saveAccountToDisk(account, options);
|
||||
}
|
||||
|
||||
private async saveAccountToDisk(account: Account, options: StorageOptions): Promise<void> {
|
||||
protected async saveAccountToDisk(account: TAccount, options: StorageOptions): Promise<void> {
|
||||
const storageLocation = options.useSecureStorage
|
||||
? this.secureStorageService
|
||||
: this.storageService;
|
||||
|
||||
const state = (await storageLocation.get<State>("state", options)) ?? new State();
|
||||
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 this.pushAccounts();
|
||||
}
|
||||
|
||||
private async saveAccountToMemory(account: Account): Promise<void> {
|
||||
protected async saveAccountToMemory(account: TAccount): Promise<void> {
|
||||
if (this.getAccountFromMemory({ userId: account.profile.userId }) !== null) {
|
||||
this.state.accounts[account.profile.userId] = account;
|
||||
}
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountStorage(account: Account): Promise<void> {
|
||||
protected async scaffoldNewAccountStorage(account: TAccount): Promise<void> {
|
||||
await this.scaffoldNewAccountLocalStorage(account);
|
||||
await this.scaffoldNewAccountSessionStorage(account);
|
||||
await this.scaffoldNewAccountMemoryStorage(account);
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountLocalStorage(account: Account): Promise<void> {
|
||||
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State>("state", await this.defaultOnDiskLocalOptions())) ??
|
||||
new State();
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskLocalOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
account = {
|
||||
settings: storedAccount.settings,
|
||||
profile: account.profile,
|
||||
tokens: account.tokens,
|
||||
keys: account.keys,
|
||||
data: account.data,
|
||||
};
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskLocalOptions());
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountMemoryStorage(account: Account): Promise<void> {
|
||||
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State>("state", await this.defaultOnDiskMemoryOptions())) ??
|
||||
new State();
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskMemoryOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
account = {
|
||||
settings: storedAccount.settings,
|
||||
profile: account.profile,
|
||||
tokens: account.tokens,
|
||||
keys: account.keys,
|
||||
data: account.data,
|
||||
};
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskMemoryOptions());
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountSessionStorage(account: Account): Promise<void> {
|
||||
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
||||
const storedState =
|
||||
(await this.storageService.get<State>("state", await this.defaultOnDiskOptions())) ??
|
||||
new State();
|
||||
(await this.storageService.get<State<TAccount>>(
|
||||
"state",
|
||||
await this.defaultOnDiskOptions()
|
||||
)) ?? new State<TAccount>();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
account = {
|
||||
settings: storedAccount.settings,
|
||||
profile: account.profile,
|
||||
tokens: account.tokens,
|
||||
keys: account.keys,
|
||||
data: account.data,
|
||||
};
|
||||
account.settings = storedAccount.settings;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.saveStateToStorage(storedState, await this.defaultOnDiskOptions());
|
||||
}
|
||||
|
||||
private async pushAccounts(): Promise<void> {
|
||||
protected async pushAccounts(): Promise<void> {
|
||||
await this.pruneInMemoryAccounts();
|
||||
if (this.state?.accounts == null || Object.keys(this.state.accounts).length < 1) {
|
||||
this.accounts.next(null);
|
||||
|
@ -2245,7 +2258,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
this.accounts.next(this.state.accounts);
|
||||
}
|
||||
|
||||
private reconcileOptions(
|
||||
protected reconcileOptions(
|
||||
requestedOptions: StorageOptions,
|
||||
defaultOptions: StorageOptions
|
||||
): StorageOptions {
|
||||
|
@ -2263,11 +2276,11 @@ export class StateService implements StateServiceAbstraction {
|
|||
return requestedOptions;
|
||||
}
|
||||
|
||||
private get defaultInMemoryOptions(): StorageOptions {
|
||||
protected get defaultInMemoryOptions(): StorageOptions {
|
||||
return { storageLocation: StorageLocation.Memory, userId: this.state.activeUserId };
|
||||
}
|
||||
|
||||
private async defaultOnDiskOptions(): Promise<StorageOptions> {
|
||||
protected async defaultOnDiskOptions(): Promise<StorageOptions> {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||
|
@ -2276,7 +2289,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
private async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
||||
protected async defaultOnDiskLocalOptions(): Promise<StorageOptions> {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
|
@ -2285,7 +2298,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
private async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
||||
protected async defaultOnDiskMemoryOptions(): Promise<StorageOptions> {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Memory,
|
||||
|
@ -2294,7 +2307,7 @@ export class StateService implements StateServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
private async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
||||
protected async defaultSecureStorageOptions(): Promise<StorageOptions> {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
useSecureStorage: true,
|
||||
|
@ -2302,46 +2315,38 @@ export class StateService implements StateServiceAbstraction {
|
|||
};
|
||||
}
|
||||
|
||||
private async getActiveUserIdFromStorage(): Promise<string> {
|
||||
const state = await this.storageService.get<State>("state");
|
||||
protected async getActiveUserIdFromStorage(): Promise<string> {
|
||||
const state = await this.storageService.get<State<TAccount>>("state");
|
||||
return state?.activeUserId;
|
||||
}
|
||||
|
||||
private async removeAccountFromLocalStorage(
|
||||
protected async removeAccountFromLocalStorage(
|
||||
userId: string = this.state.activeUserId
|
||||
): Promise<void> {
|
||||
const state = await this.storageService.get<State>("state", {
|
||||
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
});
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.accounts[userId] = new Account({
|
||||
settings: state.accounts[userId].settings,
|
||||
});
|
||||
|
||||
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||
await this.saveStateToStorage(state, await this.defaultOnDiskLocalOptions());
|
||||
}
|
||||
|
||||
private async removeAccountFromSessionStorage(
|
||||
protected async removeAccountFromSessionStorage(
|
||||
userId: string = this.state.activeUserId
|
||||
): Promise<void> {
|
||||
const state = await this.storageService.get<State>("state", {
|
||||
const state = await this.storageService.get<State<TAccount>>("state", {
|
||||
htmlStorageLocation: HtmlStorageLocation.Session,
|
||||
});
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.accounts[userId] = new Account({
|
||||
settings: state.accounts[userId].settings,
|
||||
});
|
||||
|
||||
state.accounts[userId] = this.resetAccount(state.accounts[userId]);
|
||||
await this.saveStateToStorage(state, await this.defaultOnDiskOptions());
|
||||
}
|
||||
|
||||
private async removeAccountFromSecureStorage(
|
||||
protected async removeAccountFromSecureStorage(
|
||||
userId: string = this.state.activeUserId
|
||||
): Promise<void> {
|
||||
await this.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
|
@ -2349,15 +2354,18 @@ export class StateService implements StateServiceAbstraction {
|
|||
await this.setCryptoMasterKeyB64(null, { userId: userId });
|
||||
}
|
||||
|
||||
private removeAccountFromMemory(userId: string = this.state.activeUserId): void {
|
||||
protected removeAccountFromMemory(userId: string = this.state.activeUserId): void {
|
||||
delete this.state.accounts[userId];
|
||||
}
|
||||
|
||||
private async saveStateToStorage(state: State, options: StorageOptions): Promise<void> {
|
||||
protected async saveStateToStorage(
|
||||
state: State<TAccount>,
|
||||
options: StorageOptions
|
||||
): Promise<void> {
|
||||
await this.storageService.save("state", state, options);
|
||||
}
|
||||
|
||||
private async pruneInMemoryAccounts() {
|
||||
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) {
|
||||
if (!(await this.getIsAuthenticated({ userId: userId }))) {
|
||||
|
@ -2365,4 +2373,13 @@ export class StateService implements StateServiceAbstraction {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// settings persist even on reset
|
||||
protected resetAccount(account: TAccount) {
|
||||
account.data = new AccountData();
|
||||
account.keys = new AccountKeys();
|
||||
account.profile = new AccountProfile();
|
||||
account.tokens = new AccountTokens();
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,19 +114,22 @@ export class StateMigrationService {
|
|||
readonly latestVersion: number = 2;
|
||||
|
||||
constructor(
|
||||
private storageService: StorageService,
|
||||
private secureStorageService: StorageService
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService
|
||||
) {}
|
||||
|
||||
async needsMigration(): Promise<boolean> {
|
||||
const currentStateVersion = (await this.storageService.get<State>("state"))?.globals
|
||||
?.stateVersion;
|
||||
const currentStateVersion = (
|
||||
await this.storageService.get<State<Account>>("state", {
|
||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
||||
})
|
||||
)?.globals?.stateVersion;
|
||||
return currentStateVersion == null || currentStateVersion < this.latestVersion;
|
||||
}
|
||||
|
||||
async migrate(): Promise<void> {
|
||||
let currentStateVersion =
|
||||
(await this.storageService.get<State>("state"))?.globals?.stateVersion ?? 1;
|
||||
(await this.storageService.get<State<Account>>("state"))?.globals?.stateVersion ?? 1;
|
||||
while (currentStateVersion < this.latestVersion) {
|
||||
switch (currentStateVersion) {
|
||||
case 1:
|
||||
|
@ -138,10 +141,10 @@ export class StateMigrationService {
|
|||
}
|
||||
}
|
||||
|
||||
private async migrateStateFrom1To2(): Promise<void> {
|
||||
protected async migrateStateFrom1To2(): Promise<void> {
|
||||
const options: StorageOptions = { htmlStorageLocation: HtmlStorageLocation.Local };
|
||||
const userId = await this.storageService.get<string>("userId");
|
||||
const initialState: State =
|
||||
const initialState: State<Account> =
|
||||
userId == null
|
||||
? {
|
||||
globals: {
|
||||
|
@ -174,6 +177,7 @@ export class StateMigrationService {
|
|||
v1Keys.enableBiometric,
|
||||
options
|
||||
),
|
||||
environmentUrls: await this.storageService.get<any>(v1Keys.environmentUrls, options),
|
||||
installedVersion: await this.storageService.get<string>(
|
||||
v1Keys.installedVersion,
|
||||
options
|
||||
|
@ -439,10 +443,6 @@ export class StateMigrationService {
|
|||
options
|
||||
),
|
||||
enableTray: await this.storageService.get<boolean>(v1Keys.enableTray, options),
|
||||
environmentUrls: await this.storageService.get<any>(
|
||||
v1Keys.environmentUrls,
|
||||
options
|
||||
),
|
||||
equivalentDomains: await this.storageService.get<any>(
|
||||
v1Keys.equivalentDomains,
|
||||
options
|
||||
|
|
|
@ -53,7 +53,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
async isLocked(userId?: string): Promise<boolean> {
|
||||
const neverLock =
|
||||
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
|
||||
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
||||
(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
||||
if (neverLock) {
|
||||
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
|
||||
// We should refactor here.
|
||||
|
|
|
@ -9,7 +9,7 @@ import { StringResponse } from "./models/response/stringResponse";
|
|||
|
||||
export abstract class BaseProgram {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
protected stateService: StateService,
|
||||
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
|
||||
) {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue