1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-6404] Fully Integrate clearOn Events (#8134)

* Add New KeyDefinitionOption

* Add New Services

* Add WebStorageServiceProvider Tests

* Update Error Message

* Add `UserKeyDefinition`

* Fix Deserialization Helpers

* Fix KeyDefinition

* Add `UserKeyDefinition`

* Fix Deserialization Helpers

* Fix KeyDefinition

* Move `ClearEvent`

* Cleanup

* Fix Imports

* Integrate onClear Events

* Remove Accidental Addition

* Fix Test

* Add VaultTimeoutService Tests

* Only Register When Current State is Null

* Address Feedback
This commit is contained in:
Justin Baur
2024-03-04 14:33:25 -06:00
committed by GitHub
parent 4ba2717eb4
commit c3eba7f2c8
28 changed files with 471 additions and 345 deletions

View File

@@ -93,6 +93,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { import {
@@ -100,6 +101,7 @@ import {
DerivedStateProvider, DerivedStateProvider,
GlobalStateProvider, GlobalStateProvider,
SingleUserStateProvider, SingleUserStateProvider,
StateEventRunnerService,
StateProvider, StateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@@ -107,6 +109,7 @@ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service"; import { ApiService } from "@bitwarden/common/services/api.service";
@@ -299,6 +302,7 @@ export default class MainBackground {
organizationVaultExportService: OrganizationVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
vaultSettingsService: VaultSettingsServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction;
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction; ssoLoginService: SsoLoginServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc. // Passed to the popup for Safari to workaround issues with theming, downloading, etc.
@@ -366,10 +370,24 @@ export default class MainBackground {
this.keyGenerationService, this.keyGenerationService,
) )
: new BackgroundMemoryStorageService(); : new BackgroundMemoryStorageService();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders, const storageServiceProvider = new StorageServiceProvider(
this.storageService as BrowserLocalStorageService, this.storageService as BrowserLocalStorageService,
this.memoryStorageForStateProviders,
); );
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);
this.encryptService = flagEnabled("multithreadDecryption") this.encryptService = flagEnabled("multithreadDecryption")
? new MultithreadEncryptServiceImplementation( ? new MultithreadEncryptServiceImplementation(
this.cryptoFunctionService, this.cryptoFunctionService,
@@ -379,8 +397,8 @@ export default class MainBackground {
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders, storageServiceProvider,
this.storageService as BrowserLocalStorageService, stateEventRegistrarService,
); );
this.accountService = new AccountServiceImplementation( this.accountService = new AccountServiceImplementation(
this.messagingService, this.messagingService,
@@ -389,8 +407,8 @@ export default class MainBackground {
); );
this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService, this.accountService,
this.memoryStorageForStateProviders, storageServiceProvider,
this.storageService as BrowserLocalStorageService, stateEventRegistrarService,
); );
this.derivedStateProvider = new BackgroundDerivedStateProvider( this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageForStateProviders, this.memoryStorageForStateProviders,
@@ -666,6 +684,7 @@ export default class MainBackground {
this.stateService, this.stateService,
this.authService, this.authService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback, lockedCallback,
logoutCallback, logoutCallback,
); );
@@ -1113,6 +1132,8 @@ export default class MainBackground {
this.searchService.clearIndex(); this.searchService.clearIndex();
} }
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
if (newActiveUser != null) { if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application // we have a new active user, do not continue tearing down application
await this.switchAccount(newActiveUser as UserId); await this.switchAccount(newActiveUser as UserId);

View File

@@ -21,6 +21,10 @@ import {
platformUtilsServiceFactory, platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions, PlatformUtilsServiceInitOptions,
} from "../../platform/background/service-factories/platform-utils-service.factory"; } from "../../platform/background/service-factories/platform-utils-service.factory";
import {
stateEventRunnerServiceFactory,
StateEventRunnerServiceInitOptions,
} from "../../platform/background/service-factories/state-event-runner-service.factory";
import { import {
StateServiceInitOptions, StateServiceInitOptions,
stateServiceFactory, stateServiceFactory,
@@ -62,7 +66,8 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
SearchServiceInitOptions & SearchServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
AuthServiceInitOptions & AuthServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions; VaultTimeoutSettingsServiceInitOptions &
StateEventRunnerServiceInitOptions;
export function vaultTimeoutServiceFactory( export function vaultTimeoutServiceFactory(
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices, cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
@@ -84,6 +89,7 @@ export function vaultTimeoutServiceFactory(
await stateServiceFactory(cache, opts), await stateServiceFactory(cache, opts),
await authServiceFactory(cache, opts), await authServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts),
await stateEventRunnerServiceFactory(cache, opts),
opts.vaultTimeoutServiceOptions.lockedCallback, opts.vaultTimeoutServiceOptions.lockedCallback,
opts.vaultTimeoutServiceOptions.loggedOutCallback, opts.vaultTimeoutServiceOptions.loggedOutCallback,
), ),

View File

@@ -9,18 +9,20 @@ import {
import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { CachedServices, FactoryOptions, factory } from "./factory-options";
import { import {
DiskStorageServiceInitOptions, StateEventRegistrarServiceInitOptions,
MemoryStorageServiceInitOptions, stateEventRegistrarServiceFactory,
observableDiskStorageServiceFactory, } from "./state-event-registrar-service.factory";
observableMemoryStorageServiceFactory, import {
} from "./storage-service.factory"; StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type ActiveUserStateProviderFactory = FactoryOptions; type ActiveUserStateProviderFactory = FactoryOptions;
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory & export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
AccountServiceInitOptions & AccountServiceInitOptions &
MemoryStorageServiceInitOptions & StorageServiceProviderInitOptions &
DiskStorageServiceInitOptions; StateEventRegistrarServiceInitOptions;
export async function activeUserStateProviderFactory( export async function activeUserStateProviderFactory(
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices, cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
@@ -33,8 +35,8 @@ export async function activeUserStateProviderFactory(
async () => async () =>
new DefaultActiveUserStateProvider( new DefaultActiveUserStateProvider(
await accountServiceFactory(cache, opts), await accountServiceFactory(cache, opts),
await observableMemoryStorageServiceFactory(cache, opts), await storageServiceProviderFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts), await stateEventRegistrarServiceFactory(cache, opts),
), ),
); );
} }

View File

@@ -4,17 +4,14 @@ import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/imp
import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { CachedServices, FactoryOptions, factory } from "./factory-options";
import { import {
DiskStorageServiceInitOptions, StorageServiceProviderInitOptions,
MemoryStorageServiceInitOptions, storageServiceProviderFactory,
observableDiskStorageServiceFactory, } from "./storage-service-provider.factory";
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
type GlobalStateProviderFactoryOptions = FactoryOptions; type GlobalStateProviderFactoryOptions = FactoryOptions;
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions & export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
MemoryStorageServiceInitOptions & StorageServiceProviderInitOptions;
DiskStorageServiceInitOptions;
export async function globalStateProviderFactory( export async function globalStateProviderFactory(
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices, cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
@@ -24,10 +21,6 @@ export async function globalStateProviderFactory(
cache, cache,
"globalStateProvider", "globalStateProvider",
opts, opts,
async () => async () => new DefaultGlobalStateProvider(await storageServiceProviderFactory(cache, opts)),
new DefaultGlobalStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
),
); );
} }

View File

@@ -4,17 +4,19 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { CachedServices, FactoryOptions, factory } from "./factory-options";
import { import {
DiskStorageServiceInitOptions, StateEventRegistrarServiceInitOptions,
MemoryStorageServiceInitOptions, stateEventRegistrarServiceFactory,
observableDiskStorageServiceFactory, } from "./state-event-registrar-service.factory";
observableMemoryStorageServiceFactory, import {
} from "./storage-service.factory"; StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type SingleUserStateProviderFactoryOptions = FactoryOptions; type SingleUserStateProviderFactoryOptions = FactoryOptions;
export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions & export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions &
MemoryStorageServiceInitOptions & StorageServiceProviderInitOptions &
DiskStorageServiceInitOptions; StateEventRegistrarServiceInitOptions;
export async function singleUserStateProviderFactory( export async function singleUserStateProviderFactory(
cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices, cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices,
@@ -26,8 +28,8 @@ export async function singleUserStateProviderFactory(
opts, opts,
async () => async () =>
new DefaultSingleUserStateProvider( new DefaultSingleUserStateProvider(
await observableMemoryStorageServiceFactory(cache, opts), await storageServiceProviderFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts), await stateEventRegistrarServiceFactory(cache, opts),
), ),
); );
} }

View File

@@ -0,0 +1,33 @@
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type StateEventRunnerServiceFactoryOptions = FactoryOptions;
export type StateEventRunnerServiceInitOptions = StateEventRunnerServiceFactoryOptions &
GlobalStateProviderInitOptions &
StorageServiceProviderInitOptions;
export function stateEventRunnerServiceFactory(
cache: { stateEventRunnerService?: StateEventRunnerService } & CachedServices,
opts: StateEventRunnerServiceInitOptions,
): Promise<StateEventRunnerService> {
return factory(
cache,
"stateEventRunnerService",
opts,
async () =>
new StateEventRunnerService(
await globalStateProviderFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
),
);
}

View File

@@ -60,11 +60,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { import {
ActiveUserStateProvider, ActiveUserStateProvider,
DerivedStateProvider, DerivedStateProvider,
GlobalStateProvider, GlobalStateProvider,
SingleUserStateProvider, SingleUserStateProvider,
StateEventRunnerService,
StateProvider, StateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@@ -73,6 +75,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
import { AuditService } from "@bitwarden/common/services/audit.service"; import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -208,6 +211,7 @@ export class Main {
derivedStateProvider: DerivedStateProvider; derivedStateProvider: DerivedStateProvider;
stateProvider: StateProvider; stateProvider: StateProvider;
loginStrategyService: LoginStrategyServiceAbstraction; loginStrategyService: LoginStrategyServiceAbstraction;
stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
constructor() { constructor() {
@@ -249,14 +253,26 @@ export class Main {
this.memoryStorageService = new MemoryStorageService(); this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders(); this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.globalStateProvider = new DefaultGlobalStateProvider( const storageServiceProvider = new StorageServiceProvider(
this.memoryStorageForStateProviders,
this.storageService, this.storageService,
this.memoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
); );
this.singleUserStateProvider = new DefaultSingleUserStateProvider( this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders, storageServiceProvider,
this.storageService, stateEventRegistrarService,
); );
this.messagingService = new NoopMessagingService(); this.messagingService = new NoopMessagingService();
@@ -269,8 +285,8 @@ export class Main {
this.activeUserStateProvider = new DefaultActiveUserStateProvider( this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService, this.accountService,
this.memoryStorageForStateProviders, storageServiceProvider,
this.storageService, stateEventRegistrarService,
); );
this.derivedStateProvider = new DefaultDerivedStateProvider( this.derivedStateProvider = new DefaultDerivedStateProvider(
@@ -530,6 +546,7 @@ export class Main {
this.stateService, this.stateService,
this.authService, this.authService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback, lockedCallback,
null, null,
); );
@@ -639,6 +656,9 @@ export class Main {
this.policyService.clear(userId), this.policyService.clear(userId),
this.passwordGenerationService.clear(), this.passwordGenerationService.clear(),
]); ]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
await this.stateService.clean(); await this.stateService.clean();
process.env.BW_SESSION = null; process.env.BW_SESSION = null;
} }

View File

@@ -39,6 +39,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -149,6 +150,7 @@ export class AppComponent implements OnInit, OnDestroy {
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private dialogService: DialogService, private dialogService: DialogService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -219,13 +221,13 @@ export class AppComponent implements OnInit, OnDestroy {
const currentUser = await this.stateService.getUserId(); const currentUser = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.stateService.accounts$); const accounts = await firstValueFrom(this.stateService.accounts$);
await this.vaultTimeoutService.lock(currentUser); await this.vaultTimeoutService.lock(currentUser);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. for (const account of Object.keys(accounts)) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises if (account === currentUser) {
Promise.all( continue;
Object.keys(accounts) }
.filter((u) => u !== currentUser)
.map((u) => this.vaultTimeoutService.lock(u)), await this.vaultTimeoutService.lock(account);
); }
break; break;
} }
case "locked": case "locked":
@@ -583,6 +585,8 @@ export class AppComponent implements OnInit, OnDestroy {
await this.keyConnectorService.clear(); await this.keyConnectorService.clear();
await this.biometricStateService.logout(userBeingLoggedOut as UserId); await this.biometricStateService.logout(userBeingLoggedOut as UserId);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
preLogoutActiveUserId = this.activeUserId; preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut }); await this.stateService.clean({ userId: userBeingLoggedOut });
} finally { } finally {

View File

@@ -13,11 +13,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */ /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
@@ -104,10 +106,11 @@ export class Main {
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults); this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService(); this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders(); this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
const globalStateProvider = new DefaultGlobalStateProvider( const storageServiceProvider = new StorageServiceProvider(
this.memoryStorageForStateProviders,
this.storageService, this.storageService,
this.memoryStorageForStateProviders,
); );
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const accountService = new AccountServiceImplementation( const accountService = new AccountServiceImplementation(
new NoopMessagingService(), new NoopMessagingService(),
@@ -115,13 +118,18 @@ export class Main {
globalStateProvider, globalStateProvider,
); );
const stateEventRegistrarService = new StateEventRegistrarService(
globalStateProvider,
storageServiceProvider,
);
const stateProvider = new DefaultStateProvider( const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider( new DefaultActiveUserStateProvider(
accountService, accountService,
this.memoryStorageForStateProviders, storageServiceProvider,
this.storageService, stateEventRegistrarService,
), ),
new DefaultSingleUserStateProvider(this.memoryStorageForStateProviders, this.storageService), new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
globalStateProvider, globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders), new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
); );

View File

@@ -23,6 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit {
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private dialogService: DialogService, private dialogService: DialogService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private paymentMethodWarningService: PaymentMethodWarningService, private paymentMethodWarningService: PaymentMethodWarningService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
) {} ) {}
@@ -284,6 +286,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.paymentMethodWarningService.clear(), this.paymentMethodWarningService.clear(),
]); ]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
this.searchService.clearIndex(); this.searchService.clearIndex();
this.authService.logOut(async () => { this.authService.logOut(async () => {
if (expired) { if (expired) {

View File

@@ -14,7 +14,6 @@ import {
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@@ -28,21 +27,16 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
ActiveUserStateProvider, import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
GlobalStateProvider,
SingleUserStateProvider,
} from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import { PolicyListService } from "../admin-console/core/policy-list.service"; import { PolicyListService } from "../admin-console/core/policy-list.service";
import { HtmlStorageService } from "../core/html-storage.service"; import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service"; import { I18nService } from "../core/i18n.service";
import { WebActiveUserStateProvider } from "../platform/web-active-user-state.provider";
import { WebGlobalStateProvider } from "../platform/web-global-state.provider";
import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebSingleUserStateProvider } from "../platform/web-single-user-state.provider"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { WindowStorageService } from "../platform/window-storage.service"; import { WindowStorageService } from "../platform/window-storage.service";
import { CollectionAdminService } from "../vault/core/collection-admin.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service";
@@ -124,24 +118,9 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
useFactory: () => new WindowStorageService(window.localStorage), useFactory: () => new WindowStorageService(window.localStorage),
}, },
{ {
provide: SingleUserStateProvider, provide: StorageServiceProvider,
useClass: WebSingleUserStateProvider, useClass: WebStorageServiceProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE], deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
},
{
provide: ActiveUserStateProvider,
useClass: WebActiveUserStateProvider,
deps: [
AccountService,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
],
},
{
provide: GlobalStateProvider,
useClass: WebGlobalStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
}, },
{ {
provide: MigrationRunner, provide: MigrationRunner,

View File

@@ -1,44 +0,0 @@
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebActiveUserStateProvider extends DefaultActiveUserStateProvider {
constructor(
accountService: AccountService,
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(accountService, memoryStorage, sessionStorage);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@@ -1,42 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { KeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code*/
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebGlobalStateProvider extends DefaultGlobalStateProvider {
constructor(
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorage, sessionStorage);
}
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@@ -1,43 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebSingleUserStateProvider extends DefaultSingleUserStateProvider {
constructor(
memoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
sessionStorageService: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorageService: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorageService, sessionStorageService);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorageService;
}
}
}

View File

@@ -135,6 +135,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { import {
@@ -150,6 +151,8 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
/* eslint-enable import/no-restricted-paths */ /* eslint-enable import/no-restricted-paths */
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service"; import { ApiService } from "@bitwarden/common/services/api.service";
@@ -551,6 +554,7 @@ import { ModalService } from "./modal.service";
StateServiceAbstraction, StateServiceAbstraction,
AuthServiceAbstraction, AuthServiceAbstraction,
VaultTimeoutSettingsServiceAbstraction, VaultTimeoutSettingsServiceAbstraction,
StateEventRunnerService,
LOCKED_CALLBACK, LOCKED_CALLBACK,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
], ],
@@ -890,20 +894,35 @@ import { ModalService } from "./modal.service";
LogService, LogService,
], ],
}, },
{
provide: StorageServiceProvider,
useClass: StorageServiceProvider,
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE],
},
{
provide: StateEventRegistrarService,
useClass: StateEventRegistrarService,
deps: [GlobalStateProvider, StorageServiceProvider],
},
{
provide: StateEventRunnerService,
useClass: StateEventRunnerService,
deps: [GlobalStateProvider, StorageServiceProvider],
},
{ {
provide: GlobalStateProvider, provide: GlobalStateProvider,
useClass: DefaultGlobalStateProvider, useClass: DefaultGlobalStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE], deps: [StorageServiceProvider],
}, },
{ {
provide: ActiveUserStateProvider, provide: ActiveUserStateProvider,
useClass: DefaultActiveUserStateProvider, useClass: DefaultActiveUserStateProvider,
deps: [AccountServiceAbstraction, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE], deps: [AccountServiceAbstraction, StorageServiceProvider, StateEventRegistrarService],
}, },
{ {
provide: SingleUserStateProvider, provide: SingleUserStateProvider,
useClass: DefaultSingleUserStateProvider, useClass: DefaultSingleUserStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE], deps: [StorageServiceProvider, StateEventRegistrarService],
}, },
{ {
provide: DerivedStateProvider, provide: DerivedStateProvider,

View File

@@ -65,6 +65,7 @@ export class FakeGlobalState<T> implements GlobalState<T> {
this.nextMock(newState); this.nextMock(newState);
return newState; return newState;
} }
/** Tracks update values resolved by `FakeState.update` */ /** Tracks update values resolved by `FakeState.update` */
nextMock = jest.fn<void, [T]>(); nextMock = jest.fn<void, [T]>();

View File

@@ -1,3 +1,4 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, timeout } from "rxjs"; import { firstValueFrom, timeout } from "rxjs";
import { awaitAsync } from "../../../spec"; import { awaitAsync } from "../../../spec";
@@ -14,9 +15,11 @@ import { DefaultDerivedStateProvider } from "../state/implementations/default-de
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider"; import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "../state/implementations/default-state.provider"; import { DefaultStateProvider } from "../state/implementations/default-state.provider";
/* eslint-disable import/no-restricted-paths */ import { StateEventRegistrarService } from "../state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { EnvironmentService } from "./environment.service"; import { EnvironmentService } from "./environment.service";
import { StorageServiceProvider } from "./storage-service.provider";
// There are a few main states EnvironmentService could be in when first used // There are a few main states EnvironmentService could be in when first used
// 1. Not initialized, no active user. Hopefully not to likely but possible // 1. Not initialized, no active user. Hopefully not to likely but possible
@@ -26,6 +29,8 @@ import { EnvironmentService } from "./environment.service";
describe("EnvironmentService", () => { describe("EnvironmentService", () => {
let diskStorageService: FakeStorageService; let diskStorageService: FakeStorageService;
let memoryStorageService: FakeStorageService; let memoryStorageService: FakeStorageService;
let storageServiceProvider: StorageServiceProvider;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let accountService: FakeAccountService; let accountService: FakeAccountService;
let stateProvider: StateProvider; let stateProvider: StateProvider;
@@ -37,16 +42,17 @@ describe("EnvironmentService", () => {
beforeEach(async () => { beforeEach(async () => {
diskStorageService = new FakeStorageService(); diskStorageService = new FakeStorageService();
memoryStorageService = new FakeStorageService(); memoryStorageService = new FakeStorageService();
storageServiceProvider = new StorageServiceProvider(diskStorageService, memoryStorageService);
accountService = mockAccountServiceWith(undefined); accountService = mockAccountServiceWith(undefined);
stateProvider = new DefaultStateProvider( stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider( new DefaultActiveUserStateProvider(
accountService, accountService,
memoryStorageService as any, storageServiceProvider,
diskStorageService, stateEventRegistrarService,
), ),
new DefaultSingleUserStateProvider(memoryStorageService as any, diskStorageService), new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
new DefaultGlobalStateProvider(memoryStorageService as any, diskStorageService), new DefaultGlobalStateProvider(storageServiceProvider),
new DefaultDerivedStateProvider(memoryStorageService), new DefaultDerivedStateProvider(memoryStorageService),
); );

View File

@@ -3,17 +3,14 @@ import { mock } from "jest-mock-extended";
import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { import { StorageServiceProvider } from "../../services/storage-service.provider";
AbstractMemoryStorageService, import { StateEventRegistrarService } from "../state-event-registrar.service";
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider"; import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
describe("DefaultActiveUserStateProvider", () => { describe("DefaultActiveUserStateProvider", () => {
const memoryStorage = mock<AbstractMemoryStorageService & ObservableStorageService>(); const storageServiceProvider = mock<StorageServiceProvider>();
const diskStorage = mock<AbstractStorageService & ObservableStorageService>(); const stateEventRegistrarService = mock<StateEventRegistrarService>();
const userId = "userId" as UserId; const userId = "userId" as UserId;
const accountInfo = { const accountInfo = {
id: userId, id: userId,
@@ -25,7 +22,11 @@ describe("DefaultActiveUserStateProvider", () => {
let sut: DefaultActiveUserStateProvider; let sut: DefaultActiveUserStateProvider;
beforeEach(() => { beforeEach(() => {
sut = new DefaultActiveUserStateProvider(accountService, memoryStorage, diskStorage); sut = new DefaultActiveUserStateProvider(
accountService,
storageServiceProvider,
stateEventRegistrarService,
);
}); });
afterEach(() => { afterEach(() => {

View File

@@ -2,13 +2,9 @@ import { Observable, map } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service"; import { AccountService } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { import { StorageServiceProvider } from "../../services/storage-service.provider";
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { KeyDefinition } from "../key-definition"; import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition"; import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state"; import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider } from "../user-state.provider"; import { ActiveUserStateProvider } from "../user-state.provider";
@@ -21,9 +17,9 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId | undefined>; activeUserId$: Observable<UserId | undefined>;
constructor( constructor(
protected readonly accountService: AccountService, private readonly accountService: AccountService,
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService, private readonly storageServiceProvider: StorageServiceProvider,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService, private readonly stateEventRegistrarService: StateEventRegistrarService,
) { ) {
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id)); this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
} }
@@ -32,7 +28,11 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
if (!isUserKeyDefinition(keyDefinition)) { if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition); keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
} }
const cacheKey = this.buildCacheKey(keyDefinition); const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, keyDefinition);
const existingUserState = this.cache[cacheKey]; const existingUserState = this.cache[cacheKey];
if (existingUserState != null) { if (existingUserState != 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
@@ -40,36 +40,17 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
return existingUserState as ActiveUserState<T>; return existingUserState as ActiveUserState<T>;
} }
const newUserState = this.buildActiveUserState(keyDefinition); const newUserState = new DefaultActiveUserState<T>(
keyDefinition,
this.accountService,
storageService,
this.stateEventRegistrarService,
);
this.cache[cacheKey] = newUserState; this.cache[cacheKey] = newUserState;
return newUserState; return newUserState;
} }
private buildCacheKey(keyDefinition: UserKeyDefinition<unknown>) { private buildCacheKey(location: string, keyDefinition: UserKeyDefinition<unknown>) {
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`; return `${location}_${keyDefinition.fullName}`;
}
protected buildActiveUserState<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
return new DefaultActiveUserState<T>(
keyDefinition,
this.accountService,
this.getLocation(keyDefinition.stateDefinition),
);
}
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
// The default implementations don't support the client overrides
// it is up to the client to extend this class and add that support
const location = stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
} }
} }

View File

@@ -12,6 +12,7 @@ import { AccountInfo, AccountService } from "../../../auth/abstractions/account.
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { StateDefinition } from "../state-definition"; import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition } from "../user-key-definition";
import { DefaultActiveUserState } from "./default-active-user-state"; import { DefaultActiveUserState } from "./default-active-user-state";
@@ -42,6 +43,7 @@ const testKeyDefinition = new UserKeyDefinition<TestState>(testStateDefinition,
describe("DefaultActiveUserState", () => { describe("DefaultActiveUserState", () => {
const accountService = mock<AccountService>(); const accountService = mock<AccountService>();
let diskStorageService: FakeStorageService; let diskStorageService: FakeStorageService;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>; let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
let userState: DefaultActiveUserState<TestState>; let userState: DefaultActiveUserState<TestState>;
@@ -50,7 +52,12 @@ describe("DefaultActiveUserState", () => {
accountService.activeAccount$ = activeAccountSubject; accountService.activeAccount$ = activeAccountSubject;
diskStorageService = new FakeStorageService(); diskStorageService = new FakeStorageService();
userState = new DefaultActiveUserState(testKeyDefinition, accountService, diskStorageService); userState = new DefaultActiveUserState(
testKeyDefinition,
accountService,
diskStorageService,
stateEventRegistrarService,
);
}); });
const makeUserId = (id: string) => { const makeUserId = (id: string) => {
@@ -391,6 +398,48 @@ describe("DefaultActiveUserState", () => {
"No active user at this time.", "No active user at this time.",
); );
}); });
it.each([null, undefined])(
"should register user key definition when state transitions from null-ish (%s) to non-null",
async (startingValue: TestState | null) => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": startingValue,
});
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
},
);
it("should not register user key definition when state has preexisting value", async () => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
date: new Date(2019, 1),
array: [],
},
});
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
});
it.each([null, undefined])(
"should not register user key definition when setting value to null-ish (%s) value",
async (updatedValue: TestState | null) => {
diskStorageService.internalUpdateStore({
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
date: new Date(2019, 1),
array: [],
},
});
await userState.update(() => updatedValue);
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
},
);
}); });
describe("update races", () => { describe("update races", () => {

View File

@@ -21,6 +21,7 @@ import {
AbstractStorageService, AbstractStorageService,
ObservableStorageService, ObservableStorageService,
} from "../../abstractions/storage.service"; } from "../../abstractions/storage.service";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { UserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState, CombinedState, activeMarker } from "../user-state"; import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
@@ -42,6 +43,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
protected keyDefinition: UserKeyDefinition<T>, protected keyDefinition: UserKeyDefinition<T>,
private accountService: AccountService, private accountService: AccountService,
private chosenStorageLocation: AbstractStorageService & ObservableStorageService, private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
private stateEventRegistrarService: StateEventRegistrarService,
) { ) {
this.activeUserId$ = this.accountService.activeAccount$.pipe( this.activeUserId$ = this.accountService.activeAccount$.pipe(
// We only care about the UserId but we do want to know about no user as well. // We only care about the UserId but we do want to know about no user as well.
@@ -150,6 +152,11 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
const newState = configureState(currentState, combinedDependencies); const newState = configureState(currentState, combinedDependencies);
await this.saveToStorage(key, newState); await this.saveToStorage(key, newState);
if (newState != null && currentState == null) {
// Only register this state as something clearable on the first time it saves something
// worth deleting. This is helpful in making sure there is less of a race to adding events.
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
}
return [userId, newState]; return [userId, newState];
} }

View File

@@ -1,25 +1,21 @@
import { import { StorageServiceProvider } from "../../services/storage-service.provider";
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { GlobalState } from "../global-state"; import { GlobalState } from "../global-state";
import { GlobalStateProvider } from "../global-state.provider"; import { GlobalStateProvider } from "../global-state.provider";
import { KeyDefinition } from "../key-definition"; import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { DefaultGlobalState } from "./default-global-state"; import { DefaultGlobalState } from "./default-global-state";
export class DefaultGlobalStateProvider implements GlobalStateProvider { export class DefaultGlobalStateProvider implements GlobalStateProvider {
private globalStateCache: Record<string, GlobalState<unknown>> = {}; private globalStateCache: Record<string, GlobalState<unknown>> = {};
constructor( constructor(private storageServiceProvider: StorageServiceProvider) {}
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
) {}
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> { get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
const cacheKey = this.buildCacheKey(keyDefinition); const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, keyDefinition);
const existingGlobalState = this.globalStateCache[cacheKey]; const existingGlobalState = this.globalStateCache[cacheKey];
if (existingGlobalState != null) { if (existingGlobalState != null) {
// The cast into the actual generic is safe because of rules around key definitions // The cast into the actual generic is safe because of rules around key definitions
@@ -27,30 +23,13 @@ export class DefaultGlobalStateProvider implements GlobalStateProvider {
return existingGlobalState as DefaultGlobalState<T>; return existingGlobalState as DefaultGlobalState<T>;
} }
const newGlobalState = new DefaultGlobalState<T>( const newGlobalState = new DefaultGlobalState<T>(keyDefinition, storageService);
keyDefinition,
this.getLocation(keyDefinition.stateDefinition),
);
this.globalStateCache[cacheKey] = newGlobalState; this.globalStateCache[cacheKey] = newGlobalState;
return newGlobalState; return newGlobalState;
} }
private buildCacheKey(keyDefinition: KeyDefinition<unknown>) { private buildCacheKey(location: string, keyDefinition: KeyDefinition<unknown>) {
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`; return `${location}_${keyDefinition.fullName}`;
}
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
const location = stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
} }
} }

View File

@@ -1,11 +1,7 @@
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { import { StorageServiceProvider } from "../../services/storage-service.provider";
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { KeyDefinition } from "../key-definition"; import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition"; import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { SingleUserState } from "../user-state"; import { SingleUserState } from "../user-state";
import { SingleUserStateProvider } from "../user-state.provider"; import { SingleUserStateProvider } from "../user-state.provider";
@@ -16,8 +12,8 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
private cache: Record<string, SingleUserState<unknown>> = {}; private cache: Record<string, SingleUserState<unknown>> = {};
constructor( constructor(
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService, private readonly storageServiceProvider: StorageServiceProvider,
protected readonly diskStorage: AbstractStorageService & ObservableStorageService, private readonly stateEventRegistrarService: StateEventRegistrarService,
) {} ) {}
get<T>( get<T>(
@@ -27,7 +23,11 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
if (!isUserKeyDefinition(keyDefinition)) { if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition); keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
} }
const cacheKey = this.buildCacheKey(userId, keyDefinition); const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const cacheKey = this.buildCacheKey(location, userId, keyDefinition);
const existingUserState = this.cache[cacheKey]; const existingUserState = this.cache[cacheKey];
if (existingUserState != null) { if (existingUserState != 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
@@ -35,38 +35,21 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
return existingUserState as SingleUserState<T>; return existingUserState as SingleUserState<T>;
} }
const newUserState = this.buildSingleUserState(userId, keyDefinition); const newUserState = new DefaultSingleUserState<T>(
userId,
keyDefinition,
storageService,
this.stateEventRegistrarService,
);
this.cache[cacheKey] = newUserState; this.cache[cacheKey] = newUserState;
return newUserState; return newUserState;
} }
private buildCacheKey(userId: UserId, keyDefinition: UserKeyDefinition<unknown>) { private buildCacheKey(
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}_${userId}`; location: string,
}
protected buildSingleUserState<T>(
userId: UserId, userId: UserId,
keyDefinition: UserKeyDefinition<T>, keyDefinition: UserKeyDefinition<unknown>,
): SingleUserState<T> { ) {
return new DefaultSingleUserState<T>( return `${location}_${keyDefinition.fullName}_${userId}`;
userId,
keyDefinition,
this.getLocation(keyDefinition.stateDefinition),
);
}
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return keyDefinition.stateDefinition.defaultStorageLocation;
}
protected getLocation(stateDefinition: StateDefinition) {
// The default implementations don't support the client overrides
// it is up to the client to extend this class and add that support
switch (stateDefinition.defaultStorageLocation) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
}
} }
} }

View File

@@ -3,6 +3,7 @@
* @jest-environment ../shared/test.environment.ts * @jest-environment ../shared/test.environment.ts
*/ */
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs"; import { firstValueFrom, of } from "rxjs";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
@@ -11,6 +12,7 @@ import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
import { StateDefinition } from "../state-definition"; import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition } from "../user-key-definition";
import { DefaultSingleUserState } from "./default-single-user-state"; import { DefaultSingleUserState } from "./default-single-user-state";
@@ -42,11 +44,17 @@ const userKey = testKeyDefinition.buildKey(userId);
describe("DefaultSingleUserState", () => { describe("DefaultSingleUserState", () => {
let diskStorageService: FakeStorageService; let diskStorageService: FakeStorageService;
let userState: DefaultSingleUserState<TestState>; let userState: DefaultSingleUserState<TestState>;
const stateEventRegistrarService = mock<StateEventRegistrarService>();
const newData = { date: new Date() }; const newData = { date: new Date() };
beforeEach(() => { beforeEach(() => {
diskStorageService = new FakeStorageService(); diskStorageService = new FakeStorageService();
userState = new DefaultSingleUserState(userId, testKeyDefinition, diskStorageService); userState = new DefaultSingleUserState(
userId,
testKeyDefinition,
diskStorageService,
stateEventRegistrarService,
);
}); });
afterEach(() => { afterEach(() => {
@@ -255,6 +263,49 @@ describe("DefaultSingleUserState", () => {
expect(emissions).toHaveLength(2); expect(emissions).toHaveLength(2);
expect(emissions).toEqual(expect.arrayContaining([initialState, newState])); expect(emissions).toEqual(expect.arrayContaining([initialState, newState]));
}); });
it.each([null, undefined])(
"should register user key definition when state transitions from null-ish (%s) to non-null",
async (startingValue: TestState | null) => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = startingValue;
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
},
);
it("should not register user key definition when state has preexisting value", async () => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = {
date: new Date(2019, 1),
};
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => ({ array: ["one"], date: new Date() }));
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
});
it.each([null, undefined])(
"should not register user key definition when setting value to null-ish (%s) value",
async (updatedValue: TestState | null) => {
const initialState: Record<string, TestState> = {};
initialState[userKey] = {
date: new Date(2019, 1),
};
diskStorageService.internalUpdateStore(initialState);
await userState.update(() => updatedValue);
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
},
);
}); });
describe("update races", () => { describe("update races", () => {

View File

@@ -18,6 +18,7 @@ import {
AbstractStorageService, AbstractStorageService,
ObservableStorageService, ObservableStorageService,
} from "../../abstractions/storage.service"; } from "../../abstractions/storage.service";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { UserKeyDefinition } from "../user-key-definition"; import { UserKeyDefinition } from "../user-key-definition";
import { CombinedState, SingleUserState } from "../user-state"; import { CombinedState, SingleUserState } from "../user-state";
@@ -35,6 +36,7 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
readonly userId: UserId, readonly userId: UserId,
private keyDefinition: UserKeyDefinition<T>, private keyDefinition: UserKeyDefinition<T>,
private chosenLocation: AbstractStorageService & ObservableStorageService, private chosenLocation: AbstractStorageService & ObservableStorageService,
private stateEventRegistrarService: StateEventRegistrarService,
) { ) {
this.storageKey = this.keyDefinition.buildKey(this.userId); this.storageKey = this.keyDefinition.buildKey(this.userId);
const initialStorageGet$ = defer(() => { const initialStorageGet$ = defer(() => {
@@ -100,6 +102,11 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
const newState = configureState(currentState, combinedDependencies); const newState = configureState(currentState, combinedDependencies);
await this.chosenLocation.save(this.storageKey, newState); await this.chosenLocation.save(this.storageKey, newState);
if (newState != null && currentState == null) {
// Only register this state as something clearable on the first time it saves something
// worth deleting. This is helpful in making sure there is less of a race to adding events.
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
}
return newState; return newState;
} }

View File

@@ -1,8 +1,12 @@
import { mock } from "jest-mock-extended";
import { mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition"; import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition"; import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { DefaultActiveUserState } from "./default-active-user-state"; import { DefaultActiveUserState } from "./default-active-user-state";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider"; import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
@@ -12,6 +16,9 @@ import { DefaultSingleUserState } from "./default-single-user-state";
import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider"; import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider";
describe("Specific State Providers", () => { describe("Specific State Providers", () => {
const storageServiceProvider = mock<StorageServiceProvider>();
const stateEventRegistrarService = mock<StateEventRegistrarService>();
let singleSut: DefaultSingleUserStateProvider; let singleSut: DefaultSingleUserStateProvider;
let activeSut: DefaultActiveUserStateProvider; let activeSut: DefaultActiveUserStateProvider;
let globalSut: DefaultGlobalStateProvider; let globalSut: DefaultGlobalStateProvider;
@@ -19,19 +26,20 @@ describe("Specific State Providers", () => {
const fakeUser1 = "00000000-0000-1000-a000-000000000001" as UserId; const fakeUser1 = "00000000-0000-1000-a000-000000000001" as UserId;
beforeEach(() => { beforeEach(() => {
storageServiceProvider.get.mockImplementation((location) => {
return [location, new FakeStorageService()];
});
singleSut = new DefaultSingleUserStateProvider( singleSut = new DefaultSingleUserStateProvider(
new FakeStorageService() as any, storageServiceProvider,
new FakeStorageService(), stateEventRegistrarService,
); );
activeSut = new DefaultActiveUserStateProvider( activeSut = new DefaultActiveUserStateProvider(
mockAccountServiceWith(null), mockAccountServiceWith(null),
new FakeStorageService() as any, storageServiceProvider,
new FakeStorageService(), stateEventRegistrarService,
);
globalSut = new DefaultGlobalStateProvider(
new FakeStorageService() as any,
new FakeStorageService(),
); );
globalSut = new DefaultGlobalStateProvider(storageServiceProvider);
}); });
const fakeDiskStateDefinition = new StateDefinition("fake", "disk"); const fakeDiskStateDefinition = new StateDefinition("fake", "disk");

View File

@@ -11,6 +11,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { Account } from "../../platform/models/domain/account"; import { Account } from "../../platform/models/domain/account";
import { StateEventRunnerService } from "../../platform/state";
import { CipherService } from "../../vault/abstractions/cipher.service"; import { CipherService } from "../../vault/abstractions/cipher.service";
import { CollectionService } from "../../vault/abstractions/collection.service"; import { CollectionService } from "../../vault/abstractions/collection.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
@@ -28,6 +29,7 @@ describe("VaultTimeoutService", () => {
let stateService: MockProxy<StateService>; let stateService: MockProxy<StateService>;
let authService: MockProxy<AuthService>; let authService: MockProxy<AuthService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>; let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>; let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>; let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
@@ -48,6 +50,7 @@ describe("VaultTimeoutService", () => {
stateService = mock(); stateService = mock();
authService = mock(); authService = mock();
vaultTimeoutSettingsService = mock(); vaultTimeoutSettingsService = mock();
stateEventRunnerService = mock();
lockedCallback = jest.fn(); lockedCallback = jest.fn();
loggedOutCallback = jest.fn(); loggedOutCallback = jest.fn();
@@ -73,6 +76,7 @@ describe("VaultTimeoutService", () => {
stateService, stateService,
authService, authService,
vaultTimeoutSettingsService, vaultTimeoutSettingsService,
stateEventRunnerService,
lockedCallback, lockedCallback,
loggedOutCallback, loggedOutCallback,
); );
@@ -103,7 +107,8 @@ describe("VaultTimeoutService", () => {
return Promise.resolve(accounts[userId]?.authStatus); return Promise.resolve(accounts[userId]?.authStatus);
}); });
stateService.getIsAuthenticated.mockImplementation((options) => { stateService.getIsAuthenticated.mockImplementation((options) => {
return Promise.resolve(accounts[options.userId]?.isAuthenticated); // Just like actual state service, if no userId is given fallback to active userId
return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated);
}); });
vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => { vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => {
@@ -337,4 +342,80 @@ describe("VaultTimeoutService", () => {
expectNoAction("1"); expectNoAction("1");
}); });
}); });
describe("lock", () => {
const setupLock = () => {
setupAccounts(
{
user1: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
user2: {
authStatus: AuthenticationStatus.Unlocked,
isAuthenticated: true,
},
},
{
userId: "user1",
},
);
};
it("should call state event runner with currently active user if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
});
it("should call messaging service locked message if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined });
});
it("should call locked callback if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(lockedCallback).toHaveBeenCalledWith(undefined);
});
it("should call state event runner with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2");
});
it("should call messaging service locked message with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" });
});
it("should call locked callback with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
expect(lockedCallback).toHaveBeenCalledWith("user2");
});
});
}); });

View File

@@ -11,6 +11,8 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { MessagingService } from "../../platform/abstractions/messaging.service"; import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { CipherService } from "../../vault/abstractions/cipher.service"; import { CipherService } from "../../vault/abstractions/cipher.service";
import { CollectionService } from "../../vault/abstractions/collection.service"; import { CollectionService } from "../../vault/abstractions/collection.service";
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
@@ -29,6 +31,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private stateService: StateService, private stateService: StateService,
private authService: AuthService, private authService: AuthService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private stateEventRunnerService: StateEventRunnerService,
private lockedCallback: (userId?: string) => Promise<void> = null, private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null, private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
) {} ) {}
@@ -81,7 +84,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.logOut(userId); await this.logOut(userId);
} }
if (userId == null || userId === (await this.stateService.getUserId())) { const currentUserId = await this.stateService.getUserId();
if (userId == null || userId === currentUserId) {
this.searchService.clearIndex(); this.searchService.clearIndex();
await this.folderService.clearCache(); await this.folderService.clearCache();
await this.collectionService.clearActiveUserCache(); await this.collectionService.clearActiveUserCache();
@@ -98,6 +103,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.cipherService.clearCache(userId); await this.cipherService.clearCache(userId);
await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId);
// FIXME: We should send the userId of the user that was locked, in the case of this method being passed
// undefined then it should give back the currentUserId. Better yet, this method shouldn't take
// an undefined userId at all. All receivers need to be checked for how they handle getting undefined.
this.messagingService.send("locked", { userId: userId }); this.messagingService.send("locked", { userId: userId });
if (this.lockedCallback != null) { if (this.lockedCallback != null) {