diff --git a/.eslintrc.json b/.eslintrc.json index f21e2b0872..671e7b2fab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -67,7 +67,7 @@ "pathGroupsExcludedImportTypes": ["builtin"] } ], - "rxjs-angular/prefer-takeuntil": "error", + "rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }], "rxjs/no-exposed-subjects": ["error", { "allowProtected": true }], "no-restricted-syntax": [ "error", diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cf0ce67980..0defc8aa7c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2386,12 +2386,6 @@ "message": "EU", "description": "European Union" }, - "usDomain": { - "message": "bitwarden.com" - }, - "euDomain": { - "message": "bitwarden.eu" - }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." }, diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index cf78f2ff91..32ebee7c75 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -65,7 +65,7 @@ export class AccountSwitcherService { name: account.name ?? account.email, email: account.email, id: id, - server: await this.environmentService.getHost(id), + server: (await this.environmentService.getEnvironment(id))?.getHostname(), status: account.status, isActive: id === activeAccount?.id, avatarColor: await firstValueFrom( diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index b5598f13f7..1360e6c8a6 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -94,10 +94,6 @@ export class HomeComponent implements OnInit, OnDestroy { this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); } - get selfHostedDomain() { - return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null; - } - setFormValues() { this.loginService.setEmail(this.formGroup.value.email); this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index c1dd952658..5c302455e6 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -1,6 +1,7 @@ import { Component, NgZone } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; @@ -114,7 +115,8 @@ export class LoginComponent extends BaseLoginComponent { await this.ssoLoginService.setCodeVerifier(codeVerifier); await this.ssoLoginService.setSsoState(state); - let url = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + let url = env.getWebVaultUrl(); if (url == null) { url = "https://vault.bitwarden.com"; } diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 7b61a04bfd..430bd855f1 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -1,4 +1,5 @@ import { Component, Inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; @@ -64,9 +65,9 @@ export class SsoComponent extends BaseSsoComponent { configService, ); - const url = this.environmentService.getWebVaultUrl(); - - this.redirectUri = url + "/sso-connector.html"; + environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { + this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; + }); this.clientId = "browser"; super.onSuccessfulLogin = async () => { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 0a950d6c1b..da2c3482fd 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -1,6 +1,6 @@ import { Component, Inject } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, Subscription } from "rxjs"; +import { Subject, Subscription, firstValueFrom } from "rxjs"; import { filter, first, takeUntil } from "rxjs/operators"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; @@ -225,7 +225,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } } - override launchDuoFrameless() { + override async launchDuoFrameless() { const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), @@ -234,8 +234,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { // we're using the connector here as a way to set a cookie with translations // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); const launchUrl = - this.environmentService.getWebVaultUrl() + + env.getWebVaultUrl() + "/duo-redirect-connector.html" + "?duoFramelessUrl=" + encodeURIComponent(this.duoFramelessUrl) + diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ab704a1d37..ac40bb315b 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -113,7 +113,7 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetEnableChangedPasswordPrompt: () => Promise; bgGetEnableAddedLoginPrompt: () => Promise; bgGetExcludedDomains: () => Promise; - getWebVaultUrlForNotification: () => string; + getWebVaultUrlForNotification: () => Promise; }; export { diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 0d8d52df30..3b05cf57a9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,13 +1,14 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; +import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -1348,16 +1349,21 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "getWebVaultUrlForNotification", }; - const webVaultUrl = "https://example.com"; + const env = new SelfHostedEnvironment({ webVault: "https://example.com" }); + + Object.defineProperty(environmentService, "environment$", { + configurable: true, + get: () => null, + }); + const environmentServiceSpy = jest - .spyOn(environmentService, "getWebVaultUrl") - .mockReturnValueOnce(webVaultUrl); + .spyOn(environmentService as any, "environment$", "get") + .mockReturnValue(new BehaviorSubject(env).asObservable()); sendExtensionRuntimeMessage(message); await flushPromises(); expect(environmentServiceSpy).toHaveBeenCalled(); - expect(environmentServiceSpy).toHaveReturnedWith(webVaultUrl); }); }); }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index ddabb01158..c14531ee74 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -165,6 +165,7 @@ export default class NotificationBackground { notificationQueueMessage: NotificationQueueMessageItem, ) { const notificationType = notificationQueueMessage.type; + const typeData: Record = { isVaultLocked: notificationQueueMessage.wasVaultLocked, theme: await firstValueFrom(this.themeStateService.selectedTheme$), @@ -655,8 +656,9 @@ export default class NotificationBackground { return await firstValueFrom(this.folderService.folderViews$); } - private getWebVaultUrl(): string { - return this.environmentService.getWebVaultUrl(); + private async getWebVaultUrl(): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + return env.getWebVaultUrl(); } private async removeIndividualVault(): Promise { diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index ebad626d8d..56cacb37e5 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,5 +1,5 @@ import { mock, mockReset } from "jest-mock-extended"; -import { of } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; @@ -12,9 +12,13 @@ import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; +import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { @@ -48,8 +52,6 @@ import { import OverlayBackground from "./overlay.background"; -const iconServerUrl = "https://icons.bitwarden.com/"; - describe("OverlayBackground", () => { const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -61,9 +63,15 @@ describe("OverlayBackground", () => { const cipherService = mock(); const autofillService = mock(); const authService = mock(); - const environmentService = mock({ - getIconsUrl: () => iconServerUrl, - }); + + const environmentService = mock(); + environmentService.environment$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); const stateService = mock(); const autofillSettingsService = mock(); const i18nService = mock(); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 6c3aea9dc7..63111fb6c3 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -53,7 +53,7 @@ class OverlayBackground implements OverlayBackgroundInterface { private overlayListPort: chrome.runtime.Port; private focusedFieldData: FocusedFieldData; private overlayPageTranslations: Record; - private readonly iconsServerUrl: string; + private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { openAutofillOverlay: () => this.openOverlay(false), autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message), @@ -98,9 +98,7 @@ class OverlayBackground implements OverlayBackgroundInterface { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private themeStateService: ThemeStateService, - ) { - this.iconsServerUrl = this.environmentService.getIconsUrl(); - } + ) {} /** * Removes cached page details for a tab @@ -118,6 +116,8 @@ class OverlayBackground implements OverlayBackgroundInterface { */ async init() { this.setupExtensionMessageListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); await this.getOverlayVisibility(); await this.getAuthStatus(); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b34f07f657..3e84b7544b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -613,6 +613,7 @@ export default class MainBackground { this.authService, this.environmentService, this.logService, + this.stateProvider, true, ); @@ -1032,10 +1033,6 @@ export default class MainBackground { return new Promise((resolve) => { setTimeout(async () => { - await this.environmentService.setUrlsFromStorage(); - // Workaround to ignore stateService.activeAccount until URLs are set - // TODO: Remove this when implementing ticket PM-2637 - this.environmentService.initialized = true; if (!this.isPrivateMode) { await this.refreshBadge(); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index f422fc8550..0a94e0a79a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; @@ -220,7 +222,8 @@ export default class RuntimeBackground { } break; case "authResult": { - const vaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const vaultUrl = env.getWebVaultUrl(); if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { return; @@ -241,7 +244,8 @@ export default class RuntimeBackground { break; } case "webAuthnResult": { - const vaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const vaultUrl = env.getWebVaultUrl(); if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { return; @@ -364,7 +368,8 @@ export default class RuntimeBackground { async sendBwInstalledMessageToVault() { try { - const vaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const vaultUrl = env.getWebVaultUrl(); const urlObj = new URL(vaultUrl); const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` }); diff --git a/apps/browser/src/platform/background/service-factories/config-service.factory.ts b/apps/browser/src/platform/background/service-factories/config-service.factory.ts index 9c8b485c2a..4e31fb3141 100644 --- a/apps/browser/src/platform/background/service-factories/config-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/config-service.factory.ts @@ -13,6 +13,7 @@ import { } from "./environment-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { stateProviderFactory } from "./state-provider.factory"; import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; type ConfigServiceFactoryOptions = FactoryOptions & { @@ -43,6 +44,7 @@ export function configServiceFactory( await authServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await logServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), opts.configServiceOptions?.subscribe ?? true, ), ); diff --git a/apps/browser/src/platform/services/browser-config.service.ts b/apps/browser/src/platform/services/browser-config.service.ts index 557db4f33c..be8d087f3b 100644 --- a/apps/browser/src/platform/services/browser-config.service.ts +++ b/apps/browser/src/platform/services/browser-config.service.ts @@ -7,6 +7,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; @@ -21,8 +22,17 @@ export class BrowserConfigService extends ConfigService { authService: AuthService, environmentService: EnvironmentService, logService: LogService, + stateProvider: StateProvider, subscribe = false, ) { - super(stateService, configApiService, authService, environmentService, logService, subscribe); + super( + stateService, + configApiService, + authService, + environmentService, + logService, + stateProvider, + subscribe, + ); } } diff --git a/apps/browser/src/platform/services/browser-environment.service.ts b/apps/browser/src/platform/services/browser-environment.service.ts index 77cf7de8ea..d7e22cf747 100644 --- a/apps/browser/src/platform/services/browser-environment.service.ts +++ b/apps/browser/src/platform/services/browser-environment.service.ts @@ -1,12 +1,15 @@ +import { firstValueFrom } from "rxjs"; + import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; +import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment"; import { devFlagEnabled, devFlagValue } from "../flags"; -export class BrowserEnvironmentService extends EnvironmentService { +export class BrowserEnvironmentService extends DefaultEnvironmentService { constructor( private logService: LogService, stateProvider: StateProvider, @@ -29,16 +32,18 @@ export class BrowserEnvironmentService extends EnvironmentService { return false; } - const env = await this.getManagedEnvironment(); + const managedEnv = await this.getManagedEnvironment(); + const env = await firstValueFrom(this.environment$); + const urls = env.getUrls(); return ( - env.base != this.baseUrl || - env.webVault != this.webVaultUrl || - env.api != this.webVaultUrl || - env.identity != this.identityUrl || - env.icons != this.iconsUrl || - env.notifications != this.notificationsUrl || - env.events != this.eventsUrl + managedEnv.base != urls.base || + managedEnv.webVault != urls.webVault || + managedEnv.api != urls.api || + managedEnv.identity != urls.identity || + managedEnv.icons != urls.icons || + managedEnv.notifications != urls.notifications || + managedEnv.events != urls.events ); } @@ -62,7 +67,7 @@ export class BrowserEnvironmentService extends EnvironmentService { async setUrlsToManagedEnvironment() { const env = await this.getManagedEnvironment(); - await this.setUrls({ + await this.setEnvironment(Region.SelfHosted, { base: env.base, webVault: env.webVault, api: env.api, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5e080adf16..33fe6a52af 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -222,12 +222,12 @@ function getBgService(service: keyof MainBackground) { }, { provide: BrowserEnvironmentService, - useExisting: EnvironmentService, + useClass: BrowserEnvironmentService, + deps: [LogService, StateProvider, AccountServiceAbstraction], }, { provide: EnvironmentService, - useFactory: getBgService("environmentService"), - deps: [], + useExisting: BrowserEnvironmentService, }, { provide: TotpService, useFactory: getBgService("totpService"), deps: [] }, { @@ -480,6 +480,7 @@ function getBgService(service: keyof MainBackground) { ConfigApiServiceAbstraction, AuthServiceAbstraction, EnvironmentService, + StateProvider, LogService, ], }, diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index b408617834..a4ad0ba801 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -6,33 +6,33 @@

© Bitwarden Inc. 2015-{{ year }}

{{ "version" | i18n }}: {{ version }}

- -

- {{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }} - - ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }}) + +

+ {{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} + + ({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})

- - + +

{{ "serverVersion" | i18n }} ({{ "thirdParty" | i18n }}): - {{ this.serverConfig?.version }} - - ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }}) + {{ data.serverConfig?.version }} + + ({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})

- {{ "thirdPartyServerMessage" | i18n: serverConfig.server?.name }} + {{ "thirdPartyServerMessage" | i18n: data.serverConfig.server?.name }}
-

+

{{ "serverVersion" | i18n }} ({{ "selfHostedServer" | i18n }}): - {{ this.serverConfig?.version }} - - ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }}) + {{ data.serverConfig?.version }} + + ({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})

diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts index d6f6f000b6..4cabb183ae 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/popup/settings/about.component.ts @@ -1,10 +1,9 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { Observable } from "rxjs"; +import { combineLatest, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; -import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { ButtonModule, DialogModule } from "@bitwarden/components"; @@ -16,11 +15,13 @@ import { BrowserApi } from "../../platform/browser/browser-api"; imports: [CommonModule, JslibModule, DialogModule, ButtonModule], }) export class AboutComponent { - protected serverConfig$: Observable = this.configService.serverConfig$; - protected year = new Date().getFullYear(); protected version = BrowserApi.getApplicationVersion(); - protected isCloud = this.environmentService.isCloud(); + + protected data$ = combineLatest([ + this.configService.serverConfig$, + this.environmentService.environment$.pipe(map((env) => env.isCloud())), + ]).pipe(map(([serverConfig, isCloud]) => ({ serverConfig, isCloud }))); constructor( private configService: ConfigServiceAbstraction, diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index 05f633d424..52e44a2531 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -446,9 +446,8 @@ export class SettingsComponent implements OnInit { type: "info", }); if (confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(this.environmentService.getWebVaultUrl()); + const env = await firstValueFrom(this.environmentService.environment$); + await BrowserApi.createNewTab(env.getWebVaultUrl()); } } @@ -479,10 +478,9 @@ export class SettingsComponent implements OnInit { } async webVault() { - const url = this.environmentService.getWebVaultUrl(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(url); + const env = await firstValueFrom(this.environmentService.environment$); + const url = env.getWebVaultUrl(); + await BrowserApi.createNewTab(url); } async import() { diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 97c0974ac6..a91e876e92 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -690,6 +690,8 @@ export class LoginCommand { codeChallenge: string, state: string, ): Promise<{ ssoCode: string; orgIdentifier: string }> { + const env = await firstValueFrom(this.environmentService.environment$); + return new Promise((resolve, reject) => { const callbackServer = http.createServer((req, res) => { const urlString = "http://localhost" + req.url; @@ -724,7 +726,7 @@ export class LoginCommand { } }); let foundPort = false; - const webUrl = this.environmentService.getWebVaultUrl(); + const webUrl = env.getWebVaultUrl(); for (let port = 8065; port <= 8070; port++) { try { this.ssoRedirectUri = "http://localhost:" + port; diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 55bc46e41e..7af40b1ebd 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -47,6 +47,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { BiometricStateService, @@ -62,7 +63,7 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; -import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; +import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; @@ -312,7 +313,10 @@ export class Main { this.derivedStateProvider, ); - this.environmentService = new EnvironmentService(this.stateProvider, this.accountService); + this.environmentService = new DefaultEnvironmentService( + this.stateProvider, + this.accountService, + ); this.tokenService = new TokenService( this.singleUserStateProvider, @@ -504,6 +508,7 @@ export class Main { this.authService, this.environmentService, this.logService, + this.stateProvider, true, ); @@ -703,7 +708,6 @@ export class Main { await this.storageService.init(); await this.stateService.init(); this.containerService.attachToGlobal(global); - await this.environmentService.setUrlsFromStorage(); await this.i18nService.init(); this.twoFactorService.init(); this.configService.init(); diff --git a/apps/cli/src/commands/config.command.ts b/apps/cli/src/commands/config.command.ts index 209f75a836..eb6559443d 100644 --- a/apps/cli/src/commands/config.command.ts +++ b/apps/cli/src/commands/config.command.ts @@ -1,6 +1,10 @@ import { OptionValues } from "commander"; +import { firstValueFrom } from "rxjs"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { Response } from "../models/response"; import { MessageResponse } from "../models/response/message.response"; @@ -29,16 +33,15 @@ export class ConfigCommand { !options.notifications && !options.events ) { + const env = await firstValueFrom(this.environmentService.environment$); const stringRes = new StringResponse( - this.environmentService.hasBaseUrl() - ? this.environmentService.getUrls().base - : "https://bitwarden.com", + env.hasBaseUrl() ? env.getUrls().base : "https://bitwarden.com", ); return Response.success(stringRes); } url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url; - await this.environmentService.setUrls({ + await this.environmentService.setEnvironment(Region.SelfHosted, { base: url, webVault: options.webVault || null, api: options.api || null, diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/commands/convert-to-key-connector.command.ts index de9565dfb6..654606dc06 100644 --- a/apps/cli/src/commands/convert-to-key-connector.command.ts +++ b/apps/cli/src/commands/convert-to-key-connector.command.ts @@ -1,8 +1,12 @@ import * as inquirer from "inquirer"; +import { firstValueFrom } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Response } from "../models/response"; @@ -67,9 +71,10 @@ export class ConvertToKeyConnectorCommand { await this.keyConnectorService.setUsesKeyConnector(true); // Update environment URL - required for api key login - const urls = this.environmentService.getUrls(); + const env = await firstValueFrom(this.environmentService.environment$); + const urls = env.getUrls(); urls.keyConnector = organization.keyConnectorUrl; - await this.environmentService.setUrls(urls); + await this.environmentService.setEnvironment(Region.SelfHosted, urls); return Response.success(); } else if (answer.convert === "leave") { diff --git a/apps/cli/src/commands/status.command.ts b/apps/cli/src/commands/status.command.ts index d7bc17b643..32b93a7e40 100644 --- a/apps/cli/src/commands/status.command.ts +++ b/apps/cli/src/commands/status.command.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -17,7 +19,7 @@ export class StatusCommand { async run(): Promise { try { - const baseUrl = this.baseUrl(); + const baseUrl = await this.baseUrl(); const status = await this.status(); const lastSync = await this.syncService.getLastSync(); const userId = await this.stateService.getUserId(); @@ -37,8 +39,9 @@ export class StatusCommand { } } - private baseUrl(): string { - return this.envService.getUrls().base; + private async baseUrl(): Promise { + const env = await firstValueFrom(this.envService.environment$); + return env.getUrls().base; } private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> { diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index 1f6f8059e1..a3f4b5c086 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -127,7 +127,8 @@ export class SendCreateCommand { await this.sendApiService.save([encSend, fileData]); const newSend = await this.sendService.getFromState(encSend.id); const decSend = await newSend.decrypt(); - const res = new SendResponse(decSend, this.environmentService.getWebVaultUrl()); + const env = await firstValueFrom(this.environmentService.environment$); + const res = new SendResponse(decSend, env.getWebVaultUrl()); return Response.success(res); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/tools/send/commands/get.command.ts b/apps/cli/src/tools/send/commands/get.command.ts index 9e80c350b7..2ffe242317 100644 --- a/apps/cli/src/tools/send/commands/get.command.ts +++ b/apps/cli/src/tools/send/commands/get.command.ts @@ -1,4 +1,5 @@ import { OptionValues } from "commander"; +import { firstValueFrom } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -32,7 +33,8 @@ export class SendGetCommand extends DownloadCommand { return Response.notFound(); } - const webVaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); let filter = (s: SendView) => true; let selector = async (s: SendView): Promise => Response.success(new SendResponse(s, webVaultUrl)); diff --git a/apps/cli/src/tools/send/commands/list.command.ts b/apps/cli/src/tools/send/commands/list.command.ts index e169a26ea1..ab8a4dcb1c 100644 --- a/apps/cli/src/tools/send/commands/list.command.ts +++ b/apps/cli/src/tools/send/commands/list.command.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -21,7 +23,8 @@ export class SendListCommand { sends = this.searchService.searchSends(sends, normalizedOptions.search); } - const webVaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl))); return Response.success(res); } diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index 8dfbf01fa9..dc662f0272 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -1,5 +1,6 @@ import { OptionValues } from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -46,7 +47,7 @@ export class SendReceiveCommand extends DownloadCommand { return Response.badRequest("Failed to parse the provided Send url"); } - const apiUrl = this.getApiUrl(urlObject); + const apiUrl = await this.getApiUrl(urlObject); const [id, key] = this.getIdAndKey(urlObject); if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) { @@ -108,8 +109,9 @@ export class SendReceiveCommand extends DownloadCommand { return [result[0], result[1]]; } - private getApiUrl(url: URL) { - const urls = this.environmentService.getUrls(); + private async getApiUrl(url: URL) { + const env = await firstValueFrom(this.environmentService.environment$); + const urls = env.getUrls(); if (url.origin === "https://send.bitwarden.com") { return "https://api.bitwarden.com"; } else if (url.origin === urls.api) { diff --git a/apps/cli/src/tools/send/commands/remove-password.command.ts b/apps/cli/src/tools/send/commands/remove-password.command.ts index a25ca4c07e..1c7289bf08 100644 --- a/apps/cli/src/tools/send/commands/remove-password.command.ts +++ b/apps/cli/src/tools/send/commands/remove-password.command.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { SendService } from "@bitwarden/common/tools/send/services//send.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -18,7 +20,8 @@ export class SendRemovePasswordCommand { const updatedSend = await this.sendService.get(id); const decSend = await updatedSend.decrypt(); - const webVaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); const res = new SendResponse(decSend, webVaultUrl); return Response.success(res); } catch (e) { diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 795870b305..499300086d 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -105,7 +105,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()), email: await this.tokenService.getEmail(), avatarColor: await firstValueFrom(this.avatarService.avatarColor$), - server: await this.environmentService.getHost(), + server: (await this.environmentService.getEnvironment())?.getHostname(), }; } catch { this.activeAccount = undefined; @@ -158,7 +158,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { email: baseAccounts[userId].profile.email, authenticationStatus: await this.authService.getAuthStatus(userId), avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)), - server: await this.environmentService.getHost(userId), + server: (await this.environmentService.getEnvironment(userId))?.getHostname(), }; } diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index a29dadff30..f45d530edd 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -8,7 +8,6 @@ import { NotificationsService as NotificationsServiceAbstraction } from "@bitwar import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -25,7 +24,6 @@ import { NativeMessagingService } from "../../services/native-messaging.service" export class InitService { constructor( @Inject(WINDOW) private win: Window, - private environmentService: EnvironmentServiceAbstraction, private syncService: SyncServiceAbstraction, private vaultTimeoutService: VaultTimeoutService, private i18nService: I18nServiceAbstraction, @@ -46,10 +44,6 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process - await this.environmentService.setUrlsFromStorage(); - // Workaround to ignore stateService.activeAccount until URLs are set - // TODO: Remove this when implementing ticket PM-2637 - this.environmentService.initialized = true; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.syncService.fullSync(true); diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index d2e53ad0f2..dd22a0fa37 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -49,10 +49,6 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { return this.formGroup.value.email; } - get selfHostedDomain() { - return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null; - } - constructor( devicesApiService: DevicesApiServiceAbstraction, appIdService: AppIdService, @@ -152,9 +148,6 @@ 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(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.environmentSelector.updateEnvironmentInfo(); await this.getLoginWithDevice(this.loggedEmail); }); } diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d9c30f6595..9b862e7c9f 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -1,5 +1,6 @@ import { Component, Inject, NgZone, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; @@ -141,7 +142,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } } - override launchDuoFrameless() { + override async launchDuoFrameless() { const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), @@ -150,8 +151,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { // we're using the connector here as a way to set a cookie with translations // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); const launchUrl = - this.environmentService.getWebVaultUrl() + + env.getWebVaultUrl() + "/duo-redirect-connector.html" + "?duoFramelessUrl=" + encodeURIComponent(this.duoFramelessUrl) + diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 191008821d..8a6233ec63 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2346,12 +2346,6 @@ "loggingInOn": { "message": "Logging in on" }, - "usDomain": { - "message": "bitwarden.com" - }, - "euDomain": { - "message": "bitwarden.eu" - }, "selfHostedServer": { "message": "self-hosted" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 75f9e22a87..b7ba2faf79 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -10,7 +10,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; +import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -54,7 +54,7 @@ export class Main { memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; messagingService: ElectronMainMessagingService; stateService: StateService; - environmentService: EnvironmentService; + environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; desktopCredentialStorageListener: DesktopCredentialStorageListener; migrationRunner: MigrationRunner; @@ -148,7 +148,7 @@ export class Main { new DefaultDerivedStateProvider(this.memoryStorageForStateProviders), ); - this.environmentService = new EnvironmentService(stateProvider, accountService); + this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); this.tokenService = new TokenService( singleUserStateProvider, diff --git a/apps/desktop/src/main/menu/menu.main.ts b/apps/desktop/src/main/menu/menu.main.ts index 413583982f..4d3dc0d62e 100644 --- a/apps/desktop/src/main/menu/menu.main.ts +++ b/apps/desktop/src/main/menu/menu.main.ts @@ -1,4 +1,5 @@ import { app, Menu } from "electron"; +import { firstValueFrom } from "rxjs"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -45,7 +46,8 @@ export class MenuMain { } private async getWebVaultUrl() { - return this.environmentService.getWebVaultUrl() ?? cloudWebVaultUrl; + const env = await firstValueFrom(this.environmentService.environment$); + return env.getWebVaultUrl() ?? cloudWebVaultUrl; } private initContextMenu() { diff --git a/apps/desktop/webpack.renderer.js b/apps/desktop/webpack.renderer.js index 535edbcb5e..1ebeadef05 100644 --- a/apps/desktop/webpack.renderer.js +++ b/apps/desktop/webpack.renderer.js @@ -177,6 +177,7 @@ const renderer = { ENV: ENV, FLAGS: envConfig.flags, DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {}, + ADDITIONAL_REGIONS: envConfig.additionalRegions ?? [], }), ], }; diff --git a/apps/web/config/development.json b/apps/web/config/development.json index a2ded40f61..97742aee3d 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -10,6 +10,15 @@ "proxyNotifications": "http://localhost:61840", "wsConnectSrc": "ws://localhost:61840" }, + "additionalRegions": [ + { + "key": "LOCAL", + "domain": "localhost", + "urls": { + "webVault": "https://localhost:8080" + } + } + ], "flags": { "secretsManager": true, "showPasswordless": true, diff --git a/apps/web/config/euqa.json b/apps/web/config/euqa.json index 496b52065c..70caf3db62 100644 --- a/apps/web/config/euqa.json +++ b/apps/web/config/euqa.json @@ -4,6 +4,22 @@ "notifications": "https://notifications.euqa.bitwarden.pw", "scim": "https://scim.euqa.bitwarden.pw" }, + "additionalRegions": [ + { + "key": "USQA", + "domain": "qa.bitwarden.pw", + "urls": { + "webVault": "https://vault.qa.bitwarden.pw" + } + }, + { + "key": "EUQA", + "domain": "euqa.bitwarden.pw", + "urls": { + "webVault": "https://vault.euqa.bitwarden.pw" + } + } + ], "flags": { "secretsManager": true, "showPasswordless": true diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index be9911cc57..0ce5f3dc7f 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -10,6 +10,22 @@ "proxyEvents": "https://events.qa.bitwarden.pw", "proxyNotifications": "https://notifications.qa.bitwarden.pw" }, + "additionalRegions": [ + { + "key": "USQA", + "domain": "qa.bitwarden.pw", + "urls": { + "webVault": "https://vault.qa.bitwarden.pw" + } + }, + { + "key": "EUQA", + "domain": "euqa.bitwarden.pw", + "urls": { + "webVault": "https://vault.euqa.bitwarden.pw" + } + } + ], "flags": { "secretsManager": true, "showPasswordless": true, diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 06b76b6b54..a47a7a2848 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -127,7 +127,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest await this.submit(); }; - override launchDuoFrameless() { + override async launchDuoFrameless() { const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("thisWindowWillCloseIn5Seconds"), diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index e6e63264d5..fa17821d56 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -44,11 +44,11 @@ export class PremiumComponent implements OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, ) { this.selfHosted = platformUtilsService.isSelfHost(); - this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } async ngOnInit() { + this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 3ec0cd54d1..76e542c338 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -49,10 +49,10 @@ export class UserSubscriptionComponent implements OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, ) { this.selfHosted = platformUtilsService.isSelfHost(); - this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); } async ngOnInit() { + this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); this.presentUserWithOffboardingSurvey$ = this.configService.getFeatureFlag$( FeatureFlag.AC1607_PresentUserOffboardingSurvey, ); diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index 1b66f5400a..081e825bcd 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -82,11 +82,11 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private i18nService: I18nService, private environmentService: EnvironmentService, private dialogService: DialogService, - ) { - this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl(); - } + ) {} async ngOnInit() { + this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); + this.route.params .pipe( concatMap(async (params) => { diff --git a/apps/web/src/app/billing/shared/add-credit.component.ts b/apps/web/src/app/billing/shared/add-credit.component.ts index 57091196bd..25d49fac9e 100644 --- a/apps/web/src/app/billing/shared/add-credit.component.ts +++ b/apps/web/src/app/billing/shared/add-credit.component.ts @@ -14,11 +14,15 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; -import { PayPalConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +export type PayPalConfig = { + businessId?: string; + buttonAction?: string; +}; + @Component({ selector: "app-add-credit", templateUrl: "add-credit.component.html", diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index 847830ce60..4be0db2ba4 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -1,38 +1,25 @@
- {{ "usDomain" | i18n }} - - - - {{ "euDomain" | i18n }} + {{ region.domain }} diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.ts b/apps/web/src/app/components/environment-selector/environment-selector.component.ts index 2e82e383b6..132b68c6e2 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.ts +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.ts @@ -1,7 +1,10 @@ import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { RegionDomain } from "@bitwarden/common/platform/abstractions/environment.service"; +import { + EnvironmentService, + RegionConfig, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -12,19 +15,21 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; export class EnvironmentSelectorComponent implements OnInit { constructor( private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, private router: Router, ) {} - isEuServer: boolean; - isUsServer: boolean; - showRegionSelector = false; - routeAndParams: string; + protected availableRegions = this.environmentService.availableRegions(); + protected currentRegion?: RegionConfig; + + protected showRegionSelector = false; + protected routeAndParams: string; async ngOnInit() { - const domain = Utils.getDomain(window.location.href); - this.isEuServer = domain.includes(RegionDomain.EU); - this.isUsServer = domain.includes(RegionDomain.US) || domain.includes(RegionDomain.USQA); this.showRegionSelector = !this.platformUtilsService.isSelfHost(); this.routeAndParams = `/#${this.router.url}`; + + const host = Utils.getHost(window.location.href); + this.currentRegion = this.availableRegions.find((r) => Utils.getHost(r.urls.webVault) === host); } } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index f44ab9f09d..e2d3f64f2d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -11,11 +11,14 @@ import { OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE, + WINDOW, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { LoginService } from "@bitwarden/common/auth/services/login.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -28,9 +31,9 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; -import { GlobalStateProvider } from "@bitwarden/common/platform/state"; +/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ +import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; /* eslint-enable import/no-restricted-paths -- Implementation for memory storage */ import { @@ -41,6 +44,7 @@ import { import { PolicyListService } from "../admin-console/core/policy-list.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; +import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { WindowStorageService } from "../platform/window-storage.service"; @@ -138,6 +142,11 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service"; OBSERVABLE_DISK_LOCAL_STORAGE, ], }, + { + provide: EnvironmentService, + useClass: WebEnvironmentService, + deps: [WINDOW, StateProvider, AccountService], + }, { provide: ThemeStateService, useFactory: (globalStateProvider: GlobalStateProvider) => diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 3d940f3596..60f3dea915 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -8,10 +8,6 @@ import { NotificationsService as NotificationsServiceAbstraction } from "@bitwar import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { - EnvironmentService as EnvironmentServiceAbstraction, - Urls, -} from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; @@ -23,7 +19,6 @@ import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/va export class InitService { constructor( @Inject(WINDOW) private win: Window, - private environmentService: EnvironmentServiceAbstraction, private notificationsService: NotificationsServiceAbstraction, private vaultTimeoutService: VaultTimeoutService, private i18nService: I18nServiceAbstraction, @@ -41,13 +36,6 @@ export class InitService { return async () => { await this.stateService.init(); - const urls = process.env.URLS as Urls; - urls.base ??= this.win.location.origin; - await this.environmentService.setUrls(urls); - // Workaround to ignore stateService.activeAccount until process.env.URLS are set - // TODO: Remove this when implementing ticket PM-2637 - this.environmentService.initialized = true; - setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); await this.i18nService.init(); diff --git a/apps/web/src/app/platform/web-environment.service.ts b/apps/web/src/app/platform/web-environment.service.ts new file mode 100644 index 0000000000..c2eb37eea5 --- /dev/null +++ b/apps/web/src/app/platform/web-environment.service.ts @@ -0,0 +1,62 @@ +import { ReplaySubject } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { + Environment, + Region, + RegionConfig, + Urls, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + CloudEnvironment, + DefaultEnvironmentService, + SelfHostedEnvironment, +} from "@bitwarden/common/platform/services/default-environment.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +/** + * Web specific environment service. Ensures that the urls are set from the window location. + */ +export class WebEnvironmentService extends DefaultEnvironmentService { + constructor( + private win: Window, + stateProvider: StateProvider, + accountService: AccountService, + ) { + super(stateProvider, accountService); + + // The web vault always uses the current location as the base url + const urls = process.env.URLS as Urls; + urls.base ??= this.win.location.origin; + + // Find the region + const domain = Utils.getDomain(this.win.location.href); + const region = this.availableRegions().find((r) => Utils.getDomain(r.urls.webVault) === domain); + + let environment: Environment; + if (region) { + environment = new WebCloudEnvironment(region, urls); + } else { + environment = new SelfHostedEnvironment(urls); + } + + // Override the environment observable with a replay subject + const subject = new ReplaySubject(1); + subject.next(environment); + this.environment$ = subject.asObservable(); + } + + // Web cannot set environment + async setEnvironment(region: Region, urls?: Urls): Promise { + return; + } +} + +class WebCloudEnvironment extends CloudEnvironment { + constructor(config: RegionConfig, urls: Urls) { + super(config); + // We override the urls to avoid CORS issues + this.urls = urls; + } +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 07bffad857..34e3c5d754 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7063,12 +7063,6 @@ "enforceOnLoginDesc": { "message": "Require existing members to change their passwords" }, - "usDomain": { - "message": "bitwarden.com" - }, - "euDomain": { - "message": "bitwarden.eu" - }, "smProjectDeleteAccessRestricted": { "message": "You don't have permissions to delete this project", "description": "The individual description shown to the user when the user doesn't have access to delete a project." diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index b62299a7a7..f088aadb75 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -171,6 +171,7 @@ const plugins = [ PAYPAL_CONFIG: envConfig["paypal"] ?? {}, FLAGS: envConfig["flags"] ?? {}, DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {}, + ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [], }), new AngularWebpackPlugin({ tsConfigPath: "tsconfig.json", diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index ef8ba2838a..a22de64c39 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { UntypedFormBuilder, FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -76,13 +77,13 @@ export class ScimComponent implements OnInit { apiKeyRequest, ); this.formData.setValue({ - endpointUrl: this.getScimEndpointUrl(), + endpointUrl: await this.getScimEndpointUrl(), clientSecret: apiKeyResponse.apiKey, }); } async copyScimUrl() { - this.platformUtilsService.copyToClipboard(this.getScimEndpointUrl()); + this.platformUtilsService.copyToClipboard(await this.getScimEndpointUrl()); } async rotateScimKey() { @@ -148,8 +149,9 @@ export class ScimComponent implements OnInit { this.formPromise = null; } - getScimEndpointUrl() { - return this.environmentService.getScimUrl() + "/" + this.organizationId; + async getScimEndpointUrl() { + const env = await firstValueFrom(this.environmentService.environment$); + return env.getScimUrl() + "/" + this.organizationId; } toggleScimKey() { @@ -163,7 +165,7 @@ export class ScimComponent implements OnInit { this.showScimSettings = true; this.enabled.setValue(true); this.formData.setValue({ - endpointUrl: this.getScimEndpointUrl(), + endpointUrl: await this.getScimEndpointUrl(), clientSecret: "", }); await this.loadApiKey(); diff --git a/libs/angular/src/auth/components/captcha-protected.component.ts b/libs/angular/src/auth/components/captcha-protected.component.ts index 7a48b8f2e5..0f549d2498 100644 --- a/libs/angular/src/auth/components/captcha-protected.component.ts +++ b/libs/angular/src/auth/components/captcha-protected.component.ts @@ -1,4 +1,5 @@ import { Directive, Input } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -19,7 +20,8 @@ export abstract class CaptchaProtectedComponent { ) {} async setupCaptcha() { - const webVaultUrl = this.environmentService.getWebVaultUrl(); + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); this.captcha = new CaptchaIFrame( window, diff --git a/libs/angular/src/auth/components/environment-selector.component.html b/libs/angular/src/auth/components/environment-selector.component.html index 8ee8a4d578..8ad0602385 100644 --- a/libs/angular/src/auth/components/environment-selector.component.html +++ b/libs/angular/src/auth/components/environment-selector.component.html @@ -7,17 +7,15 @@ #trigger="cdkOverlayOrigin" aria-haspopup="dialog" aria-controls="cdk-overlay-container" - [ngSwitch]="selectedEnvironment" > - {{ - "usDomain" | i18n - }} - {{ - "euDomain" | i18n - }} - {{ - "selfHostedServer" | i18n - }} + + + {{ selectedRegion.domain }} + + + {{ "selfHostedServer" | i18n }} + +
@@ -41,40 +39,23 @@ role="dialog" aria-modal="true" > - -
- -
+ + +
+