Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Commit

Permalink
[bug] Allow for GlobalState to be extended and modified in clients (#646
Browse files Browse the repository at this point in the history
)

Some clients have unique global setting defaults (and unique global settings)
For example: the web vault defaults to light theme, but most clients with theme support default to system theme.

The current way we handle GlobalState is buried in jslib and not easily extendible in clients.

To fix this, we need to treat GlobalState as a generic in the StateService and StateMigration service and allow for its extension in those methods and anywhere GlobalState is inited.
  • Loading branch information
Addison Beck committed Jan 31, 2022
1 parent e372bf2 commit 92a65b7
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 38 deletions.
18 changes: 15 additions & 3 deletions angular/src/services/jslib-services.module.ts
Expand Up @@ -76,7 +76,11 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";

import { Account, AccountFactory } from "jslib-common/models/domain/account";
import { Account } from "jslib-common/models/domain/account";
import { GlobalState } from "jslib-common/models/domain/globalState";

import { GlobalStateFactory } from "jslib-common/factories/globalStateFactory";
import { StateFactory } from "jslib-common/factories/stateFactory";

@NgModule({
declarations: [],
Expand Down Expand Up @@ -338,7 +342,7 @@ import { Account, AccountFactory } from "jslib-common/models/domain/account";
secureStorageService,
logService,
stateMigrationService,
new AccountFactory(Account)
new StateFactory(GlobalState, Account)
),
deps: [
StorageServiceAbstraction,
Expand All @@ -349,7 +353,15 @@ import { Account, AccountFactory } from "jslib-common/models/domain/account";
},
{
provide: StateMigrationServiceAbstraction,
useClass: StateMigrationService,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction
) =>
new StateMigrationService(
storageService,
secureStorageService,
new GlobalStateFactory(GlobalState)
),
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
Expand Down
13 changes: 13 additions & 0 deletions common/src/factories/accountFactory.ts
@@ -0,0 +1,13 @@
import { Account } from "../models/domain/account";

export class AccountFactory<T extends Account = Account> {
private accountConstructor: new (init: Partial<T>) => T;

constructor(accountConstructor: new (init: Partial<T>) => T) {
this.accountConstructor = accountConstructor;
}

create(args: Partial<T>) {
return new this.accountConstructor(args);
}
}
13 changes: 13 additions & 0 deletions common/src/factories/globalStateFactory.ts
@@ -0,0 +1,13 @@
import { GlobalState } from "../models/domain/globalState";

export class GlobalStateFactory<T extends GlobalState = GlobalState> {
private globalStateConstructor: new (init: Partial<T>) => T;

constructor(globalStateConstructor: new (init: Partial<T>) => T) {
this.globalStateConstructor = globalStateConstructor;
}

create(args?: Partial<T>) {
return new this.globalStateConstructor(args);
}
}
25 changes: 25 additions & 0 deletions common/src/factories/stateFactory.ts
@@ -0,0 +1,25 @@
import { Account } from "../models/domain/account";
import { GlobalState } from "../models/domain/globalState";
import { AccountFactory } from "./accountFactory";
import { GlobalStateFactory } from "./globalStateFactory";

export class StateFactory<TAccount extends Account, TGlobal extends GlobalState> {
private globalStateFactory: GlobalStateFactory<TGlobal>;
private accountFactory: AccountFactory<TAccount>;

constructor(
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
accountConstructor: new (init: Partial<TAccount>) => TAccount
) {
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
this.accountFactory = new AccountFactory(accountConstructor);
}

createGlobal(args: Partial<TGlobal>): TGlobal {
return this.globalStateFactory.create(args);
}

createAccount(args: Partial<TAccount>): TAccount {
return this.accountFactory.create(args);
}
}
12 changes: 0 additions & 12 deletions common/src/models/domain/account.ts
Expand Up @@ -175,15 +175,3 @@ export class Account {
});
}
}

export class AccountFactory<T extends Account = Account> {
private accountConstructor: new (init: Partial<T>) => T;

constructor(accountConstructor: new (init: Partial<T>) => T) {
this.accountConstructor = accountConstructor;
}

create(args: Partial<T>) {
return new this.accountConstructor(args);
}
}
2 changes: 1 addition & 1 deletion common/src/models/domain/globalState.ts
Expand Up @@ -13,7 +13,7 @@ export class GlobalState {
ssoOrganizationIdentifier?: string;
ssoState?: string;
rememberedEmail?: string;
theme?: ThemeType = ThemeType.Light;
theme?: ThemeType = ThemeType.System;
window?: WindowState = new WindowState();
twoFactorToken?: string;
disableFavicon?: boolean;
Expand Down
11 changes: 9 additions & 2 deletions common/src/models/domain/state.ts
@@ -1,9 +1,16 @@
import { Account } from "./account";
import { GlobalState } from "./globalState";

export class State<TAccount extends Account = Account> {
export class State<
TAccount extends Account = Account,
TGlobalState extends GlobalState = GlobalState
> {
accounts: { [userId: string]: TAccount } = {};
globals: GlobalState = new GlobalState();
globals: TGlobalState;
activeUserId: string;
authenticatedAccounts: string[] = [];

constructor(globals: TGlobalState) {
this.globals = globals;
}
}
40 changes: 25 additions & 15 deletions common/src/services/state.service.ts
@@ -1,6 +1,6 @@
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";

import { Account, AccountData, AccountFactory } from "../models/domain/account";
import { Account, AccountData } from "../models/domain/account";

import { LogService } from "../abstractions/log.service";
import { StorageService } from "../abstractions/storage.service";
Expand Down Expand Up @@ -39,6 +39,8 @@ import { StateMigrationService } from "../abstractions/stateMigration.service";
import { EnvironmentUrls } from "../models/domain/environmentUrls";
import { WindowState } from "../models/domain/windowState";

import { StateFactory } from "../factories/stateFactory";

const keys = {
global: "global",
authenticatedAccounts: "authenticatedAccounts",
Expand All @@ -52,13 +54,17 @@ const partialKeys = {
masterKey: "_masterkey",
};

export class StateService<TAccount extends Account = Account>
implements StateServiceAbstraction<TAccount>
export class StateService<
TAccount extends Account = Account,
TGlobalState extends GlobalState = GlobalState
> implements StateServiceAbstraction<TAccount>
{
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
activeAccount = new BehaviorSubject<string>(null);

protected state: State<TAccount> = new State<TAccount>();
protected state: State<TAccount, TGlobalState> = new State<TAccount, TGlobalState>(
this.createGlobals()
);

private hasBeenInited: boolean = false;

Expand All @@ -67,7 +73,7 @@ export class StateService<TAccount extends Account = Account>
protected secureStorageService: StorageService,
protected logService: LogService,
protected stateMigrationService: StateMigrationService,
protected accountFactory: AccountFactory<TAccount>
protected stateFactory: StateFactory<TAccount, TGlobalState>
) {}

async init(): Promise<void> {
Expand Down Expand Up @@ -2086,8 +2092,8 @@ export class StateService<TAccount extends Account = Account>
);
}

protected async getGlobals(options: StorageOptions): Promise<GlobalState> {
let globals: GlobalState;
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
let globals: TGlobalState;
if (this.useMemory(options.storageLocation)) {
globals = this.getGlobalsFromMemory();
}
Expand All @@ -2096,28 +2102,28 @@ export class StateService<TAccount extends Account = Account>
globals = await this.getGlobalsFromDisk(options);
}

return globals ?? new GlobalState();
return globals ?? this.createGlobals();
}

protected async saveGlobals(globals: GlobalState, options: StorageOptions) {
protected async saveGlobals(globals: TGlobalState, options: StorageOptions) {
return this.useMemory(options.storageLocation)
? this.saveGlobalsToMemory(globals)
: await this.saveGlobalsToDisk(globals, options);
}

protected getGlobalsFromMemory(): GlobalState {
protected getGlobalsFromMemory(): TGlobalState {
return this.state.globals;
}

protected async getGlobalsFromDisk(options: StorageOptions): Promise<GlobalState> {
return await this.storageService.get<GlobalState>(keys.global, options);
protected async getGlobalsFromDisk(options: StorageOptions): Promise<TGlobalState> {
return await this.storageService.get<TGlobalState>(keys.global, options);
}

protected saveGlobalsToMemory(globals: GlobalState): void {
protected saveGlobalsToMemory(globals: TGlobalState): void {
this.state.globals = globals;
}

protected async saveGlobalsToDisk(globals: GlobalState, options: StorageOptions): Promise<void> {
protected async saveGlobalsToDisk(globals: TGlobalState, options: StorageOptions): Promise<void> {
if (options.useSecureStorage) {
await this.secureStorageService.save(keys.global, globals, options);
} else {
Expand Down Expand Up @@ -2412,7 +2418,11 @@ export class StateService<TAccount extends Account = Account>
}

protected createAccount(init: Partial<TAccount> = null): TAccount {
return this.accountFactory.create(init);
return this.stateFactory.createAccount(init);
}

protected createGlobals(init: Partial<TGlobalState> = null): TGlobalState {
return this.stateFactory.createGlobal(init);
}

protected async deAuthenticateAccount(userId: string) {
Expand Down
13 changes: 8 additions & 5 deletions common/src/services/stateMigration.service.ts
Expand Up @@ -20,6 +20,8 @@ import { ThemeType } from "../enums/themeType";

import { EnvironmentUrls } from "../models/domain/environmentUrls";

import { GlobalStateFactory } from "../factories/globalStateFactory";

// Originally (before January 2022) storage was handled as a flat key/value pair store.
// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration.
const v1Keys: { [key: string]: string } = {
Expand Down Expand Up @@ -126,10 +128,11 @@ const partialKeys = {
masterKey: "_masterkey",
};

export class StateMigrationService {
export class StateMigrationService<TGlobalState extends GlobalState = GlobalState> {
constructor(
protected storageService: StorageService,
protected secureStorageService: StorageService
protected secureStorageService: StorageService,
protected globalStateFactory: GlobalStateFactory<TGlobalState>
) {}

async needsMigration(): Promise<boolean> {
Expand Down Expand Up @@ -174,7 +177,7 @@ export class StateMigrationService {
// 1. Check for an existing storage value from the old storage structure OR
// 2. Check for a value already set by processes that run before migration OR
// 3. Assign the default value
const globals = (await this.get<GlobalState>(keys.global)) ?? new GlobalState();
const globals = (await this.get<GlobalState>(keys.global)) ?? this.globalStateFactory.create();
globals.stateVersion = StateVersion.Two;
globals.environmentUrls =
(await this.get<EnvironmentUrls>(v1Keys.environmentUrls)) ?? globals.environmentUrls;
Expand Down Expand Up @@ -438,8 +441,8 @@ export class StateMigrationService {
return this.storageService.save(key, value, this.options);
}

protected async getGlobals(): Promise<GlobalState> {
return await this.get<GlobalState>(keys.global);
protected async getGlobals(): Promise<TGlobalState> {
return await this.get<TGlobalState>(keys.global);
}

protected async getCurrentStateVersion(): Promise<StateVersion> {
Expand Down

0 comments on commit 92a65b7

Please sign in to comment.