mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
[PM-5273] Migrate state in CipherService (#8314)
* PM-5273 Initial migration work for localData
* PM-5273 Encrypted and Decrypted ciphers migration to state provider
* pm-5273 Update references
* pm5273 Ensure prototype on cipher
* PM-5273 Add CipherId
* PM-5273 Remove migrated methods and updated references
* pm-5273 Fix versions
* PM-5273 Added missing options
* Conflict resolution
* Revert "Conflict resolution"
This reverts commit 0c0c2039ed.
* PM-5273 Fix PR comments
* Pm-5273 Fix comments
* PM-5273 Changed decryptedCiphers to use ActiveUserState
* PM-5273 Fix tests
* PM-5273 Fix pr comments
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
@@ -21,13 +21,15 @@ import Domain from "../../platform/models/domain/domain-base";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, StateProvider } from "../../platform/state";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { UserKey, OrgKey } from "../../types/key";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherType } from "../enums/cipher-type";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
import { LocalData } from "../models/data/local.data";
|
||||
import { Attachment } from "../models/domain/attachment";
|
||||
import { Card } from "../models/domain/card";
|
||||
import { Cipher } from "../models/domain/cipher";
|
||||
@@ -54,6 +56,14 @@ import { AttachmentView } from "../models/view/attachment.view";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { FieldView } from "../models/view/field.view";
|
||||
import { PasswordHistoryView } from "../models/view/password-history.view";
|
||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||
|
||||
import {
|
||||
ENCRYPTED_CIPHERS,
|
||||
LOCAL_DATA_KEY,
|
||||
ADD_EDIT_CIPHER_INFO_KEY,
|
||||
DECRYPTED_CIPHERS,
|
||||
} from "./key-state/ciphers.state";
|
||||
|
||||
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
|
||||
|
||||
@@ -62,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.sortCiphersByLastUsed,
|
||||
);
|
||||
|
||||
localData$: Observable<Record<CipherId, LocalData>>;
|
||||
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
|
||||
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
|
||||
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
|
||||
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
|
||||
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
@@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private encryptService: EncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
|
||||
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
|
||||
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
|
||||
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
|
||||
|
||||
async getDecryptedCipherCache(): Promise<CipherView[]> {
|
||||
const decryptedCiphers = await this.stateService.getDecryptedCiphers();
|
||||
return decryptedCiphers;
|
||||
this.localData$ = this.localDataState.state$;
|
||||
this.ciphers$ = this.encryptedCiphersState.state$;
|
||||
this.cipherViews$ = this.decryptedCiphersState.state$;
|
||||
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
|
||||
}
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[]) {
|
||||
@@ -85,7 +111,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
// if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again.
|
||||
// We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption.
|
||||
if (value == null || value.length !== 0) {
|
||||
await this.stateService.setDecryptedCiphers(value);
|
||||
await this.setDecryptedCiphers(value);
|
||||
}
|
||||
if (this.searchService != null) {
|
||||
if (value == null) {
|
||||
@@ -96,6 +122,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
private async setDecryptedCiphers(value: CipherView[]) {
|
||||
const cipherViews: { [id: string]: CipherView } = {};
|
||||
value?.forEach((c) => {
|
||||
cipherViews[c.id] = c;
|
||||
});
|
||||
await this.decryptedCiphersState.update(() => cipherViews);
|
||||
}
|
||||
|
||||
async clearCache(userId?: string): Promise<void> {
|
||||
await this.clearDecryptedCiphersState(userId);
|
||||
}
|
||||
@@ -268,24 +302,27 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Cipher> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localData = await this.stateService.getLocalData();
|
||||
return new Cipher(ciphers[id], localData ? localData[id] : null);
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const cipherId = id as CipherId;
|
||||
|
||||
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Cipher[]> {
|
||||
const localData = await this.stateService.getLocalData();
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
const response: Cipher[] = [];
|
||||
for (const id in ciphers) {
|
||||
// eslint-disable-next-line
|
||||
if (ciphers.hasOwnProperty(id)) {
|
||||
response.push(new Cipher(ciphers[id], localData ? localData[id] : null));
|
||||
const cipherId = id as CipherId;
|
||||
response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
@@ -293,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
@sequentialize(() => "getAllDecrypted")
|
||||
async getAllDecrypted(): Promise<CipherView[]> {
|
||||
if ((await this.getDecryptedCipherCache()) != null) {
|
||||
let decCiphers = await this.getDecryptedCiphers();
|
||||
if (decCiphers != null && decCiphers.length !== 0) {
|
||||
await this.reindexCiphers();
|
||||
return await this.getDecryptedCipherCache();
|
||||
return await this.getDecryptedCiphers();
|
||||
}
|
||||
|
||||
const ciphers = await this.getAll();
|
||||
decCiphers = await this.decryptCiphers(await this.getAll());
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
private async getDecryptedCiphers() {
|
||||
return Object.values(await firstValueFrom(this.cipherViews$));
|
||||
}
|
||||
|
||||
private async decryptCiphers(ciphers: Cipher[]) {
|
||||
const orgKeys = await this.cryptoService.getOrgKeys();
|
||||
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
|
||||
if (Object.keys(orgKeys).length === 0 && userKey == null) {
|
||||
@@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers);
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
@@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.searchService != null &&
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId);
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,22 +495,24 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async updateLastUsedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
|
||||
if (ciphersLocalData[id]) {
|
||||
ciphersLocalData[id].lastUsedDate = new Date().getTime();
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphersLocalData[cipherId]) {
|
||||
ciphersLocalData[cipherId].lastUsedDate = new Date().getTime();
|
||||
} else {
|
||||
ciphersLocalData[id] = {
|
||||
ciphersLocalData[cipherId] = {
|
||||
lastUsedDate: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
@@ -471,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
for (let i = 0; i < decryptedCipherCache.length; i++) {
|
||||
const cached = decryptedCipherCache[i];
|
||||
if (cached.id === id) {
|
||||
cached.localData = ciphersLocalData[id];
|
||||
cached.localData = ciphersLocalData[id as CipherId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
|
||||
await this.setDecryptedCiphers(decryptedCipherCache);
|
||||
}
|
||||
|
||||
async updateLastLaunchedDate(id: string): Promise<void> {
|
||||
let ciphersLocalData = await this.stateService.getLocalData();
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
|
||||
if (ciphersLocalData[id]) {
|
||||
ciphersLocalData[id].lastLaunched = new Date().getTime();
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphersLocalData[cipherId]) {
|
||||
ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
|
||||
} else {
|
||||
ciphersLocalData[id] = {
|
||||
ciphersLocalData[cipherId] = {
|
||||
lastUsedDate: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
await this.stateService.setLocalData(ciphersLocalData);
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.stateService.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
@@ -502,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
for (let i = 0; i < decryptedCipherCache.length; i++) {
|
||||
const cached = decryptedCipherCache[i];
|
||||
if (cached.id === id) {
|
||||
cached.localData = ciphersLocalData[id];
|
||||
cached.localData = ciphersLocalData[id as CipherId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedCiphers(decryptedCipherCache);
|
||||
await this.setDecryptedCiphers(decryptedCipherCache);
|
||||
}
|
||||
|
||||
async saveNeverDomain(domain: string): Promise<void> {
|
||||
@@ -711,7 +762,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
|
||||
|
||||
// Update the local state
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
|
||||
for (const id of cipherIds) {
|
||||
const cipher = ciphers[id];
|
||||
@@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async upsert(cipher: CipherData | CipherData[]): Promise<any> {
|
||||
let ciphers = await this.stateService.getEncryptedCiphers();
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
|
||||
if (cipher instanceof CipherData) {
|
||||
const c = cipher as CipherData;
|
||||
ciphers[c.id] = c;
|
||||
} else {
|
||||
(cipher as CipherData[]).forEach((c) => {
|
||||
ciphers[c.id] = c;
|
||||
});
|
||||
}
|
||||
|
||||
await this.replace(ciphers);
|
||||
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
|
||||
await this.updateEncryptedCipherState((current) => {
|
||||
ciphers.forEach((c) => current[c.id as CipherId]);
|
||||
return current;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
|
||||
await this.updateEncryptedCipherState(() => ciphers);
|
||||
}
|
||||
|
||||
private async updateEncryptedCipherState(
|
||||
update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>,
|
||||
) {
|
||||
await this.clearDecryptedCiphersState();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update((current) => {
|
||||
const result = update(current ?? {});
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
@@ -762,7 +812,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
|
||||
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
|
||||
|
||||
let ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
@@ -770,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
ids.forEach((id) => {
|
||||
// eslint-disable-next-line
|
||||
if (ciphers.hasOwnProperty(id)) {
|
||||
ciphers[id].folderId = folderId;
|
||||
ciphers[id as CipherId].folderId = folderId;
|
||||
}
|
||||
});
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof id === "string") {
|
||||
if (ciphers[id] == null) {
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
delete ciphers[id];
|
||||
delete ciphers[cipherId];
|
||||
} else {
|
||||
(id as string[]).forEach((i) => {
|
||||
(id as CipherId[]).forEach((i) => {
|
||||
delete ciphers[i];
|
||||
});
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
}
|
||||
|
||||
async deleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
@@ -820,21 +871,26 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async deleteAttachment(id: string, attachmentId: string): Promise<void> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
const cipherId = id as CipherId;
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) {
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < ciphers[id].attachments.length; i++) {
|
||||
if (ciphers[id].attachments[i].id === attachmentId) {
|
||||
ciphers[id].attachments.splice(i, 1);
|
||||
for (let i = 0; i < ciphers[cipherId].attachments.length; i++) {
|
||||
if (ciphers[cipherId].attachments[i].id === attachmentId) {
|
||||
ciphers[cipherId].attachments.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> {
|
||||
@@ -917,12 +973,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
async softDelete(id: string | string[]): Promise<any> {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setDeletedDate = (cipherId: string) => {
|
||||
const setDeletedDate = (cipherId: CipherId) => {
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
@@ -930,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
};
|
||||
|
||||
if (typeof id === "string") {
|
||||
setDeletedDate(id);
|
||||
setDeletedDate(id as CipherId);
|
||||
} else {
|
||||
(id as string[]).forEach(setDeletedDate);
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
@@ -963,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
async restore(
|
||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
||||
) {
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearDeletedDate = (c: { id: string; revisionDate: string }) => {
|
||||
if (ciphers[c.id] == null) {
|
||||
const cipherId = c.id as CipherId;
|
||||
if (ciphers[cipherId] == null) {
|
||||
return;
|
||||
}
|
||||
ciphers[c.id].deletedDate = null;
|
||||
ciphers[c.id].revisionDate = c.revisionDate;
|
||||
ciphers[cipherId].deletedDate = null;
|
||||
ciphers[cipherId].revisionDate = c.revisionDate;
|
||||
};
|
||||
|
||||
if (cipher.constructor.name === Array.name) {
|
||||
@@ -983,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.stateService.setEncryptedCiphers(ciphers);
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
return ciphers;
|
||||
});
|
||||
}
|
||||
|
||||
async restoreWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
@@ -1025,6 +1092,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async setAddEditCipherInfo(value: AddEditCipherInfo) {
|
||||
await this.addEditCipherInfoState.update(() => value);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// In the case of a cipher that is being shared with an organization, we want to decrypt the
|
||||
@@ -1350,11 +1421,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
private async clearEncryptedCiphersState(userId?: string) {
|
||||
await this.stateService.setEncryptedCiphers(null, { userId: userId });
|
||||
await this.encryptedCiphersState.update(() => ({}));
|
||||
}
|
||||
|
||||
private async clearDecryptedCiphersState(userId?: string) {
|
||||
await this.stateService.setDecryptedCiphers(null, { userId: userId });
|
||||
await this.setDecryptedCiphers(null);
|
||||
this.clearSortedCiphers();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user