mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-31 22:51:28 +01:00
Rebase: Implement FolderService
This commit is contained in:
parent
d1d12ce61f
commit
5e8feb22d0
@ -56,6 +56,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.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 { DefaultActiveUserStateProviderService } from "@bitwarden/common/platform/services/default-active-user-state-provider.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
@ -332,11 +333,21 @@ export default class MainBackground {
|
||||
this.encryptService,
|
||||
this.cipherFileUploadService
|
||||
);
|
||||
|
||||
// TODO: This is just to make it compile
|
||||
const activeUserStateProviderService = new DefaultActiveUserStateProviderService(
|
||||
this.stateService,
|
||||
this.memoryStorageService,
|
||||
this.storageService,
|
||||
this.secureStorageService
|
||||
);
|
||||
|
||||
this.folderService = new BrowserFolderService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.cipherService,
|
||||
this.stateService
|
||||
this.stateService,
|
||||
activeUserStateProviderService
|
||||
);
|
||||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||
this.collectionService = new CollectionService(
|
||||
|
@ -64,6 +64,7 @@ import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { ActiveUserStateProvider } from "@bitwarden/common/platform/abstractions/active-user-state.provider";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
@ -91,9 +92,9 @@ 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 { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/services/default-active-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 { 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";
|
||||
@ -303,6 +304,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
ActiveUserStateProvider,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
},
|
||||
@ -720,12 +722,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
deps: [CryptoServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: ActiveUserStateProviderService,
|
||||
useClass: BaseActiveUserStateProviderService,
|
||||
provide: ActiveUserStateProvider,
|
||||
useClass: DefaultActiveUserStateProvider,
|
||||
deps: [
|
||||
// TODO: Do other storage services
|
||||
StateServiceAbstraction,
|
||||
AbstractStorageService
|
||||
EncryptService,
|
||||
MEMORY_STORAGE,
|
||||
AbstractStorageService,
|
||||
SECURE_STORAGE
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -1,3 +0,0 @@
|
||||
export abstract class ActiveUserStateProviderService {
|
||||
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { ActiveUserState } from "../interfaces/active-user-state";
|
||||
import { KeyDefinition } from "../types/key-definition";
|
||||
|
||||
export abstract class ActiveUserStateProvider {
|
||||
create: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export abstract class GlobalStateProviderService {
|
||||
create: <T>(location: StorageLocation, domainToken: DomainToken<T>) => State<T>;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { GlobalState } from "../interfaces/global-state";
|
||||
import { KeyDefinition } from "../types/key-definition";
|
||||
|
||||
export abstract class GlobalStateProvider {
|
||||
create: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;
|
||||
}
|
11
libs/common/src/platform/interfaces/active-user-state.ts
Normal file
11
libs/common/src/platform/interfaces/active-user-state.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { DerivedActiveUserState } from "../services/default-active-user-state.provider";
|
||||
import { DerivedStateDefinition } from "../types/derived-state-definition";
|
||||
|
||||
export interface ActiveUserState<T> {
|
||||
readonly state$: Observable<T>
|
||||
readonly getFromState: () => Promise<T>
|
||||
readonly update: (configureState: (state: T) => void) => Promise<void>
|
||||
createDerived: <TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>) => DerivedActiveUserState<T, TTo>
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
export interface State<T> {
|
||||
export interface GlobalState<T> {
|
||||
update: (configureState: (state: T) => void) => Promise<void>
|
||||
state$: Observable<T>
|
||||
}
|
16
libs/common/src/platform/misc/key-builders.ts
Normal file
16
libs/common/src/platform/misc/key-builders.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { KeyDefinition } from "../types/key-definition";
|
||||
|
||||
// TODO: Use Matt's `UserId` type
|
||||
export function userKeyBuilder(
|
||||
userId: string,
|
||||
keyDefinition: KeyDefinition<unknown>
|
||||
): string {
|
||||
return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`;
|
||||
}
|
||||
|
||||
export function globalKeyBuilder(
|
||||
keyDefinition: KeyDefinition<unknown>
|
||||
): string {
|
||||
// TODO: Do we want the _global_ part?
|
||||
return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`;
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,15 @@
|
||||
import { matches, mock, mockReset, notNull } from "jest-mock-extended";
|
||||
import { matches, mock, mockReset } 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 { KeyDefinition } from "../types/key-definition";
|
||||
import { StateDefinition } from "../types/state-definition";
|
||||
|
||||
import { DomainToken, BaseActiveUserStateProviderService } from "./default-global-state-provider.service"
|
||||
|
||||
|
||||
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
||||
|
||||
class TestState {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
date: Date;
|
||||
array: string[]
|
||||
// TODO: More complex data types
|
||||
@ -26,16 +22,18 @@ class TestState {
|
||||
}
|
||||
}
|
||||
|
||||
const fakeDomainToken = new DomainToken<TestState>("fake", TestState.fromJSON);
|
||||
const testStateDefinition = new StateDefinition("fake", "disk");
|
||||
|
||||
describe("BaseStateProviderService", () => {
|
||||
const testKeyDefinition = new KeyDefinition<TestState>(testStateDefinition, "fake", TestState.fromJSON);
|
||||
|
||||
describe("DefaultStateProvider", () => {
|
||||
const stateService = mock<StateService>();
|
||||
const memoryStorageService = mock<AbstractMemoryStorageService>();
|
||||
const diskStorageService = mock<AbstractMemoryStorageService>();
|
||||
|
||||
const activeAccountSubject = new BehaviorSubject<string>(undefined);
|
||||
|
||||
let stateProviderService: BaseActiveUserStateProviderService;
|
||||
let activeUserStateProvider: DefaultActiveUserStateProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(stateService);
|
||||
@ -44,7 +42,13 @@ describe("BaseStateProviderService", () => {
|
||||
|
||||
stateService.activeAccount$ = activeAccountSubject;
|
||||
|
||||
stateProviderService = new BaseActiveUserStateProviderService(stateService, diskStorageService);
|
||||
activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
stateService,
|
||||
null, // Not testing derived state
|
||||
null, // Not testing memory storage
|
||||
diskStorageService,
|
||||
null // Not testing secure storage
|
||||
);
|
||||
});
|
||||
|
||||
it("createUserState", async () => {
|
||||
@ -56,7 +60,7 @@ describe("BaseStateProviderService", () => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const fakeDomainState = stateProviderService.create(fakeDomainToken);
|
||||
const fakeDomainState = activeUserStateProvider.create(testKeyDefinition);
|
||||
|
||||
const subscribeCallback = jest.fn<void, [TestState]>();
|
||||
const subscription = fakeDomainState.state$.subscribe(subscribeCallback);
|
||||
@ -66,8 +70,8 @@ describe("BaseStateProviderService", () => {
|
||||
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));
|
||||
await fakeDomainState.update(state => state.array.push("value3"));
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 10));
|
||||
|
||||
subscription.unsubscribe();
|
||||
|
||||
@ -76,13 +80,13 @@ describe("BaseStateProviderService", () => {
|
||||
|
||||
// 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" &&
|
||||
return value != null &&
|
||||
typeof value.date == "object" &&
|
||||
value.date.getFullYear() == 2023 &&
|
||||
value.array.length == 3
|
||||
}));
|
@ -0,0 +1,199 @@
|
||||
import { BehaviorSubject, Observable, defer, firstValueFrom, map, share, switchMap, tap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { ActiveUserState } from "../interfaces/active-user-state";
|
||||
import { userKeyBuilder } from "../misc/key-builders";
|
||||
import { UserKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { KeyDefinition } from "../types/key-definition";
|
||||
|
||||
import { StorageLocation } from "./default-global-state.provider";
|
||||
|
||||
class ConverterContext {
|
||||
constructor(
|
||||
readonly activeUserKey: UserKey,
|
||||
readonly encryptService: EncryptService
|
||||
) { }
|
||||
}
|
||||
|
||||
class DerivedStateDefinition<TFrom, TTo> {
|
||||
constructor(
|
||||
readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>
|
||||
) { }
|
||||
}
|
||||
|
||||
export class DerivedActiveUserState<TFrom, TTo> {
|
||||
state$: Observable<TTo>
|
||||
|
||||
// TODO: Probably needs to take state service
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
private derivedStateDefinition: DerivedStateDefinition<TFrom, TTo>,
|
||||
private encryptService: EncryptService,
|
||||
private activeUserState: ActiveUserState<TFrom>
|
||||
) {
|
||||
this.state$ = activeUserState.state$
|
||||
.pipe(switchMap(async from => {
|
||||
// TODO: How do I get the key?
|
||||
const convertedData = await derivedStateDefinition.converter(from, new ConverterContext(null, encryptService));
|
||||
return convertedData;
|
||||
}));
|
||||
}
|
||||
|
||||
async getFromState(): Promise<TTo> {
|
||||
const encryptedFromState = await this.activeUserState.getFromState();
|
||||
|
||||
const context = new ConverterContext(null, this.encryptService);
|
||||
|
||||
const decryptedData = await this.derivedStateDefinition.converter(encryptedFromState, context);
|
||||
return decryptedData;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
||||
private seededInitial = false;
|
||||
|
||||
private formattedKey$: Observable<string>;
|
||||
private chosenStorageLocation: AbstractStorageService;
|
||||
|
||||
// TODO: Use BitSubject
|
||||
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
||||
private stateSubject$ = this.stateSubject.asObservable();
|
||||
|
||||
state$: Observable<T>;
|
||||
|
||||
constructor(
|
||||
private keyDefinition: KeyDefinition<T>,
|
||||
private stateService: StateService,
|
||||
private encryptService: EncryptService,
|
||||
private memoryStorageService: AbstractMemoryStorageService,
|
||||
private secureStorageService: AbstractStorageService,
|
||||
private diskStorageService: AbstractStorageService
|
||||
) {
|
||||
this.chosenStorageLocation = this.chooseStorage(
|
||||
this.keyDefinition.stateDefinition.storageLocation
|
||||
);
|
||||
|
||||
const unformattedKey = `${this.keyDefinition.stateDefinition.name}_{userId}_${this.keyDefinition.key}`;
|
||||
|
||||
// startWith?
|
||||
this.formattedKey$ = this.stateService.activeAccount$
|
||||
.pipe(
|
||||
map(accountId => accountId != null
|
||||
? unformattedKey.replace("{userId}", accountId)
|
||||
: null)
|
||||
);
|
||||
|
||||
const activeAccountData$ = this.formattedKey$
|
||||
.pipe(switchMap(async key => {
|
||||
console.log("user emitted", key);
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
const jsonData = await this.chosenStorageLocation.get<Jsonify<T>>(key);
|
||||
const data = keyDefinition.serializer(jsonData);
|
||||
return data;
|
||||
}),
|
||||
tap(data => {
|
||||
console.log("data:", 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(() => {
|
||||
console.log("starting subscription.");
|
||||
const subscription = activeAccountData$.subscribe();
|
||||
return this.stateSubject$
|
||||
.pipe(tap({
|
||||
complete: () => subscription.unsubscribe(),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async update(configureState: (state: T) => void): Promise<void> {
|
||||
const currentState = await firstValueFrom(this.state$);
|
||||
console.log("data to update:", currentState);
|
||||
configureState(currentState);
|
||||
const key = await this.createKey();
|
||||
if (key == null) {
|
||||
throw new Error("Attempting to active user state, when no user is active.");
|
||||
}
|
||||
console.log(`updating ${key} to ${currentState}`);
|
||||
await this.chosenStorageLocation.save(await this.createKey(), currentState);
|
||||
this.stateSubject.next(currentState);
|
||||
}
|
||||
|
||||
async getFromState(): Promise<T> {
|
||||
const activeUserId = await this.stateService.getUserId();
|
||||
const key = userKeyBuilder(activeUserId, this.keyDefinition);
|
||||
const data = await this.chosenStorageLocation.get(key) as Jsonify<T>;
|
||||
return this.keyDefinition.serializer(data);
|
||||
}
|
||||
|
||||
createDerived<TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>): DerivedActiveUserState<T, TTo> {
|
||||
return new DerivedActiveUserState<T, TTo>(
|
||||
derivedStateDefinition,
|
||||
this.encryptService,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
private async createKey(): Promise<string> {
|
||||
return `${(await firstValueFrom(this.formattedKey$))}`;
|
||||
}
|
||||
|
||||
private chooseStorage(storageLocation: StorageLocation): AbstractStorageService {
|
||||
switch (storageLocation) {
|
||||
case "disk":
|
||||
return this.diskStorageService;
|
||||
case "secure":
|
||||
return this.secureStorageService;
|
||||
case "memory":
|
||||
return this.memoryStorageService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService, // 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>): DefaultActiveUserState<T> {
|
||||
const locationDomainKey =
|
||||
`${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
||||
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>(
|
||||
keyDefinition,
|
||||
this.stateService,
|
||||
this.encryptService,
|
||||
this.memoryStorage,
|
||||
this.secureStorage,
|
||||
this.diskStorage
|
||||
);
|
||||
this.userStateCache[locationDomainKey] = newActiveUserState;
|
||||
return newActiveUserState;
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
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,84 @@
|
||||
import { BehaviorSubject, Observable, defer, firstValueFrom } from "rxjs";
|
||||
|
||||
import { GlobalStateProvider } from "../abstractions/global-state.provider";
|
||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { ActiveUserState } from "../interfaces/active-user-state";
|
||||
import { KeyDefinition } from "../types/key-definition";
|
||||
import { Jsonify } from "type-fest";
|
||||
import { globalKeyBuilder } from "../misc/key-builders";
|
||||
|
||||
|
||||
|
||||
// TODO: Move type
|
||||
export type StorageLocation = "memory" | "disk" | "secure";
|
||||
|
||||
// class DefaultGlobalState<T> implements ActiveUserState<T> {
|
||||
// private storageKey: string;
|
||||
|
||||
// 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);
|
||||
|
||||
// // TODO: When subsribed to, we need to read data from the chosen storage location
|
||||
// // and give it back
|
||||
// this.state$ = new Observable<T>()
|
||||
// }
|
||||
|
||||
// async update(configureState: (state: T) => void): Promise<void> {
|
||||
// const currentState = await firstValueFrom(this.state$);
|
||||
// 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.serializer(data);
|
||||
// }
|
||||
// }
|
||||
|
||||
// export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
||||
// private globalStateCache: Record<string, DefaultGlobalState<unknown>> = {};
|
||||
|
||||
// constructor(
|
||||
// private memoryStorage: AbstractMemoryStorageService,
|
||||
// private diskStorage: AbstractStorageService,
|
||||
// private secureStorage: AbstractStorageService) {
|
||||
// }
|
||||
|
||||
// create<T>(keyDefinition: KeyDefinition<T>): DefaultGlobalState<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;
|
||||
// }
|
||||
// }
|
||||
// }
|
18
libs/common/src/platform/types/derived-state-definition.ts
Normal file
18
libs/common/src/platform/types/derived-state-definition.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { UserKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { StorageLocation } from "../services/default-global-state.provider";
|
||||
|
||||
// TODO: Move type
|
||||
export class DeriveContext {
|
||||
constructor(
|
||||
readonly activeUserKey: UserKey,
|
||||
readonly encryptService: EncryptService
|
||||
) { }
|
||||
}
|
||||
|
||||
export class DerivedStateDefinition<TFrom, TTo> {
|
||||
constructor(
|
||||
readonly location: StorageLocation,
|
||||
readonly converter: (data: TFrom, context: DeriveContext) => Promise<TTo>
|
||||
) {}
|
||||
}
|
52
libs/common/src/platform/types/key-definition.ts
Normal file
52
libs/common/src/platform/types/key-definition.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeriveContext, DerivedStateDefinition } from "./derived-state-definition";
|
||||
import { StateDefinition, StorageLocation } from "./state-definition";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
// TODO Import Purpose type
|
||||
export class KeyDefinition<T> {
|
||||
/**
|
||||
* Creates a new instance of a KeyDefinition
|
||||
* @param stateDefinition The state definition for which this key belongs to.
|
||||
* @param key The name of the key, this should be unique per domain
|
||||
* @param serializer A function to use to safely convert your type from json to your expected type.
|
||||
*/
|
||||
constructor(
|
||||
readonly stateDefinition: StateDefinition,
|
||||
readonly key: string,
|
||||
readonly serializer: (jsonValue: Jsonify<T>) => T
|
||||
) { }
|
||||
|
||||
static array<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) {
|
||||
return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => {
|
||||
// TODO: Should we handle null for them, I feel like we should discourage null for an array?
|
||||
return jsonValue.map(v => serializer(v));
|
||||
});
|
||||
}
|
||||
|
||||
static record<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) {
|
||||
return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => {
|
||||
const output: Record<string, T> = {};
|
||||
for (const key in jsonValue) {
|
||||
output[key] = serializer((jsonValue as Record<string, Jsonify<T>>)[key]);
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for defining a derived definition that will often be used alongside a given key
|
||||
* @param storageLocation
|
||||
* @param decrypt
|
||||
* @returns
|
||||
*/
|
||||
createDerivedDefinition<TTo>(
|
||||
storageLocation: StorageLocation,
|
||||
decrypt: (data: T, context: DeriveContext) => Promise<TTo>
|
||||
) {
|
||||
return new DerivedStateDefinition(storageLocation, decrypt);
|
||||
}
|
||||
}
|
19
libs/common/src/platform/types/state-definition.ts
Normal file
19
libs/common/src/platform/types/state-definition.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// TODO: How can we protect the creation of these so that platform can maintain the allowed creations?
|
||||
|
||||
// TODO: Where should this live
|
||||
export type StorageLocation = "disk" | "memory" | "secure";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class StateDefinition {
|
||||
/**
|
||||
*
|
||||
* @param name The name of the state, this needs to be unique from all other {@link StateDefinition}'s.
|
||||
* @param storageLocation The location of where this state should be stored.
|
||||
*/
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly storageLocation: StorageLocation
|
||||
) { }
|
||||
}
|
22
libs/common/src/platform/types/state-definitions.spec.ts
Normal file
22
libs/common/src/platform/types/state-definitions.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { StateDefinition } from "./state-definition";
|
||||
import * as definitions from "./state-definitions";
|
||||
|
||||
it("has all unique definitions", () => {
|
||||
const uniqueNames: string[] = [];
|
||||
const keys = Object.keys(definitions);
|
||||
|
||||
for (const key of keys) {
|
||||
const definition = (definitions as unknown as Record<string, StateDefinition>)[key];
|
||||
if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) {
|
||||
throw new Error(`${key} from import ./state-definitions is expected to be a StateDefinition but wasn't.`);
|
||||
}
|
||||
|
||||
const name = `${definition.name}_${definition.storageLocation}`;
|
||||
|
||||
if (uniqueNames.includes(name)) {
|
||||
throw new Error(`Definition ${key} is invalid, it's elements have already been claimed. Please choose a unique name.`);
|
||||
}
|
||||
|
||||
uniqueNames.push(name);
|
||||
}
|
||||
});
|
3
libs/common/src/platform/types/state-definitions.ts
Normal file
3
libs/common/src/platform/types/state-definitions.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { StateDefinition } from "./state-definition";
|
||||
|
||||
export const FOLDER_SERVICE_DISK = new StateDefinition("FolderService", "disk");
|
@ -11,10 +11,11 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
import { MoveFolderToOwnedMigrator } from "./migrations/9-move-folder-to-owned";
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 2;
|
||||
export const CURRENT_VERSION = 8;
|
||||
export const CURRENT_VERSION = 9;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export async function migrate(
|
||||
@ -38,7 +39,8 @@ export async function migrate(
|
||||
.with(AddKeyTypeToOrgKeysMigrator, 4, 5)
|
||||
.with(RemoveLegacyEtmKeyMigrator, 5, 6)
|
||||
.with(MoveBiometricAutoPromptToAccount, 6, 7)
|
||||
.with(MoveStateVersionMigrator, 7, CURRENT_VERSION)
|
||||
.with(MoveStateVersionMigrator, 7, 8)
|
||||
.with(MoveFolderToOwnedMigrator, 8, CURRENT_VERSION)
|
||||
.migrate(migrationHelper);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
// TODO: Add message
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
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";
|
||||
// TODO: Add message
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { StateDefinition } from "../../platform/types/state-definition";
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { IRREVERSIBLE, Migrator } from "../migrator";
|
||||
|
||||
type ExpectedAccountType = {
|
||||
data: {
|
||||
folders: {
|
||||
encrypted: Record<string, { name: string, id: string, revisionDate: string }>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const FOLDER_STATE = new StateDefinition("FolderService", "disk");
|
||||
|
||||
const INITIAL_FOLDER_USER_KEY = new KeyDefinition<unknown>(FOLDER_STATE, "folders", (s) => s);
|
||||
|
||||
export class MoveFolderToOwnedMigrator extends Migrator<8, 9> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
async function updateAccount(userId: string, account: ExpectedAccountType) {
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = userKeyBuilder(userId, INITIAL_FOLDER_USER_KEY);
|
||||
await helper.set(userKey, account.data.folders.encrypted);
|
||||
|
||||
// TODO: Is there ever anything more on the folders object than the encrypted prop
|
||||
// delete account.data.folders;
|
||||
helper.info(`Would delete: ${JSON.stringify(account.data.folders)}`);
|
||||
// TODO:
|
||||
// await helper.set("", account);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
accounts.map(({ userId, account}) => updateAccount(userId, account))
|
||||
);
|
||||
}
|
||||
|
||||
rollback(helper: MigrationHelper): Promise<void> {
|
||||
// TODO: This doesn't actually need to be irreversible
|
||||
throw IRREVERSIBLE;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
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,66 +1,57 @@
|
||||
import { BehaviorSubject, concatMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ActiveUserStateProvider } from "../../../platform/abstractions/active-user-state.provider";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { ActiveUserState } from "../../../platform/interfaces/active-user-state";
|
||||
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 { DerivedActiveUserState } from "../../../platform/services/default-active-user-state.provider";
|
||||
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";
|
||||
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";
|
||||
|
||||
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([]);
|
||||
|
||||
folders$ = this._folders.asObservable();
|
||||
folderViews$ = this._folderViews.asObservable();
|
||||
folderState: ActiveUserState<Record<string, Folder>>;
|
||||
decryptedFolderState: DerivedActiveUserState<Record<string, Folder>, FolderView[]>
|
||||
|
||||
private folderState: State<UserFolderState>;
|
||||
folders$: Observable<Folder[]>;
|
||||
folderViews$: Observable<FolderView[]>;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService,
|
||||
private activeUserStateProviderService: ActiveUserStateProviderService
|
||||
private activeUserStateProvider: ActiveUserStateProvider,
|
||||
private stateService: StateService
|
||||
) {
|
||||
this.folderState = activeUserStateProviderService.create<UserFolderState>(new DomainToken("folder", UserFolderState.fromJSON));
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
if (Utils.global.bitwardenContainerService == null) {
|
||||
return;
|
||||
}
|
||||
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => {
|
||||
const folders = this.flattenMap(foldersMap);
|
||||
const decryptedFolders = await this.decryptFolders(folders);
|
||||
return decryptedFolders;
|
||||
})
|
||||
|
||||
if (!unlocked) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
return;
|
||||
}
|
||||
this.folderState = this.activeUserStateProvider.create(FOLDERS);
|
||||
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
this.folders$ = this.folderState.state$
|
||||
.pipe(map(foldersMap => {
|
||||
return this.flattenMap(foldersMap);
|
||||
}));
|
||||
|
||||
await this.updateObservables(data);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition);
|
||||
this.folderViews$ = this.decryptedFolderState.state$;
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
this._folderViews.next([]);
|
||||
// TODO: I don't really have a replacement for this right now
|
||||
// this._folderViews.next([]);
|
||||
}
|
||||
|
||||
// TODO: This should be moved to EncryptService or something
|
||||
@ -72,21 +63,13 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Folder> {
|
||||
const folders = this._folders.getValue();
|
||||
|
||||
return folders.find((folder) => folder.id === id);
|
||||
const folders = await firstValueFrom(this.folderState.state$);
|
||||
return folders[id];
|
||||
}
|
||||
|
||||
async getAllFromState(): Promise<Folder[]> {
|
||||
const folders = await this.stateService.getEncryptedFolders();
|
||||
const response: Folder[] = [];
|
||||
for (const id in folders) {
|
||||
// eslint-disable-next-line
|
||||
if (folders.hasOwnProperty(id)) {
|
||||
response.push(new Folder(folders[id]));
|
||||
}
|
||||
}
|
||||
return response;
|
||||
const foldersMap = await this.folderState.getFromState();
|
||||
return this.flattenMap(foldersMap);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,76 +77,58 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
* @param id id of the folder
|
||||
*/
|
||||
async getFromState(id: string): Promise<Folder> {
|
||||
const foldersMap = await this.stateService.getEncryptedFolders();
|
||||
const foldersMap = await this.folderState.getFromState();
|
||||
const folder = foldersMap[id];
|
||||
if (folder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Folder(folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
async getAllDecryptedFromState(): Promise<FolderView[]> {
|
||||
const data = await this.stateService.getEncryptedFolders();
|
||||
const folders = Object.values(data || {}).map((f) => new Folder(f));
|
||||
|
||||
return this.decryptFolders(folders);
|
||||
return await this.decryptedFolderState.getFromState();
|
||||
}
|
||||
|
||||
async upsert(folder: FolderData | FolderData[]): Promise<void> {
|
||||
let folders = await this.stateService.getEncryptedFolders();
|
||||
if (folders == null) {
|
||||
folders = {};
|
||||
}
|
||||
|
||||
if (folder instanceof FolderData) {
|
||||
const f = folder as FolderData;
|
||||
folders[f.id] = f;
|
||||
} else {
|
||||
(folder as FolderData[]).forEach((f) => {
|
||||
folders[f.id] = f;
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
console.log("upsert", folder);
|
||||
await this.folderState.update(folders => {
|
||||
if (folder instanceof FolderData) {
|
||||
const f = folder as FolderData;
|
||||
folders[f.id] = new Folder(f);
|
||||
} else {
|
||||
(folder as FolderData[]).forEach((f) => {
|
||||
folders[f.id] = new Folder(f);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async replace(folders: { [id: string]: FolderData }): Promise<void> {
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => {
|
||||
agg[key] = new Folder(value);
|
||||
return agg;
|
||||
}, {} as Record<string, Folder>);
|
||||
console.log("replace", folders, convertedFolders);
|
||||
await this.folderState.update(f => f = convertedFolders);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<any> {
|
||||
if (userId == null || userId == (await this.stateService.getUserId())) {
|
||||
this._folders.next([]);
|
||||
this._folderViews.next([]);
|
||||
}
|
||||
await this.stateService.setEncryptedFolders(null, { userId: userId });
|
||||
console.log("clear", userId);
|
||||
await this.folderState.update(f => f = null);
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const folders = await this.stateService.getEncryptedFolders();
|
||||
if (folders == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof id === "string") {
|
||||
if (folders[id] == null) {
|
||||
return;
|
||||
async delete(id: string | string[]): Promise<void> {
|
||||
const folderIds = typeof id === "string" ? [id] : id;
|
||||
console.log("delete", folderIds);
|
||||
await this.folderState.update(folders => {
|
||||
for (const folderId in folderIds) {
|
||||
delete folders[folderId];
|
||||
}
|
||||
delete folders[id];
|
||||
} else {
|
||||
(id as string[]).forEach((i) => {
|
||||
delete folders[i];
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateObservables(folders);
|
||||
await this.stateService.setEncryptedFolders(folders);
|
||||
});
|
||||
|
||||
// Items in a deleted folder are re-assigned to "No Folder"
|
||||
const ciphers = await this.stateService.getEncryptedCiphers();
|
||||
@ -181,16 +146,6 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateObservables(foldersMap: { [id: string]: FolderData }) {
|
||||
const folders = Object.values(foldersMap || {}).map((f) => new Folder(f));
|
||||
|
||||
this._folders.next(folders);
|
||||
|
||||
if (await this.cryptoService.hasUserKey()) {
|
||||
this._folderViews.next(await this.decryptFolders(folders));
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptFolders(folders: Folder[]) {
|
||||
const decryptFolderPromises = folders.map((f) => f.decrypt());
|
||||
const decryptedFolders = await Promise.all(decryptFolderPromises);
|
||||
@ -203,4 +158,12 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
|
||||
return decryptedFolders;
|
||||
}
|
||||
|
||||
private flattenMap(foldersMap: Record<string, Folder>): Folder[] {
|
||||
const folders: Folder[] = [];
|
||||
for (const id in foldersMap) {
|
||||
folders.push(foldersMap[id]);
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
}
|
||||
|
6
libs/common/src/vault/types/key-definitions.ts
Normal file
6
libs/common/src/vault/types/key-definitions.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { KeyDefinition } from "../../platform/types/key-definition";
|
||||
import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions";
|
||||
import { Folder } from "../models/domain/folder";
|
||||
|
||||
// FolderService Keys
|
||||
export const FOLDERS = KeyDefinition.record<Folder>(FOLDER_SERVICE_DISK, "folders", Folder.fromJSON);
|
Loading…
Reference in New Issue
Block a user