diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts
index de5ee51a7ae..bb7a99cca02 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 bd26ed94363..84608acff0f 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 ed3e000624f..6260d34c1d1 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 e4d6a550014..68327f4fd63 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 a36176395de..e5b4e17fb8d 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 4295ba91331..a8cea255c17 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 13a296883b4..dfe3c6c417f 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 02e0fcfab23..b2f62a22b19 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 899bff14ba4..dce5d76440a 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()))