mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
Ps/pm 2910/handle switch messaging (#6823)
* Handle switch messaging TODO: handle loading state for account switcher * Async updates required for state * Fallback to email for current account avatar * Await un-awaited promises * Remove unnecessary Prune Prune was getting confused in browser and deleting memory in browser on account switch. This method isn't needed since logout already removes memory data, which is the condition for pruning * Fix temp password in browser * Use direct memory access until data is serializable Safari uses a different message object extraction than firefox/chrome and is removing `UInt8Array`s. Until all data passed into StorageService is guaranteed serializable, we need to use direct access in state service * Reload badge and context menu on switch * Gracefully switch account as they log out. * Maintain location on account switch * Remove unused state definitions * Prefer null for state undefined can be misinterpreted to indicate a value has not been set. * Hack: structured clone in memory storage We are currently getting dead objects on account switch due to updating the object in the foreground state service. However, the storage service is owned by the background. This structured clone hack ensures that all objects stored in memory are owned by the appropriate context * Null check nullable values active account can be null, so we should include null safety in the equality * Correct background->foreground switch command * Already providing background memory storage * Handle connection and clipboard on switch account * Prefer strict equal * Ensure structuredClone is available to jsdom This is a deficiency in jsdom -- https://github.com/jsdom/jsdom/issues/3363 -- structured clone is well supported. * Fixup types in faker class
This commit is contained in:
@@ -17,6 +17,7 @@ import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/
|
||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { SendView } from "../../tools/send/models/view/send.view";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UriMatchType } from "../../vault/enums";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../vault/models/data/collection.data";
|
||||
@@ -48,7 +49,7 @@ export abstract class StateService<T extends Account = Account> {
|
||||
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
setActiveUser: (userId: string) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<UserId>;
|
||||
init: () => Promise<void>;
|
||||
|
||||
getAccessToken: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -29,6 +29,9 @@ export class MemoryStorageService extends AbstractMemoryStorageService {
|
||||
if (obj == null) {
|
||||
return this.remove(key);
|
||||
}
|
||||
// TODO: Remove once foreground/background contexts are separated in browser
|
||||
// Needed to ensure ownership of all memory by the context running the storage service
|
||||
obj = structuredClone(obj);
|
||||
this.store.set(key, obj);
|
||||
this.updatesSubject.next({ key, updateType: "save" });
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -173,13 +173,13 @@ export class StateService<
|
||||
// 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 });
|
||||
this.accountService.addAccount(state.activeUserId as UserId, {
|
||||
await this.accountService.addAccount(state.activeUserId as UserId, {
|
||||
name: activeDiskAccount.profile.name,
|
||||
email: activeDiskAccount.profile.email,
|
||||
status: AuthenticationStatus.LoggedOut,
|
||||
});
|
||||
}
|
||||
this.accountService.switchAccount(state.activeUserId as UserId);
|
||||
await this.accountService.switchAccount(state.activeUserId as UserId);
|
||||
// End TODO
|
||||
|
||||
return state;
|
||||
@@ -198,7 +198,7 @@ export class StateService<
|
||||
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
||||
state.accounts[userId].profile = diskAccount.profile;
|
||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||
this.accountService.addAccount(userId as UserId, {
|
||||
await this.accountService.addAccount(userId as UserId, {
|
||||
status: AuthenticationStatus.Locked,
|
||||
name: diskAccount.profile.name,
|
||||
email: diskAccount.profile.email,
|
||||
@@ -218,7 +218,7 @@ export class StateService<
|
||||
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.
|
||||
this.accountService.addAccount(account.profile.userId as UserId, {
|
||||
await this.accountService.addAccount(account.profile.userId as UserId, {
|
||||
status: AuthenticationStatus.Locked,
|
||||
name: account.profile.name,
|
||||
email: account.profile.email,
|
||||
@@ -228,13 +228,13 @@ export class StateService<
|
||||
}
|
||||
|
||||
async setActiveUser(userId: string): Promise<void> {
|
||||
this.clearDecryptedDataForActiveUser();
|
||||
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.
|
||||
this.accountService.switchAccount(userId as UserId);
|
||||
await this.accountService.switchAccount(userId as UserId);
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -242,16 +242,18 @@ export class StateService<
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
async clean(options?: StorageOptions): Promise<void> {
|
||||
async clean(options?: StorageOptions): Promise<UserId> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
await this.deAuthenticateAccount(options.userId);
|
||||
if (options.userId === (await this.state())?.activeUserId) {
|
||||
await this.dynamicallySetActiveUser();
|
||||
let currentUser = (await this.state())?.activeUserId;
|
||||
if (options.userId === currentUser) {
|
||||
currentUser = await this.dynamicallySetActiveUser();
|
||||
}
|
||||
|
||||
await this.removeAccountFromDisk(options?.userId);
|
||||
this.removeAccountFromMemory(options?.userId);
|
||||
await this.removeAccountFromMemory(options?.userId);
|
||||
await this.pushAccounts();
|
||||
return currentUser as UserId;
|
||||
}
|
||||
|
||||
async getAccessToken(options?: StorageOptions): Promise<string> {
|
||||
@@ -577,7 +579,7 @@ export class StateService<
|
||||
);
|
||||
|
||||
const nextStatus = value != null ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked;
|
||||
this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
||||
await this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
||||
|
||||
if (options.userId == this.activeAccountSubject.getValue()) {
|
||||
const nextValue = value != null;
|
||||
@@ -613,7 +615,7 @@ export class StateService<
|
||||
);
|
||||
|
||||
const nextStatus = value != null ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked;
|
||||
this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
||||
await this.accountService.setAccountStatus(options.userId as UserId, nextStatus);
|
||||
|
||||
if (options?.userId == this.activeAccountSubject.getValue()) {
|
||||
const nextValue = value != null;
|
||||
@@ -3137,7 +3139,6 @@ export class StateService<
|
||||
}
|
||||
|
||||
protected async pushAccounts(): Promise<void> {
|
||||
await this.pruneInMemoryAccounts();
|
||||
await this.state().then((state) => {
|
||||
if (state.accounts == null || Object.keys(state.accounts).length < 1) {
|
||||
this.accountsSubject.next({});
|
||||
@@ -3253,16 +3254,7 @@ export class StateService<
|
||||
return state;
|
||||
});
|
||||
// TODO: Invert this logic, we should remove accounts based on logged out emit
|
||||
this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut);
|
||||
}
|
||||
|
||||
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 (await this.state())?.accounts) {
|
||||
if (!(await this.getIsAuthenticated({ userId: userId }))) {
|
||||
await this.removeAccountFromMemory(userId);
|
||||
}
|
||||
}
|
||||
await this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut);
|
||||
}
|
||||
|
||||
// settings persist even on reset, and are not affected by this method
|
||||
@@ -3333,18 +3325,22 @@ export class StateService<
|
||||
const accounts = (await this.state())?.accounts;
|
||||
if (accounts == null || Object.keys(accounts).length < 1) {
|
||||
await this.setActiveUser(null);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
let newActiveUser;
|
||||
for (const userId in accounts) {
|
||||
if (userId == null) {
|
||||
continue;
|
||||
}
|
||||
if (await this.getIsAuthenticated({ userId: userId })) {
|
||||
await this.setActiveUser(userId);
|
||||
newActiveUser = userId;
|
||||
break;
|
||||
}
|
||||
await this.setActiveUser(null);
|
||||
newActiveUser = null;
|
||||
}
|
||||
await this.setActiveUser(newActiveUser);
|
||||
return newActiveUser;
|
||||
}
|
||||
|
||||
private async getTimeoutBasedStorageOptions(options?: StorageOptions): Promise<StorageOptions> {
|
||||
|
||||
@@ -3,5 +3,6 @@ export { GlobalState } from "./global-state";
|
||||
export { GlobalStateProvider } from "./global-state.provider";
|
||||
export { UserState } from "./user-state";
|
||||
export { UserStateProvider } from "./user-state.provider";
|
||||
export { KeyDefinition } from "./key-definition";
|
||||
|
||||
export * from "./key-definitions";
|
||||
export * from "./state-definitions";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AccountInfo } from "../../auth/abstractions/account.service";
|
||||
import { AccountsDeserializer } from "../../auth/services/account.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import { KeyDefinition } from "./key-definition";
|
||||
import { StateDefinition } from "./state-definition";
|
||||
|
||||
const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const ACCOUNT_ACCOUNTS = new KeyDefinition<Record<UserId, AccountInfo>>(
|
||||
ACCOUNT_MEMORY,
|
||||
"accounts",
|
||||
{
|
||||
deserializer: (obj) => AccountsDeserializer(obj),
|
||||
}
|
||||
);
|
||||
export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", {
|
||||
deserializer: (id: UserId) => id,
|
||||
});
|
||||
3
libs/common/src/platform/state/state-definitions.ts
Normal file
3
libs/common/src/platform/state/state-definitions.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { StateDefinition } from "./state-definition";
|
||||
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
Reference in New Issue
Block a user