mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-31 22:51:28 +01:00
Rebase: Bad Commit
This commit is contained in:
parent
d0037bb257
commit
d1d12ce61f
@ -93,6 +93,7 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { ActiveUserStateProviderService, BaseActiveUserStateProviderService } from "@bitwarden/common/platform/services/state-provider.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";
|
||||
@ -174,6 +175,7 @@ import { ModalService } from "./modal.service";
|
||||
import { ThemingService } from "./theming/theming.service";
|
||||
import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
providers: [
|
||||
@ -717,6 +719,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
useClass: AuthRequestCryptoServiceImplementation,
|
||||
deps: [CryptoServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: ActiveUserStateProviderService,
|
||||
useClass: BaseActiveUserStateProviderService,
|
||||
deps: [
|
||||
// TODO: Do other storage services
|
||||
StateServiceAbstraction,
|
||||
AbstractStorageService
|
||||
]
|
||||
}
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
@ -0,0 +1,3 @@
|
||||
export abstract class ActiveUserStateProviderService {
|
||||
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export abstract class GlobalStateProviderService {
|
||||
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
|
||||
}
|
6
libs/common/src/platform/interfaces/state.ts
Normal file
6
libs/common/src/platform/interfaces/state.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
export interface State<T> {
|
||||
update: (configureState: (state: T) => void) => Promise<void>
|
||||
state$: Observable<T>
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, map, share, switchMap, tap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { State } from "../interfaces/state";
|
||||
|
||||
import { DomainToken, StorageLocation } from "./default-global-state-provider.service";
|
||||
|
||||
class DefaultActiveUserState<T> implements State<T> {
|
||||
private formattedKey$: Observable<string>;
|
||||
|
||||
// TODO: Use BitSubject
|
||||
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
||||
private stateSubject$ = this.stateSubject.asObservable();
|
||||
|
||||
state$: Observable<T>;
|
||||
|
||||
// Global:
|
||||
// FolderService = <data>
|
||||
|
||||
// User (super flat)
|
||||
// FolderService_{userId}_someData = <data>
|
||||
// FolderService_{userId}_moreData = <data>
|
||||
// -- or --
|
||||
// User (not as flat)
|
||||
// FolderService_{userId} = <data>
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private storageLocation: AbstractStorageService,
|
||||
domainToken: DomainToken<T>) {
|
||||
|
||||
const unformattedKey = `${domainToken.domainName}_{userId}`;
|
||||
|
||||
// startWith?
|
||||
this.formattedKey$ = this.stateService.activeAccount$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(account => account != null),
|
||||
map(accountId => unformattedKey.replace("{userId}", accountId))
|
||||
);
|
||||
|
||||
// TODO: Don't use async if possible
|
||||
const activeAccountData$ = this.formattedKey$
|
||||
.pipe(switchMap(async key => {
|
||||
// TODO: Force this in the storages so I don't have to `as`
|
||||
const jsonData = await this.storageLocation.get<T>(key) as Jsonify<T>;
|
||||
const data = domainToken.serializer(jsonData);
|
||||
return data;
|
||||
}),
|
||||
tap(data => this.stateSubject.next(data)),
|
||||
// Share the execution
|
||||
share()
|
||||
);
|
||||
|
||||
// Whomever subscribes to this data, should be notified of updated data
|
||||
// if someone calls my update() method, or the active user changes.
|
||||
this.state$ = defer(() => {
|
||||
const subscription = activeAccountData$.subscribe();
|
||||
return this.stateSubject$
|
||||
.pipe(tap({
|
||||
complete: () => subscription.unsubscribe(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async update(configureState: (state: T) => void): Promise<void> {
|
||||
// wait for lock
|
||||
try {
|
||||
const currentState = this.stateSubject.getValue();
|
||||
configureState(currentState);
|
||||
await this.storageLocation.save(await this.createKey(), currentState);
|
||||
this.stateSubject.next(currentState);
|
||||
}
|
||||
finally {
|
||||
// TODO: Free lock
|
||||
}
|
||||
}
|
||||
|
||||
private async createKey(): Promise<string> {
|
||||
return await firstValueFrom(this.formattedKey$);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DefaultActiveUserStateProviderService implements ActiveUserStateProviderService {
|
||||
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService, // Inject the lightest weight service that provides accountUserId$
|
||||
private memoryStorage: AbstractMemoryStorageService,
|
||||
private diskStorage: AbstractStorageService,
|
||||
private secureStorage: AbstractStorageService) {
|
||||
}
|
||||
|
||||
create<T>(location: StorageLocation, domainToken: DomainToken<T>): DefaultActiveUserState<T> {
|
||||
const locationDomainKey = `${location}_${domainToken.domainName}`;
|
||||
const existingActiveUserState = this.userStateCache[locationDomainKey];
|
||||
if (existingActiveUserState != null) {
|
||||
// I have to cast out of the unknown generic but this should be safe if rules
|
||||
// around domain token are made
|
||||
return existingActiveUserState as DefaultActiveUserState<T>;
|
||||
}
|
||||
|
||||
const newActiveUserState = new DefaultActiveUserState<T>(
|
||||
this.stateService,
|
||||
this.getLocation(location),
|
||||
domainToken);
|
||||
this.userStateCache[locationDomainKey] = newActiveUserState;
|
||||
return newActiveUserState;
|
||||
}
|
||||
|
||||
private getLocation(location: StorageLocation) {
|
||||
switch (location) {
|
||||
case "disk":
|
||||
return this.diskStorage;
|
||||
case "secure":
|
||||
return this.secureStorage;
|
||||
case "memory":
|
||||
return this.memoryStorage;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, forkJoin, map, merge, share, switchMap, tap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service";
|
||||
import { GlobalStateProviderService } from "../abstractions/global-state-provider.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { State } from "../interfaces/state";
|
||||
|
||||
// TODO: Move type
|
||||
// TODO: How can we protect the creation of these so that platform can maintain the allowed creations?
|
||||
export class DomainToken<T> {
|
||||
constructor(
|
||||
public domainName: string,
|
||||
public serializer: (jsonData: Jsonify<T>) => T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move type
|
||||
export type StorageLocation = "memory" | "disk" | "secure";
|
||||
|
||||
class DefaultGlobalState<T> implements State<T> {
|
||||
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
||||
|
||||
state$: Observable<T>;
|
||||
|
||||
constructor(private storageLocation: AbstractStorageService, private domainToken: DomainToken<T>) {
|
||||
this.state$ = this.stateSubject.asObservable();
|
||||
}
|
||||
|
||||
async update(configureState: (state: T) => void): Promise<void> {
|
||||
// wait for lock
|
||||
try {
|
||||
const currentState = this.stateSubject.getValue();
|
||||
configureState(currentState);
|
||||
await this.storageLocation.save(this.domainToken.domainName, currentState);
|
||||
this.stateSubject.next(currentState);
|
||||
}
|
||||
finally {
|
||||
// TODO: Free lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultGlobalStateProviderService implements GlobalStateProviderService {
|
||||
private globalStateCache: Record<string, DefaultGlobalState<unknown>> = {};
|
||||
|
||||
constructor(
|
||||
private memoryStorage: AbstractMemoryStorageService,
|
||||
private diskStorage: AbstractStorageService,
|
||||
private secureStorage: AbstractStorageService) {
|
||||
}
|
||||
|
||||
create<T>(location: StorageLocation, domainToken: DomainToken<T>): DefaultGlobalState<T> {
|
||||
const locationDomainKey = `${location}_${domainToken.domainName}`;
|
||||
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>(this.getLocation(location), domainToken);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import { matches, mock, mockReset, notNull } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { StateService } from "../abstractions/state.service"
|
||||
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
|
||||
|
||||
import { DomainToken, BaseActiveUserStateProviderService } from "./default-global-state-provider.service"
|
||||
|
||||
|
||||
|
||||
class TestState {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
date: Date;
|
||||
array: string[]
|
||||
// TODO: More complex data types
|
||||
|
||||
static fromJSON(jsonState: Jsonify<TestState>) {
|
||||
const state = new TestState();
|
||||
state.date = new Date(jsonState.date);
|
||||
state.array = jsonState.array;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const fakeDomainToken = new DomainToken<TestState>("fake", TestState.fromJSON);
|
||||
|
||||
describe("BaseStateProviderService", () => {
|
||||
const stateService = mock<StateService>();
|
||||
const memoryStorageService = mock<AbstractMemoryStorageService>();
|
||||
const diskStorageService = mock<AbstractMemoryStorageService>();
|
||||
|
||||
const activeAccountSubject = new BehaviorSubject<string>(undefined);
|
||||
|
||||
let stateProviderService: BaseActiveUserStateProviderService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(stateService);
|
||||
mockReset(memoryStorageService);
|
||||
mockReset(diskStorageService);
|
||||
|
||||
stateService.activeAccount$ = activeAccountSubject;
|
||||
|
||||
stateProviderService = new BaseActiveUserStateProviderService(stateService, diskStorageService);
|
||||
});
|
||||
|
||||
it("createUserState", async () => {
|
||||
diskStorageService.get
|
||||
.mockImplementation(async (key, options) => {
|
||||
if (key == "fake_1") {
|
||||
return {date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"]}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const fakeDomainState = stateProviderService.create(fakeDomainToken);
|
||||
|
||||
const subscribeCallback = jest.fn<void, [TestState]>();
|
||||
const subscription = fakeDomainState.state$.subscribe(subscribeCallback);
|
||||
|
||||
// User signs in
|
||||
activeAccountSubject.next("1");
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Service does an update
|
||||
fakeDomainState.update(state => state.array.push("value3"));
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 1));
|
||||
|
||||
subscription.unsubscribe();
|
||||
|
||||
// No user at the start, no data
|
||||
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
|
||||
|
||||
// Gotten starter user data
|
||||
expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches<TestState>(value => {
|
||||
console.log("Called", value);
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Gotten update callback data
|
||||
expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches<TestState>((value) => {
|
||||
return typeof value.date == "object" &&
|
||||
value.date.getFullYear() == 2023 &&
|
||||
value.array.length == 3
|
||||
}));
|
||||
});
|
||||
});
|
59
libs/common/src/state-migrations/owned-migrator.ts
Normal file
59
libs/common/src/state-migrations/owned-migrator.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { DomainToken } from "../platform/services/default-global-state-provider.service";
|
||||
|
||||
import { MigrationHelper } from "./migration-helper";
|
||||
import { Migrator } from "./migrator";
|
||||
|
||||
export class OwnedMigrationHelper {
|
||||
|
||||
currentVersion: number;
|
||||
logService: LogService;
|
||||
|
||||
constructor(
|
||||
public domainToken: DomainToken<unknown>,
|
||||
private migrationHelper: MigrationHelper) {
|
||||
this.currentVersion = migrationHelper.currentVersion;
|
||||
this.logService = migrationHelper.logService;
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
return this.migrationHelper.get(key);
|
||||
}
|
||||
|
||||
getFromOwned<T>(key: string): Promise<T> {
|
||||
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): Promise<void> {
|
||||
// Create the key
|
||||
return undefined;
|
||||
}
|
||||
info(message: string): void {
|
||||
// Add domain info?
|
||||
this.migrationHelper.info("$OwnedMigratonHelper: {message}");
|
||||
}
|
||||
|
||||
getAccounts<ExpectedAccountType>(): Promise<{ userId: string; account: ExpectedAccountType; }[]> {
|
||||
return this.migrationHelper.getAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class OwnedMigrator<TFrom extends number, TTo extends number, TState> extends Migrator<TFrom, TTo> {
|
||||
constructor(public domainToken: DomainToken<TState>,
|
||||
fromVersion: TFrom, toVersion: TTo) {
|
||||
super(fromVersion, toVersion)
|
||||
}
|
||||
|
||||
override async migrate(helper: MigrationHelper): Promise<void> {
|
||||
// Create our custom helper
|
||||
const ownedHelper = new OwnedMigrationHelper(this.domainToken, helper);
|
||||
await this.migrateOwned(ownedHelper);
|
||||
const accounts = await ownedHelper.getAccounts();
|
||||
for (const account of accounts) {
|
||||
await this.migrateUserOwned(account.userId, ownedHelper);
|
||||
}
|
||||
}
|
||||
|
||||
abstract migrateOwned(helper: OwnedMigrationHelper): Promise<void>;
|
||||
abstract migrateUserOwned(userId: string, helper: OwnedMigrationHelper): Promise<void>;
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import { BehaviorSubject, concatMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserStateProviderService, DomainToken, State } from "../../../platform/services/default-global-state-provider.service";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
@ -12,6 +14,13 @@ import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { Folder } from "../../../vault/models/domain/folder";
|
||||
import { FolderView } from "../../../vault/models/view/folder.view";
|
||||
|
||||
class UserFolderState {
|
||||
static fromJSON(jsonState: Jsonify<UserFolderState>) {
|
||||
const state = new UserFolderState();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderService implements InternalFolderServiceAbstraction {
|
||||
protected _folders: BehaviorSubject<Folder[]> = new BehaviorSubject([]);
|
||||
protected _folderViews: BehaviorSubject<FolderView[]> = new BehaviorSubject([]);
|
||||
@ -19,12 +28,16 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
folders$ = this._folders.asObservable();
|
||||
folderViews$ = this._folderViews.asObservable();
|
||||
|
||||
private folderState: State<UserFolderState>;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService
|
||||
private stateService: StateService,
|
||||
private activeUserStateProviderService: ActiveUserStateProviderService
|
||||
) {
|
||||
this.folderState = activeUserStateProviderService.create<UserFolderState>(new DomainToken("folder", UserFolderState.fromJSON));
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
|
Loading…
Reference in New Issue
Block a user