1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-02 03:41:09 +01:00

Move state stuff to a single location

This commit is contained in:
Matt Gibson 2023-10-05 10:36:27 -04:00
parent e5889f2280
commit dfdde4daba
No known key found for this signature in database
GPG Key ID: 0B3AF4C7D6472DD1
22 changed files with 222 additions and 189 deletions

View File

@ -92,12 +92,12 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { DefaultUserStateProvider } from "@bitwarden/common/platform/services/default-user-state.provider";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { DefaultUserStateProvider } from "@bitwarden/common/platform/state";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service";
import { ApiService } from "@bitwarden/common/services/api.service";

View File

@ -1,5 +1,5 @@
import { GlobalState } from "../interfaces/global-state";
import { KeyDefinition } from "../types/key-definition";
import { KeyDefinition } from "../state/key-definition";
export abstract class GlobalStateProvider {
create: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;

View File

@ -1,5 +1,5 @@
import { UserState } from "../interfaces/user-state";
import { KeyDefinition } from "../types/key-definition";
import { KeyDefinition } from "../state/key-definition";
export abstract class UserStateProvider {
create: <T>(keyDefinition: KeyDefinition<T>) => UserState<T>;

View File

@ -1,8 +1,7 @@
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
import { DerivedUserState } from "../services/default-user-state.provider";
import { DerivedStateDefinition } from "../types/derived-state-definition";
import { DerivedStateDefinition , DerivedUserState } from "../state/";
export interface UserState<T> {
readonly state$: Observable<T>;

View File

@ -1,4 +1,4 @@
import { KeyDefinition } from "../types/key-definition";
import { KeyDefinition } from "../state/key-definition";
// TODO: Use Matts `UserId` type
export function userKeyBuilder(userId: string, keyDefinition: KeyDefinition<unknown>): string {

View File

@ -1,89 +0,0 @@
import { BehaviorSubject, Observable } from "rxjs";
import { Jsonify } from "type-fest";
import { GlobalStateProvider } from "../abstractions/global-state.provider";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { GlobalState } from "../interfaces/global-state";
import { globalKeyBuilder } from "../misc/key-builders";
import { KeyDefinition } from "../types/key-definition";
// TODO: Move type
export type StorageLocation = "memory" | "disk" | "secure";
class GlobalStateImplementation<T> implements GlobalState<T> {
private storageKey: string;
private seededPromise: Promise<void>;
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
state$: Observable<T>;
constructor(
private keyDefinition: KeyDefinition<T>,
private chosenLocation: AbstractStorageService
) {
this.storageKey = globalKeyBuilder(this.keyDefinition);
this.seededPromise = this.chosenLocation.get<Jsonify<T>>(this.storageKey).then((data) => {
const serializedData = this.keyDefinition.deserializer(data);
this.stateSubject.next(serializedData);
});
this.state$ = this.stateSubject.asObservable();
}
async update(configureState: (state: T) => void): Promise<void> {
await this.seededPromise;
const currentState = this.stateSubject.getValue();
configureState(currentState);
await this.chosenLocation.save(this.storageKey, currentState);
this.stateSubject.next(currentState);
}
async getFromState(): Promise<T> {
const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
return this.keyDefinition.deserializer(data);
}
}
export class DefaultGlobalStateProvider implements GlobalStateProvider {
private globalStateCache: Record<string, GlobalState<unknown>> = {};
constructor(
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService
) {}
create<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingGlobalState = this.globalStateCache[locationDomainKey];
if (existingGlobalState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingGlobalState as GlobalStateImplementation<T>;
}
const newGlobalState = new GlobalStateImplementation<T>(
keyDefinition,
this.getLocation(keyDefinition.stateDefinition.storageLocation)
);
this.globalStateCache[locationDomainKey] = newGlobalState;
return newGlobalState;
}
private getLocation(location: StorageLocation) {
switch (location) {
case "disk":
return this.diskStorage;
case "secure":
return this.secureStorage;
case "memory":
return this.memoryStorage;
}
}
}

View File

@ -0,0 +1,49 @@
import { GlobalStateProvider } from "../abstractions/global-state.provider";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { GlobalState } from "../interfaces/global-state";
import { DefaultGlobalState } from "./default-global-state";
import { KeyDefinition } from "./key-definition";
import { StorageLocation } from "./state-definition";
export class DefaultGlobalStateProvider implements GlobalStateProvider {
private globalStateCache: Record<string, GlobalState<unknown>> = {};
constructor(
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService
) {}
create<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingGlobalState = this.globalStateCache[locationDomainKey];
if (existingGlobalState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingGlobalState as DefaultGlobalState<T>;
}
const newGlobalState = new DefaultGlobalState<T>(
keyDefinition,
this.getLocation(keyDefinition.stateDefinition.storageLocation)
);
this.globalStateCache[locationDomainKey] = newGlobalState;
return newGlobalState;
}
private getLocation(location: StorageLocation) {
switch (location) {
case "disk":
return this.diskStorage;
case "secure":
return this.secureStorage;
case "memory":
return this.memoryStorage;
}
}
}

View File

@ -0,0 +1,44 @@
import { BehaviorSubject, Observable } from "rxjs";
import { Jsonify } from "type-fest";
import { AbstractStorageService } from "../abstractions/storage.service";
import { GlobalState } from "../interfaces/global-state";
import { globalKeyBuilder } from "../misc/key-builders";
import { KeyDefinition } from "./key-definition";
export class DefaultGlobalState<T> implements GlobalState<T> {
private storageKey: string;
private seededPromise: Promise<void>;
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
state$: Observable<T>;
constructor(
private keyDefinition: KeyDefinition<T>,
private chosenLocation: AbstractStorageService
) {
this.storageKey = globalKeyBuilder(this.keyDefinition);
this.seededPromise = this.chosenLocation.get<Jsonify<T>>(this.storageKey).then((data) => {
const serializedData = this.keyDefinition.deserializer(data);
this.stateSubject.next(serializedData);
});
this.state$ = this.stateSubject.asObservable();
}
async update(configureState: (state: T) => void): Promise<void> {
await this.seededPromise;
const currentState = this.stateSubject.getValue();
configureState(currentState);
await this.chosenLocation.save(this.storageKey, currentState);
this.stateSubject.next(currentState);
}
async getFromState(): Promise<T> {
const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
return this.keyDefinition.deserializer(data);
}
}

View File

@ -6,8 +6,8 @@ import { AccountInfo, AccountService } from "../../auth/abstractions/account.ser
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { UserId } from "../../types/guid";
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
import { KeyDefinition } from "../types/key-definition";
import { StateDefinition } from "../types/state-definition";
import { KeyDefinition } from "../state/key-definition";
import { StateDefinition } from "../state/state-definition";
import { DefaultUserStateProvider } from "./default-user-state.provider";

View File

@ -0,0 +1,42 @@
import { AccountService } from "../../auth/abstractions/account.service";
import { EncryptService } from "../abstractions/encrypt.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { UserStateProvider } from "../abstractions/user-state.provider";
import { DefaultUserState } from "../state/default-user-state";
import { KeyDefinition } from "../state/key-definition";
export class DefaultUserStateProvider implements UserStateProvider {
private userStateCache: Record<string, DefaultUserState<unknown>> = {};
constructor(
private accountService: AccountService, // Inject the lightest weight service that provides accountUserId$
private encryptService: EncryptService,
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService
) {}
create<T>(keyDefinition: KeyDefinition<T>): DefaultUserState<T> {
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingUserState = this.userStateCache[locationDomainKey];
if (existingUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingUserState as DefaultUserState<T>;
}
const newUserState = new DefaultUserState<T>(
keyDefinition,
this.accountService,
this.encryptService,
this.memoryStorage,
this.secureStorage,
this.diskStorage
);
this.userStateCache[locationDomainKey] = newUserState;
return newUserState;
}
}

View File

@ -1,13 +1,13 @@
import {
BehaviorSubject,
Observable,
defer,
firstValueFrom,
BehaviorSubject,
map,
share,
shareReplay,
switchMap,
tap,
share,
defer,
firstValueFrom,
} from "rxjs";
import { Jsonify } from "type-fest";
@ -15,52 +15,18 @@ import { AccountService } from "../../auth/abstractions/account.service";
import { UserId } from "../../types/guid";
import { EncryptService } from "../abstractions/encrypt.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
AbstractMemoryStorageService,
} from "../abstractions/storage.service";
import { UserStateProvider } from "../abstractions/user-state.provider";
import { UserState } from "../interfaces/user-state";
import { userKeyBuilder } from "../misc/key-builders";
import { DeriveContext, DerivedStateDefinition } from "../types/derived-state-definition";
import { KeyDefinition } from "../types/key-definition";
import { StorageLocation } from "./default-global-state.provider";
import { DerivedStateDefinition } from "./derived-state-definition";
import { DerivedUserState } from "./derived-user-state";
import { KeyDefinition } from "./key-definition";
import { StorageLocation } from "./state-definition";
export class DerivedUserState<TFrom, TTo> {
state$: Observable<TTo>;
// TODO: Probably needs to take state service
/**
*
*/
constructor(
private derivedStateDefinition: DerivedStateDefinition<TFrom, TTo>,
private encryptService: EncryptService,
private userState: UserState<TFrom>
) {
this.state$ = userState.state$.pipe(
switchMap(async (from) => {
// TODO: How do I get the key?
const convertedData = await derivedStateDefinition.converter(
from,
new DeriveContext(null, encryptService)
);
return convertedData;
})
);
}
async getFromState(): Promise<TTo> {
const encryptedFromState = await this.userState.getFromState();
const context = new DeriveContext(null, this.encryptService);
const decryptedData = await this.derivedStateDefinition.converter(encryptedFromState, context);
return decryptedData;
}
}
class DefaultUserState<T> implements UserState<T> {
export class DefaultUserState<T> implements UserState<T> {
private seededInitial = false;
private formattedKey$: Observable<string>;
@ -192,36 +158,3 @@ class DefaultUserState<T> implements UserState<T> {
}
}
}
export class DefaultUserStateProvider implements UserStateProvider {
private userStateCache: Record<string, DefaultUserState<unknown>> = {};
constructor(
private accountService: AccountService, // Inject the lightest weight service that provides accountUserId$
private encryptService: EncryptService,
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService
) {}
create<T>(keyDefinition: KeyDefinition<T>): DefaultUserState<T> {
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingUserState = this.userStateCache[locationDomainKey];
if (existingUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingUserState as DefaultUserState<T>;
}
const newUserState = new DefaultUserState<T>(
keyDefinition,
this.accountService,
this.encryptService,
this.memoryStorage,
this.secureStorage,
this.diskStorage
);
this.userStateCache[locationDomainKey] = newUserState;
return newUserState;
}
}

View File

@ -1,6 +1,7 @@
import { EncryptService } from "../abstractions/encrypt.service";
import { UserKey } from "../models/domain/symmetric-crypto-key";
import { StorageLocation } from "../services/default-global-state.provider";
import { StorageLocation } from "./state-definition";
// TODO: Move type
export class DeriveContext {

View File

@ -0,0 +1,40 @@
import { Observable, switchMap } from "rxjs";
import { EncryptService } from "../abstractions/encrypt.service";
import { UserState } from "../interfaces/user-state";
import { DerivedStateDefinition, DeriveContext } from "./derived-state-definition";
export class DerivedUserState<TFrom, TTo> {
state$: Observable<TTo>;
// TODO: Probably needs to take state service
/**
*
*/
constructor(
private derivedStateDefinition: DerivedStateDefinition<TFrom, TTo>,
private encryptService: EncryptService,
private userState: UserState<TFrom>
) {
this.state$ = userState.state$.pipe(
switchMap(async (from) => {
// TODO: How do I get the key?
const convertedData = await derivedStateDefinition.converter(
from,
new DeriveContext(null, encryptService)
);
return convertedData;
})
);
}
async getFromState(): Promise<TTo> {
const encryptedFromState = await this.userState.getFromState();
const context = new DeriveContext(null, this.encryptService);
const decryptedData = await this.derivedStateDefinition.converter(encryptedFromState, context);
return decryptedData;
}
}

View File

@ -0,0 +1,13 @@
// Export all established state definitions
export * from "./state-definitions";
// Export all established KeyDefinitions
export * from "./key-definitions";
export { DefaultGlobalStateProvider } from "./default-global-state.provider";
export { DefaultGlobalState } from "./default-global-state";
export { DefaultUserStateProvider } from "./default-user-state.provider";
export { DefaultUserState } from "./default-user-state";
export { DerivedStateDefinition } from "./derived-state-definition";
export { DerivedUserState } from "./derived-user-state";
// TODO: should this be exported?
export { KeyDefinition } from "./key-definition";
export { StorageLocation } from "./state-definition";

View File

@ -0,0 +1,11 @@
import { FolderData } from "../../vault/models/data/folder.data";
import { KeyDefinition } from "./key-definition";
import { FOLDER_SERVICE_DISK } from "./state-definitions";
// FolderService Keys
export const FOLDERS = KeyDefinition.record<FolderData>(
FOLDER_SERVICE_DISK,
"folders",
FolderData.fromJSON
);

View File

@ -3,10 +3,10 @@
import { userKeyBuilder } from "../../platform/misc/key-builders";
// TODO: Add message
// eslint-disable-next-line import/no-restricted-paths
import { KeyDefinition } from "../../platform/types/key-definition";
import { KeyDefinition } from "../../platform/state/key-definition";
// TODO: Add message
// eslint-disable-next-line import/no-restricted-paths
import { StateDefinition } from "../../platform/types/state-definition";
import { StateDefinition } from "../../platform/state/state-definition";
import { MigrationHelper } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";

View File

@ -7,7 +7,8 @@ import { UserStateProvider } from "../../../platform/abstractions/user-state.pro
import { UserState } from "../../../platform/interfaces/user-state";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { DerivedUserState } from "../../../platform/services/default-user-state.provider";
import { DerivedUserState } from "../../../platform/state";
import { FOLDERS } from "../../../platform/state/key-definitions";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
@ -15,7 +16,6 @@ import { CipherData } from "../../../vault/models/data/cipher.data";
import { FolderData } from "../../../vault/models/data/folder.data";
import { Folder } from "../../../vault/models/domain/folder";
import { FolderView } from "../../../vault/models/view/folder.view";
import { FOLDERS } from "../../types/key-definitions";
export class FolderService implements InternalFolderServiceAbstraction {
folderState: UserState<Record<string, FolderData>>;

View File

@ -1,10 +0,0 @@
import { KeyDefinition } from "../../platform/types/key-definition";
import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions";
import { FolderData } from "../models/data/folder.data";
// FolderService Keys
export const FOLDERS = KeyDefinition.record<FolderData>(
FOLDER_SERVICE_DISK,
"folders",
FolderData.fromJSON
);