mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[PM-5979] Refactor EnvironmentService (#8040)
Refactor environment service to emit a single observable. This required significant changes to how the environment service behaves and tackles much of the tech debt planned for it.
This commit is contained in:
parent
7a42b4ebc6
commit
e767295c86
@ -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",
|
||||
|
@ -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."
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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) +
|
||||
|
@ -113,7 +113,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||
getWebVaultUrlForNotification: () => string;
|
||||
getWebVaultUrlForNotification: () => Promise<string>;
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -165,6 +165,7 @@ export default class NotificationBackground {
|
||||
notificationQueueMessage: NotificationQueueMessageItem,
|
||||
) {
|
||||
const notificationType = notificationQueueMessage.type;
|
||||
|
||||
const typeData: Record<string, any> = {
|
||||
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<string> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
return env.getWebVaultUrl();
|
||||
}
|
||||
|
||||
private async removeIndividualVault(): Promise<boolean> {
|
||||
|
@ -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<CipherService>();
|
||||
const autofillService = mock<AutofillService>();
|
||||
const authService = mock<AuthService>();
|
||||
const environmentService = mock<EnvironmentService>({
|
||||
getIconsUrl: () => iconServerUrl,
|
||||
});
|
||||
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
environmentService.environment$ = new BehaviorSubject(
|
||||
new CloudEnvironment({
|
||||
key: Region.US,
|
||||
domain: "bitwarden.com",
|
||||
urls: { icons: "https://icons.bitwarden.com/" },
|
||||
}),
|
||||
);
|
||||
const stateService = mock<BrowserStateService>();
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const i18nService = mock<I18nService>();
|
||||
|
@ -53,7 +53,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private overlayListPort: chrome.runtime.Port;
|
||||
private focusedFieldData: FocusedFieldData;
|
||||
private overlayPageTranslations: Record<string, string>;
|
||||
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();
|
||||
}
|
||||
|
@ -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<void>((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();
|
||||
}
|
||||
|
@ -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}*` });
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -222,12 +222,12 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||
},
|
||||
{
|
||||
provide: BrowserEnvironmentService,
|
||||
useExisting: EnvironmentService,
|
||||
useClass: BrowserEnvironmentService,
|
||||
deps: [LogService, StateProvider, AccountServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useFactory: getBgService<EnvironmentService>("environmentService"),
|
||||
deps: [],
|
||||
useExisting: BrowserEnvironmentService,
|
||||
},
|
||||
{ provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
|
||||
{
|
||||
@ -480,6 +480,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||
ConfigApiServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
EnvironmentService,
|
||||
StateProvider,
|
||||
LogService,
|
||||
],
|
||||
},
|
||||
|
@ -6,33 +6,33 @@
|
||||
<div bitDialogContent>
|
||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||
<p>{{ "version" | i18n }}: {{ version }}</p>
|
||||
<ng-container *ngIf="serverConfig$ | async as serverConfig">
|
||||
<p *ngIf="isCloud">
|
||||
{{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }}
|
||||
<span *ngIf="!serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<p *ngIf="data.isCloud">
|
||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||
<span *ngIf="!data.serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<ng-container *ngIf="!isCloud">
|
||||
<ng-container *ngIf="serverConfig.server">
|
||||
<ng-container *ngIf="!data.isCloud">
|
||||
<ng-container *ngIf="data.serverConfig.server">
|
||||
<p>
|
||||
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
||||
{{ this.serverConfig?.version }}
|
||||
<span *ngIf="!serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
||||
{{ data.serverConfig?.version }}
|
||||
<span *ngIf="!data.serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||
</span>
|
||||
</p>
|
||||
<div>
|
||||
<small>{{ "thirdPartyServerMessage" | i18n: serverConfig.server?.name }}</small>
|
||||
<small>{{ "thirdPartyServerMessage" | i18n: data.serverConfig.server?.name }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<p *ngIf="!serverConfig.server">
|
||||
<p *ngIf="!data.serverConfig.server">
|
||||
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
||||
{{ this.serverConfig?.version }}
|
||||
<span *ngIf="!serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
||||
{{ data.serverConfig?.version }}
|
||||
<span *ngIf="!data.serverConfig.isValid()">
|
||||
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||
</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
@ -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<ServerConfig> = 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,
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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") {
|
||||
|
@ -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<Response> {
|
||||
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<string> {
|
||||
const env = await firstValueFrom(this.envService.environment$);
|
||||
return env.getUrls().base;
|
||||
}
|
||||
|
||||
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
|
||||
|
@ -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);
|
||||
|
@ -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> =>
|
||||
Response.success(new SendResponse(s, webVaultUrl));
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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) +
|
||||
|
@ -2346,12 +2346,6 @@
|
||||
"loggingInOn": {
|
||||
"message": "Logging in on"
|
||||
},
|
||||
"usDomain": {
|
||||
"message": "bitwarden.com"
|
||||
},
|
||||
"euDomain": {
|
||||
"message": "bitwarden.eu"
|
||||
},
|
||||
"selfHostedServer": {
|
||||
"message": "self-hosted"
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -177,6 +177,7 @@ const renderer = {
|
||||
ENV: ENV,
|
||||
FLAGS: envConfig.flags,
|
||||
DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {},
|
||||
ADDITIONAL_REGIONS: envConfig.additionalRegions ?? [],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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$<boolean>(
|
||||
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
||||
);
|
||||
|
@ -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) => {
|
||||
|
@ -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",
|
||||
|
@ -1,38 +1,25 @@
|
||||
<div class="tw-mb-1" *ngIf="showRegionSelector">
|
||||
<bit-menu #environmentOptions>
|
||||
<a
|
||||
*ngFor="let region of availableRegions"
|
||||
bitMenuItem
|
||||
[attr.href]="
|
||||
isUsServer ? 'javascript:void(0)' : 'https://vault.bitwarden.com' + routeAndParams
|
||||
region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams
|
||||
"
|
||||
class="pr-4"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check pb-1"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="isUsServer ? 'visible' : 'hidden'"
|
||||
[style.visibility]="region == currentRegion ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
{{ "usDomain" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitMenuItem
|
||||
[attr.href]="
|
||||
isEuServer ? 'javascript:void(0)' : 'https://vault.bitwarden.eu' + routeAndParams
|
||||
"
|
||||
class="pr-4"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check pb-1"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="isEuServer ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
{{ "euDomain" | i18n }}
|
||||
{{ region.domain }}
|
||||
</a>
|
||||
</bit-menu>
|
||||
<div>
|
||||
{{ "server" | i18n }}:
|
||||
<a [routerLink]="[]" [bitMenuTriggerFor]="environmentOptions">
|
||||
<b>{{ isEuServer ? ("euDomain" | i18n) : ("usDomain" | i18n) }}</b
|
||||
<b>{{ currentRegion?.domain }}</b
|
||||
><i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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();
|
||||
|
62
apps/web/src/app/platform/web-environment.service.ts
Normal file
62
apps/web/src/app/platform/web-environment.service.ts
Normal file
@ -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<Environment>(1);
|
||||
subject.next(environment);
|
||||
this.environment$ = subject.asObservable();
|
||||
}
|
||||
|
||||
// Web cannot set environment
|
||||
async setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class WebCloudEnvironment extends CloudEnvironment {
|
||||
constructor(config: RegionConfig, urls: Urls) {
|
||||
super(config);
|
||||
// We override the urls to avoid CORS issues
|
||||
this.urls = urls;
|
||||
}
|
||||
}
|
@ -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."
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -7,17 +7,15 @@
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
[ngSwitch]="selectedEnvironment"
|
||||
>
|
||||
<span *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{
|
||||
"usDomain" | i18n
|
||||
}}</span>
|
||||
<span *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{
|
||||
"euDomain" | i18n
|
||||
}}</span>
|
||||
<span *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
|
||||
"selfHostedServer" | i18n
|
||||
}}</span>
|
||||
<span class="text-primary">
|
||||
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
|
||||
{{ selectedRegion.domain }}
|
||||
</ng-container>
|
||||
<ng-template #fallback>
|
||||
{{ "selfHostedServer" | i18n }}
|
||||
</ng-template>
|
||||
</span>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -41,40 +39,23 @@
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.US)"
|
||||
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.US ? 'true' : 'false'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<span>{{ "usDomain" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.EU)"
|
||||
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.EU ? 'true' : 'false'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<span>{{ "euDomain" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
<ng-container *ngFor="let region of availableRegions">
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
<span>{{ region.domain }}</span>
|
||||
</button>
|
||||
<br />
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
Region,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@Component({
|
||||
@ -34,7 +34,7 @@ import {
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
export class EnvironmentSelectorComponent {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||
isOpen = false;
|
||||
showingModal = false;
|
||||
@ -48,59 +48,34 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
|
||||
protected availableRegions = this.environmentService.availableRegions();
|
||||
protected selectedRegion$: Observable<RegionConfig | undefined> =
|
||||
this.environmentService.environment$.pipe(
|
||||
map((e) => e.getRegion()),
|
||||
map((r) => this.availableRegions.find((ar) => ar.key === r)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentServiceAbstraction,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.configService.serverConfig$.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
|
||||
// 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.updateEnvironmentInfo();
|
||||
});
|
||||
// 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.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.componentDestroyed$.next();
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: Region) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.updateEnvironmentInfo();
|
||||
|
||||
if (option === Region.SelfHosted) {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.environmentService.setRegion(option);
|
||||
// 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.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
async updateEnvironmentInfo() {
|
||||
this.selectedEnvironment = this.environmentService.selectedRegion;
|
||||
await this.environmentService.setEnvironment(option);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
// 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.updateEnvironmentInfo();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import {
|
||||
EnvironmentService,
|
||||
@ -27,21 +28,29 @@ export class EnvironmentComponent {
|
||||
protected i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
if (this.environmentService.selectedRegion != Region.SelfHosted) {
|
||||
return;
|
||||
}
|
||||
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||
if (env.getRegion() !== Region.SelfHosted) {
|
||||
this.baseUrl = "";
|
||||
this.webVaultUrl = "";
|
||||
this.apiUrl = "";
|
||||
this.identityUrl = "";
|
||||
this.iconsUrl = "";
|
||||
this.notificationsUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
this.apiUrl = urls.api || "";
|
||||
this.identityUrl = urls.identity || "";
|
||||
this.iconsUrl = urls.icons || "";
|
||||
this.notificationsUrl = urls.notifications || "";
|
||||
const urls = env.getUrls();
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
this.apiUrl = urls.api || "";
|
||||
this.identityUrl = urls.identity || "";
|
||||
this.iconsUrl = urls.icons || "";
|
||||
this.notificationsUrl = urls.notifications || "";
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const resUrls = await this.environmentService.setUrls({
|
||||
await this.environmentService.setEnvironment(Region.SelfHosted, {
|
||||
base: this.baseUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
@ -50,14 +59,6 @@ export class EnvironmentComponent {
|
||||
notifications: this.notificationsUrl,
|
||||
});
|
||||
|
||||
// re-set urls since service can change them, ex: prefixing https://
|
||||
this.baseUrl = resUrls.base;
|
||||
this.apiUrl = resUrls.api;
|
||||
this.identityUrl = resUrls.identity;
|
||||
this.webVaultUrl = resUrls.webVault;
|
||||
this.iconsUrl = resUrls.icons;
|
||||
this.notificationsUrl = resUrls.notifications;
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||
this.saved();
|
||||
}
|
||||
|
@ -346,7 +346,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
!this.platformUtilsService.supportsSecureStorage());
|
||||
this.email = await this.stateService.getEmail();
|
||||
|
||||
this.webVaultHostname = await this.environmentService.getHost();
|
||||
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject } from "rxjs";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
import { take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
@ -84,10 +84,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
}
|
||||
|
||||
get selfHostedDomain() {
|
||||
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
if (!params) {
|
||||
@ -245,7 +241,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||
|
||||
// Build URI
|
||||
const webUrl = this.environmentService.getWebVaultUrl();
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
|
||||
// Launch browser
|
||||
this.platformUtilsService.launchUri(
|
||||
|
@ -157,8 +157,10 @@ export class SsoComponent {
|
||||
// Save state (regardless of new or existing)
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
let authorizeUrl =
|
||||
this.environmentService.getIdentityUrl() +
|
||||
env.getIdentityUrl() +
|
||||
"/connect/authorize?" +
|
||||
"client_id=" +
|
||||
this.clientId +
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@ -31,8 +32,9 @@ export class TwoFactorOptionsComponent implements OnInit {
|
||||
this.onProviderSelected.emit(p.type);
|
||||
}
|
||||
|
||||
recover() {
|
||||
const webVault = this.environmentService.getWebVaultUrl();
|
||||
async recover() {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVault = env.getWebVaultUrl();
|
||||
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
|
||||
this.onRecoverSelected.emit();
|
||||
}
|
||||
|
@ -116,7 +116,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
|
||||
if (this.win != null && this.webAuthnSupported) {
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
this.webAuthn = new WebAuthnIFrame(
|
||||
this.win,
|
||||
webVaultUrl,
|
||||
@ -494,5 +495,5 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
|
||||
// implemented in clients
|
||||
launchDuoFrameless() {}
|
||||
async launchDuoFrameless() {}
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstraction
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
@ -140,7 +140,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
|
||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-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 { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
@ -363,7 +363,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
@ -477,8 +477,8 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EnvironmentServiceAbstraction,
|
||||
useClass: EnvironmentService,
|
||||
provide: EnvironmentService,
|
||||
useClass: DefaultEnvironmentService,
|
||||
deps: [StateProvider, AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
@ -545,7 +545,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
deps: [
|
||||
TokenServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
AppIdServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
LOGOUT_CALLBACK,
|
||||
@ -647,7 +647,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
LogService,
|
||||
STATE_FACTORY,
|
||||
AccountServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
TokenServiceAbstraction,
|
||||
MigrationRunner,
|
||||
STATE_SERVICE_USE_CACHE,
|
||||
@ -711,7 +711,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
SyncServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
LOGOUT_CALLBACK,
|
||||
StateServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
@ -853,8 +853,9 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
StateServiceAbstraction,
|
||||
ConfigApiServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
EnvironmentService,
|
||||
LogService,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@ -869,7 +870,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
safeProvider({
|
||||
provide: AnonymousHubServiceAbstraction,
|
||||
useClass: AnonymousHubService,
|
||||
deps: [EnvironmentServiceAbstraction, LoginStrategyServiceAbstraction, LogService],
|
||||
deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ValidationServiceAbstraction,
|
||||
@ -949,7 +950,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginApiServiceAbstraction,
|
||||
useClass: WebAuthnLoginApiService,
|
||||
deps: [ApiServiceAbstraction, EnvironmentServiceAbstraction],
|
||||
deps: [ApiServiceAbstraction, EnvironmentService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginServiceAbstraction,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { BehaviorSubject, Subject, concatMap, firstValueFrom, map, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
@ -123,7 +123,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
{ name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true },
|
||||
{ name: i18nService.t("sendTypeText"), value: SendType.Text, premium: false },
|
||||
];
|
||||
this.sendLinkBaseUrl = this.environmentService.getSendUrl();
|
||||
}
|
||||
|
||||
get link(): string {
|
||||
@ -190,6 +189,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
this.sendLinkBaseUrl = env.getSendUrl();
|
||||
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((hasPremiumFromAnySource) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@ -198,9 +198,9 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
copy(s: SendView) {
|
||||
const sendLinkBaseUrl = this.environmentService.getSendUrl();
|
||||
const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key;
|
||||
async copy(s: SendView) {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key;
|
||||
this.platformUtilsService.copyToClipboard(link);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
|
@ -39,11 +39,12 @@ export class IconComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const iconsUrl = this.environmentService.getIconsUrl();
|
||||
|
||||
this.data$ = combineLatest([
|
||||
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
|
||||
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
|
||||
this.cipher$.pipe(filter((c) => c !== undefined)),
|
||||
]).pipe(map(([showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)));
|
||||
]).pipe(
|
||||
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { OnInit, Directive } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
@ -11,12 +11,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class PremiumComponent {
|
||||
export class PremiumComponent implements OnInit {
|
||||
isPremium$: Observable<boolean>;
|
||||
price = 10;
|
||||
refreshPromise: Promise<any>;
|
||||
cloudWebVaultUrl: string;
|
||||
private directiveIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
@ -25,13 +24,16 @@ export class PremiumComponent {
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected dialogService: DialogService,
|
||||
environmentService: EnvironmentService,
|
||||
private environmentService: EnvironmentService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
this.cloudWebVaultUrl = environmentService.getCloudWebVaultUrl();
|
||||
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.refreshPromise = this.apiService.refreshIdentityToken();
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
@ -8,7 +9,10 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@ -145,8 +149,11 @@ describe("UserApiLoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
const env = mock<Environment>();
|
||||
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
environmentService.environment$ = new BehaviorSubject(env);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
@ -160,8 +167,11 @@ describe("UserApiLoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
const env = mock<Environment>();
|
||||
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
environmentService.environment$ = new BehaviorSubject(env);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@ -85,7 +85,8 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
if (response.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const keyConnectorUrl = env.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
export class EnvironmentUrls {
|
||||
base: string = null;
|
||||
api: string = null;
|
||||
identity: string = null;
|
||||
icons: string = null;
|
||||
notifications: string = null;
|
||||
events: string = null;
|
||||
webVault: string = null;
|
||||
keyConnector: string = null;
|
||||
|
||||
static fromJSON(obj: Jsonify<EnvironmentUrls>): EnvironmentUrls {
|
||||
return Object.assign(new EnvironmentUrls(), obj);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import {
|
||||
IHubProtocol,
|
||||
} from "@microsoft/signalr";
|
||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../../../../auth/src/common/abstractions/login-strategy.service";
|
||||
import {
|
||||
@ -26,7 +27,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
||||
) {}
|
||||
|
||||
async createHubConnection(token: string) {
|
||||
this.url = this.environmentService.getNotificationsUrl();
|
||||
this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl();
|
||||
|
||||
this.anonHubConnection = new HubConnectionBuilder()
|
||||
.withUrl(this.url + "/anonymous-hub?Token=" + token, {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { EnvironmentService } from "../../../platform/abstractions/environment.service";
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
@ -11,13 +13,14 @@ export class WebAuthnLoginApiService implements WebAuthnLoginApiServiceAbstracti
|
||||
) {}
|
||||
|
||||
async getCredentialAssertionOptions(): Promise<CredentialAssertionOptionsResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
`/accounts/webauthn/assertion-options`,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
this.environmentService.getIdentityUrl(),
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new CredentialAssertionOptionsResponse(response);
|
||||
}
|
||||
|
@ -14,64 +14,119 @@ export type Urls = {
|
||||
scim?: string;
|
||||
};
|
||||
|
||||
export type PayPalConfig = {
|
||||
businessId?: string;
|
||||
buttonAction?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A subset of available regions, additional regions can be loaded through configuration.
|
||||
*/
|
||||
export enum Region {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
|
||||
export enum RegionDomain {
|
||||
US = "bitwarden.com",
|
||||
EU = "bitwarden.eu",
|
||||
USQA = "bitwarden.pw",
|
||||
/**
|
||||
* The possible cloud regions.
|
||||
*/
|
||||
export type CloudRegion = Exclude<Region, Region.SelfHosted>;
|
||||
|
||||
export type RegionConfig = {
|
||||
// Beware this isn't completely true, it's actually a string for custom environments,
|
||||
// which are currently only supported in web where it doesn't matter.
|
||||
key: Region;
|
||||
domain: string;
|
||||
urls: Urls;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Environment interface represents a server environment.
|
||||
*
|
||||
* It provides methods to retrieve the URLs of the different services.
|
||||
*/
|
||||
export interface Environment {
|
||||
/**
|
||||
* Retrieve the current region.
|
||||
*/
|
||||
getRegion(): Region;
|
||||
/**
|
||||
* Retrieve the urls, should only be used when configuring the environment.
|
||||
*/
|
||||
getUrls(): Urls;
|
||||
|
||||
/**
|
||||
* Identify if the region is a cloud environment.
|
||||
*
|
||||
* @returns true if the environment is a cloud environment, false otherwise.
|
||||
*/
|
||||
isCloud(): boolean;
|
||||
|
||||
getApiUrl(): string;
|
||||
getEventsUrl(): string;
|
||||
getIconsUrl(): string;
|
||||
getIdentityUrl(): string;
|
||||
|
||||
/**
|
||||
* @deprecated This is currently only used by the CLI. This functionality should be extracted since
|
||||
* the CLI relies on changing environment mid-login.
|
||||
*
|
||||
* @remarks
|
||||
* Expect this to be null unless the CLI has explicitly set it during the login flow.
|
||||
*/
|
||||
getKeyConnectorUrl(): string | null;
|
||||
getNotificationsUrl(): string;
|
||||
getScimUrl(): string;
|
||||
getSendUrl(): string;
|
||||
getWebVaultUrl(): string;
|
||||
|
||||
/**
|
||||
* Get a friendly hostname for the environment.
|
||||
*
|
||||
* - For self-hosted this is the web vault url without protocol prefix.
|
||||
* - For cloud environments it's the domain key.
|
||||
*/
|
||||
getHostname(): string;
|
||||
|
||||
// Not sure why we provide this, evaluate if we can remove it.
|
||||
hasBaseUrl(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The environment service. Provides access to set the current environment urls and region.
|
||||
*/
|
||||
export abstract class EnvironmentService {
|
||||
urls: Observable<void>;
|
||||
usUrls: Urls;
|
||||
euUrls: Urls;
|
||||
selectedRegion?: Region;
|
||||
initialized = true;
|
||||
abstract environment$: Observable<Environment>;
|
||||
abstract cloudWebVaultUrl$: Observable<string>;
|
||||
|
||||
hasBaseUrl: () => boolean;
|
||||
getNotificationsUrl: () => string;
|
||||
getWebVaultUrl: () => string;
|
||||
/**
|
||||
* Retrieves the URL of the cloud web vault app.
|
||||
* Retrieve all the available regions for environment selectors.
|
||||
*
|
||||
* @returns {string} The URL of the cloud web vault app.
|
||||
* @remarks Use this method only in views exclusive to self-host instances.
|
||||
* This currently relies on compile time provided constants, and will not change at runtime.
|
||||
* Expect all builds to include production environments, QA builds to also include QA
|
||||
* environments and dev builds to include localhost.
|
||||
*/
|
||||
getCloudWebVaultUrl: () => string;
|
||||
abstract availableRegions(): RegionConfig[];
|
||||
|
||||
/**
|
||||
* Set the global environment.
|
||||
*/
|
||||
abstract setEnvironment(region: Region, urls?: Urls): Promise<Urls>;
|
||||
|
||||
/**
|
||||
* Seed the environment state for a given user based on the global environment.
|
||||
*
|
||||
* @remarks
|
||||
* Expected to be called only by the StateService when adding a new account.
|
||||
*/
|
||||
abstract seedUserEnvironment(userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the URL of the cloud web vault app based on the region parameter.
|
||||
*
|
||||
* @param {Region} region - The region of the cloud web vault app.
|
||||
* @param userId - The user id to set the cloud web vault app URL for. If null or undefined the global environment is set.
|
||||
* @param region - The region of the cloud web vault app.
|
||||
*/
|
||||
setCloudWebVaultUrl: (region: Region) => void;
|
||||
abstract setCloudRegion(userId: UserId, region: Region): Promise<void>;
|
||||
|
||||
/**
|
||||
* Seed the environment for a given user based on the globally set defaults.
|
||||
* Get the environment from state. Useful if you need to get the environment for another user.
|
||||
*/
|
||||
seedUserEnvironment: (userId: UserId) => Promise<void>;
|
||||
|
||||
getSendUrl: () => string;
|
||||
getIconsUrl: () => string;
|
||||
getApiUrl: () => string;
|
||||
getIdentityUrl: () => string;
|
||||
getEventsUrl: () => string;
|
||||
getKeyConnectorUrl: () => string;
|
||||
getScimUrl: () => string;
|
||||
setUrlsFromStorage: () => Promise<void>;
|
||||
setUrls: (urls: Urls) => Promise<Urls>;
|
||||
getHost: (userId?: string) => Promise<string>;
|
||||
setRegion: (region: Region) => Promise<void>;
|
||||
getUrls: () => Urls;
|
||||
isCloud: () => boolean;
|
||||
isEmpty: () => boolean;
|
||||
abstract getEnvironment(userId?: string): Promise<Environment | undefined>;
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { ReplaySubject, skip, take } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
@ -14,6 +16,7 @@ import {
|
||||
ServerConfigResponse,
|
||||
ThirdPartyServerConfigResponse,
|
||||
} from "../../models/response/server-config.response";
|
||||
import { StateProvider } from "../../state";
|
||||
|
||||
import { ConfigService } from "./config.service";
|
||||
|
||||
@ -23,6 +26,8 @@ describe("ConfigService", () => {
|
||||
let authService: MockProxy<AuthService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let replaySubject: ReplaySubject<Environment>;
|
||||
let stateProvider: StateProvider;
|
||||
|
||||
let serverResponseCount: number; // increments to track distinct responses received from server
|
||||
|
||||
@ -35,6 +40,7 @@ describe("ConfigService", () => {
|
||||
authService,
|
||||
environmentService,
|
||||
logService,
|
||||
stateProvider,
|
||||
);
|
||||
configService.init();
|
||||
return configService;
|
||||
@ -46,8 +52,11 @@ describe("ConfigService", () => {
|
||||
authService = mock();
|
||||
environmentService = mock();
|
||||
logService = mock();
|
||||
replaySubject = new ReplaySubject<Environment>(1);
|
||||
const accountService = mockAccountServiceWith("0" as UserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
environmentService.urls = new ReplaySubject<void>(1);
|
||||
environmentService.environment$ = replaySubject.asObservable();
|
||||
|
||||
serverResponseCount = 1;
|
||||
configApiService.get.mockImplementation(() =>
|
||||
@ -139,7 +148,7 @@ describe("ConfigService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
(environmentService.urls as ReplaySubject<void>).next();
|
||||
replaySubject.next(null);
|
||||
});
|
||||
|
||||
it("when triggerServerConfigFetch() is called", (done) => {
|
||||
|
@ -22,6 +22,7 @@ import { EnvironmentService, Region } from "../../abstractions/environment.servi
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
import { StateProvider } from "../../state";
|
||||
|
||||
const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600;
|
||||
|
||||
@ -44,6 +45,7 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
private authService: AuthService,
|
||||
private environmentService: EnvironmentService,
|
||||
private logService: LogService,
|
||||
private stateProvider: StateProvider,
|
||||
|
||||
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
|
||||
private subscribe = true,
|
||||
@ -67,7 +69,7 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here
|
||||
merge(
|
||||
this.refreshTimer$, // an overridable interval
|
||||
this.environmentService.urls, // when environment URLs change (including when app is started)
|
||||
this.environmentService.environment$, // when environment URLs change (including when app is started)
|
||||
this._forceFetchConfig, // manual
|
||||
)
|
||||
.pipe(
|
||||
@ -104,8 +106,9 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
await this.stateService.setServerConfig(data);
|
||||
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
|
||||
await this.environmentService.setCloudRegion(userId, data.environment?.cloudRegion);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,418 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, awaitAsync } from "../../../spec";
|
||||
import { FakeAccountService } from "../../../spec/fake-account-service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CloudRegion, Region } from "../abstractions/environment.service";
|
||||
|
||||
import {
|
||||
ENVIRONMENT_KEY,
|
||||
DefaultEnvironmentService,
|
||||
EnvironmentUrls,
|
||||
} from "./default-environment.service";
|
||||
|
||||
// There are a few main states EnvironmentService could be in when first used
|
||||
// 1. Not initialized, no active user. Hopefully not to likely but possible
|
||||
// 2. Not initialized, with active user. Not likely
|
||||
// 3. Initialized, no active user.
|
||||
// 4. Initialized, with active user.
|
||||
describe("EnvironmentService", () => {
|
||||
let accountService: FakeAccountService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
let sut: DefaultEnvironmentService;
|
||||
|
||||
const testUser = "00000000-0000-1000-a000-000000000001" as UserId;
|
||||
const alternateTestUser = "00000000-0000-1000-a000-000000000002" as UserId;
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = new FakeAccountService({
|
||||
[testUser]: {
|
||||
name: "name",
|
||||
email: "email",
|
||||
status: AuthenticationStatus.Locked,
|
||||
},
|
||||
[alternateTestUser]: {
|
||||
name: "name",
|
||||
email: "email",
|
||||
status: AuthenticationStatus.Locked,
|
||||
},
|
||||
});
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
sut = new DefaultEnvironmentService(stateProvider, accountService);
|
||||
});
|
||||
|
||||
const switchUser = async (userId: UserId) => {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
});
|
||||
await awaitAsync();
|
||||
};
|
||||
|
||||
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
|
||||
stateProvider.global.getFake(ENVIRONMENT_KEY).stateSubject.next({
|
||||
region: region,
|
||||
urls: environmentUrls,
|
||||
});
|
||||
};
|
||||
|
||||
const setUserData = (
|
||||
region: Region,
|
||||
environmentUrls: EnvironmentUrls,
|
||||
userId: UserId = testUser,
|
||||
) => {
|
||||
stateProvider.singleUser.getFake(userId, ENVIRONMENT_KEY).nextState({
|
||||
region: region,
|
||||
urls: environmentUrls,
|
||||
});
|
||||
};
|
||||
|
||||
const REGION_SETUP = [
|
||||
{
|
||||
region: Region.US,
|
||||
expectedUrls: {
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
api: "https://api.bitwarden.com",
|
||||
icons: "https://icons.bitwarden.net",
|
||||
notifications: "https://notifications.bitwarden.com",
|
||||
events: "https://events.bitwarden.com",
|
||||
scim: "https://scim.bitwarden.com/v2",
|
||||
send: "https://send.bitwarden.com/#",
|
||||
},
|
||||
},
|
||||
{
|
||||
region: Region.EU,
|
||||
expectedUrls: {
|
||||
webVault: "https://vault.bitwarden.eu",
|
||||
identity: "https://identity.bitwarden.eu",
|
||||
api: "https://api.bitwarden.eu",
|
||||
icons: "https://icons.bitwarden.eu",
|
||||
notifications: "https://notifications.bitwarden.eu",
|
||||
events: "https://events.bitwarden.eu",
|
||||
scim: "https://scim.bitwarden.eu/v2",
|
||||
send: "https://vault.bitwarden.eu/#/send/",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("with user", () => {
|
||||
it.each(REGION_SETUP)(
|
||||
"sets correct urls for each region %s",
|
||||
async ({ region, expectedUrls }) => {
|
||||
setUserData(region, new EnvironmentUrls());
|
||||
await switchUser(testUser);
|
||||
|
||||
const env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.hasBaseUrl()).toBe(false);
|
||||
expect(env.getWebVaultUrl()).toBe(expectedUrls.webVault);
|
||||
expect(env.getIdentityUrl()).toBe(expectedUrls.identity);
|
||||
expect(env.getApiUrl()).toBe(expectedUrls.api);
|
||||
expect(env.getIconsUrl()).toBe(expectedUrls.icons);
|
||||
expect(env.getNotificationsUrl()).toBe(expectedUrls.notifications);
|
||||
expect(env.getEventsUrl()).toBe(expectedUrls.events);
|
||||
expect(env.getScimUrl()).toBe(expectedUrls.scim);
|
||||
expect(env.getSendUrl()).toBe(expectedUrls.send);
|
||||
expect(env.getKeyConnectorUrl()).toBe(undefined);
|
||||
expect(env.isCloud()).toBe(true);
|
||||
expect(env.getUrls()).toEqual({
|
||||
base: null,
|
||||
cloudWebVault: undefined,
|
||||
webVault: expectedUrls.webVault,
|
||||
identity: expectedUrls.identity,
|
||||
api: expectedUrls.api,
|
||||
icons: expectedUrls.icons,
|
||||
notifications: expectedUrls.notifications,
|
||||
events: expectedUrls.events,
|
||||
scim: expectedUrls.scim.replace("/v2", ""),
|
||||
keyConnector: undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("returns user data", async () => {
|
||||
const globalEnvironmentUrls = new EnvironmentUrls();
|
||||
globalEnvironmentUrls.base = "https://global-url.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalEnvironmentUrls);
|
||||
|
||||
const userEnvironmentUrls = new EnvironmentUrls();
|
||||
userEnvironmentUrls.base = "https://user-url.example.com";
|
||||
setUserData(Region.SelfHosted, userEnvironmentUrls);
|
||||
|
||||
await switchUser(testUser);
|
||||
|
||||
const env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.getWebVaultUrl()).toBe("https://user-url.example.com");
|
||||
expect(env.getIdentityUrl()).toBe("https://user-url.example.com/identity");
|
||||
expect(env.getApiUrl()).toBe("https://user-url.example.com/api");
|
||||
expect(env.getIconsUrl()).toBe("https://user-url.example.com/icons");
|
||||
expect(env.getNotificationsUrl()).toBe("https://user-url.example.com/notifications");
|
||||
expect(env.getEventsUrl()).toBe("https://user-url.example.com/events");
|
||||
expect(env.getScimUrl()).toBe("https://user-url.example.com/scim/v2");
|
||||
expect(env.getSendUrl()).toBe("https://user-url.example.com/#/send/");
|
||||
expect(env.isCloud()).toBe(false);
|
||||
expect(env.getUrls()).toEqual({
|
||||
base: "https://user-url.example.com",
|
||||
api: null,
|
||||
cloudWebVault: undefined,
|
||||
events: null,
|
||||
icons: null,
|
||||
identity: null,
|
||||
keyConnector: null,
|
||||
notifications: null,
|
||||
scim: null,
|
||||
webVault: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("without user", () => {
|
||||
it.each(REGION_SETUP)("gets default urls %s", async ({ region, expectedUrls }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
const env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.hasBaseUrl()).toBe(false);
|
||||
expect(env.getWebVaultUrl()).toBe(expectedUrls.webVault);
|
||||
expect(env.getIdentityUrl()).toBe(expectedUrls.identity);
|
||||
expect(env.getApiUrl()).toBe(expectedUrls.api);
|
||||
expect(env.getIconsUrl()).toBe(expectedUrls.icons);
|
||||
expect(env.getNotificationsUrl()).toBe(expectedUrls.notifications);
|
||||
expect(env.getEventsUrl()).toBe(expectedUrls.events);
|
||||
expect(env.getScimUrl()).toBe(expectedUrls.scim);
|
||||
expect(env.getSendUrl()).toBe(expectedUrls.send);
|
||||
expect(env.getKeyConnectorUrl()).toBe(undefined);
|
||||
expect(env.isCloud()).toBe(true);
|
||||
expect(env.getUrls()).toEqual({
|
||||
base: null,
|
||||
cloudWebVault: undefined,
|
||||
webVault: expectedUrls.webVault,
|
||||
identity: expectedUrls.identity,
|
||||
api: expectedUrls.api,
|
||||
icons: expectedUrls.icons,
|
||||
notifications: expectedUrls.notifications,
|
||||
events: expectedUrls.events,
|
||||
scim: expectedUrls.scim.replace("/v2", ""),
|
||||
keyConnector: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("gets global data", async () => {
|
||||
const globalEnvironmentUrls = new EnvironmentUrls();
|
||||
globalEnvironmentUrls.base = "https://global-url.example.com";
|
||||
globalEnvironmentUrls.keyConnector = "https://global-key-connector.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalEnvironmentUrls);
|
||||
|
||||
const userEnvironmentUrls = new EnvironmentUrls();
|
||||
userEnvironmentUrls.base = "https://user-url.example.com";
|
||||
userEnvironmentUrls.keyConnector = "https://user-key-connector.example.com";
|
||||
setUserData(Region.SelfHosted, userEnvironmentUrls);
|
||||
|
||||
const env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.getWebVaultUrl()).toBe("https://global-url.example.com");
|
||||
expect(env.getIdentityUrl()).toBe("https://global-url.example.com/identity");
|
||||
expect(env.getApiUrl()).toBe("https://global-url.example.com/api");
|
||||
expect(env.getIconsUrl()).toBe("https://global-url.example.com/icons");
|
||||
expect(env.getNotificationsUrl()).toBe("https://global-url.example.com/notifications");
|
||||
expect(env.getEventsUrl()).toBe("https://global-url.example.com/events");
|
||||
expect(env.getScimUrl()).toBe("https://global-url.example.com/scim/v2");
|
||||
expect(env.getSendUrl()).toBe("https://global-url.example.com/#/send/");
|
||||
expect(env.getKeyConnectorUrl()).toBe("https://global-key-connector.example.com");
|
||||
expect(env.isCloud()).toBe(false);
|
||||
expect(env.getUrls()).toEqual({
|
||||
api: null,
|
||||
base: "https://global-url.example.com",
|
||||
cloudWebVault: undefined,
|
||||
webVault: null,
|
||||
events: null,
|
||||
icons: null,
|
||||
identity: null,
|
||||
keyConnector: "https://global-key-connector.example.com",
|
||||
notifications: null,
|
||||
scim: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setEnvironment", () => {
|
||||
it("self-hosted with base-url", async () => {
|
||||
await sut.setEnvironment(Region.SelfHosted, {
|
||||
base: "base.example.com",
|
||||
});
|
||||
await awaitAsync();
|
||||
|
||||
const env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.getRegion()).toBe(Region.SelfHosted);
|
||||
expect(env.getUrls()).toEqual({
|
||||
base: "https://base.example.com",
|
||||
api: null,
|
||||
identity: null,
|
||||
webVault: null,
|
||||
icons: null,
|
||||
notifications: null,
|
||||
scim: null,
|
||||
events: null,
|
||||
keyConnector: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("self-hosted and sets all urls", async () => {
|
||||
let env = await firstValueFrom(sut.environment$);
|
||||
expect(env.getScimUrl()).toBe("https://scim.bitwarden.com/v2");
|
||||
|
||||
await sut.setEnvironment(Region.SelfHosted, {
|
||||
base: "base.example.com",
|
||||
api: "api.example.com",
|
||||
identity: "identity.example.com",
|
||||
webVault: "vault.example.com",
|
||||
icons: "icons.example.com",
|
||||
notifications: "notifications.example.com",
|
||||
scim: "scim.example.com",
|
||||
});
|
||||
|
||||
env = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(env.getRegion()).toBe(Region.SelfHosted);
|
||||
expect(env.getUrls()).toEqual({
|
||||
base: "https://base.example.com",
|
||||
api: "https://api.example.com",
|
||||
identity: "https://identity.example.com",
|
||||
webVault: "https://vault.example.com",
|
||||
icons: "https://icons.example.com",
|
||||
notifications: "https://notifications.example.com",
|
||||
scim: null,
|
||||
events: null,
|
||||
keyConnector: null,
|
||||
});
|
||||
expect(env.getScimUrl()).toBe("https://vault.example.com/scim/v2");
|
||||
});
|
||||
|
||||
it("sets the region", async () => {
|
||||
await sut.setEnvironment(Region.US);
|
||||
|
||||
const data = await firstValueFrom(sut.environment$);
|
||||
|
||||
expect(data.getRegion()).toBe(Region.US);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnvironment", () => {
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])("gets it from user data if there is an active user", async ({ region, expectedHost }) => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(region, new EnvironmentUrls());
|
||||
|
||||
await switchUser(testUser);
|
||||
|
||||
const env = await sut.getEnvironment();
|
||||
expect(env.getHostname()).toBe(expectedHost);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])("gets it from global data if there is no active user", async ({ region, expectedHost }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
|
||||
const env = await sut.getEnvironment();
|
||||
expect(env.getHostname()).toBe(expectedHost);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])(
|
||||
"gets it from global state if there is no active user even if a user id is passed in.",
|
||||
async ({ region, expectedHost }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
|
||||
const env = await sut.getEnvironment(testUser);
|
||||
expect(env.getHostname()).toBe(expectedHost);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])(
|
||||
"gets it from the passed in userId if there is any active user: %s",
|
||||
async ({ region, expectedHost }) => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
setUserData(region, new EnvironmentUrls(), alternateTestUser);
|
||||
|
||||
await switchUser(testUser);
|
||||
|
||||
const env = await sut.getEnvironment(alternateTestUser);
|
||||
expect(env.getHostname()).toBe(expectedHost);
|
||||
},
|
||||
);
|
||||
|
||||
it("gets it from base url saved in self host config", async () => {
|
||||
const globalSelfHostUrls = new EnvironmentUrls();
|
||||
globalSelfHostUrls.base = "https://base.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalSelfHostUrls);
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
const env = await sut.getEnvironment();
|
||||
expect(env.getHostname()).toBe("base.example.com");
|
||||
});
|
||||
|
||||
it("gets it from webVault url saved in self host config", async () => {
|
||||
const globalSelfHostUrls = new EnvironmentUrls();
|
||||
globalSelfHostUrls.webVault = "https://vault.example.com";
|
||||
globalSelfHostUrls.base = "https://base.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalSelfHostUrls);
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
const env = await sut.getEnvironment();
|
||||
expect(env.getHostname()).toBe("vault.example.com");
|
||||
});
|
||||
|
||||
it("gets it from saved self host config from passed in user when there is an active user", async () => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
const selfHostUserUrls = new EnvironmentUrls();
|
||||
selfHostUserUrls.base = "https://base.example.com";
|
||||
setUserData(Region.SelfHosted, selfHostUserUrls, alternateTestUser);
|
||||
|
||||
await switchUser(testUser);
|
||||
|
||||
const env = await sut.getEnvironment(alternateTestUser);
|
||||
expect(env.getHostname()).toBe("base.example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cloudWebVaultUrl$", () => {
|
||||
it("no extra initialization, returns US vault", async () => {
|
||||
expect(await firstValueFrom(sut.cloudWebVaultUrl$)).toBe("https://vault.bitwarden.com");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedVault: "https://vault.bitwarden.com" },
|
||||
{ region: Region.EU, expectedVault: "https://vault.bitwarden.eu" },
|
||||
{ region: Region.SelfHosted, expectedVault: "https://vault.bitwarden.com" },
|
||||
])(
|
||||
"no extra initialization, returns expected host for each region %s",
|
||||
async ({ region, expectedVault }) => {
|
||||
await switchUser(testUser);
|
||||
|
||||
expect(await sut.setCloudRegion(testUser, region as CloudRegion));
|
||||
expect(await firstValueFrom(sut.cloudWebVaultUrl$)).toBe(expectedVault);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
433
libs/common/src/platform/services/default-environment.service.ts
Normal file
433
libs/common/src/platform/services/default-environment.service.ts
Normal file
@ -0,0 +1,433 @@
|
||||
import { distinctUntilChanged, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Environment,
|
||||
Region,
|
||||
RegionConfig,
|
||||
Urls,
|
||||
CloudRegion,
|
||||
} from "../abstractions/environment.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import {
|
||||
ENVIRONMENT_DISK,
|
||||
ENVIRONMENT_MEMORY,
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
} from "../state";
|
||||
|
||||
export class EnvironmentUrls {
|
||||
base: string = null;
|
||||
api: string = null;
|
||||
identity: string = null;
|
||||
icons: string = null;
|
||||
notifications: string = null;
|
||||
events: string = null;
|
||||
webVault: string = null;
|
||||
keyConnector: string = null;
|
||||
}
|
||||
|
||||
class EnvironmentState {
|
||||
region: Region;
|
||||
urls: EnvironmentUrls;
|
||||
|
||||
static fromJSON(obj: Jsonify<EnvironmentState>): EnvironmentState {
|
||||
return Object.assign(new EnvironmentState(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
export const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>(
|
||||
ENVIRONMENT_DISK,
|
||||
"environment",
|
||||
{
|
||||
deserializer: EnvironmentState.fromJSON,
|
||||
},
|
||||
);
|
||||
|
||||
export const CLOUD_REGION_KEY = new KeyDefinition<CloudRegion>(ENVIRONMENT_MEMORY, "cloudRegion", {
|
||||
deserializer: (b) => b,
|
||||
});
|
||||
|
||||
/**
|
||||
* The production regions available for selection.
|
||||
*
|
||||
* In the future we desire to load these urls from the config endpoint.
|
||||
*/
|
||||
export const PRODUCTION_REGIONS: RegionConfig[] = [
|
||||
{
|
||||
key: Region.US,
|
||||
domain: "bitwarden.com",
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: Region.EU,
|
||||
domain: "bitwarden.eu",
|
||||
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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The default region when starting the app.
|
||||
*/
|
||||
const DEFAULT_REGION = Region.US;
|
||||
|
||||
/**
|
||||
* The default region configuration.
|
||||
*/
|
||||
const DEFAULT_REGION_CONFIG = PRODUCTION_REGIONS.find((r) => r.key === DEFAULT_REGION);
|
||||
|
||||
export class DefaultEnvironmentService implements EnvironmentService {
|
||||
private globalState: GlobalState<EnvironmentState | null>;
|
||||
private globalCloudRegionState: GlobalState<CloudRegion | null>;
|
||||
|
||||
// We intentionally don't want the helper on account service, we want the null back if there is no active user
|
||||
private activeAccountId$: Observable<UserId | null> = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
);
|
||||
|
||||
environment$: Observable<Environment>;
|
||||
cloudWebVaultUrl$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.globalState = this.stateProvider.getGlobal(ENVIRONMENT_KEY);
|
||||
this.globalCloudRegionState = this.stateProvider.getGlobal(CLOUD_REGION_KEY);
|
||||
|
||||
const account$ = this.activeAccountId$.pipe(
|
||||
// Use == here to not trigger on undefined -> null transition
|
||||
distinctUntilChanged((oldUserId: UserId, newUserId: UserId) => oldUserId == newUserId),
|
||||
);
|
||||
|
||||
this.environment$ = account$.pipe(
|
||||
switchMap((userId) => {
|
||||
const t = userId
|
||||
? this.stateProvider.getUser(userId, ENVIRONMENT_KEY).state$
|
||||
: this.stateProvider.getGlobal(ENVIRONMENT_KEY).state$;
|
||||
return t;
|
||||
}),
|
||||
map((state) => {
|
||||
return this.buildEnvironment(state?.region, state?.urls);
|
||||
}),
|
||||
);
|
||||
this.cloudWebVaultUrl$ = account$.pipe(
|
||||
switchMap((userId) => {
|
||||
const t = userId
|
||||
? this.stateProvider.getUser(userId, CLOUD_REGION_KEY).state$
|
||||
: this.stateProvider.getGlobal(CLOUD_REGION_KEY).state$;
|
||||
return t;
|
||||
}),
|
||||
map((region) => {
|
||||
if (region != null) {
|
||||
const config = this.getRegionConfig(region);
|
||||
|
||||
if (config != null) {
|
||||
return config.urls.webVault;
|
||||
}
|
||||
}
|
||||
return DEFAULT_REGION_CONFIG.urls.webVault;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
availableRegions(): RegionConfig[] {
|
||||
const additionalRegions = (process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[]) ?? [];
|
||||
return PRODUCTION_REGIONS.concat(additionalRegions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region configuration for the given region.
|
||||
*/
|
||||
private getRegionConfig(region: Region): RegionConfig | undefined {
|
||||
return this.availableRegions().find((r) => r.key === region);
|
||||
}
|
||||
|
||||
async setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||
// Unknown regions are treated as self-hosted
|
||||
if (this.getRegionConfig(region) == null) {
|
||||
region = Region.SelfHosted;
|
||||
}
|
||||
|
||||
// If self-hosted ensure urls are valid else fallback to default region
|
||||
if (region == Region.SelfHosted && isEmpty(urls)) {
|
||||
region = DEFAULT_REGION;
|
||||
}
|
||||
|
||||
if (region != Region.SelfHosted) {
|
||||
await this.globalState.update(() => ({
|
||||
region: region,
|
||||
urls: null,
|
||||
}));
|
||||
|
||||
return null;
|
||||
} else {
|
||||
// Clean the urls
|
||||
urls.base = formatUrl(urls.base);
|
||||
urls.webVault = formatUrl(urls.webVault);
|
||||
urls.api = formatUrl(urls.api);
|
||||
urls.identity = formatUrl(urls.identity);
|
||||
urls.icons = formatUrl(urls.icons);
|
||||
urls.notifications = formatUrl(urls.notifications);
|
||||
urls.events = formatUrl(urls.events);
|
||||
urls.keyConnector = formatUrl(urls.keyConnector);
|
||||
urls.scim = null;
|
||||
|
||||
await this.globalState.update(() => ({
|
||||
region: region,
|
||||
urls: {
|
||||
base: urls.base,
|
||||
api: urls.api,
|
||||
identity: urls.identity,
|
||||
webVault: urls.webVault,
|
||||
icons: urls.icons,
|
||||
notifications: urls.notifications,
|
||||
events: urls.events,
|
||||
keyConnector: urls.keyConnector,
|
||||
},
|
||||
}));
|
||||
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for building the environment from state. Performs some general sanitization to avoid invalid regions and urls.
|
||||
*/
|
||||
protected buildEnvironment(region: Region, urls: Urls) {
|
||||
// Unknown regions are treated as self-hosted
|
||||
if (this.getRegionConfig(region) == null) {
|
||||
region = Region.SelfHosted;
|
||||
}
|
||||
|
||||
// If self-hosted ensure urls are valid else fallback to default region
|
||||
if (region == Region.SelfHosted && isEmpty(urls)) {
|
||||
region = DEFAULT_REGION;
|
||||
}
|
||||
|
||||
// Load urls from region config
|
||||
if (region != Region.SelfHosted) {
|
||||
const regionConfig = this.getRegionConfig(region);
|
||||
if (regionConfig != null) {
|
||||
return new CloudEnvironment(regionConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return new SelfHostedEnvironment(urls);
|
||||
}
|
||||
|
||||
async setCloudRegion(userId: UserId, region: CloudRegion) {
|
||||
if (userId == null) {
|
||||
await this.globalCloudRegionState.update(() => region);
|
||||
} else {
|
||||
await this.stateProvider.getUser(userId, CLOUD_REGION_KEY).update(() => region);
|
||||
}
|
||||
}
|
||||
|
||||
async getEnvironment(userId?: UserId) {
|
||||
if (userId == null) {
|
||||
return await firstValueFrom(this.environment$);
|
||||
}
|
||||
|
||||
const state = await this.getEnvironmentState(userId);
|
||||
return this.buildEnvironment(state.region, state.urls);
|
||||
}
|
||||
|
||||
private async getEnvironmentState(userId: UserId | null) {
|
||||
// Previous rules dictated that we only get from user scoped state if there is an active user.
|
||||
const activeUserId = await firstValueFrom(this.activeAccountId$);
|
||||
return activeUserId == null
|
||||
? await firstValueFrom(this.globalState.state$)
|
||||
: await firstValueFrom(
|
||||
this.stateProvider.getUser(userId ?? activeUserId, ENVIRONMENT_KEY).state$,
|
||||
);
|
||||
}
|
||||
|
||||
async seedUserEnvironment(userId: UserId) {
|
||||
const global = await firstValueFrom(this.globalState.state$);
|
||||
await this.stateProvider.getUser(userId, ENVIRONMENT_KEY).update(() => global);
|
||||
}
|
||||
}
|
||||
|
||||
function formatUrl(url: string): string {
|
||||
if (url == null || url === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
url = url.replace(/\/+$/g, "");
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
return url.trim();
|
||||
}
|
||||
|
||||
function isEmpty(u?: Urls): boolean {
|
||||
if (u == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
u.base == null &&
|
||||
u.webVault == null &&
|
||||
u.api == null &&
|
||||
u.identity == null &&
|
||||
u.icons == null &&
|
||||
u.notifications == null &&
|
||||
u.events == null
|
||||
);
|
||||
}
|
||||
|
||||
abstract class UrlEnvironment implements Environment {
|
||||
constructor(
|
||||
protected region: Region,
|
||||
protected urls: Urls,
|
||||
) {
|
||||
// Scim is always null for self-hosted
|
||||
if (region == Region.SelfHosted) {
|
||||
this.urls.scim = null;
|
||||
}
|
||||
}
|
||||
|
||||
abstract getHostname(): string;
|
||||
|
||||
getRegion() {
|
||||
return this.region;
|
||||
}
|
||||
|
||||
getUrls() {
|
||||
return {
|
||||
base: this.urls.base,
|
||||
webVault: this.urls.webVault,
|
||||
api: this.urls.api,
|
||||
identity: this.urls.identity,
|
||||
icons: this.urls.icons,
|
||||
notifications: this.urls.notifications,
|
||||
events: this.urls.events,
|
||||
keyConnector: this.urls.keyConnector,
|
||||
scim: this.urls.scim,
|
||||
};
|
||||
}
|
||||
|
||||
hasBaseUrl() {
|
||||
return this.urls.base != null;
|
||||
}
|
||||
|
||||
getWebVaultUrl() {
|
||||
return this.getUrl("webVault", "");
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
return this.getUrl("api", "/api");
|
||||
}
|
||||
|
||||
getEventsUrl() {
|
||||
return this.getUrl("events", "/events");
|
||||
}
|
||||
|
||||
getIconsUrl() {
|
||||
return this.getUrl("icons", "/icons");
|
||||
}
|
||||
|
||||
getIdentityUrl() {
|
||||
return this.getUrl("identity", "/identity");
|
||||
}
|
||||
|
||||
getKeyConnectorUrl() {
|
||||
return this.urls.keyConnector;
|
||||
}
|
||||
|
||||
getNotificationsUrl() {
|
||||
return this.getUrl("notifications", "/notifications");
|
||||
}
|
||||
|
||||
getScimUrl() {
|
||||
if (this.urls.scim != null) {
|
||||
return this.urls.scim + "/v2";
|
||||
}
|
||||
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://scim.bitwarden.com/v2"
|
||||
: this.getWebVaultUrl() + "/scim/v2";
|
||||
}
|
||||
|
||||
getSendUrl() {
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://send.bitwarden.com/#"
|
||||
: this.getWebVaultUrl() + "/#/send/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Presume that if the region is not self-hosted, it is cloud.
|
||||
*/
|
||||
isCloud(): boolean {
|
||||
return this.region !== Region.SelfHosted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getting an URL.
|
||||
*
|
||||
* @param key Key of the URL to get from URLs
|
||||
* @param baseSuffix Suffix to append to the base URL if the url is not set
|
||||
* @returns
|
||||
*/
|
||||
private getUrl(key: keyof Urls, baseSuffix: string) {
|
||||
if (this.urls[key] != null) {
|
||||
return this.urls[key];
|
||||
}
|
||||
|
||||
if (this.urls.base) {
|
||||
return this.urls.base + baseSuffix;
|
||||
}
|
||||
|
||||
return DEFAULT_REGION_CONFIG.urls[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Denote a cloud environment.
|
||||
*/
|
||||
export class CloudEnvironment extends UrlEnvironment {
|
||||
constructor(private config: RegionConfig) {
|
||||
super(config.key, config.urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud always returns nice urls, i.e. bitwarden.com instead of vault.bitwarden.com.
|
||||
*/
|
||||
getHostname() {
|
||||
return this.config.domain;
|
||||
}
|
||||
}
|
||||
|
||||
export class SelfHostedEnvironment extends UrlEnvironment {
|
||||
constructor(urls: Urls) {
|
||||
super(Region.SelfHosted, urls);
|
||||
}
|
||||
|
||||
getHostname() {
|
||||
return Utils.getHost(this.getWebVaultUrl());
|
||||
}
|
||||
}
|
@ -1,535 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, timeout } from "rxjs";
|
||||
|
||||
import { awaitAsync } from "../../../spec";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeStorageService } from "../../../spec/fake-storage.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { Region } from "../abstractions/environment.service";
|
||||
import { StateProvider } from "../state";
|
||||
/* eslint-disable import/no-restricted-paths -- Rare testing need */
|
||||
import { DefaultActiveUserStateProvider } from "../state/implementations/default-active-user-state.provider";
|
||||
import { DefaultDerivedStateProvider } from "../state/implementations/default-derived-state.provider";
|
||||
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
|
||||
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
|
||||
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
|
||||
import { StateEventRegistrarService } from "../state/state-event-registrar.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
|
||||
import { EnvironmentService } from "./environment.service";
|
||||
import { StorageServiceProvider } from "./storage-service.provider";
|
||||
|
||||
// There are a few main states EnvironmentService could be in when first used
|
||||
// 1. Not initialized, no active user. Hopefully not to likely but possible
|
||||
// 2. Not initialized, with active user. Not likely
|
||||
// 3. Initialized, no active user.
|
||||
// 4. Initialized, with active user.
|
||||
describe("EnvironmentService", () => {
|
||||
let diskStorageService: FakeStorageService;
|
||||
let memoryStorageService: FakeStorageService;
|
||||
let storageServiceProvider: StorageServiceProvider;
|
||||
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||
let accountService: FakeAccountService;
|
||||
let stateProvider: StateProvider;
|
||||
|
||||
let sut: EnvironmentService;
|
||||
|
||||
const testUser = "00000000-0000-1000-a000-000000000001" as UserId;
|
||||
const alternateTestUser = "00000000-0000-1000-a000-000000000002" as UserId;
|
||||
|
||||
beforeEach(async () => {
|
||||
diskStorageService = new FakeStorageService();
|
||||
memoryStorageService = new FakeStorageService();
|
||||
storageServiceProvider = new StorageServiceProvider(diskStorageService, memoryStorageService);
|
||||
|
||||
accountService = mockAccountServiceWith(undefined);
|
||||
const singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||
storageServiceProvider,
|
||||
stateEventRegistrarService,
|
||||
);
|
||||
stateProvider = new DefaultStateProvider(
|
||||
new DefaultActiveUserStateProvider(accountService, singleUserStateProvider),
|
||||
singleUserStateProvider,
|
||||
new DefaultGlobalStateProvider(storageServiceProvider),
|
||||
new DefaultDerivedStateProvider(memoryStorageService),
|
||||
);
|
||||
|
||||
sut = new EnvironmentService(stateProvider, accountService);
|
||||
});
|
||||
|
||||
const switchUser = async (userId: UserId) => {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
});
|
||||
await awaitAsync();
|
||||
};
|
||||
|
||||
const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => {
|
||||
const data = diskStorageService.internalStore;
|
||||
data["global_environment_region"] = region;
|
||||
data["global_environment_urls"] = environmentUrls;
|
||||
diskStorageService.internalUpdateStore(data);
|
||||
};
|
||||
|
||||
const getGlobalData = () => {
|
||||
const storage = diskStorageService.internalStore;
|
||||
return {
|
||||
region: storage?.["global_environment_region"],
|
||||
urls: storage?.["global_environment_urls"],
|
||||
};
|
||||
};
|
||||
|
||||
const setUserData = (
|
||||
region: Region,
|
||||
environmentUrls: EnvironmentUrls,
|
||||
userId: UserId = testUser,
|
||||
) => {
|
||||
const data = diskStorageService.internalStore;
|
||||
data[`user_${userId}_environment_region`] = region;
|
||||
data[`user_${userId}_environment_urls`] = environmentUrls;
|
||||
|
||||
diskStorageService.internalUpdateStore(data);
|
||||
};
|
||||
// END: CAN CHANGE
|
||||
|
||||
const initialize = async (options: { switchUser: boolean }) => {
|
||||
await sut.setUrlsFromStorage();
|
||||
sut.initialized = true;
|
||||
|
||||
if (options.switchUser) {
|
||||
await switchUser(testUser);
|
||||
}
|
||||
};
|
||||
|
||||
const REGION_SETUP = [
|
||||
{
|
||||
region: Region.US,
|
||||
expectedUrls: {
|
||||
webVault: "https://vault.bitwarden.com",
|
||||
identity: "https://identity.bitwarden.com",
|
||||
api: "https://api.bitwarden.com",
|
||||
icons: "https://icons.bitwarden.net",
|
||||
notifications: "https://notifications.bitwarden.com",
|
||||
events: "https://events.bitwarden.com",
|
||||
scim: "https://scim.bitwarden.com/v2",
|
||||
send: "https://send.bitwarden.com/#",
|
||||
},
|
||||
},
|
||||
{
|
||||
region: Region.EU,
|
||||
expectedUrls: {
|
||||
webVault: "https://vault.bitwarden.eu",
|
||||
identity: "https://identity.bitwarden.eu",
|
||||
api: "https://api.bitwarden.eu",
|
||||
icons: "https://icons.bitwarden.eu",
|
||||
notifications: "https://notifications.bitwarden.eu",
|
||||
events: "https://events.bitwarden.eu",
|
||||
scim: "https://scim.bitwarden.eu/v2",
|
||||
send: "https://vault.bitwarden.eu/#/send/",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("with user", () => {
|
||||
it.each(REGION_SETUP)(
|
||||
"sets correct urls for each region %s",
|
||||
async ({ region, expectedUrls }) => {
|
||||
setUserData(region, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
expect(sut.hasBaseUrl()).toBe(false);
|
||||
expect(sut.getWebVaultUrl()).toBe(expectedUrls.webVault);
|
||||
expect(sut.getIdentityUrl()).toBe(expectedUrls.identity);
|
||||
expect(sut.getApiUrl()).toBe(expectedUrls.api);
|
||||
expect(sut.getIconsUrl()).toBe(expectedUrls.icons);
|
||||
expect(sut.getNotificationsUrl()).toBe(expectedUrls.notifications);
|
||||
expect(sut.getEventsUrl()).toBe(expectedUrls.events);
|
||||
expect(sut.getScimUrl()).toBe(expectedUrls.scim);
|
||||
expect(sut.getSendUrl()).toBe(expectedUrls.send);
|
||||
expect(sut.getKeyConnectorUrl()).toBe(null);
|
||||
expect(sut.isCloud()).toBe(true);
|
||||
expect(sut.getUrls()).toEqual({
|
||||
base: null,
|
||||
cloudWebVault: undefined,
|
||||
webVault: expectedUrls.webVault,
|
||||
identity: expectedUrls.identity,
|
||||
api: expectedUrls.api,
|
||||
icons: expectedUrls.icons,
|
||||
notifications: expectedUrls.notifications,
|
||||
events: expectedUrls.events,
|
||||
scim: expectedUrls.scim.replace("/v2", ""),
|
||||
keyConnector: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("returns user data", async () => {
|
||||
const globalEnvironmentUrls = new EnvironmentUrls();
|
||||
globalEnvironmentUrls.base = "https://global-url.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalEnvironmentUrls);
|
||||
|
||||
const userEnvironmentUrls = new EnvironmentUrls();
|
||||
userEnvironmentUrls.base = "https://user-url.example.com";
|
||||
setUserData(Region.SelfHosted, userEnvironmentUrls);
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
expect(sut.getWebVaultUrl()).toBe("https://user-url.example.com");
|
||||
expect(sut.getIdentityUrl()).toBe("https://user-url.example.com/identity");
|
||||
expect(sut.getApiUrl()).toBe("https://user-url.example.com/api");
|
||||
expect(sut.getIconsUrl()).toBe("https://user-url.example.com/icons");
|
||||
expect(sut.getNotificationsUrl()).toBe("https://user-url.example.com/notifications");
|
||||
expect(sut.getEventsUrl()).toBe("https://user-url.example.com/events");
|
||||
expect(sut.getScimUrl()).toBe("https://user-url.example.com/scim/v2");
|
||||
expect(sut.getSendUrl()).toBe("https://user-url.example.com/#/send/");
|
||||
expect(sut.isCloud()).toBe(false);
|
||||
expect(sut.getUrls()).toEqual({
|
||||
base: "https://user-url.example.com",
|
||||
api: null,
|
||||
cloudWebVault: undefined,
|
||||
events: null,
|
||||
icons: null,
|
||||
identity: null,
|
||||
keyConnector: null,
|
||||
notifications: null,
|
||||
scim: null,
|
||||
webVault: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("without user", () => {
|
||||
it.each(REGION_SETUP)("gets default urls %s", async ({ region, expectedUrls }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
expect(sut.hasBaseUrl()).toBe(false);
|
||||
expect(sut.getWebVaultUrl()).toBe(expectedUrls.webVault);
|
||||
expect(sut.getIdentityUrl()).toBe(expectedUrls.identity);
|
||||
expect(sut.getApiUrl()).toBe(expectedUrls.api);
|
||||
expect(sut.getIconsUrl()).toBe(expectedUrls.icons);
|
||||
expect(sut.getNotificationsUrl()).toBe(expectedUrls.notifications);
|
||||
expect(sut.getEventsUrl()).toBe(expectedUrls.events);
|
||||
expect(sut.getScimUrl()).toBe(expectedUrls.scim);
|
||||
expect(sut.getSendUrl()).toBe(expectedUrls.send);
|
||||
expect(sut.getKeyConnectorUrl()).toBe(null);
|
||||
expect(sut.isCloud()).toBe(true);
|
||||
expect(sut.getUrls()).toEqual({
|
||||
base: null,
|
||||
cloudWebVault: undefined,
|
||||
webVault: expectedUrls.webVault,
|
||||
identity: expectedUrls.identity,
|
||||
api: expectedUrls.api,
|
||||
icons: expectedUrls.icons,
|
||||
notifications: expectedUrls.notifications,
|
||||
events: expectedUrls.events,
|
||||
scim: expectedUrls.scim.replace("/v2", ""),
|
||||
keyConnector: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("gets global data", async () => {
|
||||
const globalEnvironmentUrls = new EnvironmentUrls();
|
||||
globalEnvironmentUrls.base = "https://global-url.example.com";
|
||||
globalEnvironmentUrls.keyConnector = "https://global-key-connector.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalEnvironmentUrls);
|
||||
|
||||
const userEnvironmentUrls = new EnvironmentUrls();
|
||||
userEnvironmentUrls.base = "https://user-url.example.com";
|
||||
userEnvironmentUrls.keyConnector = "https://user-key-connector.example.com";
|
||||
setUserData(Region.SelfHosted, userEnvironmentUrls);
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
expect(sut.getWebVaultUrl()).toBe("https://global-url.example.com");
|
||||
expect(sut.getIdentityUrl()).toBe("https://global-url.example.com/identity");
|
||||
expect(sut.getApiUrl()).toBe("https://global-url.example.com/api");
|
||||
expect(sut.getIconsUrl()).toBe("https://global-url.example.com/icons");
|
||||
expect(sut.getNotificationsUrl()).toBe("https://global-url.example.com/notifications");
|
||||
expect(sut.getEventsUrl()).toBe("https://global-url.example.com/events");
|
||||
expect(sut.getScimUrl()).toBe("https://global-url.example.com/scim/v2");
|
||||
expect(sut.getSendUrl()).toBe("https://global-url.example.com/#/send/");
|
||||
expect(sut.getKeyConnectorUrl()).toBe("https://global-key-connector.example.com");
|
||||
expect(sut.isCloud()).toBe(false);
|
||||
expect(sut.getUrls()).toEqual({
|
||||
api: null,
|
||||
base: "https://global-url.example.com",
|
||||
cloudWebVault: undefined,
|
||||
webVault: null,
|
||||
events: null,
|
||||
icons: null,
|
||||
identity: null,
|
||||
keyConnector: "https://global-key-connector.example.com",
|
||||
notifications: null,
|
||||
scim: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("returns US defaults when not initialized", async () => {
|
||||
setGlobalData(Region.EU, new EnvironmentUrls());
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
expect(sut.initialized).toBe(false);
|
||||
|
||||
expect(sut.hasBaseUrl()).toBe(false);
|
||||
expect(sut.getWebVaultUrl()).toBe("https://vault.bitwarden.com");
|
||||
expect(sut.getIdentityUrl()).toBe("https://identity.bitwarden.com");
|
||||
expect(sut.getApiUrl()).toBe("https://api.bitwarden.com");
|
||||
expect(sut.getIconsUrl()).toBe("https://icons.bitwarden.net");
|
||||
expect(sut.getNotificationsUrl()).toBe("https://notifications.bitwarden.com");
|
||||
expect(sut.getEventsUrl()).toBe("https://events.bitwarden.com");
|
||||
expect(sut.getScimUrl()).toBe("https://scim.bitwarden.com/v2");
|
||||
expect(sut.getKeyConnectorUrl()).toBe(undefined);
|
||||
expect(sut.isCloud()).toBe(true);
|
||||
});
|
||||
|
||||
describe("setUrls", () => {
|
||||
it("set just a base url", async () => {
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
await sut.setUrls({
|
||||
base: "base.example.com",
|
||||
});
|
||||
|
||||
const globalData = getGlobalData();
|
||||
expect(globalData.region).toBe(Region.SelfHosted);
|
||||
expect(globalData.urls).toEqual({
|
||||
base: "https://base.example.com",
|
||||
api: null,
|
||||
identity: null,
|
||||
webVault: null,
|
||||
icons: null,
|
||||
notifications: null,
|
||||
events: null,
|
||||
keyConnector: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets all urls", async () => {
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
expect(sut.getScimUrl()).toBe("https://scim.bitwarden.com/v2");
|
||||
|
||||
await sut.setUrls({
|
||||
base: "base.example.com",
|
||||
api: "api.example.com",
|
||||
identity: "identity.example.com",
|
||||
webVault: "vault.example.com",
|
||||
icons: "icons.example.com",
|
||||
notifications: "notifications.example.com",
|
||||
scim: "scim.example.com",
|
||||
});
|
||||
|
||||
const globalData = getGlobalData();
|
||||
expect(globalData.region).toBe(Region.SelfHosted);
|
||||
expect(globalData.urls).toEqual({
|
||||
base: "https://base.example.com",
|
||||
api: "https://api.example.com",
|
||||
identity: "https://identity.example.com",
|
||||
webVault: "https://vault.example.com",
|
||||
icons: "https://icons.example.com",
|
||||
notifications: "https://notifications.example.com",
|
||||
events: null,
|
||||
keyConnector: null,
|
||||
});
|
||||
expect(sut.getScimUrl()).toBe("https://scim.example.com/v2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setRegion", () => {
|
||||
it("sets the region on the global object even if there is a user.", async () => {
|
||||
setGlobalData(Region.EU, new EnvironmentUrls());
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
await sut.setRegion(Region.US);
|
||||
|
||||
const globalData = getGlobalData();
|
||||
expect(globalData.region).toBe(Region.US);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHost", () => {
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])("gets it from user data if there is an active user", async ({ region, expectedHost }) => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(region, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
const host = await sut.getHost();
|
||||
expect(host).toBe(expectedHost);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])("gets it from global data if there is no active user", async ({ region, expectedHost }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
const host = await sut.getHost();
|
||||
expect(host).toBe(expectedHost);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])(
|
||||
"gets it from global state if there is no active user even if a user id is passed in.",
|
||||
async ({ region, expectedHost }) => {
|
||||
setGlobalData(region, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
const host = await sut.getHost(testUser);
|
||||
expect(host).toBe(expectedHost);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedHost: "bitwarden.com" },
|
||||
{ region: Region.EU, expectedHost: "bitwarden.eu" },
|
||||
])(
|
||||
"gets it from the passed in userId if there is any active user: %s",
|
||||
async ({ region, expectedHost }) => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(Region.US, new EnvironmentUrls());
|
||||
setUserData(region, new EnvironmentUrls(), alternateTestUser);
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
const host = await sut.getHost(alternateTestUser);
|
||||
expect(host).toBe(expectedHost);
|
||||
},
|
||||
);
|
||||
|
||||
it("gets it from base url saved in self host config", async () => {
|
||||
const globalSelfHostUrls = new EnvironmentUrls();
|
||||
globalSelfHostUrls.base = "https://base.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalSelfHostUrls);
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
const host = await sut.getHost();
|
||||
expect(host).toBe("base.example.com");
|
||||
});
|
||||
|
||||
it("gets it from webVault url saved in self host config", async () => {
|
||||
const globalSelfHostUrls = new EnvironmentUrls();
|
||||
globalSelfHostUrls.webVault = "https://vault.example.com";
|
||||
globalSelfHostUrls.base = "https://base.example.com";
|
||||
setGlobalData(Region.SelfHosted, globalSelfHostUrls);
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
await initialize({ switchUser: false });
|
||||
|
||||
const host = await sut.getHost();
|
||||
expect(host).toBe("vault.example.com");
|
||||
});
|
||||
|
||||
it("gets it from saved self host config from passed in user when there is an active user", async () => {
|
||||
setGlobalData(Region.US, new EnvironmentUrls());
|
||||
setUserData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
const selfHostUserUrls = new EnvironmentUrls();
|
||||
selfHostUserUrls.base = "https://base.example.com";
|
||||
setUserData(Region.SelfHosted, selfHostUserUrls, alternateTestUser);
|
||||
|
||||
await initialize({ switchUser: true });
|
||||
|
||||
const host = await sut.getHost(alternateTestUser);
|
||||
expect(host).toBe("base.example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUrlsFromStorage", () => {
|
||||
it("will set the global data to Region US if no existing data", async () => {
|
||||
await sut.setUrlsFromStorage();
|
||||
|
||||
expect(sut.getWebVaultUrl()).toBe("https://vault.bitwarden.com");
|
||||
|
||||
const globalData = getGlobalData();
|
||||
expect(globalData.region).toBe(Region.US);
|
||||
});
|
||||
|
||||
it("will set the urls to whatever is in global", async () => {
|
||||
setGlobalData(Region.EU, new EnvironmentUrls());
|
||||
|
||||
await sut.setUrlsFromStorage();
|
||||
|
||||
expect(sut.getWebVaultUrl()).toBe("https://vault.bitwarden.eu");
|
||||
});
|
||||
|
||||
it("recovers from previous bug", async () => {
|
||||
const buggedEnvironmentUrls = new EnvironmentUrls();
|
||||
buggedEnvironmentUrls.base = "https://vault.bitwarden.com";
|
||||
buggedEnvironmentUrls.notifications = null;
|
||||
setGlobalData(null, buggedEnvironmentUrls);
|
||||
|
||||
const urlEmission = firstValueFrom(sut.urls.pipe(timeout(100)));
|
||||
|
||||
await sut.setUrlsFromStorage();
|
||||
|
||||
await urlEmission;
|
||||
|
||||
const globalData = getGlobalData();
|
||||
expect(globalData.region).toBe(Region.US);
|
||||
expect(globalData.urls).toEqual({
|
||||
base: null,
|
||||
api: null,
|
||||
identity: null,
|
||||
events: null,
|
||||
icons: null,
|
||||
notifications: null,
|
||||
keyConnector: null,
|
||||
webVault: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("will get urls from signed in user", async () => {
|
||||
await switchUser(testUser);
|
||||
|
||||
const userUrls = new EnvironmentUrls();
|
||||
userUrls.base = "base.example.com";
|
||||
setUserData(Region.SelfHosted, userUrls);
|
||||
|
||||
await sut.setUrlsFromStorage();
|
||||
|
||||
expect(sut.getWebVaultUrl()).toBe("base.example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCloudWebVaultUrl", () => {
|
||||
it("no extra initialization, returns US vault", () => {
|
||||
expect(sut.getCloudWebVaultUrl()).toBe("https://vault.bitwarden.com");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ region: Region.US, expectedVault: "https://vault.bitwarden.com" },
|
||||
{ region: Region.EU, expectedVault: "https://vault.bitwarden.eu" },
|
||||
{ region: Region.SelfHosted, expectedVault: "https://vault.bitwarden.com" },
|
||||
])(
|
||||
"no extra initialization, returns expected host for each region %s",
|
||||
({ region, expectedVault }) => {
|
||||
expect(sut.setCloudWebVaultUrl(region));
|
||||
expect(sut.getCloudWebVaultUrl()).toBe(expectedVault);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -1,418 +0,0 @@
|
||||
import {
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import { UserId } from "../../types/guid";
|
||||
import {
|
||||
EnvironmentService as EnvironmentServiceAbstraction,
|
||||
Region,
|
||||
RegionDomain,
|
||||
Urls,
|
||||
} from "../abstractions/environment.service";
|
||||
import { Utils } from "../misc/utils";
|
||||
import { ENVIRONMENT_DISK, GlobalState, KeyDefinition, StateProvider } from "../state";
|
||||
|
||||
const REGION_KEY = new KeyDefinition<Region>(ENVIRONMENT_DISK, "region", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
|
||||
const URLS_KEY = new KeyDefinition<EnvironmentUrls>(ENVIRONMENT_DISK, "urls", {
|
||||
deserializer: EnvironmentUrls.fromJSON,
|
||||
});
|
||||
|
||||
export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
private readonly urlsSubject = new ReplaySubject<void>(1);
|
||||
urls: Observable<void> = this.urlsSubject.asObservable();
|
||||
selectedRegion?: Region;
|
||||
initialized = false;
|
||||
|
||||
protected baseUrl: string;
|
||||
protected webVaultUrl: string;
|
||||
protected apiUrl: string;
|
||||
protected identityUrl: string;
|
||||
protected iconsUrl: string;
|
||||
protected notificationsUrl: string;
|
||||
protected eventsUrl: string;
|
||||
private keyConnectorUrl: string;
|
||||
private scimUrl: string = null;
|
||||
private cloudWebVaultUrl: string;
|
||||
|
||||
private regionGlobalState: GlobalState<Region | null>;
|
||||
private urlsGlobalState: GlobalState<EnvironmentUrls | null>;
|
||||
|
||||
private activeAccountId$: Observable<UserId | 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",
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
// We intentionally don't want the helper on account service, we want the null back if there is no active user
|
||||
this.activeAccountId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
// TODO: Get rid of early subscription during EnvironmentService refactor
|
||||
this.activeAccountId$
|
||||
.pipe(
|
||||
// Use == here to not trigger on undefined -> null transition
|
||||
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
|
||||
concatMap(async () => {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
await this.setUrlsFromStorage();
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.regionGlobalState = this.stateProvider.getGlobal(REGION_KEY);
|
||||
this.urlsGlobalState = this.stateProvider.getGlobal(URLS_KEY);
|
||||
}
|
||||
|
||||
hasBaseUrl() {
|
||||
return this.baseUrl != null;
|
||||
}
|
||||
|
||||
getNotificationsUrl() {
|
||||
if (this.notificationsUrl != null) {
|
||||
return this.notificationsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl != null) {
|
||||
return this.baseUrl + "/notifications";
|
||||
}
|
||||
|
||||
return "https://notifications.bitwarden.com";
|
||||
}
|
||||
|
||||
getWebVaultUrl() {
|
||||
if (this.webVaultUrl != null) {
|
||||
return this.webVaultUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl;
|
||||
}
|
||||
return "https://vault.bitwarden.com";
|
||||
}
|
||||
|
||||
getCloudWebVaultUrl() {
|
||||
if (this.cloudWebVaultUrl != null) {
|
||||
return this.cloudWebVaultUrl;
|
||||
}
|
||||
|
||||
return this.usUrls.webVault;
|
||||
}
|
||||
|
||||
setCloudWebVaultUrl(region: Region) {
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
this.cloudWebVaultUrl = this.euUrls.webVault;
|
||||
break;
|
||||
case Region.US:
|
||||
default:
|
||||
this.cloudWebVaultUrl = this.usUrls.webVault;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getSendUrl() {
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://send.bitwarden.com/#"
|
||||
: this.getWebVaultUrl() + "/#/send/";
|
||||
}
|
||||
|
||||
getIconsUrl() {
|
||||
if (this.iconsUrl != null) {
|
||||
return this.iconsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/icons";
|
||||
}
|
||||
|
||||
return "https://icons.bitwarden.net";
|
||||
}
|
||||
|
||||
getApiUrl() {
|
||||
if (this.apiUrl != null) {
|
||||
return this.apiUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/api";
|
||||
}
|
||||
|
||||
return "https://api.bitwarden.com";
|
||||
}
|
||||
|
||||
getIdentityUrl() {
|
||||
if (this.identityUrl != null) {
|
||||
return this.identityUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/identity";
|
||||
}
|
||||
|
||||
return "https://identity.bitwarden.com";
|
||||
}
|
||||
|
||||
getEventsUrl() {
|
||||
if (this.eventsUrl != null) {
|
||||
return this.eventsUrl;
|
||||
}
|
||||
|
||||
if (this.baseUrl) {
|
||||
return this.baseUrl + "/events";
|
||||
}
|
||||
|
||||
return "https://events.bitwarden.com";
|
||||
}
|
||||
|
||||
getKeyConnectorUrl() {
|
||||
return this.keyConnectorUrl;
|
||||
}
|
||||
|
||||
getScimUrl() {
|
||||
if (this.scimUrl != null) {
|
||||
return this.scimUrl + "/v2";
|
||||
}
|
||||
|
||||
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
|
||||
? "https://scim.bitwarden.com/v2"
|
||||
: this.getWebVaultUrl() + "/scim/v2";
|
||||
}
|
||||
|
||||
async setUrlsFromStorage(): Promise<void> {
|
||||
const activeUserId = await firstValueFrom(this.activeAccountId$);
|
||||
|
||||
const region = await this.getRegion(activeUserId);
|
||||
const savedUrls = await this.getEnvironmentUrls(activeUserId);
|
||||
const envUrls = new EnvironmentUrls();
|
||||
|
||||
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
|
||||
// This check will detect these cases and convert them to the proper region instead.
|
||||
// We are detecting this by checking for the presence of the web vault URL in the `base` and the absence of the `notifications` property.
|
||||
// This is because the `notifications` will not be `null` in the web vault, and we don't want to migrate the URLs in that case.
|
||||
if (savedUrls.base === "https://vault.bitwarden.com" && savedUrls.notifications == null) {
|
||||
await this.setRegion(Region.US);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (region) {
|
||||
case Region.EU:
|
||||
await this.setRegion(Region.EU);
|
||||
return;
|
||||
case Region.US:
|
||||
await this.setRegion(Region.US);
|
||||
return;
|
||||
case Region.SelfHosted:
|
||||
case null:
|
||||
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;
|
||||
await this.setRegion(Region.SelfHosted);
|
||||
// scimUrl is not saved to storage
|
||||
this.urlsSubject.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async setUrls(urls: Urls): Promise<Urls> {
|
||||
urls.base = this.formatUrl(urls.base);
|
||||
urls.webVault = this.formatUrl(urls.webVault);
|
||||
urls.api = this.formatUrl(urls.api);
|
||||
urls.identity = this.formatUrl(urls.identity);
|
||||
urls.icons = this.formatUrl(urls.icons);
|
||||
urls.notifications = this.formatUrl(urls.notifications);
|
||||
urls.events = this.formatUrl(urls.events);
|
||||
urls.keyConnector = this.formatUrl(urls.keyConnector);
|
||||
|
||||
// scimUrl cannot be cleared
|
||||
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
|
||||
|
||||
// Don't save scim url
|
||||
await this.urlsGlobalState.update(() => ({
|
||||
base: urls.base,
|
||||
api: urls.api,
|
||||
identity: urls.identity,
|
||||
webVault: urls.webVault,
|
||||
icons: urls.icons,
|
||||
notifications: urls.notifications,
|
||||
events: urls.events,
|
||||
keyConnector: urls.keyConnector,
|
||||
}));
|
||||
|
||||
this.baseUrl = urls.base;
|
||||
this.webVaultUrl = urls.webVault;
|
||||
this.apiUrl = urls.api;
|
||||
this.identityUrl = urls.identity;
|
||||
this.iconsUrl = urls.icons;
|
||||
this.notificationsUrl = urls.notifications;
|
||||
this.eventsUrl = urls.events;
|
||||
this.keyConnectorUrl = urls.keyConnector;
|
||||
this.scimUrl = urls.scim;
|
||||
|
||||
await this.setRegion(Region.SelfHosted);
|
||||
|
||||
this.urlsSubject.next();
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
getUrls() {
|
||||
return {
|
||||
base: this.baseUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
cloudWebVault: this.cloudWebVaultUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
events: this.eventsUrl,
|
||||
keyConnector: this.keyConnectorUrl,
|
||||
scim: this.scimUrl,
|
||||
};
|
||||
}
|
||||
|
||||
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 getHost(userId?: UserId) {
|
||||
const region = await this.getRegion(userId);
|
||||
|
||||
switch (region) {
|
||||
case Region.US:
|
||||
return RegionDomain.US;
|
||||
case Region.EU:
|
||||
return RegionDomain.EU;
|
||||
default: {
|
||||
// Environment is self-hosted
|
||||
const envUrls = await this.getEnvironmentUrls(userId);
|
||||
return Utils.getHost(envUrls.webVault || envUrls.base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getRegion(userId: UserId | null) {
|
||||
// Previous rules dictated that we only get from user scoped state if there is an active user.
|
||||
const activeUserId = await firstValueFrom(this.activeAccountId$);
|
||||
return activeUserId == null
|
||||
? await firstValueFrom(this.regionGlobalState.state$)
|
||||
: await firstValueFrom(this.stateProvider.getUser(userId ?? activeUserId, REGION_KEY).state$);
|
||||
}
|
||||
|
||||
private async getEnvironmentUrls(userId: UserId | null) {
|
||||
return userId == null
|
||||
? (await firstValueFrom(this.urlsGlobalState.state$)) ?? new EnvironmentUrls()
|
||||
: (await firstValueFrom(this.stateProvider.getUser(userId, URLS_KEY).state$)) ??
|
||||
new EnvironmentUrls();
|
||||
}
|
||||
|
||||
async setRegion(region: Region) {
|
||||
this.selectedRegion = region;
|
||||
await this.regionGlobalState.update(() => region);
|
||||
|
||||
if (region === Region.SelfHosted) {
|
||||
// If user saves a self-hosted region with empty fields, default to US
|
||||
if (this.isEmpty()) {
|
||||
await this.setRegion(Region.US);
|
||||
}
|
||||
} else {
|
||||
// If we are setting the region to EU or US, clear the self-hosted URLs
|
||||
await this.urlsGlobalState.update(() => new EnvironmentUrls());
|
||||
if (region === Region.EU) {
|
||||
this.setUrlsInternal(this.euUrls);
|
||||
} else if (region === Region.US) {
|
||||
this.setUrlsInternal(this.usUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async seedUserEnvironment(userId: UserId) {
|
||||
const globalRegion = await firstValueFrom(this.regionGlobalState.state$);
|
||||
const globalUrls = await firstValueFrom(this.urlsGlobalState.state$);
|
||||
await this.stateProvider.getUser(userId, REGION_KEY).update(() => globalRegion);
|
||||
await this.stateProvider.getUser(userId, URLS_KEY).update(() => globalUrls);
|
||||
}
|
||||
|
||||
private setUrlsInternal(urls: Urls) {
|
||||
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;
|
||||
}
|
||||
|
||||
url = url.replace(/\/+$/g, "");
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
return url.trim();
|
||||
}
|
||||
|
||||
isCloud(): boolean {
|
||||
return [
|
||||
"https://api.bitwarden.com",
|
||||
"https://vault.bitwarden.com/api",
|
||||
"https://api.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu/api",
|
||||
].includes(this.getApiUrl());
|
||||
}
|
||||
}
|
@ -77,6 +77,7 @@ export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||
export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk");
|
||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||
export const ENVIRONMENT_MEMORY = new StateDefinition("environment", "memory");
|
||||
export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
||||
@ -204,10 +206,12 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
? request.toIdentityToken()
|
||||
: request.toIdentityToken(this.platformUtilsService.getClientType());
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
const response = await this.fetch(
|
||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
||||
new Request(env.getIdentityUrl() + "/connect/token", {
|
||||
body: this.qsStringify(identityToken),
|
||||
credentials: this.getCredentials(),
|
||||
credentials: await this.getCredentials(),
|
||||
cache: "no-store",
|
||||
headers: headers,
|
||||
method: "POST",
|
||||
@ -323,13 +327,14 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
"/accounts/prelogin",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
this.environmentService.getIdentityUrl(),
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new PreloginResponse(r);
|
||||
}
|
||||
@ -368,13 +373,14 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
"/accounts/register",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
this.environmentService.getIdentityUrl(),
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new RegisterResponse(r);
|
||||
}
|
||||
@ -1457,10 +1463,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
if (this.customUserAgent != null) {
|
||||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.fetch(
|
||||
new Request(this.environmentService.getEventsUrl() + "/collect", {
|
||||
new Request(env.getEventsUrl() + "/collect", {
|
||||
cache: "no-store",
|
||||
credentials: this.getCredentials(),
|
||||
credentials: await this.getCredentials(),
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers: headers,
|
||||
@ -1617,11 +1624,12 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const path = `/sso/prevalidate?domainHint=${encodeURIComponent(identifier)}`;
|
||||
const response = await this.fetch(
|
||||
new Request(this.environmentService.getIdentityUrl() + path, {
|
||||
new Request(env.getIdentityUrl() + path, {
|
||||
cache: "no-store",
|
||||
credentials: this.getCredentials(),
|
||||
credentials: await this.getCredentials(),
|
||||
headers: headers,
|
||||
method: "GET",
|
||||
}),
|
||||
@ -1751,16 +1759,17 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const decodedToken = await this.tokenService.decodeAccessToken();
|
||||
const response = await this.fetch(
|
||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
||||
new Request(env.getIdentityUrl() + "/connect/token", {
|
||||
body: this.qsStringify({
|
||||
grant_type: "refresh_token",
|
||||
client_id: decodedToken.client_id,
|
||||
refresh_token: refreshToken,
|
||||
}),
|
||||
cache: "no-store",
|
||||
credentials: this.getCredentials(),
|
||||
credentials: await this.getCredentials(),
|
||||
headers: headers,
|
||||
method: "POST",
|
||||
}),
|
||||
@ -1822,7 +1831,8 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
apiUrl?: string,
|
||||
alterHeaders?: (headers: Headers) => void,
|
||||
): Promise<any> {
|
||||
apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.environmentService.getApiUrl() : apiUrl;
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
apiUrl = Utils.isNullOrWhitespace(apiUrl) ? env.getApiUrl() : apiUrl;
|
||||
|
||||
// Prevent directory traversal from malicious paths
|
||||
const pathParts = path.split("?");
|
||||
@ -1838,7 +1848,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
cache: "no-store",
|
||||
credentials: this.getCredentials(),
|
||||
credentials: await this.getCredentials(),
|
||||
method: method,
|
||||
};
|
||||
|
||||
@ -1917,8 +1927,9 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
.join("&");
|
||||
}
|
||||
|
||||
private getCredentials(): RequestCredentials {
|
||||
if (!this.isWebClient || this.environmentService.hasBaseUrl()) {
|
||||
private async getCredentials(): Promise<RequestCredentials> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
if (!this.isWebClient || env.hasBaseUrl()) {
|
||||
return "include";
|
||||
}
|
||||
return undefined;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as signalR from "@microsoft/signalr";
|
||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||
@ -38,7 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
private authService: AuthService,
|
||||
private messagingService: MessagingService,
|
||||
) {
|
||||
this.environmentService.urls.subscribe(() => {
|
||||
this.environmentService.environment$.subscribe(() => {
|
||||
if (!this.inited) {
|
||||
return;
|
||||
}
|
||||
@ -51,7 +52,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.inited = false;
|
||||
this.url = this.environmentService.getNotificationsUrl();
|
||||
this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl();
|
||||
|
||||
// Set notifications server URL to `https://-` to effectively disable communication
|
||||
// with the notifications server from the client app
|
||||
|
@ -40,6 +40,7 @@ import { EventCollectionMigrator } from "./migrations/41-move-event-collection-t
|
||||
import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider";
|
||||
import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider";
|
||||
import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider";
|
||||
import { MergeEnvironmentState } from "./migrations/45-merge-environment-state";
|
||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
@ -48,7 +49,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 44;
|
||||
export const CURRENT_VERSION = 45;
|
||||
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@ -94,7 +96,8 @@ export function createMigrationBuilder() {
|
||||
.with(EventCollectionMigrator, 40, 41)
|
||||
.with(EnableFaviconMigrator, 41, 42)
|
||||
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
|
||||
.with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION);
|
||||
.with(UserDecryptionOptionsMigrator, 43, 44)
|
||||
.with(MergeEnvironmentState, 44, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
@ -0,0 +1,164 @@
|
||||
import { runMigrator } from "../migration-helper.spec";
|
||||
|
||||
import { MergeEnvironmentState } from "./45-merge-environment-state";
|
||||
|
||||
describe("MergeEnvironmentState", () => {
|
||||
const migrator = new MergeEnvironmentState(44, 45);
|
||||
|
||||
it("can migrate all data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_environment_region: "US",
|
||||
global_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
user_user1_environment_region: "US",
|
||||
user_user2_environment_region: "EU",
|
||||
user_user1_environment_urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
user_user2_environment_urls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
global_environment_environment: {
|
||||
region: "US",
|
||||
urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
extra: "data",
|
||||
user_user1_environment_environment: {
|
||||
region: "US",
|
||||
urls: {
|
||||
base: "example.com",
|
||||
},
|
||||
},
|
||||
user_user2_environment_environment: {
|
||||
region: "EU",
|
||||
urls: {
|
||||
base: "other.example.com",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing parts", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1", "user2"],
|
||||
global: {
|
||||
extra: "data",
|
||||
},
|
||||
user1: {
|
||||
extra: "data",
|
||||
settings: {
|
||||
extra: "data",
|
||||
},
|
||||
},
|
||||
user2: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only global data", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: [],
|
||||
global_environment_region: "Self-Hosted",
|
||||
global: {},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: [],
|
||||
global_environment_environment: {
|
||||
region: "Self-Hosted",
|
||||
urls: undefined,
|
||||
},
|
||||
global: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("can migrate only user state", async () => {
|
||||
const output = await runMigrator(migrator, {
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: { settings: {} },
|
||||
user_user1_environment_region: "Self-Hosted",
|
||||
user_user1_environment_urls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
});
|
||||
|
||||
expect(output).toEqual({
|
||||
authenticatedAccounts: ["user1"] as const,
|
||||
global: null,
|
||||
user1: { settings: {} },
|
||||
user_user1_environment_environment: {
|
||||
region: "Self-Hosted",
|
||||
urls: {
|
||||
base: "some-base-url",
|
||||
api: "some-api-url",
|
||||
identity: "some-identity-url",
|
||||
icons: "some-icons-url",
|
||||
notifications: "some-notifications-url",
|
||||
events: "some-events-url",
|
||||
webVault: "some-webVault-url",
|
||||
keyConnector: "some-keyConnector-url",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,83 @@
|
||||
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
const ENVIRONMENT_STATE: StateDefinitionLike = { name: "environment" };
|
||||
|
||||
const ENVIRONMENT_REGION: KeyDefinitionLike = {
|
||||
key: "region",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
const ENVIRONMENT_URLS: KeyDefinitionLike = {
|
||||
key: "urls",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
const ENVIRONMENT_ENVIRONMENT: KeyDefinitionLike = {
|
||||
key: "environment",
|
||||
stateDefinition: ENVIRONMENT_STATE,
|
||||
};
|
||||
|
||||
export class MergeEnvironmentState extends Migrator<44, 45> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<unknown>();
|
||||
|
||||
async function migrateAccount(userId: string, account: unknown): Promise<void> {
|
||||
const region = await helper.getFromUser(userId, ENVIRONMENT_REGION);
|
||||
const urls = await helper.getFromUser(userId, ENVIRONMENT_URLS);
|
||||
|
||||
if (region == null && urls == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ENVIRONMENT_ENVIRONMENT, {
|
||||
region,
|
||||
urls,
|
||||
});
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_REGION);
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_URLS);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
const region = await helper.getFromGlobal(ENVIRONMENT_REGION);
|
||||
const urls = await helper.getFromGlobal(ENVIRONMENT_URLS);
|
||||
|
||||
if (region == null && urls == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await helper.setToGlobal(ENVIRONMENT_ENVIRONMENT, {
|
||||
region,
|
||||
urls,
|
||||
});
|
||||
await helper.removeFromGlobal(ENVIRONMENT_REGION);
|
||||
await helper.removeFromGlobal(ENVIRONMENT_URLS);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<unknown>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: unknown): Promise<void> {
|
||||
const state = (await helper.getFromUser(userId, ENVIRONMENT_ENVIRONMENT)) as {
|
||||
region: string;
|
||||
urls: string;
|
||||
} | null;
|
||||
|
||||
await helper.setToUser(userId, ENVIRONMENT_REGION, state?.region);
|
||||
await helper.setToUser(userId, ENVIRONMENT_URLS, state?.urls);
|
||||
await helper.removeFromUser(userId, ENVIRONMENT_ENVIRONMENT);
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
const state = (await helper.getFromGlobal(ENVIRONMENT_ENVIRONMENT)) as {
|
||||
region: string;
|
||||
urls: string;
|
||||
} | null;
|
||||
|
||||
await helper.setToGlobal(ENVIRONMENT_REGION, state?.region);
|
||||
await helper.setToGlobal(ENVIRONMENT_URLS, state?.urls);
|
||||
await helper.removeFromGlobal(ENVIRONMENT_ENVIRONMENT);
|
||||
}
|
||||
}
|
@ -121,7 +121,7 @@ export class LastPassDirectImportService {
|
||||
this.oidcClient = new OidcClient({
|
||||
authority: this.vault.userType.openIDConnectAuthorityBase,
|
||||
client_id: this.vault.userType.openIDConnectClientId,
|
||||
redirect_uri: this.getOidcRedirectUrl(),
|
||||
redirect_uri: await this.getOidcRedirectUrl(),
|
||||
response_type: "code",
|
||||
scope: this.vault.userType.oidcScope,
|
||||
response_mode: "query",
|
||||
@ -151,12 +151,13 @@ export class LastPassDirectImportService {
|
||||
return redirectUri + "&" + params;
|
||||
}
|
||||
|
||||
private getOidcRedirectUrl() {
|
||||
private async getOidcRedirectUrl() {
|
||||
const clientType = this.platformUtilsService.getClientType();
|
||||
if (clientType === ClientType.Desktop) {
|
||||
return "bitwarden://import-callback-lp";
|
||||
}
|
||||
const webUrl = this.environmentService.getWebVaultUrl();
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
return webUrl + "/sso-connector.html?lp=1";
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user