From 1052f00b87bdcb6dda41d6eb53e4dbc431992608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Fri, 16 Jun 2023 14:09:16 +0100 Subject: [PATCH] [PM-2475][PM-2536] Clicking "US" in region selector sets base URL (#5604) --- .../desktop/src/auth/login/login.component.ts | 1 + .../layouts/frontend-layout.component.html | 2 +- .../app/layouts/frontend-layout.component.ts | 4 +- .../environment-selector.component.ts | 41 +++--- .../src/components/environment.component.ts | 8 +- .../abstractions/environment.service.ts | 11 ++ .../platform/abstractions/state.service.ts | 2 + .../src/platform/models/domain/account.ts | 1 + .../platform/models/domain/global-state.ts | 1 + .../platform/services/environment.service.ts | 125 ++++++++++++++++-- .../src/platform/services/state.service.ts | 22 +++ 11 files changed, 177 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index de5ee51a7a..bb7a99cca0 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -151,6 +151,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { // eslint-disable-next-line rxjs/no-async-subscribe childComponent.onSaved.pipe(takeUntil(this.componentDestroyed$)).subscribe(async () => { modal.close(); + this.environmentSelector.updateEnvironmentInfo(); await this.getLoginWithDevice(this.loggedEmail); }); } diff --git a/apps/web/src/app/layouts/frontend-layout.component.html b/apps/web/src/app/layouts/frontend-layout.component.html index bd26ed9436..84608acff0 100644 --- a/apps/web/src/app/layouts/frontend-layout.component.html +++ b/apps/web/src/app/layouts/frontend-layout.component.html @@ -1,6 +1,6 @@
-
+
= new Subject(); constructor( - protected environmentService: EnvironmentService, + protected environmentService: EnvironmentServiceAbstraction, protected configService: ConfigServiceAbstraction, protected router: Router ) {} @@ -67,18 +70,20 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { this.componentDestroyed$.complete(); } - async toggle(option: ServerEnvironment) { + async toggle(option: Region) { this.isOpen = !this.isOpen; if (option === null) { return; } - if (option === ServerEnvironment.EU) { - await this.environmentService.setUrls({ base: "https://vault.bitwarden.eu" }); - } else if (option === ServerEnvironment.US) { - await this.environmentService.setUrls({ base: "https://vault.bitwarden.com" }); - } else if (option === ServerEnvironment.SelfHosted) { + + this.updateEnvironmentInfo(); + + if (option === Region.SelfHosted) { this.onOpenSelfHostedSettings.emit(); + return; } + + await this.environmentService.setRegion(option); this.updateEnvironmentInfo(); } @@ -86,14 +91,8 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { this.euServerFlagEnabled = await this.configService.getFeatureFlagBool( FeatureFlag.DisplayEuEnvironmentFlag ); - const webvaultUrl = this.environmentService.getWebVaultUrl(); - if (this.environmentService.isSelfHosted()) { - this.selectedEnvironment = ServerEnvironment.SelfHosted; - } else if (webvaultUrl != null && webvaultUrl.includes("bitwarden.eu")) { - this.selectedEnvironment = ServerEnvironment.EU; - } else { - this.selectedEnvironment = ServerEnvironment.US; - } + + this.selectedEnvironment = this.environmentService.selectedRegion; } close() { @@ -101,9 +100,3 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { this.updateEnvironmentInfo(); } } - -enum ServerEnvironment { - US = "US", - EU = "EU", - SelfHosted = "Self-hosted", -} diff --git a/libs/angular/src/components/environment.component.ts b/libs/angular/src/components/environment.component.ts index ed3e000624..6260d34c1d 100644 --- a/libs/angular/src/components/environment.component.ts +++ b/libs/angular/src/components/environment.component.ts @@ -1,6 +1,9 @@ import { Directive, EventEmitter, Output } from "@angular/core"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -25,6 +28,9 @@ export class EnvironmentComponent { private modalService: ModalService ) { const urls = this.environmentService.getUrls(); + if (this.environmentService.selectedRegion != Region.SelfHosted) { + return; + } this.baseUrl = urls.base || ""; this.webVaultUrl = urls.webVault || ""; diff --git a/libs/common/src/platform/abstractions/environment.service.ts b/libs/common/src/platform/abstractions/environment.service.ts index e4d6a55001..68327f4fd6 100644 --- a/libs/common/src/platform/abstractions/environment.service.ts +++ b/libs/common/src/platform/abstractions/environment.service.ts @@ -17,8 +17,17 @@ export type PayPalConfig = { buttonAction?: string; }; +export enum Region { + US = "US", + EU = "EU", + SelfHosted = "Self-hosted", +} + export abstract class EnvironmentService { urls: Observable; + usUrls: Urls; + euUrls: Urls; + selectedRegion?: Region; hasBaseUrl: () => boolean; getNotificationsUrl: () => string; @@ -32,8 +41,10 @@ export abstract class EnvironmentService { getScimUrl: () => string; setUrlsFromStorage: () => Promise; setUrls: (urls: Urls) => Promise; + setRegion: (region: Region) => Promise; getUrls: () => Urls; isCloud: () => boolean; + isEmpty: () => boolean; /** * @remarks For desktop and browser use only. * For web, use PlatformUtilsService.isSelfHost() diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index a36176395d..e5b4e17fb8 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -263,6 +263,8 @@ export abstract class StateService { setEntityType: (value: string, options?: StorageOptions) => Promise; getEnvironmentUrls: (options?: StorageOptions) => Promise; setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise; + getRegion: (options?: StorageOptions) => Promise; + setRegion: (value: string, options?: StorageOptions) => Promise; getEquivalentDomains: (options?: StorageOptions) => Promise; setEquivalentDomains: (value: string, options?: StorageOptions) => Promise; getEventCollection: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 4295ba9133..a8cea255c1 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -233,6 +233,7 @@ export class AccountSettings { approveLoginRequests?: boolean; avatarColor?: string; activateAutoFillOnPageLoadFromPolicy?: boolean; + region?: string; smOnboardingTasks?: Record>; static fromJSON(obj: Jsonify): AccountSettings { diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 13a296883b..dfe3c6c417 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -36,4 +36,5 @@ export class GlobalState { enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; + region?: string; } diff --git a/libs/common/src/platform/services/environment.service.ts b/libs/common/src/platform/services/environment.service.ts index 02e0fcfab2..b2f62a22b1 100644 --- a/libs/common/src/platform/services/environment.service.ts +++ b/libs/common/src/platform/services/environment.service.ts @@ -3,6 +3,7 @@ import { concatMap, Observable, Subject } from "rxjs"; import { EnvironmentUrls } from "../../auth/models/domain/environment-urls"; import { EnvironmentService as EnvironmentServiceAbstraction, + Region, Urls, } from "../abstractions/environment.service"; import { StateService } from "../abstractions/state.service"; @@ -10,6 +11,7 @@ import { StateService } from "../abstractions/state.service"; export class EnvironmentService implements EnvironmentServiceAbstraction { private readonly urlsSubject = new Subject(); urls: Observable = this.urlsSubject.asObservable(); + selectedRegion?: Region; protected baseUrl: string; protected webVaultUrl: string; @@ -21,6 +23,28 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { private keyConnectorUrl: string; private scimUrl: string = null; + readonly usUrls: Urls = { + base: null, + api: "https://api.bitwarden.com", + identity: "https://identity.bitwarden.com", + icons: "https://icons.bitwarden.net", + webVault: "https://vault.bitwarden.com", + notifications: "https://notifications.bitwarden.com", + events: "https://events.bitwarden.com", + scim: "https://scim.bitwarden.com/v2", + }; + + readonly euUrls: Urls = { + base: null, + api: "https://api.bitwarden.eu", + identity: "https://identity.bitwarden.eu", + icons: "https://icons.bitwarden.eu", + webVault: "https://vault.bitwarden.eu", + notifications: "https://notifications.bitwarden.eu", + events: "https://events.bitwarden.eu", + scim: "https://scim.bitwarden.eu/v2", + }; + constructor(private stateService: StateService) { this.stateService.activeAccount$ .pipe( @@ -127,20 +151,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } async setUrlsFromStorage(): Promise { - const urls: any = await this.stateService.getEnvironmentUrls(); + const region = await this.stateService.getRegion(); + const savedUrls = await this.stateService.getEnvironmentUrls(); const envUrls = new EnvironmentUrls(); - this.baseUrl = envUrls.base = urls.base; - this.webVaultUrl = urls.webVault; - this.apiUrl = envUrls.api = urls.api; - this.identityUrl = envUrls.identity = urls.identity; - this.iconsUrl = urls.icons; - this.notificationsUrl = urls.notifications; - this.eventsUrl = envUrls.events = urls.events; - this.keyConnectorUrl = urls.keyConnector; - // scimUrl is not saved to storage + // fix environment urls for old users + if (savedUrls.base === "https://vault.bitwarden.com") { + this.setRegion(Region.US); + return; + } + if (savedUrls.base === "https://vault.bitwarden.eu") { + this.setRegion(Region.EU); + return; + } - this.urlsSubject.next(); + switch (region) { + case Region.EU: + this.setRegion(Region.EU); + return; + case Region.US: + this.setRegion(Region.US); + return; + case Region.SelfHosted: + default: + this.baseUrl = envUrls.base = savedUrls.base; + this.webVaultUrl = savedUrls.webVault; + this.apiUrl = envUrls.api = savedUrls.api; + this.identityUrl = envUrls.identity = savedUrls.identity; + this.iconsUrl = savedUrls.icons; + this.notificationsUrl = savedUrls.notifications; + this.eventsUrl = envUrls.events = savedUrls.events; + this.keyConnectorUrl = savedUrls.keyConnector; + // scimUrl is not saved to storage + this.urlsSubject.next(); + this.setRegion(Region.SelfHosted); + break; + } } async setUrls(urls: Urls): Promise { @@ -178,6 +224,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.keyConnectorUrl = urls.keyConnector; this.scimUrl = urls.scim; + await this.setRegion(Region.SelfHosted); + this.urlsSubject.next(); return urls; @@ -197,6 +245,52 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { }; } + isEmpty(): boolean { + return ( + this.baseUrl == null && + this.webVaultUrl == null && + this.apiUrl == null && + this.identityUrl == null && + this.iconsUrl == null && + this.notificationsUrl == null && + this.eventsUrl == null + ); + } + + async setRegion(region: Region) { + this.selectedRegion = region; + await this.stateService.setRegion(region); + switch (region) { + case Region.EU: + this.setUrlsInternal(this.euUrls); + break; + case Region.US: + this.setUrlsInternal(this.usUrls); + break; + case Region.SelfHosted: + // if user saves with empty fields, default to US + if (this.isEmpty()) { + this.setRegion(Region.US); + } + break; + } + } + + private setUrlsInternal(urls: Urls) { + this.baseUrl = this.formatUrl(urls.base); + this.webVaultUrl = this.formatUrl(urls.webVault); + this.apiUrl = this.formatUrl(urls.api); + this.identityUrl = this.formatUrl(urls.identity); + this.iconsUrl = this.formatUrl(urls.icons); + this.notificationsUrl = this.formatUrl(urls.notifications); + this.eventsUrl = this.formatUrl(urls.events); + this.keyConnectorUrl = this.formatUrl(urls.keyConnector); + + // scimUrl cannot be cleared + this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl; + this.urlsSubject.next(); + } + private formatUrl(url: string): string { if (url == null || url === "") { return null; @@ -211,9 +305,12 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } isCloud(): boolean { - return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes( - this.getApiUrl() - ); + return [ + "https://api.bitwarden.com", + "https://vault.bitwarden.com/api", + "https://api.bitwarden.eu", + "https://vault.bitwarden.eu/api", + ].includes(this.getApiUrl()); } isSelfHosted(): boolean { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 899bff14ba..dce5d76440 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1598,6 +1598,28 @@ export class StateService< ); } + async getRegion(options?: StorageOptions): Promise { + 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 { + // 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 { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))