mirror of
https://github.com/bitwarden/jslib
synced 2025-12-19 17:53:48 +00:00
[bug] Scaffold memory storage for web
Not properly creating storage objects on signin was creating weird behavior when logging out, locking, and logging back in. Namely, encrypted data that was recently synced had nowhere to save to and was lost.
This commit is contained in:
@@ -33,101 +33,101 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||
}
|
||||
|
||||
export class AccountData {
|
||||
ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<CipherData, CipherView>();
|
||||
folders?: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<FolderData, FolderView>();
|
||||
localData?: any;
|
||||
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
|
||||
collections?: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<CollectionData, CollectionView>();
|
||||
kdfIterations?: number;
|
||||
kdfType?: KdfType;
|
||||
policies?: DataEncryptionPair<PolicyData, Policy> = new DataEncryptionPair<PolicyData, Policy>();
|
||||
passwordGenerationHistory?: EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
||||
addEditCipherInfo?: any;
|
||||
collapsedGroupings?: Set<string>;
|
||||
eventCollection?: EventData[];
|
||||
organizations?: { [id: string]: OrganizationData };
|
||||
providers?: { [id: string]: ProviderData };
|
||||
ciphers: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair<CipherData, CipherView>();
|
||||
folders: DataEncryptionPair<FolderData, FolderView> = new DataEncryptionPair<FolderData, FolderView>();
|
||||
localData: any;
|
||||
sends: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
|
||||
collections: DataEncryptionPair<CollectionData, CollectionView> = new DataEncryptionPair<CollectionData, CollectionView>();
|
||||
policies: DataEncryptionPair<PolicyData, Policy> = new DataEncryptionPair<PolicyData, Policy>();
|
||||
passwordGenerationHistory: EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
||||
addEditCipherInfo: any;
|
||||
collapsedGroupings: Set<string>;
|
||||
eventCollection: EventData[];
|
||||
organizations: { [id: string]: OrganizationData };
|
||||
providers: { [id: string]: ProviderData };
|
||||
}
|
||||
|
||||
export class AccountKeys {
|
||||
cryptoMasterKey?: SymmetricCryptoKey;
|
||||
cryptoMasterKeyAuto?: SymmetricCryptoKey;
|
||||
cryptoMasterKeyB64?: string;
|
||||
cryptoMasterKeyBiometric?: SymmetricCryptoKey;
|
||||
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<string, SymmetricCryptoKey>();
|
||||
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
|
||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
|
||||
keyHash?: string;
|
||||
legacyEtmKey?: SymmetricCryptoKey;
|
||||
publicKey?: ArrayBuffer;
|
||||
cryptoMasterKey: SymmetricCryptoKey;
|
||||
cryptoMasterKeyAuto: SymmetricCryptoKey;
|
||||
cryptoMasterKeyB64: string;
|
||||
cryptoMasterKeyBiometric: SymmetricCryptoKey;
|
||||
cryptoSymmetricKey: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<string, SymmetricCryptoKey>();
|
||||
organizationKeys: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
|
||||
providerKeys: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<any, Map<string, SymmetricCryptoKey>>();
|
||||
privateKey: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
legacyEtmKey: SymmetricCryptoKey;
|
||||
publicKey: ArrayBuffer;
|
||||
apiKeyClientSecret: string;
|
||||
}
|
||||
|
||||
export class AccountProfile {
|
||||
apiKeyClientId?: string;
|
||||
apiKeyClientSecret?: string;
|
||||
authenticationStatus?: AuthenticationStatus;
|
||||
convertAccountToKeyConnector?: boolean;
|
||||
email?: string;
|
||||
emailVerified?: boolean;
|
||||
entityId?: string;
|
||||
entityType?: string;
|
||||
everBeenUnlocked?: boolean;
|
||||
forcePasswordReset?: boolean;
|
||||
hasPremiumPersonally?: boolean;
|
||||
lastActive?: number;
|
||||
lastSync?: string;
|
||||
ssoCodeVerifier?: string;
|
||||
ssoOrganizationIdentifier?: string;
|
||||
ssoState?: string;
|
||||
userId?: string;
|
||||
usesKeyConnector?: boolean;
|
||||
apiKeyClientId: string;
|
||||
authenticationStatus: AuthenticationStatus;
|
||||
convertAccountToKeyConnector: boolean;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
entityId: string;
|
||||
entityType: string;
|
||||
everBeenUnlocked: boolean;
|
||||
forcePasswordReset: boolean;
|
||||
hasPremiumPersonally: boolean;
|
||||
lastActive: number;
|
||||
lastSync: string;
|
||||
ssoCodeVerifier: string;
|
||||
ssoOrganizationIdentifier: string;
|
||||
ssoState: string;
|
||||
userId: string;
|
||||
usesKeyConnector: boolean;
|
||||
keyHash: string;
|
||||
kdfIterations: number;
|
||||
kdfType: KdfType;
|
||||
}
|
||||
|
||||
export class AccountSettings {
|
||||
alwaysShowDock?: boolean;
|
||||
autoConfirmFingerPrints?: boolean;
|
||||
autoFillOnPageLoadDefault?: boolean;
|
||||
biometricLocked?: boolean;
|
||||
biometricText?: string;
|
||||
biometricUnlock?: boolean;
|
||||
clearClipboard?: number;
|
||||
defaultUriMatch?: UriMatchType;
|
||||
disableAddLoginNotification?: boolean;
|
||||
disableAutoBiometricsPrompt?: boolean;
|
||||
disableAutoTotpCopy?: boolean;
|
||||
disableBadgeCounter?: boolean;
|
||||
disableChangedPasswordNotification?: boolean;
|
||||
disableContextMenuItem?: boolean;
|
||||
disableGa?: boolean;
|
||||
dontShowCardsCurrentTab?: boolean;
|
||||
dontShowIdentitiesCurrentTab?: boolean;
|
||||
enableAlwaysOnTop?: boolean;
|
||||
enableAutoFillOnPageLoad?: boolean;
|
||||
enableBiometric?: boolean;
|
||||
enableBiometrics?: boolean;
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableCloseToTray?: boolean;
|
||||
enableFullWidth?: boolean;
|
||||
enableGravitars?: boolean;
|
||||
enableMinimizeToTray?: boolean;
|
||||
enableStartToTray?: boolean;
|
||||
enableTray?: boolean;
|
||||
environmentUrls?: any;
|
||||
alwaysShowDock: boolean;
|
||||
autoConfirmFingerPrints: boolean;
|
||||
autoFillOnPageLoadDefault: boolean;
|
||||
biometricLocked: boolean;
|
||||
biometricText: string;
|
||||
biometricUnlock: boolean;
|
||||
clearClipboard: number;
|
||||
defaultUriMatch: UriMatchType;
|
||||
disableAddLoginNotification: boolean;
|
||||
disableAutoBiometricsPrompt: boolean;
|
||||
disableAutoTotpCopy: boolean;
|
||||
disableBadgeCounter: boolean;
|
||||
disableChangedPasswordNotification: boolean;
|
||||
disableContextMenuItem: boolean;
|
||||
disableGa: boolean;
|
||||
dontShowCardsCurrentTab: boolean;
|
||||
dontShowIdentitiesCurrentTab: boolean;
|
||||
enableAlwaysOnTop: boolean;
|
||||
enableAutoFillOnPageLoad: boolean;
|
||||
enableBiometric: boolean;
|
||||
enableBiometrics: boolean;
|
||||
enableBrowserIntegration: boolean;
|
||||
enableBrowserIntegrationFingerprint: boolean;
|
||||
enableCloseToTray: boolean;
|
||||
enableFullWidth: boolean;
|
||||
enableGravitars: boolean;
|
||||
enableMinimizeToTray: boolean;
|
||||
enableStartToTray: boolean;
|
||||
enableTray: boolean;
|
||||
environmentUrls: any;
|
||||
equivalentDomains?: any;
|
||||
locale?: string;
|
||||
minimizeOnCopyToClipboard?: boolean;
|
||||
neverDomains?: { [id: string]: any };
|
||||
noAutoPromptBiometrics?: boolean;
|
||||
noAutoPromptBiometricsText?: string;
|
||||
openAtLogin?: boolean;
|
||||
passwordGenerationOptions?: any;
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
protectedPin?: string;
|
||||
settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string;
|
||||
locale: string;
|
||||
minimizeOnCopyToClipboard: boolean;
|
||||
neverDomains: { [id: string]: any };
|
||||
noAutoPromptBiometrics: boolean;
|
||||
noAutoPromptBiometricsText: string;
|
||||
openAtLogin: boolean;
|
||||
passwordGenerationOptions: any;
|
||||
pinProtected: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
protectedPin: string;
|
||||
settings: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
|
||||
vaultTimeout: number;
|
||||
vaultTimeoutAction: string;
|
||||
|
||||
get serverUrl(): string {
|
||||
return this.environmentUrls?.base ?? 'bitwarden.com';
|
||||
@@ -152,23 +152,23 @@ export class Account {
|
||||
Object.assign(this, {
|
||||
data: {
|
||||
...new AccountData(),
|
||||
...init.data,
|
||||
...init?.data,
|
||||
},
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...init.keys,
|
||||
...init?.keys,
|
||||
},
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...init.profile,
|
||||
...init?.profile,
|
||||
},
|
||||
settings: {
|
||||
...new AccountSettings(),
|
||||
...init.settings,
|
||||
...init?.settings,
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...init.tokens,
|
||||
...init?.tokens,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HashPurpose } from '../enums/hashPurpose';
|
||||
import { KdfType } from '../enums/kdfType';
|
||||
import { TwoFactorProviderType } from '../enums/twoFactorProviderType';
|
||||
|
||||
import { Account } from '../models/domain/account';
|
||||
import { Account, AccountData, AccountProfile, AccountTokens } from '../models/domain/account';
|
||||
import { AuthResult } from '../models/domain/authResult';
|
||||
import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey';
|
||||
|
||||
@@ -355,19 +355,23 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||
await this.stateService.addAccount({
|
||||
profile: {
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
apiKeyClientId: clientId,
|
||||
apiKeyClientSecret: clientSecret,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
},
|
||||
data: {
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
apiKeyClientId: clientId,
|
||||
apiKeyClientSecret: clientSecret,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -120,12 +120,12 @@ export class StateService implements StateServiceAbstraction {
|
||||
}
|
||||
|
||||
async getApiKeyClientSecret(options?: StorageOptions): Promise<string> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.apiKeyClientSecret;
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.apiKeyClientSecret;
|
||||
}
|
||||
|
||||
async setApiKeyClientSecret(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
account.profile.apiKeyClientId = value;
|
||||
account.keys.apiKeyClientSecret = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
}
|
||||
|
||||
@@ -619,7 +619,7 @@ export class StateService implements StateServiceAbstraction {
|
||||
}
|
||||
|
||||
async getEnableGravitars(options?: StorageOptions): Promise<boolean> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableGravitars ?? true;
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())))?.settings?.enableGravitars ?? false;
|
||||
}
|
||||
async setEnableGravitars(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()));
|
||||
@@ -831,30 +831,30 @@ export class StateService implements StateServiceAbstraction {
|
||||
}
|
||||
|
||||
async getKdfIterations(options?: StorageOptions): Promise<number> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.kdfIterations;
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfIterations;
|
||||
}
|
||||
async setKdfIterations(value: number, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
account.data.kdfIterations = value;
|
||||
account.profile.kdfIterations = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
}
|
||||
|
||||
async getKdfType(options?: StorageOptions): Promise<KdfType> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.data?.kdfType;
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.kdfType;
|
||||
}
|
||||
async setKdfType(value: KdfType, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
account.data.kdfType = value;
|
||||
account.profile.kdfType = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
}
|
||||
|
||||
async getKeyHash(options?: StorageOptions): Promise<string> {
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.keys?.keyHash;
|
||||
return (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))?.profile?.keyHash;
|
||||
}
|
||||
|
||||
async setKeyHash(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
account.keys.keyHash = value;
|
||||
account.profile.keyHash = value;
|
||||
await this.saveAccount(account, this.reconcileOptions(options, await this.defaultOnDiskOptions()));
|
||||
}
|
||||
|
||||
@@ -1284,7 +1284,14 @@ export class StateService implements StateServiceAbstraction {
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountStorage(account: Account): Promise<void> {
|
||||
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskOptions()) ?? new State();
|
||||
await this.scaffoldNewAccountLocalStorage(account);
|
||||
await this.scaffoldNewAccountSessionStorage(account);
|
||||
await this.scaffoldNewAccountMemoryStorage(account);
|
||||
await this.scaffoldNewAccountSecureStorage(account);
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountLocalStorage(account: Account): Promise<void> {
|
||||
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskLocalOptions()) ?? new State();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
storedAccount.tokens.accessToken = account.tokens.accessToken;
|
||||
@@ -1293,10 +1300,30 @@ export class StateService implements StateServiceAbstraction {
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.storageService.save('state', storedState, await this.defaultOnDiskLocalOptions());
|
||||
await this.storageService.save('state', storedState, await this.defaultOnDiskMemoryOptions());
|
||||
await this.storageService.save('state', storedState, await this.defaultOnDiskOptions());
|
||||
}
|
||||
|
||||
await this.scaffoldNewAccountSecureStorage(account);
|
||||
private async scaffoldNewAccountMemoryStorage(account: Account): Promise<void> {
|
||||
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskMemoryOptions()) ?? new State();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
storedAccount.tokens.accessToken = account.tokens.accessToken;
|
||||
storedAccount.tokens.refreshToken = account.tokens.refreshToken;
|
||||
account = storedAccount;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.storageService.save('state', storedState, await this.defaultOnDiskMemoryOptions());
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountSessionStorage(account: Account): Promise<void> {
|
||||
const storedState = await this.storageService.get<State>('state', await this.defaultOnDiskOptions()) ?? new State();
|
||||
const storedAccount = storedState.accounts[account.profile.userId];
|
||||
if (storedAccount != null) {
|
||||
storedAccount.tokens.accessToken = account.tokens.accessToken;
|
||||
storedAccount.tokens.refreshToken = account.tokens.refreshToken;
|
||||
account = storedAccount;
|
||||
}
|
||||
storedState.accounts[account.profile.userId] = account;
|
||||
await this.storageService.save('state', storedState, await this.defaultOnDiskOptions());
|
||||
}
|
||||
|
||||
private async scaffoldNewAccountSecureStorage(account: Account): Promise<void> {
|
||||
@@ -1368,7 +1395,7 @@ export class StateService implements StateServiceAbstraction {
|
||||
return {
|
||||
storageLocation: StorageLocation.Disk,
|
||||
htmlStorageLocation: HtmlStorageLocation.Memory,
|
||||
userId: await this.storageService.get('activeUserId'),
|
||||
userId: await this.getUserId(),
|
||||
useSecureStorage: false,
|
||||
};
|
||||
}
|
||||
@@ -1386,38 +1413,47 @@ export class StateService implements StateServiceAbstraction {
|
||||
return state?.activeUserId;
|
||||
}
|
||||
|
||||
private async removeAccountFromLocalStorage(userId: string): Promise<void> {
|
||||
private async removeAccountFromLocalStorage(userId: string = this.state.activeUserId): Promise<void> {
|
||||
const state = await this.secureStorageService.get<State>('state', { htmlStorageLocation: HtmlStorageLocation.Local });
|
||||
if (state?.accounts[userId ?? this.state.activeUserId] == null) {
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete state.accounts[userId ?? this.state.activeUserId];
|
||||
state.accounts[userId] = new Account({
|
||||
profile: state.accounts[userId].profile,
|
||||
settings: state.accounts[userId].settings,
|
||||
data: state.accounts[userId].data,
|
||||
});
|
||||
|
||||
await this.storageService.save('state', state, { htmlStorageLocation: HtmlStorageLocation.Local });
|
||||
}
|
||||
|
||||
private async removeAccountFromSessionStorage(userId: string): Promise<void> {
|
||||
private async removeAccountFromSessionStorage(userId: string = this.state.activeUserId): Promise<void> {
|
||||
const state = await this.secureStorageService.get<State>('state', { htmlStorageLocation: HtmlStorageLocation.Session });
|
||||
const account = state?.accounts[userId ?? this.state.activeUserId];
|
||||
if (account == null) {
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete state.accounts[userId ?? this.state.activeUserId];
|
||||
state.accounts[userId] = new Account({
|
||||
profile: state.accounts[userId].profile,
|
||||
settings: state.accounts[userId].settings,
|
||||
data: state.accounts[userId].data,
|
||||
});
|
||||
|
||||
await this.storageService.save('state', state, { htmlStorageLocation: HtmlStorageLocation.Session });
|
||||
}
|
||||
|
||||
private async removeAccountFromSecureStorage(userId: string): Promise<void> {
|
||||
private async removeAccountFromSecureStorage(userId: string = this.state.activeUserId): Promise<void> {
|
||||
const state = await this.secureStorageService.get<State>('state');
|
||||
if (state?.accounts[userId ?? this.state.activeUserId] == null) {
|
||||
if (state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
delete state.accounts[userId ?? this.state.activeUserId];
|
||||
state.accounts[userId] = null;
|
||||
await this.secureStorageService.save('state', state);
|
||||
}
|
||||
|
||||
private removeAccountFromMemory(userId: string): void {
|
||||
if (this.state?.accounts[userId ?? this.state.activeUserId] == null) {
|
||||
private removeAccountFromMemory(userId: string = this.state.activeUserId): void {
|
||||
if (this.state?.accounts[userId] == null) {
|
||||
return;
|
||||
}
|
||||
delete this.state.accounts[userId ?? this.state.activeUserId];
|
||||
|
||||
Reference in New Issue
Block a user