1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

Rebase: Start Implementing AccountService

This commit is contained in:
Justin Baur
2023-10-02 16:54:35 -04:00
parent 39574fe6e4
commit 33c8d55f0d
18 changed files with 286 additions and 225 deletions

View File

@@ -2,6 +2,7 @@ import * as path from "path";
import { app } from "electron"; import { app } from "electron";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
@@ -28,6 +29,7 @@ export class Main {
storageService: ElectronStorageService; storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService; memoryStorageService: MemoryStorageService;
messagingService: ElectronMainMessagingService; messagingService: ElectronMainMessagingService;
accountService: AccountServiceImplementation;
stateService: ElectronStateService; stateService: ElectronStateService;
desktopCredentialStorageListener: DesktopCredentialStorageListener; desktopCredentialStorageListener: DesktopCredentialStorageListener;
@@ -91,6 +93,7 @@ export class Main {
this.memoryStorageService, this.memoryStorageService,
this.logService, this.logService,
new StateFactory(GlobalState, Account), 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 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.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
this.messagingMain.onMessage(message); this.messagingMain.onMessage(message);
}); });
this.accountService = new AccountServiceImplementation(this.messagingService, this.logService);
this.powerMonitorMain = new PowerMonitorMain(this.messagingService); this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
this.menuMain = new MenuMain( this.menuMain = new MenuMain(
this.i18nService, this.i18nService,

View File

@@ -176,7 +176,6 @@ import { ModalService } from "./modal.service";
import { ThemingService } from "./theming/theming.service"; import { ThemingService } from "./theming/theming.service";
import { AbstractThemingService } from "./theming/theming.service.abstraction"; import { AbstractThemingService } from "./theming/theming.service.abstraction";
@NgModule({ @NgModule({
declarations: [], declarations: [],
providers: [ providers: [
@@ -731,9 +730,9 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
EncryptService, EncryptService,
MEMORY_STORAGE, MEMORY_STORAGE,
AbstractStorageService, AbstractStorageService,
SECURE_STORAGE SECURE_STORAGE,
] ],
} },
], ],
}) })
export class JslibServicesModule {} export class JslibServicesModule {}

View File

@@ -5,6 +5,7 @@ import {
map, map,
distinctUntilChanged, distinctUntilChanged,
share, share,
tap,
} from "rxjs"; } from "rxjs";
import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service"; import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service";
@@ -23,6 +24,7 @@ export class AccountServiceImplementation implements InternalAccountService {
activeAccount$ = this.activeAccountId.pipe( activeAccount$ = this.activeAccountId.pipe(
combineLatestWith(this.accounts$), combineLatestWith(this.accounts$),
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)), map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
tap((stuff) => console.log("stuff", stuff)),
distinctUntilChanged(), distinctUntilChanged(),
share() share()
); );

View File

@@ -4,8 +4,10 @@ import { DerivedActiveUserState } from "../services/default-active-user-state.pr
import { DerivedStateDefinition } from "../types/derived-state-definition"; import { DerivedStateDefinition } from "../types/derived-state-definition";
export interface ActiveUserState<T> { export interface ActiveUserState<T> {
readonly state$: Observable<T> readonly state$: Observable<T>;
readonly getFromState: () => Promise<T> readonly getFromState: () => Promise<T>;
readonly update: (configureState: (state: T) => void) => Promise<void> readonly update: (configureState: (state: T) => void) => Promise<void>;
createDerived: <TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>) => DerivedActiveUserState<T, TTo> createDerived: <TTo>(
derivedStateDefinition: DerivedStateDefinition<T, TTo>
) => DerivedActiveUserState<T, TTo>;
} }

View File

@@ -1,6 +1,6 @@
import { Observable } from "rxjs" import { Observable } from "rxjs";
export interface GlobalState<T> { export interface GlobalState<T> {
update: (configureState: (state: T) => void) => Promise<void> update: (configureState: (state: T) => void) => Promise<void>;
state$: Observable<T> state$: Observable<T>;
} }

View File

@@ -1,16 +1,14 @@
import { KeyDefinition } from "../types/key-definition"; import { KeyDefinition } from "../types/key-definition";
// TODO: Use Matt's `UserId` type // TODO: Use Matts `UserId` type
export function userKeyBuilder( export function userKeyBuilder(userId: string, keyDefinition: KeyDefinition<unknown>): string {
userId: string, if (userId == null) {
keyDefinition: KeyDefinition<unknown> throw new Error("You cannot build a user key without");
): string { }
return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`; return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`;
} }
export function globalKeyBuilder( export function globalKeyBuilder(keyDefinition: KeyDefinition<unknown>): string {
keyDefinition: KeyDefinition<unknown>
): string {
// TODO: Do we want the _global_ part? // TODO: Do we want the _global_ part?
return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`; return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`;
} }

View File

@@ -2,7 +2,7 @@ import { matches, mock, mockReset } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { StateService } from "../abstractions/state.service" import { StateService } from "../abstractions/state.service";
import { AbstractMemoryStorageService } from "../abstractions/storage.service"; import { AbstractMemoryStorageService } from "../abstractions/storage.service";
import { KeyDefinition } from "../types/key-definition"; import { KeyDefinition } from "../types/key-definition";
import { StateDefinition } from "../types/state-definition"; import { StateDefinition } from "../types/state-definition";
@@ -11,7 +11,7 @@ import { DefaultActiveUserStateProvider } from "./default-active-user-state.prov
class TestState { class TestState {
date: Date; date: Date;
array: string[] array: string[];
// TODO: More complex data types // TODO: More complex data types
static fromJSON(jsonState: Jsonify<TestState>) { static fromJSON(jsonState: Jsonify<TestState>) {
@@ -24,7 +24,11 @@ class TestState {
const testStateDefinition = new StateDefinition("fake", "disk"); const testStateDefinition = new StateDefinition("fake", "disk");
const testKeyDefinition = new KeyDefinition<TestState>(testStateDefinition, "fake", TestState.fromJSON); const testKeyDefinition = new KeyDefinition<TestState>(
testStateDefinition,
"fake",
TestState.fromJSON
);
describe("DefaultStateProvider", () => { describe("DefaultStateProvider", () => {
const stateService = mock<StateService>(); const stateService = mock<StateService>();
@@ -52,13 +56,12 @@ describe("DefaultStateProvider", () => {
}); });
it("createUserState", async () => { it("createUserState", async () => {
diskStorageService.get diskStorageService.get.mockImplementation(async (key, options) => {
.mockImplementation(async (key, options) => { if (key == "fake_1") {
if (key == "fake_1") { return { date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"] };
return {date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"]} }
} return undefined;
return undefined; });
});
const fakeDomainState = activeUserStateProvider.create(testKeyDefinition); const fakeDomainState = activeUserStateProvider.create(testKeyDefinition);
@@ -67,11 +70,11 @@ describe("DefaultStateProvider", () => {
// User signs in // User signs in
activeAccountSubject.next("1"); activeAccountSubject.next("1");
await new Promise<void>(resolve => setTimeout(resolve, 10)); await new Promise<void>((resolve) => setTimeout(resolve, 10));
// Service does an update // Service does an update
await fakeDomainState.update(state => state.array.push("value3")); await fakeDomainState.update((state) => state.array.push("value3"));
await new Promise<void>(resolve => setTimeout(resolve, 10)); await new Promise<void>((resolve) => setTimeout(resolve, 10));
subscription.unsubscribe(); subscription.unsubscribe();
@@ -79,16 +82,24 @@ describe("DefaultStateProvider", () => {
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null); expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
// Gotten starter user data // Gotten starter user data
expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches<TestState>(value => { expect(subscribeCallback).toHaveBeenNthCalledWith(
return true; 2,
})); matches<TestState>((value) => {
return true;
})
);
// Gotten update callback data // Gotten update callback data
expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches<TestState>((value) => { expect(subscribeCallback).toHaveBeenNthCalledWith(
return value != null && 3,
typeof value.date == "object" && matches<TestState>((value) => {
value.date.getFullYear() == 2023 && return (
value.array.length == 3 value != null &&
})); typeof value.date == "object" &&
value.date.getFullYear() == 2023 &&
value.array.length == 3
);
})
);
}); });
}); });

View File

@@ -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 { Jsonify } from "type-fest";
import { AccountService } from "../../auth/abstractions/account.service";
import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider"; import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider";
import { EncryptService } from "../abstractions/encrypt.service"; import { EncryptService } from "../abstractions/encrypt.service";
import { StateService } from "../abstractions/state.service"; import {
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service"; AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { ActiveUserState } from "../interfaces/active-user-state"; import { ActiveUserState } from "../interfaces/active-user-state";
import { userKeyBuilder } from "../misc/key-builders"; import { userKeyBuilder } from "../misc/key-builders";
import { UserKey } from "../models/domain/symmetric-crypto-key"; 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"; import { StorageLocation } from "./default-global-state.provider";
class ConverterContext { class ConverterContext {
constructor( constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
readonly activeUserKey: UserKey,
readonly encryptService: EncryptService
) { }
} }
class DerivedStateDefinition<TFrom, TTo> { class DerivedStateDefinition<TFrom, TTo> {
constructor( constructor(readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>) {}
readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>
) { }
} }
export class DerivedActiveUserState<TFrom, TTo> { export class DerivedActiveUserState<TFrom, TTo> {
state$: Observable<TTo> state$: Observable<TTo>;
// TODO: Probably needs to take state service // TODO: Probably needs to take state service
/** /**
@@ -37,12 +44,16 @@ export class DerivedActiveUserState<TFrom, TTo> {
private encryptService: EncryptService, private encryptService: EncryptService,
private activeUserState: ActiveUserState<TFrom> private activeUserState: ActiveUserState<TFrom>
) { ) {
this.state$ = activeUserState.state$ this.state$ = activeUserState.state$.pipe(
.pipe(switchMap(async from => { switchMap(async (from) => {
// TODO: How do I get the key? // 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; return convertedData;
})); })
);
} }
async getFromState(): Promise<TTo> { async getFromState(): Promise<TTo> {
@@ -61,7 +72,6 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
private formattedKey$: Observable<string>; private formattedKey$: Observable<string>;
private chosenStorageLocation: AbstractStorageService; private chosenStorageLocation: AbstractStorageService;
// TODO: Use BitSubject
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null); protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
private stateSubject$ = this.stateSubject.asObservable(); private stateSubject$ = this.stateSubject.asObservable();
@@ -69,7 +79,7 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
constructor( constructor(
private keyDefinition: KeyDefinition<T>, private keyDefinition: KeyDefinition<T>,
private stateService: StateService, private accountService: AccountService,
private encryptService: EncryptService, private encryptService: EncryptService,
private memoryStorageService: AbstractMemoryStorageService, private memoryStorageService: AbstractMemoryStorageService,
private secureStorageService: AbstractStorageService, private secureStorageService: AbstractStorageService,
@@ -78,20 +88,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
this.chosenStorageLocation = this.chooseStorage( this.chosenStorageLocation = this.chooseStorage(
this.keyDefinition.stateDefinition.storageLocation this.keyDefinition.stateDefinition.storageLocation
); );
const unformattedKey = `${this.keyDefinition.stateDefinition.name}_{userId}_${this.keyDefinition.key}`;
// startWith? // startWith?
this.formattedKey$ = this.stateService.activeAccount$ this.formattedKey$ = this.accountService.activeAccount$.pipe(
.pipe( tap((user) => console.log("user", user)), // Temp
map(accountId => accountId != null map((account) =>
? unformattedKey.replace("{userId}", accountId) account != null && account.id != null
: null) ? userKeyBuilder(account.id, this.keyDefinition)
); : null
)
);
const activeAccountData$ = this.formattedKey$ const activeAccountData$ = this.formattedKey$.pipe(
.pipe(switchMap(async key => { switchMap(async (key) => {
console.log("user emitted", key); console.log("user emitted: ", key); // temp
if (key == null) { if (key == null) {
return null; return null;
} }
@@ -99,56 +108,69 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
const data = keyDefinition.serializer(jsonData); const data = keyDefinition.serializer(jsonData);
return data; return data;
}), }),
tap(data => { tap((data) => {
console.log("data:", data); this.seededInitial = true;
this.stateSubject.next(data); this.stateSubject.next(data);
}), }),
// Share the execution // Share the execution
share() share()
); );
// Whomever subscribes to this data, should be notified of updated data // Whomever subscribes to this data, should be notified of updated data
// if someone calls my update() method, or the active user changes. // if someone calls my update() method, or the active user changes.
this.state$ = defer(() => { this.state$ = defer(() => {
console.log("starting subscription.");
const subscription = activeAccountData$.subscribe(); const subscription = activeAccountData$.subscribe();
return this.stateSubject$ return this.stateSubject$.pipe(
.pipe(tap({ tap({
complete: () => subscription.unsubscribe(), complete: () => subscription.unsubscribe(),
})); })
);
}); });
} }
async update(configureState: (state: T) => void): Promise<void> { 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(); const key = await this.createKey();
if (key == null) { if (key == null) {
throw new Error("Attempting to active user state, when no user is active."); 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); await this.chosenStorageLocation.save(await this.createKey(), currentState);
this.stateSubject.next(currentState); this.stateSubject.next(currentState);
} }
async getFromState(): Promise<T> { async getFromState(): Promise<T> {
const activeUserId = await this.stateService.getUserId(); const activeUser = await firstValueFrom(this.accountService.activeAccount$);
const key = userKeyBuilder(activeUserId, this.keyDefinition); if (activeUser == null || activeUser.id == null) {
const data = await this.chosenStorageLocation.get(key) as Jsonify<T>; 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<T>;
return this.keyDefinition.serializer(data); return this.keyDefinition.serializer(data);
} }
createDerived<TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>): DerivedActiveUserState<T, TTo> { createDerived<TTo>(
return new DerivedActiveUserState<T, TTo>( derivedStateDefinition: DerivedStateDefinition<T, TTo>
derivedStateDefinition, ): DerivedActiveUserState<T, TTo> {
this.encryptService, return new DerivedActiveUserState<T, TTo>(derivedStateDefinition, this.encryptService, this);
this
);
} }
private async createKey(): Promise<string> { private async createKey(): Promise<string> {
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<T> {
const data = await this.chosenStorageLocation.get<Jsonify<T>>(key);
this.seededInitial = true;
return this.keyDefinition.serializer(data);
} }
private chooseStorage(storageLocation: StorageLocation): AbstractStorageService { private chooseStorage(storageLocation: StorageLocation): AbstractStorageService {
@@ -163,21 +185,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
} }
} }
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {}; private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
constructor( 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 encryptService: EncryptService,
private memoryStorage: AbstractMemoryStorageService, private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService, private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService) { private secureStorage: AbstractStorageService
} ) {}
create<T>(keyDefinition: KeyDefinition<T>): DefaultActiveUserState<T> { create<T>(keyDefinition: KeyDefinition<T>): DefaultActiveUserState<T> {
const locationDomainKey = const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
`${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingActiveUserState = this.userStateCache[locationDomainKey]; const existingActiveUserState = this.userStateCache[locationDomainKey];
if (existingActiveUserState != null) { if (existingActiveUserState != null) {
// I have to cast out of the unknown generic but this should be safe if rules // 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<T>( const newActiveUserState = new DefaultActiveUserState<T>(
keyDefinition, keyDefinition,
this.stateService, this.accountService,
this.encryptService, this.encryptService,
this.memoryStorage, this.memoryStorage,
this.secureStorage, this.secureStorage,

View File

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

View File

@@ -190,14 +190,21 @@ export class StateService<
state.accounts[userId] = this.createAccount(); state.accounts[userId] = this.createAccount();
const diskAccount = await this.getAccountFromDisk({ userId: userId }); const diskAccount = await this.getAccountFromDisk({ userId: userId });
state.accounts[userId].profile = diskAccount.profile; 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; 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) { 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); account = await this.setAccountEnvironment(account);
await this.updateState(async (state) => { await this.updateState(async (state) => {
state.authenticatedAccounts.push(account.profile.userId); state.authenticatedAccounts.push(account.profile.userId);

View File

@@ -4,10 +4,7 @@ import { StorageLocation } from "../services/default-global-state.provider";
// TODO: Move type // TODO: Move type
export class DeriveContext { export class DeriveContext {
constructor( constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
readonly activeUserKey: UserKey,
readonly encryptService: EncryptService
) { }
} }
export class DerivedStateDefinition<TFrom, TTo> { export class DerivedStateDefinition<TFrom, TTo> {

View File

@@ -18,16 +18,24 @@ export class KeyDefinition<T> {
readonly stateDefinition: StateDefinition, readonly stateDefinition: StateDefinition,
readonly key: string, readonly key: string,
readonly serializer: (jsonValue: Jsonify<T>) => T readonly serializer: (jsonValue: Jsonify<T>) => T
) { } ) {}
static array<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) { static array<T>(
stateDefinition: StateDefinition,
key: string,
serializer: (jsonValue: Jsonify<T>) => T
) {
return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => { return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => {
// TODO: Should we handle null for them, I feel like we should discourage null for an array? // 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<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) { static record<T>(
stateDefinition: StateDefinition,
key: string,
serializer: (jsonValue: Jsonify<T>) => T
) {
return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => { return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => {
const output: Record<string, T> = {}; const output: Record<string, T> = {};
for (const key in jsonValue) { for (const key in jsonValue) {

View File

@@ -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 // TODO: Where should this live
export type StorageLocation = "disk" | "memory" | "secure"; export type StorageLocation = "disk" | "memory" | "secure";
@@ -8,12 +8,9 @@ export type StorageLocation = "disk" | "memory" | "secure";
*/ */
export class StateDefinition { 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 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. * @param storageLocation The location of where this state should be stored.
*/ */
constructor( constructor(readonly name: string, readonly storageLocation: StorageLocation) {}
readonly name: string,
readonly storageLocation: StorageLocation
) { }
} }

View File

@@ -8,13 +8,17 @@ it("has all unique definitions", () => {
for (const key of keys) { for (const key of keys) {
const definition = (definitions as unknown as Record<string, StateDefinition>)[key]; const definition = (definitions as unknown as Record<string, StateDefinition>)[key];
if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) { 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}`; const name = `${definition.name}_${definition.storageLocation}`;
if (uniqueNames.includes(name)) { 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); uniqueNames.push(name);

View File

@@ -13,9 +13,9 @@ import { IRREVERSIBLE, Migrator } from "../migrator";
type ExpectedAccountType = { type ExpectedAccountType = {
data: { data: {
folders: { folders: {
encrypted: Record<string, { name: string, id: string, revisionDate: string }> encrypted: Record<string, { name: string; id: string; revisionDate: string }>;
} };
} };
}; };
const FOLDER_STATE = new StateDefinition("FolderService", "disk"); const FOLDER_STATE = new StateDefinition("FolderService", "disk");
@@ -40,9 +40,7 @@ export class MoveFolderToOwnedMigrator extends Migrator<8, 9> {
// await helper.set("", account); // await helper.set("", account);
} }
await Promise.all( await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account)));
accounts.map(({ userId, account}) => updateAccount(userId, account))
);
} }
rollback(helper: MigrationHelper): Promise<void> { rollback(helper: MigrationHelper): Promise<void> {

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { FolderResponse } from "../response/folder.response"; import { FolderResponse } from "../response/folder.response";
export class FolderData { export class FolderData {
@@ -5,9 +7,17 @@ export class FolderData {
name: string; name: string;
revisionDate: string; revisionDate: string;
constructor(response: FolderResponse) { constructor(response: Partial<FolderResponse>) {
this.name = response.name; this.name = response.name;
this.id = response.id; this.id = response.id;
this.revisionDate = response.revisionDate; this.revisionDate = response.revisionDate;
} }
static fromJSON(jsonObj: Jsonify<FolderData>) {
return new FolderData({
id: jsonObj.id,
name: jsonObj.name,
revisionDate: jsonObj.revisionDate,
});
}
} }

View File

@@ -16,11 +16,9 @@ import { Folder } from "../../../vault/models/domain/folder";
import { FolderView } from "../../../vault/models/view/folder.view"; import { FolderView } from "../../../vault/models/view/folder.view";
import { FOLDERS } from "../../types/key-definitions"; import { FOLDERS } from "../../types/key-definitions";
export class FolderService implements InternalFolderServiceAbstraction { export class FolderService implements InternalFolderServiceAbstraction {
folderState: ActiveUserState<Record<string, FolderData>>;
folderState: ActiveUserState<Record<string, Folder>>; decryptedFolderState: DerivedActiveUserState<Record<string, FolderData>, FolderView[]>;
decryptedFolderState: DerivedActiveUserState<Record<string, Folder>, FolderView[]>
folders$: Observable<Folder[]>; folders$: Observable<Folder[]>;
folderViews$: Observable<FolderView[]>; folderViews$: Observable<FolderView[]>;
@@ -32,18 +30,22 @@ export class FolderService implements InternalFolderServiceAbstraction {
private activeUserStateProvider: ActiveUserStateProvider, private activeUserStateProvider: ActiveUserStateProvider,
private stateService: StateService private stateService: StateService
) { ) {
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => { const derivedFoldersDefinition = FOLDERS.createDerivedDefinition(
const folders = this.flattenMap(foldersMap); "memory",
const decryptedFolders = await this.decryptFolders(folders); async (foldersMap) => {
return decryptedFolders; const folders = this.flattenMap(foldersMap);
}) const decryptedFolders = await this.decryptFolders(folders);
return decryptedFolders;
}
);
this.folderState = this.activeUserStateProvider.create(FOLDERS); this.folderState = this.activeUserStateProvider.create(FOLDERS);
this.folders$ = this.folderState.state$ this.folders$ = this.folderState.state$.pipe(
.pipe(map(foldersMap => { map((foldersMap) => {
return this.flattenMap(foldersMap); return this.flattenMap(foldersMap);
})); })
);
this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition); this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition);
this.folderViews$ = this.decryptedFolderState.state$; this.folderViews$ = this.decryptedFolderState.state$;
@@ -64,7 +66,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
async get(id: string): Promise<Folder> { async get(id: string): Promise<Folder> {
const folders = await firstValueFrom(this.folderState.state$); const folders = await firstValueFrom(this.folderState.state$);
return folders[id]; return new Folder(folders[id]);
} }
async getAllFromState(): Promise<Folder[]> { async getAllFromState(): Promise<Folder[]> {
@@ -83,7 +85,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
return null; return null;
} }
return folder; return new Folder(folder);
} }
/** /**
@@ -94,37 +96,28 @@ export class FolderService implements InternalFolderServiceAbstraction {
} }
async upsert(folder: FolderData | FolderData[]): Promise<void> { async upsert(folder: FolderData | FolderData[]): Promise<void> {
console.log("upsert", folder); await this.folderState.update((folders) => {
await this.folderState.update(folders => {
if (folder instanceof FolderData) { if (folder instanceof FolderData) {
const f = folder as FolderData; folders[folder.id] = folder;
folders[f.id] = new Folder(f);
} else { } else {
(folder as FolderData[]).forEach((f) => { (folder as FolderData[]).forEach((f) => {
folders[f.id] = new Folder(f); folders[f.id] = f;
}); });
} }
}); });
} }
async replace(folders: { [id: string]: FolderData }): Promise<void> { async replace(folders: { [id: string]: FolderData }): Promise<void> {
const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => { await this.folderState.update((f) => (f = folders));
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> { async clear(userId?: string): Promise<any> {
console.log("clear", userId); await this.folderState.update((f) => (f = null));
await this.folderState.update(f => f = null);
} }
async delete(id: string | string[]): Promise<void> { async delete(id: string | string[]): Promise<void> {
const folderIds = typeof id === "string" ? [id] : id; 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) { for (const folderId in folderIds) {
delete folders[folderId]; delete folders[folderId];
} }
@@ -159,10 +152,10 @@ export class FolderService implements InternalFolderServiceAbstraction {
return decryptedFolders; return decryptedFolders;
} }
private flattenMap(foldersMap: Record<string, Folder>): Folder[] { private flattenMap(foldersMap: Record<string, FolderData>): Folder[] {
const folders: Folder[] = []; const folders: Folder[] = [];
for (const id in foldersMap) { for (const id in foldersMap) {
folders.push(foldersMap[id]); folders.push(new Folder(foldersMap[id]));
} }
return folders; return folders;
} }

View File

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