From 33c8d55f0d6e1bdab107094de08d38a134f883a0 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:54:35 -0400 Subject: [PATCH] Rebase: Start Implementing AccountService --- apps/desktop/src/main.ts | 6 + .../src/services/jslib-services.module.ts | 7 +- .../src/auth/services/account.service.ts | 2 + .../platform/interfaces/active-user-state.ts | 10 +- .../src/platform/interfaces/global-state.ts | 6 +- libs/common/src/platform/misc/key-builders.ts | 14 +- ...default-active-user-state.provider.spec.ts | 55 ++++--- .../default-active-user-state.provider.ts | 142 ++++++++++-------- .../services/default-global-state.provider.ts | 133 ++++++++-------- .../src/platform/services/state.service.ts | 13 +- .../types/derived-state-definition.ts | 5 +- .../src/platform/types/key-definition.ts | 16 +- .../src/platform/types/state-definition.ts | 9 +- .../platform/types/state-definitions.spec.ts | 8 +- .../migrations/9-move-folder-to-owned.ts | 10 +- .../src/vault/models/data/folder.data.ts | 12 +- .../vault/services/folder/folder.service.ts | 55 +++---- .../common/src/vault/types/key-definitions.ts | 8 +- 18 files changed, 286 insertions(+), 225 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5107d31b1c5..d192e420dae 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -2,6 +2,7 @@ import * as path from "path"; import { app } from "electron"; +import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; @@ -28,6 +29,7 @@ export class Main { storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; messagingService: ElectronMainMessagingService; + accountService: AccountServiceImplementation; stateService: ElectronStateService; desktopCredentialStorageListener: DesktopCredentialStorageListener; @@ -91,6 +93,7 @@ export class Main { this.memoryStorageService, this.logService, new StateFactory(GlobalState, Account), + this.accountService, // TODO: This is circular false // Do not use disk caching because this will get out of sync with the renderer service ); @@ -108,6 +111,9 @@ export class Main { this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { this.messagingMain.onMessage(message); }); + + this.accountService = new AccountServiceImplementation(this.messagingService, this.logService); + this.powerMonitorMain = new PowerMonitorMain(this.messagingService); this.menuMain = new MenuMain( this.i18nService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9b5aebac388..21552f4de0b 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -176,7 +176,6 @@ import { ModalService } from "./modal.service"; import { ThemingService } from "./theming/theming.service"; import { AbstractThemingService } from "./theming/theming.service.abstraction"; - @NgModule({ declarations: [], providers: [ @@ -731,9 +730,9 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; EncryptService, MEMORY_STORAGE, AbstractStorageService, - SECURE_STORAGE - ] - } + SECURE_STORAGE, + ], + }, ], }) export class JslibServicesModule {} diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index b311b29d671..557c38c2f75 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -5,6 +5,7 @@ import { map, distinctUntilChanged, share, + tap, } from "rxjs"; import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service"; @@ -23,6 +24,7 @@ export class AccountServiceImplementation implements InternalAccountService { activeAccount$ = this.activeAccountId.pipe( combineLatestWith(this.accounts$), map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)), + tap((stuff) => console.log("stuff", stuff)), distinctUntilChanged(), share() ); diff --git a/libs/common/src/platform/interfaces/active-user-state.ts b/libs/common/src/platform/interfaces/active-user-state.ts index b2520705953..c5652719719 100644 --- a/libs/common/src/platform/interfaces/active-user-state.ts +++ b/libs/common/src/platform/interfaces/active-user-state.ts @@ -4,8 +4,10 @@ import { DerivedActiveUserState } from "../services/default-active-user-state.pr import { DerivedStateDefinition } from "../types/derived-state-definition"; export interface ActiveUserState { - readonly state$: Observable - readonly getFromState: () => Promise - readonly update: (configureState: (state: T) => void) => Promise - createDerived: (derivedStateDefinition: DerivedStateDefinition) => DerivedActiveUserState + readonly state$: Observable; + readonly getFromState: () => Promise; + readonly update: (configureState: (state: T) => void) => Promise; + createDerived: ( + derivedStateDefinition: DerivedStateDefinition + ) => DerivedActiveUserState; } diff --git a/libs/common/src/platform/interfaces/global-state.ts b/libs/common/src/platform/interfaces/global-state.ts index 17b9ae19705..97300993090 100644 --- a/libs/common/src/platform/interfaces/global-state.ts +++ b/libs/common/src/platform/interfaces/global-state.ts @@ -1,6 +1,6 @@ -import { Observable } from "rxjs" +import { Observable } from "rxjs"; export interface GlobalState { - update: (configureState: (state: T) => void) => Promise - state$: Observable + update: (configureState: (state: T) => void) => Promise; + state$: Observable; } diff --git a/libs/common/src/platform/misc/key-builders.ts b/libs/common/src/platform/misc/key-builders.ts index 45e961f6e89..92b0e8acf31 100644 --- a/libs/common/src/platform/misc/key-builders.ts +++ b/libs/common/src/platform/misc/key-builders.ts @@ -1,16 +1,14 @@ import { KeyDefinition } from "../types/key-definition"; -// TODO: Use Matt's `UserId` type -export function userKeyBuilder( - userId: string, - keyDefinition: KeyDefinition -): string { +// TODO: Use Matts `UserId` type +export function userKeyBuilder(userId: string, keyDefinition: KeyDefinition): string { + if (userId == null) { + throw new Error("You cannot build a user key without"); + } return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`; } -export function globalKeyBuilder( - keyDefinition: KeyDefinition -): string { +export function globalKeyBuilder(keyDefinition: KeyDefinition): string { // TODO: Do we want the _global_ part? return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`; } diff --git a/libs/common/src/platform/services/default-active-user-state.provider.spec.ts b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts index 0a404708850..34e861d0004 100644 --- a/libs/common/src/platform/services/default-active-user-state.provider.spec.ts +++ b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts @@ -2,7 +2,7 @@ import { matches, mock, mockReset } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; -import { StateService } from "../abstractions/state.service" +import { StateService } from "../abstractions/state.service"; import { AbstractMemoryStorageService } from "../abstractions/storage.service"; import { KeyDefinition } from "../types/key-definition"; import { StateDefinition } from "../types/state-definition"; @@ -11,7 +11,7 @@ import { DefaultActiveUserStateProvider } from "./default-active-user-state.prov class TestState { date: Date; - array: string[] + array: string[]; // TODO: More complex data types static fromJSON(jsonState: Jsonify) { @@ -24,7 +24,11 @@ class TestState { const testStateDefinition = new StateDefinition("fake", "disk"); -const testKeyDefinition = new KeyDefinition(testStateDefinition, "fake", TestState.fromJSON); +const testKeyDefinition = new KeyDefinition( + testStateDefinition, + "fake", + TestState.fromJSON +); describe("DefaultStateProvider", () => { const stateService = mock(); @@ -52,13 +56,12 @@ describe("DefaultStateProvider", () => { }); 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; - }); + 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 = activeUserStateProvider.create(testKeyDefinition); @@ -67,11 +70,11 @@ describe("DefaultStateProvider", () => { // User signs in activeAccountSubject.next("1"); - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); // Service does an update - await fakeDomainState.update(state => state.array.push("value3")); - await new Promise(resolve => setTimeout(resolve, 10)); + await fakeDomainState.update((state) => state.array.push("value3")); + await new Promise((resolve) => setTimeout(resolve, 10)); subscription.unsubscribe(); @@ -79,16 +82,24 @@ describe("DefaultStateProvider", () => { expect(subscribeCallback).toHaveBeenNthCalledWith(1, null); // Gotten starter user data - expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches(value => { - return true; - })); + expect(subscribeCallback).toHaveBeenNthCalledWith( + 2, + matches((value) => { + return true; + }) + ); // Gotten update callback data - expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches((value) => { - return value != null && - typeof value.date == "object" && - value.date.getFullYear() == 2023 && - value.array.length == 3 - })); + expect(subscribeCallback).toHaveBeenNthCalledWith( + 3, + matches((value) => { + return ( + value != null && + typeof value.date == "object" && + value.date.getFullYear() == 2023 && + value.array.length == 3 + ); + }) + ); }); }); diff --git a/libs/common/src/platform/services/default-active-user-state.provider.ts b/libs/common/src/platform/services/default-active-user-state.provider.ts index ba0c26a2192..06f407adb9f 100644 --- a/libs/common/src/platform/services/default-active-user-state.provider.ts +++ b/libs/common/src/platform/services/default-active-user-state.provider.ts @@ -1,10 +1,22 @@ -import { BehaviorSubject, Observable, defer, firstValueFrom, map, share, switchMap, tap } from "rxjs"; +import { + BehaviorSubject, + Observable, + defer, + firstValueFrom, + map, + share, + switchMap, + tap, +} from "rxjs"; import { Jsonify } from "type-fest"; +import { AccountService } from "../../auth/abstractions/account.service"; 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 { + 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"; @@ -13,20 +25,15 @@ import { KeyDefinition } from "../types/key-definition"; import { StorageLocation } from "./default-global-state.provider"; class ConverterContext { - constructor( - readonly activeUserKey: UserKey, - readonly encryptService: EncryptService - ) { } + constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {} } class DerivedStateDefinition { - constructor( - readonly converter: (data: TFrom, context: ConverterContext) => Promise - ) { } + constructor(readonly converter: (data: TFrom, context: ConverterContext) => Promise) {} } export class DerivedActiveUserState { - state$: Observable + state$: Observable; // TODO: Probably needs to take state service /** @@ -37,12 +44,16 @@ export class DerivedActiveUserState { private encryptService: EncryptService, private activeUserState: ActiveUserState ) { - this.state$ = activeUserState.state$ - .pipe(switchMap(async from => { + 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)); + const convertedData = await derivedStateDefinition.converter( + from, + new ConverterContext(null, encryptService) + ); return convertedData; - })); + }) + ); } async getFromState(): Promise { @@ -61,7 +72,6 @@ class DefaultActiveUserState implements ActiveUserState { private formattedKey$: Observable; private chosenStorageLocation: AbstractStorageService; - // TODO: Use BitSubject protected stateSubject: BehaviorSubject = new BehaviorSubject(null); private stateSubject$ = this.stateSubject.asObservable(); @@ -69,7 +79,7 @@ class DefaultActiveUserState implements ActiveUserState { constructor( private keyDefinition: KeyDefinition, - private stateService: StateService, + private accountService: AccountService, private encryptService: EncryptService, private memoryStorageService: AbstractMemoryStorageService, private secureStorageService: AbstractStorageService, @@ -78,20 +88,19 @@ class DefaultActiveUserState implements ActiveUserState { 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) - ); + this.formattedKey$ = this.accountService.activeAccount$.pipe( + tap((user) => console.log("user", user)), // Temp + map((account) => + account != null && account.id != null + ? userKeyBuilder(account.id, this.keyDefinition) + : null + ) + ); - const activeAccountData$ = this.formattedKey$ - .pipe(switchMap(async key => { - console.log("user emitted", key); + const activeAccountData$ = this.formattedKey$.pipe( + switchMap(async (key) => { + console.log("user emitted: ", key); // temp if (key == null) { return null; } @@ -99,56 +108,69 @@ class DefaultActiveUserState implements ActiveUserState { const data = keyDefinition.serializer(jsonData); return data; }), - tap(data => { - console.log("data:", data); - this.stateSubject.next(data); - }), - // Share the execution - share() - ); + tap((data) => { + this.seededInitial = true; + 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({ + return this.stateSubject$.pipe( + tap({ complete: () => subscription.unsubscribe(), - })); + }) + ); }); } async update(configureState: (state: T) => void): Promise { - 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}`); + const currentState = this.seededInitial + ? this.stateSubject.getValue() + : await this.seedInitial(key); + + configureState(currentState); + await this.chosenStorageLocation.save(await this.createKey(), currentState); this.stateSubject.next(currentState); } async getFromState(): Promise { - const activeUserId = await this.stateService.getUserId(); - const key = userKeyBuilder(activeUserId, this.keyDefinition); - const data = await this.chosenStorageLocation.get(key) as Jsonify; + const activeUser = await firstValueFrom(this.accountService.activeAccount$); + if (activeUser == null || activeUser.id == null) { + throw new Error("You cannot get data from state while there is no active user."); + } + const key = userKeyBuilder(activeUser.id, this.keyDefinition); + const data = (await this.chosenStorageLocation.get(key)) as Jsonify; return this.keyDefinition.serializer(data); } - createDerived(derivedStateDefinition: DerivedStateDefinition): DerivedActiveUserState { - return new DerivedActiveUserState( - derivedStateDefinition, - this.encryptService, - this - ); + createDerived( + derivedStateDefinition: DerivedStateDefinition + ): DerivedActiveUserState { + return new DerivedActiveUserState(derivedStateDefinition, this.encryptService, this); } private async createKey(): Promise { - return `${(await firstValueFrom(this.formattedKey$))}`; + const formattedKey = await firstValueFrom(this.formattedKey$); + if (formattedKey == null) { + throw new Error("Cannot create a key while there is no active user."); + } + return formattedKey; + } + + private async seedInitial(key: string): Promise { + const data = await this.chosenStorageLocation.get>(key); + this.seededInitial = true; + return this.keyDefinition.serializer(data); } private chooseStorage(storageLocation: StorageLocation): AbstractStorageService { @@ -163,21 +185,19 @@ class DefaultActiveUserState implements ActiveUserState { } } - export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { private userStateCache: Record> = {}; constructor( - private stateService: StateService, // Inject the lightest weight service that provides accountUserId$ + private accountService: AccountService, // Inject the lightest weight service that provides accountUserId$ private encryptService: EncryptService, private memoryStorage: AbstractMemoryStorageService, private diskStorage: AbstractStorageService, - private secureStorage: AbstractStorageService) { - } + private secureStorage: AbstractStorageService + ) {} create(keyDefinition: KeyDefinition): DefaultActiveUserState { - const locationDomainKey = - `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; + 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 @@ -187,7 +207,7 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { const newActiveUserState = new DefaultActiveUserState( keyDefinition, - this.stateService, + this.accountService, this.encryptService, this.memoryStorage, this.secureStorage, diff --git a/libs/common/src/platform/services/default-global-state.provider.ts b/libs/common/src/platform/services/default-global-state.provider.ts index 4fc7054883a..449985d80dc 100644 --- a/libs/common/src/platform/services/default-global-state.provider.ts +++ b/libs/common/src/platform/services/default-global-state.provider.ts @@ -1,84 +1,89 @@ -import { BehaviorSubject, Observable, defer, firstValueFrom } from "rxjs"; +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 { ActiveUserState } from "../interfaces/active-user-state"; -import { KeyDefinition } from "../types/key-definition"; -import { Jsonify } from "type-fest"; +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 DefaultGlobalState implements ActiveUserState { -// private storageKey: string; +class GlobalStateImplementation implements GlobalState { + private storageKey: string; + private seededPromise: Promise; -// protected stateSubject: BehaviorSubject = new BehaviorSubject(null); + protected stateSubject: BehaviorSubject = new BehaviorSubject(null); -// state$: Observable; + state$: Observable; -// constructor( -// private keyDefinition: KeyDefinition, -// private chosenLocation: AbstractStorageService -// ) { -// this.storageKey = globalKeyBuilder(this.keyDefinition); + constructor( + private keyDefinition: KeyDefinition, + 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() -// } + this.seededPromise = this.chosenLocation.get>(this.storageKey).then((data) => { + const serializedData = this.keyDefinition.serializer(data); + this.stateSubject.next(serializedData); + }); -// async update(configureState: (state: T) => void): Promise { -// const currentState = await firstValueFrom(this.state$); -// configureState(currentState); -// await this.chosenLocation.save(this.storageKey, currentState); -// this.stateSubject.next(currentState); -// } + this.state$ = this.stateSubject.asObservable(); + } -// async getFromState(): Promise { -// const data = await this.chosenLocation.get>(this.storageKey); -// return this.keyDefinition.serializer(data); -// } -// } + async update(configureState: (state: T) => void): Promise { + await this.seededPromise; + const currentState = this.stateSubject.getValue(); + configureState(currentState); + await this.chosenLocation.save(this.storageKey, currentState); + this.stateSubject.next(currentState); + } -// export class DefaultGlobalStateProvider implements GlobalStateProvider { -// private globalStateCache: Record> = {}; + async getFromState(): Promise { + const data = await this.chosenLocation.get>(this.storageKey); + return this.keyDefinition.serializer(data); + } +} -// constructor( -// private memoryStorage: AbstractMemoryStorageService, -// private diskStorage: AbstractStorageService, -// private secureStorage: AbstractStorageService) { -// } +export class DefaultGlobalStateProvider implements GlobalStateProvider { + private globalStateCache: Record> = {}; -// create(keyDefinition: KeyDefinition): DefaultGlobalState { -// 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; -// } + constructor( + private memoryStorage: AbstractMemoryStorageService, + private diskStorage: AbstractStorageService, + private secureStorage: AbstractStorageService + ) {} + create(keyDefinition: KeyDefinition): GlobalState { + 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; + } -// const newGlobalState = new DefaultGlobalState( -// keyDefinition, -// this.getLocation(keyDefinition.stateDefinition.storageLocation) -// ); + const newGlobalState = new GlobalStateImplementation( + keyDefinition, + this.getLocation(keyDefinition.stateDefinition.storageLocation) + ); -// this.globalStateCache[locationDomainKey] = newGlobalState; -// return newGlobalState; -// } + 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; -// } -// } -// } + private getLocation(location: StorageLocation) { + switch (location) { + case "disk": + return this.diskStorage; + case "secure": + return this.secureStorage; + case "memory": + return this.memoryStorage; + } + } +} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index eb0d4639c73..4c223e2af89 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -190,14 +190,21 @@ export class StateService< state.accounts[userId] = this.createAccount(); const diskAccount = await this.getAccountFromDisk({ userId: userId }); state.accounts[userId].profile = diskAccount.profile; + this.accountService.addAccount(userId as UserId, { + status: AuthenticationStatus.Locked, + name: diskAccount.profile.name, + email: diskAccount.profile.email, + }); return state; }); - - // TODO: Temporary update to avoid routing all account status changes through account service for now. - this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.Locked); } async addAccount(account: TAccount) { + // this.accountService.addAccount(account.profile.userId as UserId, { + // email: account.profile.email, + // name: account.profile.name, + // status: AuthenticationStatus.Locked, + // }); account = await this.setAccountEnvironment(account); await this.updateState(async (state) => { state.authenticatedAccounts.push(account.profile.userId); diff --git a/libs/common/src/platform/types/derived-state-definition.ts b/libs/common/src/platform/types/derived-state-definition.ts index acd9a1770b3..3b525ad25eb 100644 --- a/libs/common/src/platform/types/derived-state-definition.ts +++ b/libs/common/src/platform/types/derived-state-definition.ts @@ -4,10 +4,7 @@ import { StorageLocation } from "../services/default-global-state.provider"; // TODO: Move type export class DeriveContext { - constructor( - readonly activeUserKey: UserKey, - readonly encryptService: EncryptService - ) { } + constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {} } export class DerivedStateDefinition { diff --git a/libs/common/src/platform/types/key-definition.ts b/libs/common/src/platform/types/key-definition.ts index c340c1eebcf..57c3ce6c1fe 100644 --- a/libs/common/src/platform/types/key-definition.ts +++ b/libs/common/src/platform/types/key-definition.ts @@ -18,16 +18,24 @@ export class KeyDefinition { readonly stateDefinition: StateDefinition, readonly key: string, readonly serializer: (jsonValue: Jsonify) => T - ) { } + ) {} - static array(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify) => T) { + static array( + stateDefinition: StateDefinition, + key: string, + serializer: (jsonValue: Jsonify) => T + ) { return new KeyDefinition(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)); + return jsonValue.map((v) => serializer(v)); }); } - static record(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify) => T) { + static record( + stateDefinition: StateDefinition, + key: string, + serializer: (jsonValue: Jsonify) => T + ) { return new KeyDefinition>(stateDefinition, key, (jsonValue) => { const output: Record = {}; for (const key in jsonValue) { diff --git a/libs/common/src/platform/types/state-definition.ts b/libs/common/src/platform/types/state-definition.ts index 2f870e7f03f..33492d01484 100644 --- a/libs/common/src/platform/types/state-definition.ts +++ b/libs/common/src/platform/types/state-definition.ts @@ -1,4 +1,4 @@ -// TODO: How can we protect the creation of these so that platform can maintain the allowed creations? +// TODO: Make this not allowed to be able to be imported anywhere willy-nilly. // TODO: Where should this live export type StorageLocation = "disk" | "memory" | "secure"; @@ -8,12 +8,9 @@ export type StorageLocation = "disk" | "memory" | "secure"; */ export class StateDefinition { /** - * + * Creates a new instance of {@link StateDefinition}, the creation of which is owned by the platform team. * @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 - ) { } + constructor(readonly name: string, readonly storageLocation: StorageLocation) {} } diff --git a/libs/common/src/platform/types/state-definitions.spec.ts b/libs/common/src/platform/types/state-definitions.spec.ts index 735da54287f..e3b0dffc84e 100644 --- a/libs/common/src/platform/types/state-definitions.spec.ts +++ b/libs/common/src/platform/types/state-definitions.spec.ts @@ -8,13 +8,17 @@ it("has all unique definitions", () => { for (const key of keys) { const definition = (definitions as unknown as Record)[key]; if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) { - throw new Error(`${key} from import ./state-definitions is expected to be a StateDefinition but wasn't.`); + 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.`); + throw new Error( + `Definition ${key} is invalid, its elements have already been claimed. Please choose a unique name.` + ); } uniqueNames.push(name); diff --git a/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts b/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts index 37af1722f6d..a861c0a2440 100644 --- a/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts +++ b/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts @@ -13,9 +13,9 @@ import { IRREVERSIBLE, Migrator } from "../migrator"; type ExpectedAccountType = { data: { folders: { - encrypted: Record - } - } + encrypted: Record; + }; + }; }; const FOLDER_STATE = new StateDefinition("FolderService", "disk"); @@ -40,9 +40,7 @@ export class MoveFolderToOwnedMigrator extends Migrator<8, 9> { // await helper.set("", account); } - await Promise.all( - accounts.map(({ userId, account}) => updateAccount(userId, account)) - ); + await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account))); } rollback(helper: MigrationHelper): Promise { diff --git a/libs/common/src/vault/models/data/folder.data.ts b/libs/common/src/vault/models/data/folder.data.ts index 1ac444db130..e43d6452271 100644 --- a/libs/common/src/vault/models/data/folder.data.ts +++ b/libs/common/src/vault/models/data/folder.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { FolderResponse } from "../response/folder.response"; export class FolderData { @@ -5,9 +7,17 @@ export class FolderData { name: string; revisionDate: string; - constructor(response: FolderResponse) { + constructor(response: Partial) { this.name = response.name; this.id = response.id; this.revisionDate = response.revisionDate; } + + static fromJSON(jsonObj: Jsonify) { + return new FolderData({ + id: jsonObj.id, + name: jsonObj.name, + revisionDate: jsonObj.revisionDate, + }); + } } diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 733839fbdf0..c2522831d21 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -16,11 +16,9 @@ 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: ActiveUserState>; - decryptedFolderState: DerivedActiveUserState, FolderView[]> + folderState: ActiveUserState>; + decryptedFolderState: DerivedActiveUserState, FolderView[]>; folders$: Observable; folderViews$: Observable; @@ -32,18 +30,22 @@ export class FolderService implements InternalFolderServiceAbstraction { private activeUserStateProvider: ActiveUserStateProvider, private stateService: StateService ) { - const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => { - const folders = this.flattenMap(foldersMap); - const decryptedFolders = await this.decryptFolders(folders); - return decryptedFolders; - }) + const derivedFoldersDefinition = FOLDERS.createDerivedDefinition( + "memory", + async (foldersMap) => { + const folders = this.flattenMap(foldersMap); + const decryptedFolders = await this.decryptFolders(folders); + return decryptedFolders; + } + ); this.folderState = this.activeUserStateProvider.create(FOLDERS); - this.folders$ = this.folderState.state$ - .pipe(map(foldersMap => { + this.folders$ = this.folderState.state$.pipe( + map((foldersMap) => { return this.flattenMap(foldersMap); - })); + }) + ); this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition); this.folderViews$ = this.decryptedFolderState.state$; @@ -64,7 +66,7 @@ export class FolderService implements InternalFolderServiceAbstraction { async get(id: string): Promise { const folders = await firstValueFrom(this.folderState.state$); - return folders[id]; + return new Folder(folders[id]); } async getAllFromState(): Promise { @@ -83,7 +85,7 @@ export class FolderService implements InternalFolderServiceAbstraction { return null; } - return folder; + return new Folder(folder); } /** @@ -94,37 +96,28 @@ export class FolderService implements InternalFolderServiceAbstraction { } async upsert(folder: FolderData | FolderData[]): Promise { - console.log("upsert", folder); - await this.folderState.update(folders => { + await this.folderState.update((folders) => { if (folder instanceof FolderData) { - const f = folder as FolderData; - folders[f.id] = new Folder(f); + folders[folder.id] = folder; } else { (folder as FolderData[]).forEach((f) => { - folders[f.id] = new Folder(f); + folders[f.id] = f; }); } }); } async replace(folders: { [id: string]: FolderData }): Promise { - const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => { - agg[key] = new Folder(value); - return agg; - }, {} as Record); - console.log("replace", folders, convertedFolders); - await this.folderState.update(f => f = convertedFolders); + await this.folderState.update((f) => (f = folders)); } async clear(userId?: string): Promise { - console.log("clear", userId); - await this.folderState.update(f => f = null); + await this.folderState.update((f) => (f = null)); } async delete(id: string | string[]): Promise { const folderIds = typeof id === "string" ? [id] : id; - console.log("delete", folderIds); - await this.folderState.update(folders => { + await this.folderState.update((folders) => { for (const folderId in folderIds) { delete folders[folderId]; } @@ -159,10 +152,10 @@ export class FolderService implements InternalFolderServiceAbstraction { return decryptedFolders; } - private flattenMap(foldersMap: Record): Folder[] { + private flattenMap(foldersMap: Record): Folder[] { const folders: Folder[] = []; for (const id in foldersMap) { - folders.push(foldersMap[id]); + folders.push(new Folder(foldersMap[id])); } return folders; } diff --git a/libs/common/src/vault/types/key-definitions.ts b/libs/common/src/vault/types/key-definitions.ts index 9f7ee283c4d..61f21d71274 100644 --- a/libs/common/src/vault/types/key-definitions.ts +++ b/libs/common/src/vault/types/key-definitions.ts @@ -1,6 +1,10 @@ import { KeyDefinition } from "../../platform/types/key-definition"; import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions"; -import { Folder } from "../models/domain/folder"; +import { FolderData } from "../models/data/folder.data"; // FolderService Keys -export const FOLDERS = KeyDefinition.record(FOLDER_SERVICE_DISK, "folders", Folder.fromJSON); +export const FOLDERS = KeyDefinition.record( + FOLDER_SERVICE_DISK, + "folders", + FolderData.fromJSON +);