diff --git a/libs/common/spec/services/settings.service.spec.ts b/libs/common/spec/services/settings.service.spec.ts new file mode 100644 index 0000000000..f3733a1425 --- /dev/null +++ b/libs/common/spec/services/settings.service.spec.ts @@ -0,0 +1,64 @@ +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { ContainerService } from "@bitwarden/common/services/container.service"; +import { SettingsService } from "@bitwarden/common/services/settings.service"; +import { StateService } from "@bitwarden/common/services/state.service"; + +describe("SettingsService", () => { + let settingsService: SettingsService; + + let cryptoService: SubstituteOf; + let stateService: SubstituteOf; + let activeAccount: BehaviorSubject; + let activeAccountUnlocked: BehaviorSubject; + + beforeEach(() => { + cryptoService = Substitute.for(); + stateService = Substitute.for(); + activeAccount = new BehaviorSubject("123"); + activeAccountUnlocked = new BehaviorSubject(true); + + stateService.getSettings().resolves({ equivalentDomains: [["test"], ["domains"]] }); + stateService.activeAccount$.returns(activeAccount); + stateService.activeAccountUnlocked$.returns(activeAccountUnlocked); + (window as any).bitwardenContainerService = new ContainerService(cryptoService); + + settingsService = new SettingsService(stateService); + }); + + afterEach(() => { + activeAccount.complete(); + activeAccountUnlocked.complete(); + }); + + describe("getEquivalentDomains", () => { + it("returns value", async () => { + const result = await firstValueFrom(settingsService.settings$); + + expect(result).toEqual({ + equivalentDomains: [["test"], ["domains"]], + }); + }); + }); + + it("setEquivalentDomains", async () => { + await settingsService.setEquivalentDomains([["test2"], ["domains2"]]); + + stateService.received(1).setSettings(Arg.any()); + + expect((await firstValueFrom(settingsService.settings$)).equivalentDomains).toEqual([ + ["test2"], + ["domains2"], + ]); + }); + + it("clear", async () => { + await settingsService.clear(); + + stateService.received(1).setSettings(Arg.any(), Arg.any()); + + expect(await firstValueFrom(settingsService.settings$)).toEqual({}); + }); +}); diff --git a/libs/common/src/abstractions/settings.service.ts b/libs/common/src/abstractions/settings.service.ts index e7886585b4..1f6047dfa1 100644 --- a/libs/common/src/abstractions/settings.service.ts +++ b/libs/common/src/abstractions/settings.service.ts @@ -1,6 +1,10 @@ +import { Observable } from "rxjs"; + +import { AccountSettingsSettings } from "../models/domain/account"; + export abstract class SettingsService { - clearCache: () => Promise; - getEquivalentDomains: () => Promise; + settings$: Observable; + setEquivalentDomains: (equivalentDomains: string[][]) => Promise; clear: (userId?: string) => Promise; } diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index b99d8aef86..737a51c10b 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -12,7 +12,7 @@ import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; -import { Account } from "../models/domain/account"; +import { Account, AccountSettingsSettings } from "../models/domain/account"; import { EncString } from "../models/domain/encString"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; @@ -286,8 +286,14 @@ export abstract class StateService { setRememberedEmail: (value: string, options?: StorageOptions) => Promise; getSecurityStamp: (options?: StorageOptions) => Promise; setSecurityStamp: (value: string, options?: StorageOptions) => Promise; - getSettings: (options?: StorageOptions) => Promise; - setSettings: (value: string, options?: StorageOptions) => Promise; + /** + * @deprecated Do not call this directly, use SettingsService + */ + getSettings: (options?: StorageOptions) => Promise; + /** + * @deprecated Do not call this directly, use SettingsService + */ + setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise; getSsoCodeVerifier: (options?: StorageOptions) => Promise; setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise; getSsoOrgIdentifier: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/models/domain/account.ts b/libs/common/src/models/domain/account.ts index 88b6de248c..dfeacce120 100644 --- a/libs/common/src/models/domain/account.ts +++ b/libs/common/src/models/domain/account.ts @@ -137,11 +137,15 @@ export class AccountSettings { generatorOptions?: any; pinProtected?: EncryptionPair = new EncryptionPair(); protectedPin?: string; - settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly + settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; } +export type AccountSettingsSettings = { + equivalentDomains?: { [id: string]: any }; +}; + export class AccountTokens { accessToken?: string; decodedToken?: any; diff --git a/libs/common/src/services/cipher.service.ts b/libs/common/src/services/cipher.service.ts index ebc18b2184..40440ea8bf 100644 --- a/libs/common/src/services/cipher.service.ts +++ b/libs/common/src/services/cipher.service.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "../abstractions/api.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CryptoService } from "../abstractions/crypto.service"; @@ -13,6 +15,7 @@ import { UriMatchType } from "../enums/uriMatchType"; import { sequentialize } from "../misc/sequentialize"; import { Utils } from "../misc/utils"; import { CipherData } from "../models/data/cipherData"; +import { AccountSettingsSettings } from "../models/domain/account"; import { Attachment } from "../models/domain/attachment"; import { Card } from "../models/domain/card"; import { Cipher } from "../models/domain/cipher"; @@ -387,20 +390,22 @@ export class CipherService implements CipherServiceAbstraction { const eqDomainsPromise = domain == null ? Promise.resolve([]) - : this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { - let matches: any[] = []; - eqDomains.forEach((eqDomain) => { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); + : firstValueFrom(this.settingsService.settings$).then( + (settings: AccountSettingsSettings) => { + let matches: any[] = []; + settings.equivalentDomains.forEach((eqDomain: any) => { + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } + }); + + if (!matches.length) { + matches.push(domain); } - }); - if (!matches.length) { - matches.push(domain); + return matches; } - - return matches; - }); + ); const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]); const matchingDomains = result[0]; diff --git a/libs/common/src/services/settings.service.ts b/libs/common/src/services/settings.service.ts index 7f5131b0f3..923bd8970d 100644 --- a/libs/common/src/services/settings.service.ts +++ b/libs/common/src/services/settings.service.ts @@ -1,56 +1,50 @@ +import { BehaviorSubject, concatMap } from "rxjs"; + import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service"; import { StateService } from "../abstractions/state.service"; - -const Keys = { - settingsPrefix: "settings_", - equivalentDomains: "equivalentDomains", -}; +import { Utils } from "../misc/utils"; +import { AccountSettingsSettings } from "../models/domain/account"; export class SettingsService implements SettingsServiceAbstraction { - constructor(private stateService: StateService) {} + private _settings: BehaviorSubject = new BehaviorSubject({}); - async clearCache(): Promise { - await this.stateService.setSettings(null); - } + settings$ = this._settings.asObservable(); - getEquivalentDomains(): Promise { - return this.getSettingsKey(Keys.equivalentDomains); + constructor(private stateService: StateService) { + this.stateService.activeAccountUnlocked$ + .pipe( + concatMap(async (unlocked) => { + if (Utils.global.bitwardenContainerService == null) { + return; + } + + if (!unlocked) { + this._settings.next({}); + return; + } + + const data = await this.stateService.getSettings(); + + this._settings.next(data); + }) + ) + .subscribe(); } async setEquivalentDomains(equivalentDomains: string[][]): Promise { - await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); + const settings = this._settings.getValue() ?? {}; + + settings.equivalentDomains = equivalentDomains; + + this._settings.next(settings); + await this.stateService.setSettings(settings); } async clear(userId?: string): Promise { + if (userId == null || userId == (await this.stateService.getUserId())) { + this._settings.next({}); + } + await this.stateService.setSettings(null, { userId: userId }); } - - // Helpers - - private async getSettings(): Promise { - const settings = await this.stateService.getSettings(); - if (settings == null) { - // eslint-disable-next-line - const userId = await this.stateService.getUserId(); - } - return settings; - } - - private async getSettingsKey(key: string): Promise { - const settings = await this.getSettings(); - if (settings != null && settings[key]) { - return settings[key]; - } - return null; - } - - private async setSettingsKey(key: string, value: any): Promise { - let settings = await this.getSettings(); - if (!settings) { - settings = {}; - } - - settings[key] = value; - await this.stateService.setSettings(settings); - } } diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 3e007911cf..f2c5a5618d 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -20,7 +20,12 @@ import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; -import { Account, AccountData, AccountSettings } from "../models/domain/account"; +import { + Account, + AccountData, + AccountSettings, + AccountSettingsSettings, +} from "../models/domain/account"; import { EncString } from "../models/domain/encString"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; @@ -2075,13 +2080,13 @@ export class StateService< ); } - async getSettings(options?: StorageOptions): Promise { + async getSettings(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) )?.settings?.settings; } - async setSettings(value: string, options?: StorageOptions): Promise { + async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()) ); diff --git a/libs/common/src/services/stateMigration.service.ts b/libs/common/src/services/stateMigration.service.ts index 762f2b8977..359b4acec4 100644 --- a/libs/common/src/services/stateMigration.service.ts +++ b/libs/common/src/services/stateMigration.service.ts @@ -12,7 +12,7 @@ import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; -import { Account, AccountSettings } from "../models/domain/account"; +import { Account, AccountSettings, AccountSettingsSettings } from "../models/domain/account"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; import { GlobalState } from "../models/domain/globalState"; @@ -319,7 +319,10 @@ export class StateMigrationService< encrypted: await this.get(v1Keys.pinProtected), }, protectedPin: await this.get(v1Keys.protectedPin), - settings: userId == null ? null : await this.get(v1KeyPrefixes.settings + userId), + settings: + userId == null + ? null + : await this.get(v1KeyPrefixes.settings + userId), vaultTimeout: (await this.get(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout, vaultTimeoutAction: