mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-5535] Migrate Environment Service to StateProvider (#7621)
* Migrate EnvironmentService * Move Migration Test Helper * Claim StateDefinition * Add State Migration * Update StateServices * Update EnvironmentService Abstraction * Update DI * Update Browser Instantiation * Fix BrowserEnvironmentService * Update Desktop & CLI Instantiation * Update Usage * Create isStringRecord helper * Fix Old Tests * Use Existing AccountService * Don't Rely on Parameter Mutation * Fix Conflicts
This commit is contained in:
@@ -344,6 +344,11 @@ export default class MainBackground {
|
|||||||
this.globalStateProvider,
|
this.globalStateProvider,
|
||||||
this.derivedStateProvider,
|
this.derivedStateProvider,
|
||||||
);
|
);
|
||||||
|
this.environmentService = new BrowserEnvironmentService(
|
||||||
|
this.logService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.accountService,
|
||||||
|
);
|
||||||
this.stateService = new BrowserStateService(
|
this.stateService = new BrowserStateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -351,6 +356,7 @@ export default class MainBackground {
|
|||||||
this.logService,
|
this.logService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.environmentService,
|
||||||
);
|
);
|
||||||
this.platformUtilsService = new BrowserPlatformUtilsService(
|
this.platformUtilsService = new BrowserPlatformUtilsService(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
@@ -386,7 +392,6 @@ export default class MainBackground {
|
|||||||
);
|
);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
this.environmentService = new BrowserEnvironmentService(this.stateService, this.logService);
|
|
||||||
this.apiService = new ApiService(
|
this.apiService = new ApiService(
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import {
|
||||||
|
accountServiceFactory,
|
||||||
|
AccountServiceInitOptions,
|
||||||
|
} from "../../../auth/background/service-factories/account-service.factory";
|
||||||
import { BrowserEnvironmentService } from "../../services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../../services/browser-environment.service";
|
||||||
|
|
||||||
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||||
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
||||||
import {
|
import { stateProviderFactory, StateProviderInitOptions } from "./state-provider.factory";
|
||||||
stateServiceFactory as stateServiceFactory,
|
|
||||||
StateServiceInitOptions,
|
|
||||||
} from "./state-service.factory";
|
|
||||||
|
|
||||||
type EnvironmentServiceFactoryOptions = FactoryOptions;
|
type EnvironmentServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type EnvironmentServiceInitOptions = EnvironmentServiceFactoryOptions &
|
export type EnvironmentServiceInitOptions = EnvironmentServiceFactoryOptions &
|
||||||
StateServiceInitOptions &
|
StateProviderInitOptions &
|
||||||
|
AccountServiceInitOptions &
|
||||||
LogServiceInitOptions;
|
LogServiceInitOptions;
|
||||||
|
|
||||||
export function environmentServiceFactory(
|
export function environmentServiceFactory(
|
||||||
@@ -23,8 +25,9 @@ export function environmentServiceFactory(
|
|||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new BrowserEnvironmentService(
|
new BrowserEnvironmentService(
|
||||||
await stateServiceFactory(cache, opts),
|
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
import { Account } from "../../../models/account";
|
import { Account } from "../../../models/account";
|
||||||
import { BrowserStateService } from "../../services/browser-state.service";
|
import { BrowserStateService } from "../../services/browser-state.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
environmentServiceFactory,
|
||||||
|
EnvironmentServiceInitOptions,
|
||||||
|
} from "./environment-service.factory";
|
||||||
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||||
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +35,8 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
|
|||||||
SecureStorageServiceInitOptions &
|
SecureStorageServiceInitOptions &
|
||||||
MemoryStorageServiceInitOptions &
|
MemoryStorageServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
AccountServiceInitOptions;
|
AccountServiceInitOptions &
|
||||||
|
EnvironmentServiceInitOptions;
|
||||||
|
|
||||||
export async function stateServiceFactory(
|
export async function stateServiceFactory(
|
||||||
cache: { stateService?: BrowserStateService } & CachedServices,
|
cache: { stateService?: BrowserStateService } & CachedServices,
|
||||||
@@ -42,13 +47,14 @@ export async function stateServiceFactory(
|
|||||||
"stateService",
|
"stateService",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
await new BrowserStateService(
|
new BrowserStateService(
|
||||||
await diskStorageServiceFactory(cache, opts),
|
await diskStorageServiceFactory(cache, opts),
|
||||||
await secureStorageServiceFactory(cache, opts),
|
await secureStorageServiceFactory(cache, opts),
|
||||||
await memoryStorageServiceFactory(cache, opts),
|
await memoryStorageServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
opts.stateServiceOptions.stateFactory,
|
opts.stateServiceOptions.stateFactory,
|
||||||
await accountServiceFactory(cache, opts),
|
await accountServiceFactory(cache, opts),
|
||||||
|
await environmentServiceFactory(cache, opts),
|
||||||
opts.stateServiceOptions.useAccountCache,
|
opts.stateServiceOptions.useAccountCache,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
|
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
|
||||||
import { devFlagEnabled, devFlagValue } from "../flags";
|
import { devFlagEnabled, devFlagValue } from "../flags";
|
||||||
|
|
||||||
export class BrowserEnvironmentService extends EnvironmentService {
|
export class BrowserEnvironmentService extends EnvironmentService {
|
||||||
constructor(
|
constructor(
|
||||||
stateService: StateService,
|
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
stateProvider: StateProvider,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(stateService);
|
super(stateProvider, accountService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasManagedEnvironment(): Promise<boolean> {
|
async hasManagedEnvironment(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
@@ -29,6 +30,7 @@ describe("Browser State Service", () => {
|
|||||||
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
|
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
|
||||||
let useAccountCache: boolean;
|
let useAccountCache: boolean;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
|
||||||
let state: State<GlobalState, Account>;
|
let state: State<GlobalState, Account>;
|
||||||
const userId = "userId";
|
const userId = "userId";
|
||||||
@@ -41,6 +43,7 @@ describe("Browser State Service", () => {
|
|||||||
logService = mock();
|
logService = mock();
|
||||||
stateFactory = mock();
|
stateFactory = mock();
|
||||||
accountService = mock();
|
accountService = mock();
|
||||||
|
environmentService = mock();
|
||||||
// turn off account cache for tests
|
// turn off account cache for tests
|
||||||
useAccountCache = false;
|
useAccountCache = false;
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ describe("Browser State Service", () => {
|
|||||||
logService,
|
logService,
|
||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
|
environmentService,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
@@ -44,6 +45,7 @@ export class BrowserStateService
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
stateFactory: StateFactory<GlobalState, Account>,
|
stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
useAccountCache = true,
|
useAccountCache = true,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@@ -53,6 +55,7 @@ export class BrowserStateService
|
|||||||
logService,
|
logService,
|
||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
|
environmentService,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
memoryStorageService: AbstractMemoryStorageService,
|
memoryStorageService: AbstractMemoryStorageService,
|
||||||
logService: LogServiceAbstraction,
|
logService: LogServiceAbstraction,
|
||||||
accountService: AccountServiceAbstraction,
|
accountService: AccountServiceAbstraction,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
) => {
|
) => {
|
||||||
return new BrowserStateService(
|
return new BrowserStateService(
|
||||||
storageService,
|
storageService,
|
||||||
@@ -491,6 +492,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
logService,
|
logService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
accountService,
|
accountService,
|
||||||
|
environmentService,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
deps: [
|
deps: [
|
||||||
@@ -499,6 +501,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
MEMORY_STORAGE,
|
MEMORY_STORAGE,
|
||||||
LogServiceAbstraction,
|
LogServiceAbstraction,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
|
EnvironmentService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ export class Main {
|
|||||||
this.derivedStateProvider,
|
this.derivedStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
|
||||||
|
|
||||||
this.stateService = new StateService(
|
this.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -262,6 +264,7 @@ export class Main {
|
|||||||
this.logService,
|
this.logService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
this.environmentService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cryptoService = new CryptoService(
|
this.cryptoService = new CryptoService(
|
||||||
@@ -276,7 +279,6 @@ export class Main {
|
|||||||
|
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.storageService);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
this.environmentService = new EnvironmentService(this.stateService);
|
|
||||||
|
|
||||||
const customUserAgent =
|
const customUserAgent =
|
||||||
"Bitwarden_CLI/" +
|
"Bitwarden_CLI/" +
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/
|
|||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
@@ -129,6 +130,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
LogService,
|
LogService,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
|
EnvironmentService,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import { app } from "electron";
|
|||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||||
// eslint-disable-next-line 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 { 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 { 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 { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
|
/*/ eslint-enable import/no-restricted-paths */
|
||||||
|
|
||||||
import { MenuMain } from "./main/menu/menu.main";
|
import { MenuMain } from "./main/menu/menu.main";
|
||||||
import { MessagingMain } from "./main/messaging.main";
|
import { MessagingMain } from "./main/messaging.main";
|
||||||
@@ -34,6 +40,7 @@ export class Main {
|
|||||||
memoryStorageService: MemoryStorageService;
|
memoryStorageService: MemoryStorageService;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
stateService: ElectronStateService;
|
stateService: ElectronStateService;
|
||||||
|
environmentService: EnvironmentService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
|
|
||||||
windowMain: WindowMain;
|
windowMain: WindowMain;
|
||||||
@@ -93,6 +100,25 @@ export class Main {
|
|||||||
this.storageService,
|
this.storageService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const accountService = new AccountServiceImplementation(
|
||||||
|
new NoopMessagingService(),
|
||||||
|
this.logService,
|
||||||
|
globalStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stateProvider = new DefaultStateProvider(
|
||||||
|
new DefaultActiveUserStateProvider(
|
||||||
|
accountService,
|
||||||
|
this.memoryStorageService,
|
||||||
|
this.storageService,
|
||||||
|
),
|
||||||
|
new DefaultSingleUserStateProvider(this.memoryStorageService, this.storageService),
|
||||||
|
globalStateProvider,
|
||||||
|
new DefaultDerivedStateProvider(this.memoryStorageService),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.environmentService = new EnvironmentService(stateProvider, accountService);
|
||||||
|
|
||||||
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
||||||
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
|
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
|
||||||
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
|
// ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events.
|
||||||
@@ -102,11 +128,8 @@ export class Main {
|
|||||||
this.memoryStorageService,
|
this.memoryStorageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
new AccountServiceImplementation(
|
accountService, // will not broadcast logouts. This is a hack until we can remove messaging dependency
|
||||||
new NoopMessagingService(),
|
this.environmentService,
|
||||||
this.logService,
|
|
||||||
globalStateProvider,
|
|
||||||
), // will not broadcast logouts. This is a hack until we can remove messaging dependency
|
|
||||||
false, // Do not use disk caching because this will get out of sync with the renderer service
|
false, // Do not use disk caching because this will get out of sync with the renderer service
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -128,7 +151,7 @@ export class Main {
|
|||||||
this.menuMain = new MenuMain(
|
this.menuMain = new MenuMain(
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.stateService,
|
this.environmentService,
|
||||||
this.windowMain,
|
this.windowMain,
|
||||||
this.updaterMain,
|
this.updaterMain,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app, Menu } from "electron";
|
import { app, Menu } from "electron";
|
||||||
|
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
import { UpdaterMain } from "../updater.main";
|
import { UpdaterMain } from "../updater.main";
|
||||||
import { WindowMain } from "../window.main";
|
import { WindowMain } from "../window.main";
|
||||||
@@ -16,7 +16,7 @@ export class MenuMain {
|
|||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private stateService: StateService,
|
private environmentService: EnvironmentService,
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
private updaterMain: UpdaterMain,
|
private updaterMain: UpdaterMain,
|
||||||
) {}
|
) {}
|
||||||
@@ -45,16 +45,7 @@ export class MenuMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getWebVaultUrl() {
|
private async getWebVaultUrl() {
|
||||||
let webVaultUrl = cloudWebVaultUrl;
|
return this.environmentService.getWebVaultUrl() ?? cloudWebVaultUrl;
|
||||||
const urlsObj = await this.stateService.getEnvironmentUrls();
|
|
||||||
if (urlsObj != null) {
|
|
||||||
if (urlsObj.base != null) {
|
|
||||||
webVaultUrl = urlsObj.base;
|
|
||||||
} else if (urlsObj.webVault != null) {
|
|
||||||
webVaultUrl = urlsObj.webVault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return webVaultUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private initContextMenu() {
|
private initContextMenu() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
@@ -32,6 +33,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
logService: LogService,
|
logService: LogService,
|
||||||
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
@Inject(STATE_FACTORY) stateFactory: StateFactory<GlobalState, Account>,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
@Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true,
|
@Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@@ -41,6 +43,7 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
logService,
|
logService,
|
||||||
stateFactory,
|
stateFactory,
|
||||||
accountService,
|
accountService,
|
||||||
|
environmentService,
|
||||||
useAccountCache,
|
useAccountCache,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ import { ModalService } from "./modal.service";
|
|||||||
{
|
{
|
||||||
provide: EnvironmentServiceAbstraction,
|
provide: EnvironmentServiceAbstraction,
|
||||||
useClass: EnvironmentService,
|
useClass: EnvironmentService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateProvider, AccountServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: TotpServiceAbstraction,
|
provide: TotpServiceAbstraction,
|
||||||
@@ -515,6 +515,7 @@ import { ModalService } from "./modal.service";
|
|||||||
LogService,
|
LogService,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
|
EnvironmentServiceAbstraction,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
export type Urls = {
|
export type Urls = {
|
||||||
base?: string;
|
base?: string;
|
||||||
webVault?: string;
|
webVault?: string;
|
||||||
@@ -52,6 +54,12 @@ export abstract class EnvironmentService {
|
|||||||
* @param {Region} region - The region of the cloud web vault app.
|
* @param {Region} region - The region of the cloud web vault app.
|
||||||
*/
|
*/
|
||||||
setCloudWebVaultUrl: (region: Region) => void;
|
setCloudWebVaultUrl: (region: Region) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the environment for a given user based on the globally set defaults.
|
||||||
|
*/
|
||||||
|
seedUserEnvironment: (userId: UserId) => Promise<void>;
|
||||||
|
|
||||||
getSendUrl: () => string;
|
getSendUrl: () => string;
|
||||||
getIconsUrl: () => string;
|
getIconsUrl: () => string;
|
||||||
getApiUrl: () => string;
|
getApiUrl: () => string;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { PolicyData } from "../../admin-console/models/data/policy.data";
|
|||||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
|
||||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
@@ -378,10 +377,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setEntityId: (value: string, options?: StorageOptions) => Promise<void>;
|
setEntityId: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEntityType: (options?: StorageOptions) => Promise<any>;
|
getEntityType: (options?: StorageOptions) => Promise<any>;
|
||||||
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
|
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
|
|
||||||
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
|
|
||||||
getRegion: (options?: StorageOptions) => Promise<string>;
|
|
||||||
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
|
|
||||||
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
||||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
|||||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
|
||||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||||
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||||
@@ -239,7 +238,6 @@ export class AccountSettings {
|
|||||||
enableAutoFillOnPageLoad?: boolean;
|
enableAutoFillOnPageLoad?: boolean;
|
||||||
enableBiometric?: boolean;
|
enableBiometric?: boolean;
|
||||||
enableFullWidth?: boolean;
|
enableFullWidth?: boolean;
|
||||||
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
|
|
||||||
equivalentDomains?: any;
|
equivalentDomains?: any;
|
||||||
minimizeOnCopyToClipboard?: boolean;
|
minimizeOnCopyToClipboard?: boolean;
|
||||||
passwordGenerationOptions?: PasswordGeneratorOptions;
|
passwordGenerationOptions?: PasswordGeneratorOptions;
|
||||||
@@ -255,7 +253,6 @@ export class AccountSettings {
|
|||||||
approveLoginRequests?: boolean;
|
approveLoginRequests?: boolean;
|
||||||
avatarColor?: string;
|
avatarColor?: string;
|
||||||
activateAutoFillOnPageLoadFromPolicy?: boolean;
|
activateAutoFillOnPageLoadFromPolicy?: boolean;
|
||||||
region?: string;
|
|
||||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||||
trustDeviceChoiceForDecryption?: boolean;
|
trustDeviceChoiceForDecryption?: boolean;
|
||||||
biometricPromptCancelled?: boolean;
|
biometricPromptCancelled?: boolean;
|
||||||
@@ -269,7 +266,6 @@ export class AccountSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(new AccountSettings(), obj, {
|
return Object.assign(new AccountSettings(), obj, {
|
||||||
environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls),
|
|
||||||
pinProtected: EncryptionPair.fromJSON<string, EncString>(
|
pinProtected: EncryptionPair.fromJSON<string, EncString>(
|
||||||
obj?.pinProtected,
|
obj?.pinProtected,
|
||||||
EncString.fromJSON,
|
EncString.fromJSON,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
|
||||||
import { WindowState } from "../../../models/domain/window-state";
|
import { WindowState } from "../../../models/domain/window-state";
|
||||||
import { ThemeType } from "../../enums";
|
import { ThemeType } from "../../enums";
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ export class GlobalState {
|
|||||||
enableBiometrics?: boolean;
|
enableBiometrics?: boolean;
|
||||||
biometricText?: string;
|
biometricText?: string;
|
||||||
noAutoPromptBiometricsText?: string;
|
noAutoPromptBiometricsText?: string;
|
||||||
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
|
|
||||||
enableTray?: boolean;
|
enableTray?: boolean;
|
||||||
enableMinimizeToTray?: boolean;
|
enableMinimizeToTray?: boolean;
|
||||||
enableCloseToTray?: boolean;
|
enableCloseToTray?: boolean;
|
||||||
@@ -34,7 +32,6 @@ export class GlobalState {
|
|||||||
enableBrowserIntegration?: boolean;
|
enableBrowserIntegration?: boolean;
|
||||||
enableBrowserIntegrationFingerprint?: boolean;
|
enableBrowserIntegrationFingerprint?: boolean;
|
||||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||||
region?: string;
|
|
||||||
neverDomains?: { [id: string]: unknown };
|
neverDomains?: { [id: string]: unknown };
|
||||||
enablePasskeys?: boolean;
|
enablePasskeys?: boolean;
|
||||||
disableAddLoginNotification?: boolean;
|
disableAddLoginNotification?: boolean;
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
|
||||||
import { firstValueFrom, timeout } from "rxjs";
|
import { firstValueFrom, timeout } from "rxjs";
|
||||||
|
|
||||||
import { awaitAsync } from "../../../spec";
|
import { awaitAsync } from "../../../spec";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeStorageService } from "../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../spec/fake-storage.service";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { Region } from "../abstractions/environment.service";
|
import { Region } from "../abstractions/environment.service";
|
||||||
import { StateFactory } from "../factories/state-factory";
|
import { StateProvider } from "../state";
|
||||||
import { Account } from "../models/domain/account";
|
/* eslint-disable import/no-restricted-paths -- Rare testing need */
|
||||||
import { GlobalState } from "../models/domain/global-state";
|
import { DefaultActiveUserStateProvider } from "../state/implementations/default-active-user-state.provider";
|
||||||
|
import { DefaultDerivedStateProvider } from "../state/implementations/default-derived-state.provider";
|
||||||
|
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
|
||||||
|
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
|
||||||
|
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
|
||||||
|
/* eslint-disable import/no-restricted-paths */
|
||||||
|
|
||||||
import { EnvironmentService } from "./environment.service";
|
import { EnvironmentService } from "./environment.service";
|
||||||
import { StateService } from "./state.service";
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -21,51 +26,55 @@ import { StateService } from "./state.service";
|
|||||||
describe("EnvironmentService", () => {
|
describe("EnvironmentService", () => {
|
||||||
let diskStorageService: FakeStorageService;
|
let diskStorageService: FakeStorageService;
|
||||||
let memoryStorageService: FakeStorageService;
|
let memoryStorageService: FakeStorageService;
|
||||||
let stateService: StateService;
|
let accountService: FakeAccountService;
|
||||||
|
let stateProvider: StateProvider;
|
||||||
|
|
||||||
let sut: EnvironmentService;
|
let sut: EnvironmentService;
|
||||||
|
|
||||||
const testUser = "testUser1" as UserId;
|
const testUser = "00000000-0000-1000-a000-000000000001" as UserId;
|
||||||
|
const alternateTestUser = "00000000-0000-1000-a000-000000000002" as UserId;
|
||||||
|
|
||||||
// START: CAN CHANGE - When implementing new storage locations, the following code can change. But not tests
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
diskStorageService = new FakeStorageService();
|
diskStorageService = new FakeStorageService();
|
||||||
memoryStorageService = new FakeStorageService();
|
memoryStorageService = new FakeStorageService();
|
||||||
stateService = new StateService(
|
|
||||||
diskStorageService,
|
accountService = mockAccountServiceWith(undefined);
|
||||||
null,
|
stateProvider = new DefaultStateProvider(
|
||||||
memoryStorageService as any,
|
new DefaultActiveUserStateProvider(
|
||||||
mock(),
|
accountService,
|
||||||
new StateFactory<GlobalState, Account>(GlobalState, Account),
|
memoryStorageService as any,
|
||||||
mock(),
|
diskStorageService,
|
||||||
false,
|
),
|
||||||
|
new DefaultSingleUserStateProvider(memoryStorageService as any, diskStorageService),
|
||||||
|
new DefaultGlobalStateProvider(memoryStorageService as any, diskStorageService),
|
||||||
|
new DefaultDerivedStateProvider(memoryStorageService),
|
||||||
);
|
);
|
||||||
|
|
||||||
sut = new EnvironmentService(stateService);
|
sut = new EnvironmentService(stateProvider, accountService);
|
||||||
});
|
});
|
||||||
|
|
||||||
const switchUser = async (userId: UserId) => {
|
const switchUser = async (userId: UserId) => {
|
||||||
await stateService.setActiveUser(userId);
|
accountService.activeAccountSubject.next({
|
||||||
|
id: userId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: `Test Name ${userId}`,
|
||||||
|
status: AuthenticationStatus.Unlocked,
|
||||||
|
});
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
|
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
|
||||||
diskStorageService.internalUpdateStore({
|
const data = diskStorageService.internalStore;
|
||||||
...diskStorageService.internalStore,
|
data["global_environment_region"] = region;
|
||||||
global: {
|
data["global_environment_urls"] = environmentUrls;
|
||||||
region: region,
|
diskStorageService.internalUpdateStore(data);
|
||||||
environmentUrls: environmentUrls,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGlobalData = () => {
|
const getGlobalData = () => {
|
||||||
const storage = diskStorageService.internalStore as {
|
const storage = diskStorageService.internalStore;
|
||||||
global?: { region?: Region; environmentUrls?: EnvironmentUrls };
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
region: storage?.global?.region,
|
region: storage?.["global_environment_region"],
|
||||||
urls: storage?.global?.environmentUrls,
|
urls: storage?.["global_environment_urls"],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,22 +83,15 @@ describe("EnvironmentService", () => {
|
|||||||
environmentUrls: EnvironmentUrls,
|
environmentUrls: EnvironmentUrls,
|
||||||
userId: UserId = testUser,
|
userId: UserId = testUser,
|
||||||
) => {
|
) => {
|
||||||
const data = { ...diskStorageService.internalStore };
|
const data = diskStorageService.internalStore;
|
||||||
const userData = {
|
data[`user_${userId}_environment_region`] = region;
|
||||||
settings: {
|
data[`user_${userId}_environment_urls`] = environmentUrls;
|
||||||
region: region,
|
|
||||||
environmentUrls: environmentUrls,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
data[userId] = userData;
|
|
||||||
|
|
||||||
diskStorageService.internalUpdateStore(data);
|
diskStorageService.internalUpdateStore(data);
|
||||||
};
|
};
|
||||||
// END: CAN CHANGE
|
// END: CAN CHANGE
|
||||||
|
|
||||||
const initialize = async (options: { switchUser: boolean }) => {
|
const initialize = async (options: { switchUser: boolean }) => {
|
||||||
// This emulates the way EnvironmentService is initialized in each of our clients
|
|
||||||
await stateService.init();
|
|
||||||
await sut.setUrlsFromStorage();
|
await sut.setUrlsFromStorage();
|
||||||
sut.initialized = true;
|
sut.initialized = true;
|
||||||
|
|
||||||
@@ -292,7 +294,7 @@ describe("EnvironmentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const globalData = getGlobalData();
|
const globalData = getGlobalData();
|
||||||
expect(globalData.region).toBe("Self-hosted");
|
expect(globalData.region).toBe(Region.SelfHosted);
|
||||||
expect(globalData.urls).toEqual({
|
expect(globalData.urls).toEqual({
|
||||||
base: "https://base.example.com",
|
base: "https://base.example.com",
|
||||||
api: null,
|
api: null,
|
||||||
@@ -321,7 +323,7 @@ describe("EnvironmentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const globalData = getGlobalData();
|
const globalData = getGlobalData();
|
||||||
expect(globalData.region).toBe("Self-hosted");
|
expect(globalData.region).toBe(Region.SelfHosted);
|
||||||
expect(globalData.urls).toEqual({
|
expect(globalData.urls).toEqual({
|
||||||
base: "https://base.example.com",
|
base: "https://base.example.com",
|
||||||
api: "https://api.example.com",
|
api: "https://api.example.com",
|
||||||
@@ -399,15 +401,13 @@ describe("EnvironmentService", () => {
|
|||||||
])(
|
])(
|
||||||
"gets it from the passed in userId if there is any active user: %s",
|
"gets it from the passed in userId if there is any active user: %s",
|
||||||
async ({ region, expectedHost }) => {
|
async ({ region, expectedHost }) => {
|
||||||
const otherUser = "testUser2" as UserId;
|
|
||||||
|
|
||||||
setGlobalData(Region.US, new EnvironmentUrls());
|
setGlobalData(Region.US, new EnvironmentUrls());
|
||||||
setUserData(Region.US, new EnvironmentUrls());
|
setUserData(Region.US, new EnvironmentUrls());
|
||||||
setUserData(region, new EnvironmentUrls(), otherUser);
|
setUserData(region, new EnvironmentUrls(), alternateTestUser);
|
||||||
|
|
||||||
await initialize({ switchUser: true });
|
await initialize({ switchUser: true });
|
||||||
|
|
||||||
const host = await sut.getHost(otherUser);
|
const host = await sut.getHost(alternateTestUser);
|
||||||
expect(host).toBe(expectedHost);
|
expect(host).toBe(expectedHost);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -438,17 +438,16 @@ describe("EnvironmentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("gets it from saved self host config from passed in user when there is an active user", async () => {
|
it("gets it from saved self host config from passed in user when there is an active user", async () => {
|
||||||
const otherUser = "testUser2" as UserId;
|
|
||||||
setGlobalData(Region.US, new EnvironmentUrls());
|
setGlobalData(Region.US, new EnvironmentUrls());
|
||||||
setUserData(Region.EU, new EnvironmentUrls());
|
setUserData(Region.EU, new EnvironmentUrls());
|
||||||
|
|
||||||
const selfHostUserUrls = new EnvironmentUrls();
|
const selfHostUserUrls = new EnvironmentUrls();
|
||||||
selfHostUserUrls.base = "https://base.example.com";
|
selfHostUserUrls.base = "https://base.example.com";
|
||||||
setUserData(Region.SelfHosted, selfHostUserUrls, otherUser);
|
setUserData(Region.SelfHosted, selfHostUserUrls, alternateTestUser);
|
||||||
|
|
||||||
await initialize({ switchUser: true });
|
await initialize({ switchUser: true });
|
||||||
|
|
||||||
const host = await sut.getHost(otherUser);
|
const host = await sut.getHost(alternateTestUser);
|
||||||
expect(host).toBe("base.example.com");
|
expect(host).toBe("base.example.com");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -498,7 +497,6 @@ describe("EnvironmentService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will get urls from signed in user", async () => {
|
it("will get urls from signed in user", async () => {
|
||||||
await stateService.init();
|
|
||||||
await switchUser(testUser);
|
await switchUser(testUser);
|
||||||
|
|
||||||
const userUrls = new EnvironmentUrls();
|
const userUrls = new EnvironmentUrls();
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
import { concatMap, distinctUntilChanged, Observable, ReplaySubject } from "rxjs";
|
import {
|
||||||
|
concatMap,
|
||||||
|
distinctUntilChanged,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
ReplaySubject,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import {
|
import {
|
||||||
EnvironmentService as EnvironmentServiceAbstraction,
|
EnvironmentService as EnvironmentServiceAbstraction,
|
||||||
Region,
|
Region,
|
||||||
RegionDomain,
|
RegionDomain,
|
||||||
Urls,
|
Urls,
|
||||||
} from "../abstractions/environment.service";
|
} from "../abstractions/environment.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
|
import { ENVIRONMENT_DISK, GlobalState, KeyDefinition, StateProvider } from "../state";
|
||||||
|
|
||||||
|
const REGION_KEY = new KeyDefinition<Region>(ENVIRONMENT_DISK, "region", {
|
||||||
|
deserializer: (s) => s,
|
||||||
|
});
|
||||||
|
|
||||||
|
const URLS_KEY = new KeyDefinition<EnvironmentUrls>(ENVIRONMENT_DISK, "urls", {
|
||||||
|
deserializer: EnvironmentUrls.fromJSON,
|
||||||
|
});
|
||||||
|
|
||||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||||
private readonly urlsSubject = new ReplaySubject<void>(1);
|
private readonly urlsSubject = new ReplaySubject<void>(1);
|
||||||
@@ -27,6 +44,11 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
private scimUrl: string = null;
|
private scimUrl: string = null;
|
||||||
private cloudWebVaultUrl: string;
|
private cloudWebVaultUrl: string;
|
||||||
|
|
||||||
|
private regionGlobalState: GlobalState<Region | null>;
|
||||||
|
private urlsGlobalState: GlobalState<EnvironmentUrls | null>;
|
||||||
|
|
||||||
|
private activeAccountId$: Observable<UserId | null>;
|
||||||
|
|
||||||
readonly usUrls: Urls = {
|
readonly usUrls: Urls = {
|
||||||
base: null,
|
base: null,
|
||||||
api: "https://api.bitwarden.com",
|
api: "https://api.bitwarden.com",
|
||||||
@@ -49,8 +71,15 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
scim: "https://scim.bitwarden.eu",
|
scim: "https://scim.bitwarden.eu",
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private stateService: StateService) {
|
constructor(
|
||||||
this.stateService.activeAccount$
|
private stateProvider: StateProvider,
|
||||||
|
private accountService: AccountService,
|
||||||
|
) {
|
||||||
|
// We intentionally don't want the helper on account service, we want the null back if there is no active user
|
||||||
|
this.activeAccountId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||||
|
|
||||||
|
// TODO: Get rid of early subscription during EnvironmentService refactor
|
||||||
|
this.activeAccountId$
|
||||||
.pipe(
|
.pipe(
|
||||||
// Use == here to not trigger on undefined -> null transition
|
// Use == here to not trigger on undefined -> null transition
|
||||||
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
|
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
|
||||||
@@ -62,6 +91,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
this.regionGlobalState = this.stateProvider.getGlobal(REGION_KEY);
|
||||||
|
this.urlsGlobalState = this.stateProvider.getGlobal(URLS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBaseUrl() {
|
hasBaseUrl() {
|
||||||
@@ -180,8 +212,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUrlsFromStorage(): Promise<void> {
|
async setUrlsFromStorage(): Promise<void> {
|
||||||
const region = await this.stateService.getRegion();
|
const activeUserId = await firstValueFrom(this.activeAccountId$);
|
||||||
const savedUrls = await this.stateService.getEnvironmentUrls();
|
|
||||||
|
const region = await this.getRegion(activeUserId);
|
||||||
|
const savedUrls = await this.getEnvironmentUrls(activeUserId);
|
||||||
const envUrls = new EnvironmentUrls();
|
const envUrls = new EnvironmentUrls();
|
||||||
|
|
||||||
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
|
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
|
||||||
@@ -231,7 +265,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
// scimUrl cannot be cleared
|
// scimUrl cannot be cleared
|
||||||
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
|
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
|
||||||
|
|
||||||
await this.stateService.setEnvironmentUrls({
|
// Don't save scim url
|
||||||
|
await this.urlsGlobalState.update(() => ({
|
||||||
base: urls.base,
|
base: urls.base,
|
||||||
api: urls.api,
|
api: urls.api,
|
||||||
identity: urls.identity,
|
identity: urls.identity,
|
||||||
@@ -240,8 +275,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
notifications: urls.notifications,
|
notifications: urls.notifications,
|
||||||
events: urls.events,
|
events: urls.events,
|
||||||
keyConnector: urls.keyConnector,
|
keyConnector: urls.keyConnector,
|
||||||
// scimUrl is not saved to storage
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
this.baseUrl = urls.base;
|
this.baseUrl = urls.base;
|
||||||
this.webVaultUrl = urls.webVault;
|
this.webVaultUrl = urls.webVault;
|
||||||
@@ -287,8 +321,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHost(userId?: string) {
|
async getHost(userId?: UserId) {
|
||||||
const region = await this.getRegion(userId ? userId : null);
|
const region = await this.getRegion(userId);
|
||||||
|
|
||||||
switch (region) {
|
switch (region) {
|
||||||
case Region.US:
|
case Region.US:
|
||||||
@@ -297,21 +331,30 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
return RegionDomain.EU;
|
return RegionDomain.EU;
|
||||||
default: {
|
default: {
|
||||||
// Environment is self-hosted
|
// Environment is self-hosted
|
||||||
const envUrls = await this.stateService.getEnvironmentUrls(
|
const envUrls = await this.getEnvironmentUrls(userId);
|
||||||
userId ? { userId: userId } : null,
|
|
||||||
);
|
|
||||||
return Utils.getHost(envUrls.webVault || envUrls.base);
|
return Utils.getHost(envUrls.webVault || envUrls.base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRegion(userId?: string) {
|
private async getRegion(userId: UserId | null) {
|
||||||
return this.stateService.getRegion(userId ? { userId: userId } : null);
|
// Previous rules dictated that we only get from user scoped state if there is an active user.
|
||||||
|
const activeUserId = await firstValueFrom(this.activeAccountId$);
|
||||||
|
return activeUserId == null
|
||||||
|
? await firstValueFrom(this.regionGlobalState.state$)
|
||||||
|
: await firstValueFrom(this.stateProvider.getUser(userId ?? activeUserId, REGION_KEY).state$);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getEnvironmentUrls(userId: UserId | null) {
|
||||||
|
return userId == null
|
||||||
|
? (await firstValueFrom(this.urlsGlobalState.state$)) ?? new EnvironmentUrls()
|
||||||
|
: (await firstValueFrom(this.stateProvider.getUser(userId, URLS_KEY).state$)) ??
|
||||||
|
new EnvironmentUrls();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRegion(region: Region) {
|
async setRegion(region: Region) {
|
||||||
this.selectedRegion = region;
|
this.selectedRegion = region;
|
||||||
await this.stateService.setRegion(region);
|
await this.regionGlobalState.update(() => region);
|
||||||
|
|
||||||
if (region === Region.SelfHosted) {
|
if (region === Region.SelfHosted) {
|
||||||
// If user saves a self-hosted region with empty fields, default to US
|
// If user saves a self-hosted region with empty fields, default to US
|
||||||
@@ -320,7 +363,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we are setting the region to EU or US, clear the self-hosted URLs
|
// If we are setting the region to EU or US, clear the self-hosted URLs
|
||||||
await this.stateService.setEnvironmentUrls(new EnvironmentUrls());
|
await this.urlsGlobalState.update(() => new EnvironmentUrls());
|
||||||
if (region === Region.EU) {
|
if (region === Region.EU) {
|
||||||
this.setUrlsInternal(this.euUrls);
|
this.setUrlsInternal(this.euUrls);
|
||||||
} else if (region === Region.US) {
|
} else if (region === Region.US) {
|
||||||
@@ -329,6 +372,13 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async seedUserEnvironment(userId: UserId) {
|
||||||
|
const globalRegion = await firstValueFrom(this.regionGlobalState.state$);
|
||||||
|
const globalUrls = await firstValueFrom(this.urlsGlobalState.state$);
|
||||||
|
await this.stateProvider.getUser(userId, REGION_KEY).update(() => globalRegion);
|
||||||
|
await this.stateProvider.getUser(userId, URLS_KEY).update(() => globalUrls);
|
||||||
|
}
|
||||||
|
|
||||||
private setUrlsInternal(urls: Urls) {
|
private setUrlsInternal(urls: Urls) {
|
||||||
this.baseUrl = this.formatUrl(urls.base);
|
this.baseUrl = this.formatUrl(urls.base);
|
||||||
this.webVaultUrl = this.formatUrl(urls.webVault);
|
this.webVaultUrl = this.formatUrl(urls.webVault);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Policy } from "../../admin-console/models/domain/policy";
|
|||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
|
||||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
@@ -32,6 +31,7 @@ import { LocalData } from "../../vault/models/data/local.data";
|
|||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||||
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "../abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
@@ -105,6 +105,7 @@ export class StateService<
|
|||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
protected stateFactory: StateFactory<TGlobalState, TAccount>,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
|
protected environmentService: EnvironmentService,
|
||||||
protected useAccountCache: boolean = true,
|
protected useAccountCache: boolean = true,
|
||||||
) {
|
) {
|
||||||
// If the account gets changed, verify the new account is unlocked
|
// If the account gets changed, verify the new account is unlocked
|
||||||
@@ -215,7 +216,7 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addAccount(account: TAccount) {
|
async addAccount(account: TAccount) {
|
||||||
account = await this.setAccountEnvironment(account);
|
await this.environmentService.seedUserEnvironment(account.profile.userId as UserId);
|
||||||
await this.updateState(async (state) => {
|
await this.updateState(async (state) => {
|
||||||
state.authenticatedAccounts.push(account.profile.userId);
|
state.authenticatedAccounts.push(account.profile.userId);
|
||||||
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
|
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
|
||||||
@@ -1983,49 +1984,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
|
||||||
if ((await this.state())?.activeUserId == null) {
|
|
||||||
return await this.getGlobalEnvironmentUrls(options);
|
|
||||||
}
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
|
||||||
return (await this.getAccount(options))?.settings?.environmentUrls ?? new EnvironmentUrls();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEnvironmentUrls(value: EnvironmentUrls, options?: StorageOptions): Promise<void> {
|
|
||||||
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
|
|
||||||
// This is to allow setting environment values before an account is active, while still allowing individual accounts to have their own environments.
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
globals.environmentUrls = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRegion(options?: StorageOptions): Promise<string> {
|
|
||||||
if ((await this.state())?.activeUserId == null) {
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
|
||||||
return (await this.getGlobals(options)).region ?? null;
|
|
||||||
}
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
|
||||||
return (await this.getAccount(options))?.settings?.region ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRegion(value: string, options?: StorageOptions): Promise<void> {
|
|
||||||
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
|
|
||||||
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
globals.region = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
@@ -3032,17 +2990,12 @@ export class StateService<
|
|||||||
await this.defaultOnDiskLocalOptions(),
|
await this.defaultOnDiskLocalOptions(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// EnvironmentUrls and region are set before authenticating and should override whatever is stored from any previous session
|
|
||||||
const environmentUrls = account.settings.environmentUrls;
|
|
||||||
const region = account.settings.region;
|
|
||||||
if (storedAccount?.settings != null) {
|
if (storedAccount?.settings != null) {
|
||||||
account.settings = storedAccount.settings;
|
account.settings = storedAccount.settings;
|
||||||
} else if (await this.storageService.has(keys.tempAccountSettings)) {
|
} else if (await this.storageService.has(keys.tempAccountSettings)) {
|
||||||
account.settings = await this.storageService.get<AccountSettings>(keys.tempAccountSettings);
|
account.settings = await this.storageService.get<AccountSettings>(keys.tempAccountSettings);
|
||||||
await this.storageService.remove(keys.tempAccountSettings);
|
await this.storageService.remove(keys.tempAccountSettings);
|
||||||
}
|
}
|
||||||
account.settings.environmentUrls = environmentUrls;
|
|
||||||
account.settings.region = region;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut &&
|
account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut &&
|
||||||
@@ -3070,8 +3023,6 @@ export class StateService<
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (storedAccount?.settings != null) {
|
if (storedAccount?.settings != null) {
|
||||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
|
||||||
storedAccount.settings.region = account.settings.region;
|
|
||||||
account.settings = storedAccount.settings;
|
account.settings = storedAccount.settings;
|
||||||
}
|
}
|
||||||
await this.storageService.save(
|
await this.storageService.save(
|
||||||
@@ -3093,8 +3044,6 @@ export class StateService<
|
|||||||
this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()),
|
this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()),
|
||||||
);
|
);
|
||||||
if (storedAccount?.settings != null) {
|
if (storedAccount?.settings != null) {
|
||||||
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
|
|
||||||
storedAccount.settings.region = account.settings.region;
|
|
||||||
account.settings = storedAccount.settings;
|
account.settings = storedAccount.settings;
|
||||||
}
|
}
|
||||||
await this.storageService.save(
|
await this.storageService.save(
|
||||||
@@ -3237,23 +3186,6 @@ export class StateService<
|
|||||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The environment urls and region are selected before login and are transferred here to an authenticated account
|
|
||||||
protected async setAccountEnvironment(account: TAccount): Promise<TAccount> {
|
|
||||||
account.settings.region = await this.getGlobalRegion();
|
|
||||||
account.settings.environmentUrls = await this.getGlobalEnvironmentUrls();
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getGlobalEnvironmentUrls(options?: StorageOptions): Promise<EnvironmentUrls> {
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
|
||||||
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getGlobalRegion(options?: StorageOptions): Promise<string> {
|
|
||||||
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
|
|
||||||
return (await this.getGlobals(options)).region ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async clearDecryptedDataForActiveUser(): Promise<void> {
|
protected async clearDecryptedDataForActiveUser(): Promise<void> {
|
||||||
await this.updateState(async (state) => {
|
await this.updateState(async (state) => {
|
||||||
const userId = state?.activeUserId;
|
const userId = state?.activeUserId;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
|||||||
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
||||||
|
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
|
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
|
|
||||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MigrationBuilder } from "./migration-builder";
|
|||||||
import { MigrationHelper } from "./migration-helper";
|
import { MigrationHelper } from "./migration-helper";
|
||||||
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
import { EverHadUserKeyMigrator } from "./migrations/10-move-ever-had-user-key-to-state-providers";
|
||||||
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
import { OrganizationKeyMigrator } from "./migrations/11-move-org-keys-to-state-providers";
|
||||||
|
import { MoveEnvironmentStateToProviders } from "./migrations/12-move-environment-state-to-providers";
|
||||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
@@ -17,7 +18,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 2;
|
export const MIN_VERSION = 2;
|
||||||
export const CURRENT_VERSION = 11;
|
export const CURRENT_VERSION = 12;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export async function migrate(
|
export async function migrate(
|
||||||
@@ -44,7 +45,8 @@ export async function migrate(
|
|||||||
.with(MoveStateVersionMigrator, 7, 8)
|
.with(MoveStateVersionMigrator, 7, 8)
|
||||||
.with(MoveBrowserSettingsToGlobal, 8, 9)
|
.with(MoveBrowserSettingsToGlobal, 8, 9)
|
||||||
.with(EverHadUserKeyMigrator, 9, 10)
|
.with(EverHadUserKeyMigrator, 9, 10)
|
||||||
.with(OrganizationKeyMigrator, 10, CURRENT_VERSION)
|
.with(OrganizationKeyMigrator, 10, 11)
|
||||||
|
.with(MoveEnvironmentStateToProviders, 11, CURRENT_VERSION)
|
||||||
|
|
||||||
.migrate(migrationHelper);
|
.migrate(migrationHelper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||||
|
import { FakeStorageService } from "../../spec/fake-storage.service";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages
|
||||||
import { LogService } from "../platform/abstractions/log.service";
|
import { LogService } from "../platform/abstractions/log.service";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations
|
||||||
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
import { AbstractStorageService } from "../platform/abstractions/storage.service";
|
||||||
|
// eslint-disable-next-line import/no-restricted-paths -- Needed to generate unique strings for injection
|
||||||
|
import { Utils } from "../platform/misc/utils";
|
||||||
|
|
||||||
import { MigrationHelper } from "./migration-helper";
|
import { MigrationHelper } from "./migration-helper";
|
||||||
|
import { Migrator } from "./migrator";
|
||||||
|
|
||||||
const exampleJSON = {
|
const exampleJSON = {
|
||||||
authenticatedAccounts: [
|
authenticatedAccounts: [
|
||||||
@@ -172,3 +177,129 @@ export function mockMigrationHelper(
|
|||||||
mockHelper.getAccounts.mockImplementation(() => helper.getAccounts());
|
mockHelper.getAccounts.mockImplementation(() => helper.getAccounts());
|
||||||
return mockHelper;
|
return mockHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
|
||||||
|
export type InitialDataHint<TUsers extends readonly string[]> = {
|
||||||
|
/**
|
||||||
|
* A string array of the users id who are authenticated
|
||||||
|
*
|
||||||
|
* NOTE: It's recommended to as const this string array so you get type help defining the users data
|
||||||
|
*/
|
||||||
|
authenticatedAccounts?: TUsers;
|
||||||
|
/**
|
||||||
|
* Global data
|
||||||
|
*/
|
||||||
|
global?: unknown;
|
||||||
|
/**
|
||||||
|
* Other top level data
|
||||||
|
*/
|
||||||
|
[key: string]: unknown;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* A users data
|
||||||
|
*/
|
||||||
|
[userData in TUsers[number]]?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InjectedData = {
|
||||||
|
propertyName: string;
|
||||||
|
propertyValue: string;
|
||||||
|
originalPath: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is a slight lie, technically the type is `Record<string | symbol, unknown>
|
||||||
|
// but for the purposes of things in the migrations this is enough.
|
||||||
|
function isStringRecord(object: unknown | undefined): object is Record<string, unknown> {
|
||||||
|
return object && typeof object === "object" && !Array.isArray(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectData(data: Record<string, unknown>, path: string[]): InjectedData[] {
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectedData: InjectedData[] = [];
|
||||||
|
|
||||||
|
// Traverse keys for other objects
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
for (const key of keys) {
|
||||||
|
const currentProperty = data[key];
|
||||||
|
if (isStringRecord(currentProperty)) {
|
||||||
|
injectedData.push(...injectData(currentProperty, [...path, key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyName = `__injectedProperty__${Utils.newGuid()}`;
|
||||||
|
const propertyValue = `__injectedValue__${Utils.newGuid()}`;
|
||||||
|
|
||||||
|
injectedData.push({
|
||||||
|
propertyName: propertyName,
|
||||||
|
propertyValue: propertyValue,
|
||||||
|
// Track the path it was originally injected in just for a better error
|
||||||
|
originalPath: path,
|
||||||
|
});
|
||||||
|
data[propertyName] = propertyValue;
|
||||||
|
return injectedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectInjectedData(
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
injectedData: InjectedData[],
|
||||||
|
): [data: Record<string, unknown>, leftoverInjectedData: InjectedData[]] {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
for (const key of keys) {
|
||||||
|
const propertyValue = data[key];
|
||||||
|
// Injected data does not have to be found exactly where it was injected,
|
||||||
|
// just that it exists at all.
|
||||||
|
const injectedIndex = injectedData.findIndex(
|
||||||
|
(d) =>
|
||||||
|
d.propertyName === key &&
|
||||||
|
typeof propertyValue === "string" &&
|
||||||
|
propertyValue === d.propertyValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (injectedIndex !== -1) {
|
||||||
|
// We found something we injected, remove it
|
||||||
|
injectedData.splice(injectedIndex, 1);
|
||||||
|
delete data[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStringRecord(propertyValue)) {
|
||||||
|
const [updatedData, leftoverInjectedData] = expectInjectedData(propertyValue, injectedData);
|
||||||
|
data[key] = updatedData;
|
||||||
|
injectedData = leftoverInjectedData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [data, injectedData];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the {@link Migrator.migrate} method of your migrator. You may pass in your test data and get back the data after the migration.
|
||||||
|
* This also injects extra properties at every level of your state and makes sure that it can be found.
|
||||||
|
* @param migrator Your migrator to use to do the migration
|
||||||
|
* @param initalData The data to start with
|
||||||
|
* @returns State after your migration has ran.
|
||||||
|
*/
|
||||||
|
// TODO: Use const generic for TUsers in TypeScript 5.0 so consumers don't have to `as const` themselves
|
||||||
|
export async function runMigrator<
|
||||||
|
TMigrator extends Migrator<number, number>,
|
||||||
|
TUsers extends readonly string[] = string[],
|
||||||
|
>(migrator: TMigrator, initalData?: InitialDataHint<TUsers>): Promise<Record<string, unknown>> {
|
||||||
|
// Inject fake data at every level of the object
|
||||||
|
const allInjectedData = injectData(initalData, []);
|
||||||
|
|
||||||
|
const fakeStorageService = new FakeStorageService(initalData);
|
||||||
|
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
|
||||||
|
|
||||||
|
// Run their migrations
|
||||||
|
await migrator.migrate(helper);
|
||||||
|
const [data, leftoverInjectedData] = expectInjectedData(
|
||||||
|
fakeStorageService.internalStore,
|
||||||
|
allInjectedData,
|
||||||
|
);
|
||||||
|
expect(leftoverInjectedData).toHaveLength(0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { runMigrator } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { MoveEnvironmentStateToProviders } from "./12-move-environment-state-to-providers";
|
||||||
|
|
||||||
|
describe("MoveEnvironmentStateToProviders", () => {
|
||||||
|
const migrator = new MoveEnvironmentStateToProviders(11, 12);
|
||||||
|
|
||||||
|
it("can migrate all data", async () => {
|
||||||
|
const output = await runMigrator(migrator, {
|
||||||
|
authenticatedAccounts: ["user1", "user2"] as const,
|
||||||
|
global: {
|
||||||
|
region: "US",
|
||||||
|
environmentUrls: {
|
||||||
|
base: "example.com",
|
||||||
|
},
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
user1: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
extra: "data",
|
||||||
|
region: "US",
|
||||||
|
environmentUrls: {
|
||||||
|
base: "example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
region: "EU",
|
||||||
|
environmentUrls: {
|
||||||
|
base: "other.example.com",
|
||||||
|
},
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extra: "data",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
|
global: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
global_environment_region: "US",
|
||||||
|
global_environment_urls: {
|
||||||
|
base: "example.com",
|
||||||
|
},
|
||||||
|
user1: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extra: "data",
|
||||||
|
user_user1_environment_region: "US",
|
||||||
|
user_user2_environment_region: "EU",
|
||||||
|
user_user1_environment_urls: {
|
||||||
|
base: "example.com",
|
||||||
|
},
|
||||||
|
user_user2_environment_urls: {
|
||||||
|
base: "other.example.com",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing parts", async () => {
|
||||||
|
const output = await runMigrator(migrator, {
|
||||||
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
|
global: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
user1: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user2: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
|
global: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
user1: {
|
||||||
|
extra: "data",
|
||||||
|
settings: {
|
||||||
|
extra: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user2: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can migrate only global data", async () => {
|
||||||
|
const output = await runMigrator(migrator, {
|
||||||
|
authenticatedAccounts: [] as const,
|
||||||
|
global: {
|
||||||
|
region: "Self-Hosted",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
authenticatedAccounts: [],
|
||||||
|
global_environment_region: "Self-Hosted",
|
||||||
|
global: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can migrate only user state", async () => {
|
||||||
|
const output = await runMigrator(migrator, {
|
||||||
|
authenticatedAccounts: ["user1"] as const,
|
||||||
|
global: null,
|
||||||
|
user1: {
|
||||||
|
settings: {
|
||||||
|
region: "Self-Hosted",
|
||||||
|
environmentUrls: {
|
||||||
|
base: "some-base-url",
|
||||||
|
api: "some-api-url",
|
||||||
|
identity: "some-identity-url",
|
||||||
|
icons: "some-icons-url",
|
||||||
|
notifications: "some-notifications-url",
|
||||||
|
events: "some-events-url",
|
||||||
|
webVault: "some-webVault-url",
|
||||||
|
keyConnector: "some-keyConnector-url",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output).toEqual({
|
||||||
|
authenticatedAccounts: ["user1"] as const,
|
||||||
|
global: null,
|
||||||
|
user1: { settings: {} },
|
||||||
|
user_user1_environment_region: "Self-Hosted",
|
||||||
|
user_user1_environment_urls: {
|
||||||
|
base: "some-base-url",
|
||||||
|
api: "some-api-url",
|
||||||
|
identity: "some-identity-url",
|
||||||
|
icons: "some-icons-url",
|
||||||
|
notifications: "some-notifications-url",
|
||||||
|
events: "some-events-url",
|
||||||
|
webVault: "some-webVault-url",
|
||||||
|
keyConnector: "some-keyConnector-url",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type EnvironmentUrls = Record<string, string>;
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
settings?: { region?: string; environmentUrls?: EnvironmentUrls };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpectedGlobalType = { region?: string; environmentUrls?: EnvironmentUrls };
|
||||||
|
|
||||||
|
const ENVIRONMENT_STATE: StateDefinitionLike = { name: "environment" };
|
||||||
|
|
||||||
|
const REGION_KEY: KeyDefinitionLike = { key: "region", stateDefinition: ENVIRONMENT_STATE };
|
||||||
|
const URLS_KEY: KeyDefinitionLike = { key: "urls", stateDefinition: ENVIRONMENT_STATE };
|
||||||
|
|
||||||
|
export class MoveEnvironmentStateToProviders extends Migrator<11, 12> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||||
|
|
||||||
|
// Move global data
|
||||||
|
if (legacyGlobal?.region != null) {
|
||||||
|
await helper.setToGlobal(REGION_KEY, legacyGlobal.region);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyGlobal?.environmentUrls != null) {
|
||||||
|
await helper.setToGlobal(URLS_KEY, legacyGlobal.environmentUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyAccounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
legacyAccounts.map(async ({ userId, account }) => {
|
||||||
|
// Move account data
|
||||||
|
if (account?.settings?.region != null) {
|
||||||
|
await helper.setToUser(userId, REGION_KEY, account.settings.region);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account?.settings?.environmentUrls != null) {
|
||||||
|
await helper.setToUser(userId, URLS_KEY, account.settings.environmentUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old account data
|
||||||
|
delete account?.settings?.region;
|
||||||
|
delete account?.settings?.environmentUrls;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete legacy global data
|
||||||
|
delete legacyGlobal?.region;
|
||||||
|
delete legacyGlobal?.environmentUrls;
|
||||||
|
await helper.set("global", legacyGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
||||||
|
|
||||||
|
let updatedLegacyGlobal = false;
|
||||||
|
|
||||||
|
const globalRegion = await helper.getFromGlobal<string>(REGION_KEY);
|
||||||
|
|
||||||
|
if (globalRegion) {
|
||||||
|
if (!legacyGlobal) {
|
||||||
|
legacyGlobal = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedLegacyGlobal = true;
|
||||||
|
legacyGlobal.region = globalRegion;
|
||||||
|
await helper.setToGlobal(REGION_KEY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalUrls = await helper.getFromGlobal<EnvironmentUrls>(URLS_KEY);
|
||||||
|
|
||||||
|
if (globalUrls) {
|
||||||
|
if (!legacyGlobal) {
|
||||||
|
legacyGlobal = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedLegacyGlobal = true;
|
||||||
|
legacyGlobal.environmentUrls = globalUrls;
|
||||||
|
await helper.setToGlobal(URLS_KEY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedLegacyGlobal) {
|
||||||
|
await helper.set("global", legacyGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollbackUser(userId: string, account: ExpectedAccountType) {
|
||||||
|
let updatedAccount = false;
|
||||||
|
const userRegion = await helper.getFromUser<string>(userId, REGION_KEY);
|
||||||
|
|
||||||
|
if (userRegion) {
|
||||||
|
if (!account) {
|
||||||
|
account = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account.settings) {
|
||||||
|
account.settings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAccount = true;
|
||||||
|
account.settings.region = userRegion;
|
||||||
|
await helper.setToUser(userId, REGION_KEY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userUrls = await helper.getFromUser<EnvironmentUrls>(userId, URLS_KEY);
|
||||||
|
|
||||||
|
if (userUrls) {
|
||||||
|
if (!account) {
|
||||||
|
account = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account.settings) {
|
||||||
|
account.settings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAccount = true;
|
||||||
|
account.settings.environmentUrls = userUrls;
|
||||||
|
await helper.setToUser(userId, URLS_KEY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAccount) {
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,14 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { runMigrator } from "../migration-helper.spec";
|
||||||
|
|
||||||
import { FakeStorageService } from "../../../spec/fake-storage.service";
|
|
||||||
import { MigrationHelper } from "../migration-helper";
|
|
||||||
import { Migrator } from "../migrator";
|
|
||||||
|
|
||||||
import { MoveBrowserSettingsToGlobal } from "./9-move-browser-settings-to-global";
|
import { MoveBrowserSettingsToGlobal } from "./9-move-browser-settings-to-global";
|
||||||
|
|
||||||
type TestState = { authenticatedAccounts: string[] } & { [key: string]: unknown };
|
|
||||||
|
|
||||||
// This could become a helper available to anyone
|
|
||||||
const runMigrator = async <TMigrator extends Migrator<number, number>>(
|
|
||||||
migrator: TMigrator,
|
|
||||||
initalData?: Record<string, unknown>,
|
|
||||||
): Promise<Record<string, unknown>> => {
|
|
||||||
const fakeStorageService = new FakeStorageService(initalData);
|
|
||||||
const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock());
|
|
||||||
await migrator.migrate(helper);
|
|
||||||
return fakeStorageService.internalStore;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("MoveBrowserSettingsToGlobal", () => {
|
describe("MoveBrowserSettingsToGlobal", () => {
|
||||||
const myMigrator = new MoveBrowserSettingsToGlobal(8, 9);
|
const myMigrator = new MoveBrowserSettingsToGlobal(8, 9);
|
||||||
|
|
||||||
// This could be the state for a browser client who has never touched the settings or this could
|
// This could be the state for a browser client who has never touched the settings or this could
|
||||||
// be a different client who doesn't make it possible to toggle these settings
|
// be a different client who doesn't make it possible to toggle these settings
|
||||||
it("doesn't set any value to global if there is no equivalent settings on the account", async () => {
|
it("doesn't set any value to global if there is no equivalent settings on the account", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1"],
|
authenticatedAccounts: ["user1"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -35,9 +18,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// No additions to the global state
|
// No additions to the global state
|
||||||
expect(output["global"]).toEqual({
|
expect(output["global"]).toEqual({
|
||||||
@@ -55,7 +36,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
// This could be a user who opened up the settings page and toggled the checkbox, since this setting infers undefined
|
// This could be a user who opened up the settings page and toggled the checkbox, since this setting infers undefined
|
||||||
// as false this is essentially the default value.
|
// as false this is essentially the default value.
|
||||||
it("sets the setting from the users settings if they have toggled the setting but placed it back to it's inferred", async () => {
|
it("sets the setting from the users settings if they have toggled the setting but placed it back to it's inferred", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1"],
|
authenticatedAccounts: ["user1"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -71,9 +52,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// User settings should have moved to global
|
// User settings should have moved to global
|
||||||
expect(output["global"]).toEqual({
|
expect(output["global"]).toEqual({
|
||||||
@@ -94,7 +73,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
|
|
||||||
// The user has set a value and it's not the default, we should respect that choice globally
|
// The user has set a value and it's not the default, we should respect that choice globally
|
||||||
it("should take the only users settings", async () => {
|
it("should take the only users settings", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1"],
|
authenticatedAccounts: ["user1"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -110,9 +89,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// The value for the single user value should be set to global
|
// The value for the single user value should be set to global
|
||||||
expect(output["global"]).toEqual({
|
expect(output["global"]).toEqual({
|
||||||
@@ -134,7 +111,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
// but in the bizzare case, we should interpret any user having the feature turned on as the value for
|
// but in the bizzare case, we should interpret any user having the feature turned on as the value for
|
||||||
// all the accounts.
|
// all the accounts.
|
||||||
it("should take the false value if there are conflicting choices", async () => {
|
it("should take the false value if there are conflicting choices", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1", "user2"],
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -161,9 +138,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// The false settings should be respected over the true values
|
// The false settings should be respected over the true values
|
||||||
// neverDomains should be combined into a single object
|
// neverDomains should be combined into a single object
|
||||||
@@ -191,7 +166,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
// if one user has toggled the setting back to on and one user has never touched the setting,
|
// if one user has toggled the setting back to on and one user has never touched the setting,
|
||||||
// persist the false value into the global state.
|
// persist the false value into the global state.
|
||||||
it("should persist the false value if one user has that in their settings", async () => {
|
it("should persist the false value if one user has that in their settings", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1", "user2"],
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -212,9 +187,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// The false settings should be respected over the true values
|
// The false settings should be respected over the true values
|
||||||
// neverDomains should be combined into a single object
|
// neverDomains should be combined into a single object
|
||||||
@@ -241,7 +214,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
// if one user has toggled the setting off and one user has never touched the setting,
|
// if one user has toggled the setting off and one user has never touched the setting,
|
||||||
// persist the false value into the global state.
|
// persist the false value into the global state.
|
||||||
it("should persist the false value from a user with no settings since undefined is inferred as false", async () => {
|
it("should persist the false value from a user with no settings since undefined is inferred as false", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1", "user2"],
|
authenticatedAccounts: ["user1", "user2"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -262,9 +235,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// The false settings should be respected over the true values
|
// The false settings should be respected over the true values
|
||||||
// neverDomains should be combined into a single object
|
// neverDomains should be combined into a single object
|
||||||
@@ -292,7 +263,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
// id of the non-current account isn't saved to the authenticatedAccounts array so we don't have a great way to
|
// id of the non-current account isn't saved to the authenticatedAccounts array so we don't have a great way to
|
||||||
// get the state and include it in our calculations for what the global state should be.
|
// get the state and include it in our calculations for what the global state should be.
|
||||||
it("only cares about users defined in authenticatedAccounts", async () => {
|
it("only cares about users defined in authenticatedAccounts", async () => {
|
||||||
const testInput: TestState = {
|
const output = await runMigrator(myMigrator, {
|
||||||
authenticatedAccounts: ["user1"],
|
authenticatedAccounts: ["user1"],
|
||||||
global: {
|
global: {
|
||||||
theme: "system", // A real global setting that should persist after migration
|
theme: "system", // A real global setting that should persist after migration
|
||||||
@@ -319,9 +290,7 @@ describe("MoveBrowserSettingsToGlobal", () => {
|
|||||||
region: "Self-hosted",
|
region: "Self-hosted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const output = await runMigrator(myMigrator, testInput);
|
|
||||||
|
|
||||||
// The true settings should be respected over the false values because that whole users values
|
// The true settings should be respected over the false values because that whole users values
|
||||||
// shouldn't be respected.
|
// shouldn't be respected.
|
||||||
|
|||||||
Reference in New Issue
Block a user