mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +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"]
|
"pathGroupsExcludedImportTypes": ["builtin"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rxjs-angular/prefer-takeuntil": "error",
|
"rxjs-angular/prefer-takeuntil": ["error", { "alias": ["takeUntilDestroyed"] }],
|
||||||
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
|
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
|
||||||
"no-restricted-syntax": [
|
"no-restricted-syntax": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -2386,12 +2386,6 @@
|
|||||||
"message": "EU",
|
"message": "EU",
|
||||||
"description": "European Union"
|
"description": "European Union"
|
||||||
},
|
},
|
||||||
"usDomain": {
|
|
||||||
"message": "bitwarden.com"
|
|
||||||
},
|
|
||||||
"euDomain": {
|
|
||||||
"message": "bitwarden.eu"
|
|
||||||
},
|
|
||||||
"accessDenied": {
|
"accessDenied": {
|
||||||
"message": "Access denied. You do not have permission to view this page."
|
"message": "Access denied. You do not have permission to view this page."
|
||||||
},
|
},
|
||||||
|
@ -65,7 +65,7 @@ export class AccountSwitcherService {
|
|||||||
name: account.name ?? account.email,
|
name: account.name ?? account.email,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
id: id,
|
id: id,
|
||||||
server: await this.environmentService.getHost(id),
|
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
|
||||||
status: account.status,
|
status: account.status,
|
||||||
isActive: id === activeAccount?.id,
|
isActive: id === activeAccount?.id,
|
||||||
avatarColor: await firstValueFrom(
|
avatarColor: await firstValueFrom(
|
||||||
|
@ -94,10 +94,6 @@ export class HomeComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
|
this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
|
||||||
}
|
}
|
||||||
|
|
||||||
get selfHostedDomain() {
|
|
||||||
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormValues() {
|
setFormValues() {
|
||||||
this.loginService.setEmail(this.formGroup.value.email);
|
this.loginService.setEmail(this.formGroup.value.email);
|
||||||
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
|
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, NgZone } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
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.setCodeVerifier(codeVerifier);
|
||||||
await this.ssoLoginService.setSsoState(state);
|
await this.ssoLoginService.setSsoState(state);
|
||||||
|
|
||||||
let url = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
let url = env.getWebVaultUrl();
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
url = "https://vault.bitwarden.com";
|
url = "https://vault.bitwarden.com";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
|
||||||
@ -64,9 +65,9 @@ export class SsoComponent extends BaseSsoComponent {
|
|||||||
configService,
|
configService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = this.environmentService.getWebVaultUrl();
|
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||||
|
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
|
||||||
this.redirectUri = url + "/sso-connector.html";
|
});
|
||||||
this.clientId = "browser";
|
this.clientId = "browser";
|
||||||
|
|
||||||
super.onSuccessfulLogin = async () => {
|
super.onSuccessfulLogin = async () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
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 { filter, first, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
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 = {
|
const duoHandOffMessage = {
|
||||||
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
||||||
message: this.i18nService.t("youMayCloseThisWindow"),
|
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
|
// we're using the connector here as a way to set a cookie with translations
|
||||||
// before continuing to the duo frameless url
|
// before continuing to the duo frameless url
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const launchUrl =
|
const launchUrl =
|
||||||
this.environmentService.getWebVaultUrl() +
|
env.getWebVaultUrl() +
|
||||||
"/duo-redirect-connector.html" +
|
"/duo-redirect-connector.html" +
|
||||||
"?duoFramelessUrl=" +
|
"?duoFramelessUrl=" +
|
||||||
encodeURIComponent(this.duoFramelessUrl) +
|
encodeURIComponent(this.duoFramelessUrl) +
|
||||||
|
@ -113,7 +113,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
|||||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||||
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||||
getWebVaultUrlForNotification: () => string;
|
getWebVaultUrlForNotification: () => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
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 { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-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 { 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 { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
@ -1348,16 +1349,21 @@ describe("NotificationBackground", () => {
|
|||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "getWebVaultUrlForNotification",
|
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
|
const environmentServiceSpy = jest
|
||||||
.spyOn(environmentService, "getWebVaultUrl")
|
.spyOn(environmentService as any, "environment$", "get")
|
||||||
.mockReturnValueOnce(webVaultUrl);
|
.mockReturnValue(new BehaviorSubject(env).asObservable());
|
||||||
|
|
||||||
sendExtensionRuntimeMessage(message);
|
sendExtensionRuntimeMessage(message);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(environmentServiceSpy).toHaveBeenCalled();
|
expect(environmentServiceSpy).toHaveBeenCalled();
|
||||||
expect(environmentServiceSpy).toHaveReturnedWith(webVaultUrl);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -165,6 +165,7 @@ export default class NotificationBackground {
|
|||||||
notificationQueueMessage: NotificationQueueMessageItem,
|
notificationQueueMessage: NotificationQueueMessageItem,
|
||||||
) {
|
) {
|
||||||
const notificationType = notificationQueueMessage.type;
|
const notificationType = notificationQueueMessage.type;
|
||||||
|
|
||||||
const typeData: Record<string, any> = {
|
const typeData: Record<string, any> = {
|
||||||
isVaultLocked: notificationQueueMessage.wasVaultLocked,
|
isVaultLocked: notificationQueueMessage.wasVaultLocked,
|
||||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
@ -655,8 +656,9 @@ export default class NotificationBackground {
|
|||||||
return await firstValueFrom(this.folderService.folderViews$);
|
return await firstValueFrom(this.folderService.folderViews$);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWebVaultUrl(): string {
|
private async getWebVaultUrl(): Promise<string> {
|
||||||
return this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
return env.getWebVaultUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeIndividualVault(): Promise<boolean> {
|
private async removeIndividualVault(): Promise<boolean> {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { mock, mockReset } from "jest-mock-extended";
|
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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
@ -12,9 +12,13 @@ import {
|
|||||||
DefaultDomainSettingsService,
|
DefaultDomainSettingsService,
|
||||||
DomainSettingsService,
|
DomainSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
} 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 { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 { I18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import {
|
import {
|
||||||
@ -48,8 +52,6 @@ import {
|
|||||||
|
|
||||||
import OverlayBackground from "./overlay.background";
|
import OverlayBackground from "./overlay.background";
|
||||||
|
|
||||||
const iconServerUrl = "https://icons.bitwarden.com/";
|
|
||||||
|
|
||||||
describe("OverlayBackground", () => {
|
describe("OverlayBackground", () => {
|
||||||
const mockUserId = Utils.newGuid() as UserId;
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
@ -61,9 +63,15 @@ describe("OverlayBackground", () => {
|
|||||||
const cipherService = mock<CipherService>();
|
const cipherService = mock<CipherService>();
|
||||||
const autofillService = mock<AutofillService>();
|
const autofillService = mock<AutofillService>();
|
||||||
const authService = mock<AuthService>();
|
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 stateService = mock<BrowserStateService>();
|
||||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
|
@ -53,7 +53,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private overlayListPort: chrome.runtime.Port;
|
private overlayListPort: chrome.runtime.Port;
|
||||||
private focusedFieldData: FocusedFieldData;
|
private focusedFieldData: FocusedFieldData;
|
||||||
private overlayPageTranslations: Record<string, string>;
|
private overlayPageTranslations: Record<string, string>;
|
||||||
private readonly iconsServerUrl: string;
|
private iconsServerUrl: string;
|
||||||
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
|
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
|
||||||
openAutofillOverlay: () => this.openOverlay(false),
|
openAutofillOverlay: () => this.openOverlay(false),
|
||||||
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
|
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
|
||||||
@ -98,9 +98,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private themeStateService: ThemeStateService,
|
private themeStateService: ThemeStateService,
|
||||||
) {
|
) {}
|
||||||
this.iconsServerUrl = this.environmentService.getIconsUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes cached page details for a tab
|
* Removes cached page details for a tab
|
||||||
@ -118,6 +116,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
this.setupExtensionMessageListeners();
|
this.setupExtensionMessageListeners();
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
this.iconsServerUrl = env.getIconsUrl();
|
||||||
await this.getOverlayVisibility();
|
await this.getOverlayVisibility();
|
||||||
await this.getAuthStatus();
|
await this.getAuthStatus();
|
||||||
}
|
}
|
||||||
|
@ -613,6 +613,7 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.stateProvider,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1032,10 +1033,6 @@ export default class MainBackground {
|
|||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
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) {
|
if (!this.isPrivateMode) {
|
||||||
await this.refreshBadge();
|
await this.refreshBadge();
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
@ -220,7 +222,8 @@ export default class RuntimeBackground {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "authResult": {
|
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) {
|
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||||
return;
|
return;
|
||||||
@ -241,7 +244,8 @@ export default class RuntimeBackground {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "webAuthnResult": {
|
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) {
|
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
|
||||||
return;
|
return;
|
||||||
@ -364,7 +368,8 @@ export default class RuntimeBackground {
|
|||||||
|
|
||||||
async sendBwInstalledMessageToVault() {
|
async sendBwInstalledMessageToVault() {
|
||||||
try {
|
try {
|
||||||
const vaultUrl = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const vaultUrl = env.getWebVaultUrl();
|
||||||
const urlObj = new URL(vaultUrl);
|
const urlObj = new URL(vaultUrl);
|
||||||
|
|
||||||
const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
|
const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
} from "./environment-service.factory";
|
} from "./environment-service.factory";
|
||||||
import { FactoryOptions, CachedServices, factory } from "./factory-options";
|
import { FactoryOptions, CachedServices, factory } from "./factory-options";
|
||||||
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
|
||||||
|
import { stateProviderFactory } from "./state-provider.factory";
|
||||||
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
|
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
|
||||||
|
|
||||||
type ConfigServiceFactoryOptions = FactoryOptions & {
|
type ConfigServiceFactoryOptions = FactoryOptions & {
|
||||||
@ -43,6 +44,7 @@ export function configServiceFactory(
|
|||||||
await authServiceFactory(cache, opts),
|
await authServiceFactory(cache, opts),
|
||||||
await environmentServiceFactory(cache, opts),
|
await environmentServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
opts.configServiceOptions?.subscribe ?? true,
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.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";
|
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
|
||||||
|
|
||||||
@ -21,8 +22,17 @@ export class BrowserConfigService extends ConfigService {
|
|||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
|
stateProvider: StateProvider,
|
||||||
subscribe = false,
|
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 { 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 { 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 { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
|
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
|
||||||
import { devFlagEnabled, devFlagValue } from "../flags";
|
import { devFlagEnabled, devFlagValue } from "../flags";
|
||||||
|
|
||||||
export class BrowserEnvironmentService extends EnvironmentService {
|
export class BrowserEnvironmentService extends DefaultEnvironmentService {
|
||||||
constructor(
|
constructor(
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
@ -29,16 +32,18 @@ export class BrowserEnvironmentService extends EnvironmentService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = await this.getManagedEnvironment();
|
const managedEnv = await this.getManagedEnvironment();
|
||||||
|
const env = await firstValueFrom(this.environment$);
|
||||||
|
const urls = env.getUrls();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
env.base != this.baseUrl ||
|
managedEnv.base != urls.base ||
|
||||||
env.webVault != this.webVaultUrl ||
|
managedEnv.webVault != urls.webVault ||
|
||||||
env.api != this.webVaultUrl ||
|
managedEnv.api != urls.api ||
|
||||||
env.identity != this.identityUrl ||
|
managedEnv.identity != urls.identity ||
|
||||||
env.icons != this.iconsUrl ||
|
managedEnv.icons != urls.icons ||
|
||||||
env.notifications != this.notificationsUrl ||
|
managedEnv.notifications != urls.notifications ||
|
||||||
env.events != this.eventsUrl
|
managedEnv.events != urls.events
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +67,7 @@ export class BrowserEnvironmentService extends EnvironmentService {
|
|||||||
|
|
||||||
async setUrlsToManagedEnvironment() {
|
async setUrlsToManagedEnvironment() {
|
||||||
const env = await this.getManagedEnvironment();
|
const env = await this.getManagedEnvironment();
|
||||||
await this.setUrls({
|
await this.setEnvironment(Region.SelfHosted, {
|
||||||
base: env.base,
|
base: env.base,
|
||||||
webVault: env.webVault,
|
webVault: env.webVault,
|
||||||
api: env.api,
|
api: env.api,
|
||||||
|
@ -222,12 +222,12 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: BrowserEnvironmentService,
|
provide: BrowserEnvironmentService,
|
||||||
useExisting: EnvironmentService,
|
useClass: BrowserEnvironmentService,
|
||||||
|
deps: [LogService, StateProvider, AccountServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useFactory: getBgService<EnvironmentService>("environmentService"),
|
useExisting: BrowserEnvironmentService,
|
||||||
deps: [],
|
|
||||||
},
|
},
|
||||||
{ provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
|
{ provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
|
||||||
{
|
{
|
||||||
@ -480,6 +480,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
ConfigApiServiceAbstraction,
|
ConfigApiServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
|
StateProvider,
|
||||||
LogService,
|
LogService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -6,33 +6,33 @@
|
|||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||||
<p>{{ "version" | i18n }}: {{ version }}</p>
|
<p>{{ "version" | i18n }}: {{ version }}</p>
|
||||||
<ng-container *ngIf="serverConfig$ | async as serverConfig">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<p *ngIf="isCloud">
|
<p *ngIf="data.isCloud">
|
||||||
{{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }}
|
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
||||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ng-container *ngIf="!isCloud">
|
<ng-container *ngIf="!data.isCloud">
|
||||||
<ng-container *ngIf="serverConfig.server">
|
<ng-container *ngIf="data.serverConfig.server">
|
||||||
<p>
|
<p>
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
||||||
{{ this.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
||||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<small>{{ "thirdPartyServerMessage" | i18n: serverConfig.server?.name }}</small>
|
<small>{{ "thirdPartyServerMessage" | i18n: data.serverConfig.server?.name }}</small>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<p *ngIf="!serverConfig.server">
|
<p *ngIf="!data.serverConfig.server">
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
||||||
{{ this.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
||||||
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
|
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
import { combineLatest, map } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@ -16,11 +15,13 @@ import { BrowserApi } from "../../platform/browser/browser-api";
|
|||||||
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutComponent {
|
||||||
protected serverConfig$: Observable<ServerConfig> = this.configService.serverConfig$;
|
|
||||||
|
|
||||||
protected year = new Date().getFullYear();
|
protected year = new Date().getFullYear();
|
||||||
protected version = BrowserApi.getApplicationVersion();
|
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(
|
constructor(
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
|
@ -446,9 +446,8 @@ export class SettingsComponent implements OnInit {
|
|||||||
type: "info",
|
type: "info",
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await BrowserApi.createNewTab(env.getWebVaultUrl());
|
||||||
BrowserApi.createNewTab(this.environmentService.getWebVaultUrl());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,10 +478,9 @@ export class SettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async webVault() {
|
async webVault() {
|
||||||
const url = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
const url = env.getWebVaultUrl();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await BrowserApi.createNewTab(url);
|
||||||
BrowserApi.createNewTab(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async import() {
|
async import() {
|
||||||
|
@ -690,6 +690,8 @@ export class LoginCommand {
|
|||||||
codeChallenge: string,
|
codeChallenge: string,
|
||||||
state: string,
|
state: string,
|
||||||
): Promise<{ ssoCode: string; orgIdentifier: string }> {
|
): Promise<{ ssoCode: string; orgIdentifier: string }> {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const callbackServer = http.createServer((req, res) => {
|
const callbackServer = http.createServer((req, res) => {
|
||||||
const urlString = "http://localhost" + req.url;
|
const urlString = "http://localhost" + req.url;
|
||||||
@ -724,7 +726,7 @@ export class LoginCommand {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let foundPort = false;
|
let foundPort = false;
|
||||||
const webUrl = this.environmentService.getWebVaultUrl();
|
const webUrl = env.getWebVaultUrl();
|
||||||
for (let port = 8065; port <= 8070; port++) {
|
for (let port = 8065; port <= 8070; port++) {
|
||||||
try {
|
try {
|
||||||
this.ssoRedirectUri = "http://localhost:" + port;
|
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 { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
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 { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import {
|
import {
|
||||||
BiometricStateService,
|
BiometricStateService,
|
||||||
@ -62,7 +63,7 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con
|
|||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
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 { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
@ -312,7 +313,10 @@ export class Main {
|
|||||||
this.derivedStateProvider,
|
this.derivedStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
|
this.environmentService = new DefaultEnvironmentService(
|
||||||
|
this.stateProvider,
|
||||||
|
this.accountService,
|
||||||
|
);
|
||||||
|
|
||||||
this.tokenService = new TokenService(
|
this.tokenService = new TokenService(
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
@ -504,6 +508,7 @@ export class Main {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.stateProvider,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -703,7 +708,6 @@ export class Main {
|
|||||||
await this.storageService.init();
|
await this.storageService.init();
|
||||||
await this.stateService.init();
|
await this.stateService.init();
|
||||||
this.containerService.attachToGlobal(global);
|
this.containerService.attachToGlobal(global);
|
||||||
await this.environmentService.setUrlsFromStorage();
|
|
||||||
await this.i18nService.init();
|
await this.i18nService.init();
|
||||||
this.twoFactorService.init();
|
this.twoFactorService.init();
|
||||||
this.configService.init();
|
this.configService.init();
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { OptionValues } from "commander";
|
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 { Response } from "../models/response";
|
||||||
import { MessageResponse } from "../models/response/message.response";
|
import { MessageResponse } from "../models/response/message.response";
|
||||||
@ -29,16 +33,15 @@ export class ConfigCommand {
|
|||||||
!options.notifications &&
|
!options.notifications &&
|
||||||
!options.events
|
!options.events
|
||||||
) {
|
) {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const stringRes = new StringResponse(
|
const stringRes = new StringResponse(
|
||||||
this.environmentService.hasBaseUrl()
|
env.hasBaseUrl() ? env.getUrls().base : "https://bitwarden.com",
|
||||||
? this.environmentService.getUrls().base
|
|
||||||
: "https://bitwarden.com",
|
|
||||||
);
|
);
|
||||||
return Response.success(stringRes);
|
return Response.success(stringRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
|
url = url === "null" || url === "bitwarden.com" || url === "https://bitwarden.com" ? null : url;
|
||||||
await this.environmentService.setUrls({
|
await this.environmentService.setEnvironment(Region.SelfHosted, {
|
||||||
base: url,
|
base: url,
|
||||||
webVault: options.webVault || null,
|
webVault: options.webVault || null,
|
||||||
api: options.api || null,
|
api: options.api || null,
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import * as inquirer from "inquirer";
|
import * as inquirer from "inquirer";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
import { Response } from "../models/response";
|
import { Response } from "../models/response";
|
||||||
@ -67,9 +71,10 @@ export class ConvertToKeyConnectorCommand {
|
|||||||
await this.keyConnectorService.setUsesKeyConnector(true);
|
await this.keyConnectorService.setUsesKeyConnector(true);
|
||||||
|
|
||||||
// Update environment URL - required for api key login
|
// 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;
|
urls.keyConnector = organization.keyConnectorUrl;
|
||||||
await this.environmentService.setUrls(urls);
|
await this.environmentService.setEnvironment(Region.SelfHosted, urls);
|
||||||
|
|
||||||
return Response.success();
|
return Response.success();
|
||||||
} else if (answer.convert === "leave") {
|
} else if (answer.convert === "leave") {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@ -17,7 +19,7 @@ export class StatusCommand {
|
|||||||
|
|
||||||
async run(): Promise<Response> {
|
async run(): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const baseUrl = this.baseUrl();
|
const baseUrl = await this.baseUrl();
|
||||||
const status = await this.status();
|
const status = await this.status();
|
||||||
const lastSync = await this.syncService.getLastSync();
|
const lastSync = await this.syncService.getLastSync();
|
||||||
const userId = await this.stateService.getUserId();
|
const userId = await this.stateService.getUserId();
|
||||||
@ -37,8 +39,9 @@ export class StatusCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private baseUrl(): string {
|
private async baseUrl(): Promise<string> {
|
||||||
return this.envService.getUrls().base;
|
const env = await firstValueFrom(this.envService.environment$);
|
||||||
|
return env.getUrls().base;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
|
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
|
||||||
|
@ -127,7 +127,8 @@ export class SendCreateCommand {
|
|||||||
await this.sendApiService.save([encSend, fileData]);
|
await this.sendApiService.save([encSend, fileData]);
|
||||||
const newSend = await this.sendService.getFromState(encSend.id);
|
const newSend = await this.sendService.getFromState(encSend.id);
|
||||||
const decSend = await newSend.decrypt();
|
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);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.error(e);
|
return Response.error(e);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { OptionValues } from "commander";
|
import { OptionValues } from "commander";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@ -32,7 +33,8 @@ export class SendGetCommand extends DownloadCommand {
|
|||||||
return Response.notFound();
|
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 filter = (s: SendView) => true;
|
||||||
let selector = async (s: SendView): Promise<Response> =>
|
let selector = async (s: SendView): Promise<Response> =>
|
||||||
Response.success(new SendResponse(s, webVaultUrl));
|
Response.success(new SendResponse(s, webVaultUrl));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
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);
|
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)));
|
const res = new ListResponse(sends.map((s) => new SendResponse(s, webVaultUrl)));
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { OptionValues } from "commander";
|
import { OptionValues } from "commander";
|
||||||
import * as inquirer from "inquirer";
|
import * as inquirer from "inquirer";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
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");
|
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);
|
const [id, key] = this.getIdAndKey(urlObject);
|
||||||
|
|
||||||
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
|
if (Utils.isNullOrWhitespace(id) || Utils.isNullOrWhitespace(key)) {
|
||||||
@ -108,8 +109,9 @@ export class SendReceiveCommand extends DownloadCommand {
|
|||||||
return [result[0], result[1]];
|
return [result[0], result[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getApiUrl(url: URL) {
|
private async getApiUrl(url: URL) {
|
||||||
const urls = this.environmentService.getUrls();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const urls = env.getUrls();
|
||||||
if (url.origin === "https://send.bitwarden.com") {
|
if (url.origin === "https://send.bitwarden.com") {
|
||||||
return "https://api.bitwarden.com";
|
return "https://api.bitwarden.com";
|
||||||
} else if (url.origin === urls.api) {
|
} else if (url.origin === urls.api) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services//send.service.abstraction";
|
import { SendService } from "@bitwarden/common/tools/send/services//send.service.abstraction";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.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 updatedSend = await this.sendService.get(id);
|
||||||
const decSend = await updatedSend.decrypt();
|
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);
|
const res = new SendResponse(decSend, webVaultUrl);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -105,7 +105,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||||
email: await this.tokenService.getEmail(),
|
email: await this.tokenService.getEmail(),
|
||||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||||
server: await this.environmentService.getHost(),
|
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
this.activeAccount = undefined;
|
this.activeAccount = undefined;
|
||||||
@ -158,7 +158,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
email: baseAccounts[userId].profile.email,
|
email: baseAccounts[userId].profile.email,
|
||||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as 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 { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.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 { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.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 {
|
export class InitService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINDOW) private win: Window,
|
@Inject(WINDOW) private win: Window,
|
||||||
private environmentService: EnvironmentServiceAbstraction,
|
|
||||||
private syncService: SyncServiceAbstraction,
|
private syncService: SyncServiceAbstraction,
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private i18nService: I18nServiceAbstraction,
|
private i18nService: I18nServiceAbstraction,
|
||||||
@ -46,10 +44,6 @@ export class InitService {
|
|||||||
return async () => {
|
return async () => {
|
||||||
this.nativeMessagingService.init();
|
this.nativeMessagingService.init();
|
||||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.syncService.fullSync(true);
|
this.syncService.fullSync(true);
|
||||||
|
@ -49,10 +49,6 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
|||||||
return this.formGroup.value.email;
|
return this.formGroup.value.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
get selfHostedDomain() {
|
|
||||||
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
devicesApiService: DevicesApiServiceAbstraction,
|
devicesApiService: DevicesApiServiceAbstraction,
|
||||||
appIdService: AppIdService,
|
appIdService: AppIdService,
|
||||||
@ -152,9 +148,6 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
|||||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||||
childComponent.onSaved.pipe(takeUntil(this.componentDestroyed$)).subscribe(async () => {
|
childComponent.onSaved.pipe(takeUntil(this.componentDestroyed$)).subscribe(async () => {
|
||||||
modal.close();
|
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);
|
await this.getLoginWithDevice(this.loggedEmail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component, Inject, NgZone, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, Inject, NgZone, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
@ -141,7 +142,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override launchDuoFrameless() {
|
override async launchDuoFrameless() {
|
||||||
const duoHandOffMessage = {
|
const duoHandOffMessage = {
|
||||||
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
||||||
message: this.i18nService.t("youMayCloseThisWindow"),
|
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
|
// we're using the connector here as a way to set a cookie with translations
|
||||||
// before continuing to the duo frameless url
|
// before continuing to the duo frameless url
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const launchUrl =
|
const launchUrl =
|
||||||
this.environmentService.getWebVaultUrl() +
|
env.getWebVaultUrl() +
|
||||||
"/duo-redirect-connector.html" +
|
"/duo-redirect-connector.html" +
|
||||||
"?duoFramelessUrl=" +
|
"?duoFramelessUrl=" +
|
||||||
encodeURIComponent(this.duoFramelessUrl) +
|
encodeURIComponent(this.duoFramelessUrl) +
|
||||||
|
@ -2346,12 +2346,6 @@
|
|||||||
"loggingInOn": {
|
"loggingInOn": {
|
||||||
"message": "Logging in on"
|
"message": "Logging in on"
|
||||||
},
|
},
|
||||||
"usDomain": {
|
|
||||||
"message": "bitwarden.com"
|
|
||||||
},
|
|
||||||
"euDomain": {
|
|
||||||
"message": "bitwarden.eu"
|
|
||||||
},
|
|
||||||
"selfHostedServer": {
|
"selfHostedServer": {
|
||||||
"message": "self-hosted"
|
"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 { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
@ -54,7 +54,7 @@ export class Main {
|
|||||||
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
environmentService: EnvironmentService;
|
environmentService: DefaultEnvironmentService;
|
||||||
mainCryptoFunctionService: MainCryptoFunctionService;
|
mainCryptoFunctionService: MainCryptoFunctionService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
migrationRunner: MigrationRunner;
|
migrationRunner: MigrationRunner;
|
||||||
@ -148,7 +148,7 @@ export class Main {
|
|||||||
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
|
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.environmentService = new EnvironmentService(stateProvider, accountService);
|
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||||
|
|
||||||
this.tokenService = new TokenService(
|
this.tokenService = new TokenService(
|
||||||
singleUserStateProvider,
|
singleUserStateProvider,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { app, Menu } from "electron";
|
import { app, Menu } from "electron";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -45,7 +46,8 @@ export class MenuMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getWebVaultUrl() {
|
private async getWebVaultUrl() {
|
||||||
return this.environmentService.getWebVaultUrl() ?? cloudWebVaultUrl;
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
return env.getWebVaultUrl() ?? cloudWebVaultUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initContextMenu() {
|
private initContextMenu() {
|
||||||
|
@ -177,6 +177,7 @@ const renderer = {
|
|||||||
ENV: ENV,
|
ENV: ENV,
|
||||||
FLAGS: envConfig.flags,
|
FLAGS: envConfig.flags,
|
||||||
DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {},
|
DEV_FLAGS: NODE_ENV === "development" ? envConfig.devFlags : {},
|
||||||
|
ADDITIONAL_REGIONS: envConfig.additionalRegions ?? [],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,15 @@
|
|||||||
"proxyNotifications": "http://localhost:61840",
|
"proxyNotifications": "http://localhost:61840",
|
||||||
"wsConnectSrc": "ws://localhost:61840"
|
"wsConnectSrc": "ws://localhost:61840"
|
||||||
},
|
},
|
||||||
|
"additionalRegions": [
|
||||||
|
{
|
||||||
|
"key": "LOCAL",
|
||||||
|
"domain": "localhost",
|
||||||
|
"urls": {
|
||||||
|
"webVault": "https://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"flags": {
|
"flags": {
|
||||||
"secretsManager": true,
|
"secretsManager": true,
|
||||||
"showPasswordless": true,
|
"showPasswordless": true,
|
||||||
|
@ -4,6 +4,22 @@
|
|||||||
"notifications": "https://notifications.euqa.bitwarden.pw",
|
"notifications": "https://notifications.euqa.bitwarden.pw",
|
||||||
"scim": "https://scim.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": {
|
"flags": {
|
||||||
"secretsManager": true,
|
"secretsManager": true,
|
||||||
"showPasswordless": true
|
"showPasswordless": true
|
||||||
|
@ -10,6 +10,22 @@
|
|||||||
"proxyEvents": "https://events.qa.bitwarden.pw",
|
"proxyEvents": "https://events.qa.bitwarden.pw",
|
||||||
"proxyNotifications": "https://notifications.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": {
|
"flags": {
|
||||||
"secretsManager": true,
|
"secretsManager": true,
|
||||||
"showPasswordless": true,
|
"showPasswordless": true,
|
||||||
|
@ -127,7 +127,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
|||||||
await this.submit();
|
await this.submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
override launchDuoFrameless() {
|
override async launchDuoFrameless() {
|
||||||
const duoHandOffMessage = {
|
const duoHandOffMessage = {
|
||||||
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
title: this.i18nService.t("youSuccessfullyLoggedIn"),
|
||||||
message: this.i18nService.t("thisWindowWillCloseIn5Seconds"),
|
message: this.i18nService.t("thisWindowWillCloseIn5Seconds"),
|
||||||
|
@ -44,11 +44,11 @@ export class PremiumComponent implements OnInit {
|
|||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
|
||||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
@ -49,10 +49,10 @@ export class UserSubscriptionComponent implements OnInit {
|
|||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
this.presentUserWithOffboardingSurvey$ = this.configService.getFeatureFlag$<boolean>(
|
this.presentUserWithOffboardingSurvey$ = this.configService.getFeatureFlag$<boolean>(
|
||||||
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
FeatureFlag.AC1607_PresentUserOffboardingSurvey,
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormControl, FormGroup } from "@angular/forms";
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
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 i18nService: I18nService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
) {
|
) {}
|
||||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
|
|
||||||
this.route.params
|
this.route.params
|
||||||
.pipe(
|
.pipe(
|
||||||
concatMap(async (params) => {
|
concatMap(async (params) => {
|
||||||
|
@ -14,11 +14,15 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
|||||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||||
import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request";
|
import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request";
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
|
export type PayPalConfig = {
|
||||||
|
businessId?: string;
|
||||||
|
buttonAction?: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-add-credit",
|
selector: "app-add-credit",
|
||||||
templateUrl: "add-credit.component.html",
|
templateUrl: "add-credit.component.html",
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
<div class="tw-mb-1" *ngIf="showRegionSelector">
|
<div class="tw-mb-1" *ngIf="showRegionSelector">
|
||||||
<bit-menu #environmentOptions>
|
<bit-menu #environmentOptions>
|
||||||
<a
|
<a
|
||||||
|
*ngFor="let region of availableRegions"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
[attr.href]="
|
[attr.href]="
|
||||||
isUsServer ? 'javascript:void(0)' : 'https://vault.bitwarden.com' + routeAndParams
|
region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams
|
||||||
"
|
"
|
||||||
class="pr-4"
|
class="pr-4"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-fw bwi-sm bwi-check pb-1"
|
class="bwi bwi-fw bwi-sm bwi-check pb-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
[style.visibility]="isUsServer ? 'visible' : 'hidden'"
|
[style.visibility]="region == currentRegion ? 'visible' : 'hidden'"
|
||||||
></i>
|
></i>
|
||||||
{{ "usDomain" | i18n }}
|
{{ region.domain }}
|
||||||
</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 }}
|
|
||||||
</a>
|
</a>
|
||||||
</bit-menu>
|
</bit-menu>
|
||||||
<div>
|
<div>
|
||||||
{{ "server" | i18n }}:
|
{{ "server" | i18n }}:
|
||||||
<a [routerLink]="[]" [bitMenuTriggerFor]="environmentOptions">
|
<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>
|
><i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 {
|
export class EnvironmentSelectorComponent implements OnInit {
|
||||||
constructor(
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isEuServer: boolean;
|
protected availableRegions = this.environmentService.availableRegions();
|
||||||
isUsServer: boolean;
|
protected currentRegion?: RegionConfig;
|
||||||
showRegionSelector = false;
|
|
||||||
routeAndParams: string;
|
protected showRegionSelector = false;
|
||||||
|
protected routeAndParams: string;
|
||||||
|
|
||||||
async ngOnInit() {
|
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.showRegionSelector = !this.platformUtilsService.isSelfHost();
|
||||||
this.routeAndParams = `/#${this.router.url}`;
|
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_MEMORY_STORAGE,
|
||||||
OBSERVABLE_DISK_STORAGE,
|
OBSERVABLE_DISK_STORAGE,
|
||||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||||
|
WINDOW,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
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 as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
||||||
import { LoginService } from "@bitwarden/common/auth/services/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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
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 { 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";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
|
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
|
||||||
import {
|
import {
|
||||||
@ -41,6 +44,7 @@ import {
|
|||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
|
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||||
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||||
import { WindowStorageService } from "../platform/window-storage.service";
|
import { WindowStorageService } from "../platform/window-storage.service";
|
||||||
@ -138,6 +142,11 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
|||||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: EnvironmentService,
|
||||||
|
useClass: WebEnvironmentService,
|
||||||
|
deps: [WINDOW, StateProvider, AccountService],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: ThemeStateService,
|
provide: ThemeStateService,
|
||||||
useFactory: (globalStateProvider: GlobalStateProvider) =>
|
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 { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import {
|
|
||||||
EnvironmentService as EnvironmentServiceAbstraction,
|
|
||||||
Urls,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.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 {
|
export class InitService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINDOW) private win: Window,
|
@Inject(WINDOW) private win: Window,
|
||||||
private environmentService: EnvironmentServiceAbstraction,
|
|
||||||
private notificationsService: NotificationsServiceAbstraction,
|
private notificationsService: NotificationsServiceAbstraction,
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private i18nService: I18nServiceAbstraction,
|
private i18nService: I18nServiceAbstraction,
|
||||||
@ -41,13 +36,6 @@ export class InitService {
|
|||||||
return async () => {
|
return async () => {
|
||||||
await this.stateService.init();
|
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);
|
setTimeout(() => this.notificationsService.init(), 3000);
|
||||||
await this.vaultTimeoutService.init(true);
|
await this.vaultTimeoutService.init(true);
|
||||||
await this.i18nService.init();
|
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": {
|
"enforceOnLoginDesc": {
|
||||||
"message": "Require existing members to change their passwords"
|
"message": "Require existing members to change their passwords"
|
||||||
},
|
},
|
||||||
"usDomain": {
|
|
||||||
"message": "bitwarden.com"
|
|
||||||
},
|
|
||||||
"euDomain": {
|
|
||||||
"message": "bitwarden.eu"
|
|
||||||
},
|
|
||||||
"smProjectDeleteAccessRestricted": {
|
"smProjectDeleteAccessRestricted": {
|
||||||
"message": "You don't have permissions to delete this project",
|
"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."
|
"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"] ?? {},
|
PAYPAL_CONFIG: envConfig["paypal"] ?? {},
|
||||||
FLAGS: envConfig["flags"] ?? {},
|
FLAGS: envConfig["flags"] ?? {},
|
||||||
DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {},
|
DEV_FLAGS: NODE_ENV === "development" ? envConfig["devFlags"] : {},
|
||||||
|
ADDITIONAL_REGIONS: envConfig["additionalRegions"] ?? [],
|
||||||
}),
|
}),
|
||||||
new AngularWebpackPlugin({
|
new AngularWebpackPlugin({
|
||||||
tsConfigPath: "tsconfig.json",
|
tsConfigPath: "tsconfig.json",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { UntypedFormBuilder, FormControl } from "@angular/forms";
|
import { UntypedFormBuilder, FormControl } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
@ -76,13 +77,13 @@ export class ScimComponent implements OnInit {
|
|||||||
apiKeyRequest,
|
apiKeyRequest,
|
||||||
);
|
);
|
||||||
this.formData.setValue({
|
this.formData.setValue({
|
||||||
endpointUrl: this.getScimEndpointUrl(),
|
endpointUrl: await this.getScimEndpointUrl(),
|
||||||
clientSecret: apiKeyResponse.apiKey,
|
clientSecret: apiKeyResponse.apiKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyScimUrl() {
|
async copyScimUrl() {
|
||||||
this.platformUtilsService.copyToClipboard(this.getScimEndpointUrl());
|
this.platformUtilsService.copyToClipboard(await this.getScimEndpointUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
async rotateScimKey() {
|
async rotateScimKey() {
|
||||||
@ -148,8 +149,9 @@ export class ScimComponent implements OnInit {
|
|||||||
this.formPromise = null;
|
this.formPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScimEndpointUrl() {
|
async getScimEndpointUrl() {
|
||||||
return this.environmentService.getScimUrl() + "/" + this.organizationId;
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
return env.getScimUrl() + "/" + this.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleScimKey() {
|
toggleScimKey() {
|
||||||
@ -163,7 +165,7 @@ export class ScimComponent implements OnInit {
|
|||||||
this.showScimSettings = true;
|
this.showScimSettings = true;
|
||||||
this.enabled.setValue(true);
|
this.enabled.setValue(true);
|
||||||
this.formData.setValue({
|
this.formData.setValue({
|
||||||
endpointUrl: this.getScimEndpointUrl(),
|
endpointUrl: await this.getScimEndpointUrl(),
|
||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
});
|
});
|
||||||
await this.loadApiKey();
|
await this.loadApiKey();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Directive, Input } from "@angular/core";
|
import { Directive, Input } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
|
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@ -19,7 +20,8 @@ export abstract class CaptchaProtectedComponent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async setupCaptcha() {
|
async setupCaptcha() {
|
||||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const webVaultUrl = env.getWebVaultUrl();
|
||||||
|
|
||||||
this.captcha = new CaptchaIFrame(
|
this.captcha = new CaptchaIFrame(
|
||||||
window,
|
window,
|
||||||
|
@ -7,17 +7,15 @@
|
|||||||
#trigger="cdkOverlayOrigin"
|
#trigger="cdkOverlayOrigin"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-controls="cdk-overlay-container"
|
aria-controls="cdk-overlay-container"
|
||||||
[ngSwitch]="selectedEnvironment"
|
|
||||||
>
|
>
|
||||||
<span *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{
|
<span class="text-primary">
|
||||||
"usDomain" | i18n
|
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
|
||||||
}}</span>
|
{{ selectedRegion.domain }}
|
||||||
<span *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{
|
</ng-container>
|
||||||
"euDomain" | i18n
|
<ng-template #fallback>
|
||||||
}}</span>
|
{{ "selfHostedServer" | i18n }}
|
||||||
<span *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
|
</ng-template>
|
||||||
"selfHostedServer" | i18n
|
</span>
|
||||||
}}</span>
|
|
||||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -41,40 +39,23 @@
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
|
<ng-container *ngFor="let region of availableRegions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="environment-selector-dialog-item"
|
class="environment-selector-dialog-item"
|
||||||
(click)="toggle(ServerEnvironmentType.US)"
|
(click)="toggle(region.key)"
|
||||||
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.US ? 'true' : 'false'"
|
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-fw bwi-sm bwi-check"
|
class="bwi bwi-fw bwi-sm bwi-check"
|
||||||
style="padding-bottom: 1px"
|
style="padding-bottom: 1px"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
[style.visibility]="
|
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'"
|
||||||
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
|
|
||||||
"
|
|
||||||
></i>
|
></i>
|
||||||
<span>{{ "usDomain" | i18n }}</span>
|
<span>{{ region.domain }}</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>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
|
</ng-container>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="environment-selector-dialog-item"
|
class="environment-selector-dialog-item"
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
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 { 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 {
|
import {
|
||||||
EnvironmentService as EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
Region,
|
Region,
|
||||||
|
RegionConfig,
|
||||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -34,7 +34,7 @@ import {
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
export class EnvironmentSelectorComponent {
|
||||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
showingModal = false;
|
showingModal = false;
|
||||||
@ -48,59 +48,34 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
|||||||
overlayY: "top",
|
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(
|
constructor(
|
||||||
protected environmentService: EnvironmentServiceAbstraction,
|
protected environmentService: EnvironmentService,
|
||||||
protected configService: ConfigServiceAbstraction,
|
|
||||||
protected router: Router,
|
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) {
|
async toggle(option: Region) {
|
||||||
this.isOpen = !this.isOpen;
|
this.isOpen = !this.isOpen;
|
||||||
if (option === null) {
|
if (option === null) {
|
||||||
return;
|
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) {
|
if (option === Region.SelfHosted) {
|
||||||
this.onOpenSelfHostedSettings.emit();
|
this.onOpenSelfHostedSettings.emit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.environmentService.setRegion(option);
|
await this.environmentService.setEnvironment(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
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 { Directive, EventEmitter, Output } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
@ -27,21 +28,29 @@ export class EnvironmentComponent {
|
|||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
) {
|
) {
|
||||||
const urls = this.environmentService.getUrls();
|
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||||
if (this.environmentService.selectedRegion != Region.SelfHosted) {
|
if (env.getRegion() !== Region.SelfHosted) {
|
||||||
|
this.baseUrl = "";
|
||||||
|
this.webVaultUrl = "";
|
||||||
|
this.apiUrl = "";
|
||||||
|
this.identityUrl = "";
|
||||||
|
this.iconsUrl = "";
|
||||||
|
this.notificationsUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urls = env.getUrls();
|
||||||
this.baseUrl = urls.base || "";
|
this.baseUrl = urls.base || "";
|
||||||
this.webVaultUrl = urls.webVault || "";
|
this.webVaultUrl = urls.webVault || "";
|
||||||
this.apiUrl = urls.api || "";
|
this.apiUrl = urls.api || "";
|
||||||
this.identityUrl = urls.identity || "";
|
this.identityUrl = urls.identity || "";
|
||||||
this.iconsUrl = urls.icons || "";
|
this.iconsUrl = urls.icons || "";
|
||||||
this.notificationsUrl = urls.notifications || "";
|
this.notificationsUrl = urls.notifications || "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
const resUrls = await this.environmentService.setUrls({
|
await this.environmentService.setEnvironment(Region.SelfHosted, {
|
||||||
base: this.baseUrl,
|
base: this.baseUrl,
|
||||||
api: this.apiUrl,
|
api: this.apiUrl,
|
||||||
identity: this.identityUrl,
|
identity: this.identityUrl,
|
||||||
@ -50,14 +59,6 @@ export class EnvironmentComponent {
|
|||||||
notifications: this.notificationsUrl,
|
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.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
|
||||||
this.saved();
|
this.saved();
|
||||||
}
|
}
|
||||||
|
@ -346,7 +346,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||||||
!this.platformUtilsService.supportsSecureStorage());
|
!this.platformUtilsService.supportsSecureStorage());
|
||||||
this.email = await this.stateService.getEmail();
|
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 { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { Subject } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
import { take, takeUntil } from "rxjs/operators";
|
import { take, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||||
@ -84,10 +84,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
super(environmentService, i18nService, platformUtilsService);
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
}
|
}
|
||||||
|
|
||||||
get selfHostedDomain() {
|
|
||||||
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
@ -245,7 +241,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
|||||||
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||||
|
|
||||||
// Build URI
|
// Build URI
|
||||||
const webUrl = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const webUrl = env.getWebVaultUrl();
|
||||||
|
|
||||||
// Launch browser
|
// Launch browser
|
||||||
this.platformUtilsService.launchUri(
|
this.platformUtilsService.launchUri(
|
||||||
|
@ -157,8 +157,10 @@ export class SsoComponent {
|
|||||||
// Save state (regardless of new or existing)
|
// Save state (regardless of new or existing)
|
||||||
await this.ssoLoginService.setSsoState(state);
|
await this.ssoLoginService.setSsoState(state);
|
||||||
|
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
|
||||||
let authorizeUrl =
|
let authorizeUrl =
|
||||||
this.environmentService.getIdentityUrl() +
|
env.getIdentityUrl() +
|
||||||
"/connect/authorize?" +
|
"/connect/authorize?" +
|
||||||
"client_id=" +
|
"client_id=" +
|
||||||
this.clientId +
|
this.clientId +
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
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);
|
this.onProviderSelected.emit(p.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
recover() {
|
async recover() {
|
||||||
const webVault = this.environmentService.getWebVaultUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const webVault = env.getWebVaultUrl();
|
||||||
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
|
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
|
||||||
this.onRecoverSelected.emit();
|
this.onRecoverSelected.emit();
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.win != null && this.webAuthnSupported) {
|
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.webAuthn = new WebAuthnIFrame(
|
||||||
this.win,
|
this.win,
|
||||||
webVaultUrl,
|
webVaultUrl,
|
||||||
@ -494,5 +495,5 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// implemented in clients
|
// 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 { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EnvironmentService 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 { 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 { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.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 { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-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 { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
@ -363,7 +363,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
KeyConnectorServiceAbstraction,
|
KeyConnectorServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
TwoFactorServiceAbstraction,
|
TwoFactorServiceAbstraction,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
@ -477,8 +477,8 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateProvider],
|
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EnvironmentServiceAbstraction,
|
provide: EnvironmentService,
|
||||||
useClass: EnvironmentService,
|
useClass: DefaultEnvironmentService,
|
||||||
deps: [StateProvider, AccountServiceAbstraction],
|
deps: [StateProvider, AccountServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
@ -545,7 +545,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
deps: [
|
deps: [
|
||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
@ -647,7 +647,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
LogService,
|
LogService,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
MigrationRunner,
|
MigrationRunner,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
@ -711,7 +711,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
SyncServiceAbstraction,
|
SyncServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
@ -853,8 +853,9 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
ConfigApiServiceAbstraction,
|
ConfigApiServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentService,
|
||||||
LogService,
|
LogService,
|
||||||
|
StateProvider,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
@ -869,7 +870,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AnonymousHubServiceAbstraction,
|
provide: AnonymousHubServiceAbstraction,
|
||||||
useClass: AnonymousHubService,
|
useClass: AnonymousHubService,
|
||||||
deps: [EnvironmentServiceAbstraction, LoginStrategyServiceAbstraction, LogService],
|
deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ValidationServiceAbstraction,
|
provide: ValidationServiceAbstraction,
|
||||||
@ -949,7 +950,7 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: WebAuthnLoginApiServiceAbstraction,
|
provide: WebAuthnLoginApiServiceAbstraction,
|
||||||
useClass: WebAuthnLoginApiService,
|
useClass: WebAuthnLoginApiService,
|
||||||
deps: [ApiServiceAbstraction, EnvironmentServiceAbstraction],
|
deps: [ApiServiceAbstraction, EnvironmentService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: WebAuthnLoginServiceAbstraction,
|
provide: WebAuthnLoginServiceAbstraction,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DatePipe } from "@angular/common";
|
import { DatePipe } from "@angular/common";
|
||||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
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("sendTypeFile"), value: SendType.File, premium: true },
|
||||||
{ name: i18nService.t("sendTypeText"), value: SendType.Text, premium: false },
|
{ name: i18nService.t("sendTypeText"), value: SendType.Text, premium: false },
|
||||||
];
|
];
|
||||||
this.sendLinkBaseUrl = this.environmentService.getSendUrl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get link(): string {
|
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$
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((hasPremiumFromAnySource) => {
|
.subscribe((hasPremiumFromAnySource) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
|
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 { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@ -198,9 +198,9 @@ export class SendComponent implements OnInit, OnDestroy {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(s: SendView) {
|
async copy(s: SendView) {
|
||||||
const sendLinkBaseUrl = this.environmentService.getSendUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key;
|
const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key;
|
||||||
this.platformUtilsService.copyToClipboard(link);
|
this.platformUtilsService.copyToClipboard(link);
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
|
@ -39,11 +39,12 @@ export class IconComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const iconsUrl = this.environmentService.getIconsUrl();
|
|
||||||
|
|
||||||
this.data$ = combineLatest([
|
this.data$ = combineLatest([
|
||||||
|
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
|
||||||
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
|
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
|
||||||
this.cipher$.pipe(filter((c) => c !== undefined)),
|
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 { OnInit, Directive } from "@angular/core";
|
||||||
import { Observable, Subject } from "rxjs";
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class PremiumComponent {
|
export class PremiumComponent implements OnInit {
|
||||||
isPremium$: Observable<boolean>;
|
isPremium$: Observable<boolean>;
|
||||||
price = 10;
|
price = 10;
|
||||||
refreshPromise: Promise<any>;
|
refreshPromise: Promise<any>;
|
||||||
cloudWebVaultUrl: string;
|
cloudWebVaultUrl: string;
|
||||||
private directiveIsDestroyed$ = new Subject<boolean>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
@ -25,13 +24,16 @@ export class PremiumComponent {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
this.cloudWebVaultUrl = environmentService.getCloudWebVaultUrl();
|
|
||||||
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
try {
|
try {
|
||||||
this.refreshPromise = this.apiService.refreshIdentityToken();
|
this.refreshPromise = this.apiService.refreshIdentityToken();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.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 { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -145,8 +149,11 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
const tokenResponse = identityTokenResponseFactory();
|
const tokenResponse = identityTokenResponseFactory();
|
||||||
tokenResponse.apiUseKeyConnector = true;
|
tokenResponse.apiUseKeyConnector = true;
|
||||||
|
|
||||||
|
const env = mock<Environment>();
|
||||||
|
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||||
|
environmentService.environment$ = new BehaviorSubject(env);
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
|
||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
await apiLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
@ -160,8 +167,11 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
const tokenResponse = identityTokenResponseFactory();
|
const tokenResponse = identityTokenResponseFactory();
|
||||||
tokenResponse.apiUseKeyConnector = true;
|
tokenResponse.apiUseKeyConnector = true;
|
||||||
|
|
||||||
|
const env = mock<Environment>();
|
||||||
|
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||||
|
environmentService.environment$ = new BehaviorSubject(env);
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
|
||||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@ -85,7 +85,8 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const keyConnectorUrl = env.getKeyConnectorUrl();
|
||||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
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,
|
IHubProtocol,
|
||||||
} from "@microsoft/signalr";
|
} from "@microsoft/signalr";
|
||||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction } from "../../../../auth/src/common/abstractions/login-strategy.service";
|
import { LoginStrategyServiceAbstraction } from "../../../../auth/src/common/abstractions/login-strategy.service";
|
||||||
import {
|
import {
|
||||||
@ -26,7 +27,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createHubConnection(token: string) {
|
async createHubConnection(token: string) {
|
||||||
this.url = this.environmentService.getNotificationsUrl();
|
this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl();
|
||||||
|
|
||||||
this.anonHubConnection = new HubConnectionBuilder()
|
this.anonHubConnection = new HubConnectionBuilder()
|
||||||
.withUrl(this.url + "/anonymous-hub?Token=" + token, {
|
.withUrl(this.url + "/anonymous-hub?Token=" + token, {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../../abstractions/api.service";
|
import { ApiService } from "../../../abstractions/api.service";
|
||||||
import { EnvironmentService } from "../../../platform/abstractions/environment.service";
|
import { EnvironmentService } from "../../../platform/abstractions/environment.service";
|
||||||
import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction";
|
import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||||
@ -11,13 +13,14 @@ export class WebAuthnLoginApiService implements WebAuthnLoginApiServiceAbstracti
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCredentialAssertionOptions(): Promise<CredentialAssertionOptionsResponse> {
|
async getCredentialAssertionOptions(): Promise<CredentialAssertionOptionsResponse> {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const response = await this.apiService.send(
|
const response = await this.apiService.send(
|
||||||
"GET",
|
"GET",
|
||||||
`/accounts/webauthn/assertion-options`,
|
`/accounts/webauthn/assertion-options`,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
this.environmentService.getIdentityUrl(),
|
env.getIdentityUrl(),
|
||||||
);
|
);
|
||||||
return new CredentialAssertionOptionsResponse(response);
|
return new CredentialAssertionOptionsResponse(response);
|
||||||
}
|
}
|
||||||
|
@ -14,64 +14,119 @@ export type Urls = {
|
|||||||
scim?: string;
|
scim?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayPalConfig = {
|
/**
|
||||||
businessId?: string;
|
* A subset of available regions, additional regions can be loaded through configuration.
|
||||||
buttonAction?: string;
|
*/
|
||||||
};
|
|
||||||
|
|
||||||
export enum Region {
|
export enum Region {
|
||||||
US = "US",
|
US = "US",
|
||||||
EU = "EU",
|
EU = "EU",
|
||||||
SelfHosted = "Self-hosted",
|
SelfHosted = "Self-hosted",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RegionDomain {
|
/**
|
||||||
US = "bitwarden.com",
|
* The possible cloud regions.
|
||||||
EU = "bitwarden.eu",
|
*/
|
||||||
USQA = "bitwarden.pw",
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class EnvironmentService {
|
|
||||||
urls: Observable<void>;
|
|
||||||
usUrls: Urls;
|
|
||||||
euUrls: Urls;
|
|
||||||
selectedRegion?: Region;
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
hasBaseUrl: () => boolean;
|
|
||||||
getNotificationsUrl: () => string;
|
|
||||||
getWebVaultUrl: () => string;
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the URL of the cloud web vault app.
|
* The environment service. Provides access to set the current environment urls and region.
|
||||||
*
|
|
||||||
* @returns {string} The URL of the cloud web vault app.
|
|
||||||
* @remarks Use this method only in views exclusive to self-host instances.
|
|
||||||
*/
|
*/
|
||||||
getCloudWebVaultUrl: () => string;
|
export abstract class EnvironmentService {
|
||||||
|
abstract environment$: Observable<Environment>;
|
||||||
|
abstract cloudWebVaultUrl$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all the available regions for environment selectors.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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.
|
* 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>;
|
abstract getEnvironment(userId?: string): Promise<Environment | undefined>;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
import { ReplaySubject, skip, take } from "rxjs";
|
import { ReplaySubject, skip, take } from "rxjs";
|
||||||
|
|
||||||
|
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
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 { LogService } from "../../abstractions/log.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||||
@ -14,6 +16,7 @@ import {
|
|||||||
ServerConfigResponse,
|
ServerConfigResponse,
|
||||||
ThirdPartyServerConfigResponse,
|
ThirdPartyServerConfigResponse,
|
||||||
} from "../../models/response/server-config.response";
|
} from "../../models/response/server-config.response";
|
||||||
|
import { StateProvider } from "../../state";
|
||||||
|
|
||||||
import { ConfigService } from "./config.service";
|
import { ConfigService } from "./config.service";
|
||||||
|
|
||||||
@ -23,6 +26,8 @@ describe("ConfigService", () => {
|
|||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
|
let replaySubject: ReplaySubject<Environment>;
|
||||||
|
let stateProvider: StateProvider;
|
||||||
|
|
||||||
let serverResponseCount: number; // increments to track distinct responses received from server
|
let serverResponseCount: number; // increments to track distinct responses received from server
|
||||||
|
|
||||||
@ -35,6 +40,7 @@ describe("ConfigService", () => {
|
|||||||
authService,
|
authService,
|
||||||
environmentService,
|
environmentService,
|
||||||
logService,
|
logService,
|
||||||
|
stateProvider,
|
||||||
);
|
);
|
||||||
configService.init();
|
configService.init();
|
||||||
return configService;
|
return configService;
|
||||||
@ -46,8 +52,11 @@ describe("ConfigService", () => {
|
|||||||
authService = mock();
|
authService = mock();
|
||||||
environmentService = mock();
|
environmentService = mock();
|
||||||
logService = 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;
|
serverResponseCount = 1;
|
||||||
configApiService.get.mockImplementation(() =>
|
configApiService.get.mockImplementation(() =>
|
||||||
@ -139,7 +148,7 @@ describe("ConfigService", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
(environmentService.urls as ReplaySubject<void>).next();
|
replaySubject.next(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when triggerServerConfigFetch() is called", (done) => {
|
it("when triggerServerConfigFetch() is called", (done) => {
|
||||||
|
@ -22,6 +22,7 @@ import { EnvironmentService, Region } from "../../abstractions/environment.servi
|
|||||||
import { LogService } from "../../abstractions/log.service";
|
import { LogService } from "../../abstractions/log.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||||
|
import { StateProvider } from "../../state";
|
||||||
|
|
||||||
const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600;
|
const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600;
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ export class ConfigService implements ConfigServiceAbstraction {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
|
||||||
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
|
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
|
||||||
private subscribe = true,
|
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
|
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here
|
||||||
merge(
|
merge(
|
||||||
this.refreshTimer$, // an overridable interval
|
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
|
this._forceFetchConfig, // manual
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -104,8 +106,9 @@ export class ConfigService implements ConfigServiceAbstraction {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
await this.stateService.setServerConfig(data);
|
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 CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||||
export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk");
|
export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk");
|
||||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "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 THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" });
|
||||||
export const TRANSLATION_DISK = new StateDefinition("translation", "disk");
|
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 { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
|
||||||
import { OrganizationConnectionType } from "../admin-console/enums";
|
import { OrganizationConnectionType } from "../admin-console/enums";
|
||||||
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
|
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()
|
||||||
: request.toIdentityToken(this.platformUtilsService.getClientType());
|
: request.toIdentityToken(this.platformUtilsService.getClientType());
|
||||||
|
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
|
||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
new Request(env.getIdentityUrl() + "/connect/token", {
|
||||||
body: this.qsStringify(identityToken),
|
body: this.qsStringify(identityToken),
|
||||||
credentials: this.getCredentials(),
|
credentials: await this.getCredentials(),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -323,13 +327,14 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const r = await this.send(
|
const r = await this.send(
|
||||||
"POST",
|
"POST",
|
||||||
"/accounts/prelogin",
|
"/accounts/prelogin",
|
||||||
request,
|
request,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
this.environmentService.getIdentityUrl(),
|
env.getIdentityUrl(),
|
||||||
);
|
);
|
||||||
return new PreloginResponse(r);
|
return new PreloginResponse(r);
|
||||||
}
|
}
|
||||||
@ -368,13 +373,14 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
|
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const r = await this.send(
|
const r = await this.send(
|
||||||
"POST",
|
"POST",
|
||||||
"/accounts/register",
|
"/accounts/register",
|
||||||
request,
|
request,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
this.environmentService.getIdentityUrl(),
|
env.getIdentityUrl(),
|
||||||
);
|
);
|
||||||
return new RegisterResponse(r);
|
return new RegisterResponse(r);
|
||||||
}
|
}
|
||||||
@ -1457,10 +1463,11 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
if (this.customUserAgent != null) {
|
if (this.customUserAgent != null) {
|
||||||
headers.set("User-Agent", this.customUserAgent);
|
headers.set("User-Agent", this.customUserAgent);
|
||||||
}
|
}
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
new Request(this.environmentService.getEventsUrl() + "/collect", {
|
new Request(env.getEventsUrl() + "/collect", {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
credentials: this.getCredentials(),
|
credentials: await this.getCredentials(),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -1617,11 +1624,12 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
headers.set("User-Agent", this.customUserAgent);
|
headers.set("User-Agent", this.customUserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const path = `/sso/prevalidate?domainHint=${encodeURIComponent(identifier)}`;
|
const path = `/sso/prevalidate?domainHint=${encodeURIComponent(identifier)}`;
|
||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
new Request(this.environmentService.getIdentityUrl() + path, {
|
new Request(env.getIdentityUrl() + path, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
credentials: this.getCredentials(),
|
credentials: await this.getCredentials(),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
@ -1751,16 +1759,17 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
headers.set("User-Agent", this.customUserAgent);
|
headers.set("User-Agent", this.customUserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const decodedToken = await this.tokenService.decodeAccessToken();
|
const decodedToken = await this.tokenService.decodeAccessToken();
|
||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
new Request(env.getIdentityUrl() + "/connect/token", {
|
||||||
body: this.qsStringify({
|
body: this.qsStringify({
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
client_id: decodedToken.client_id,
|
client_id: decodedToken.client_id,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
}),
|
}),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
credentials: this.getCredentials(),
|
credentials: await this.getCredentials(),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
@ -1822,7 +1831,8 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
apiUrl?: string,
|
apiUrl?: string,
|
||||||
alterHeaders?: (headers: Headers) => void,
|
alterHeaders?: (headers: Headers) => void,
|
||||||
): Promise<any> {
|
): 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
|
// Prevent directory traversal from malicious paths
|
||||||
const pathParts = path.split("?");
|
const pathParts = path.split("?");
|
||||||
@ -1838,7 +1848,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
|
|
||||||
const requestInit: RequestInit = {
|
const requestInit: RequestInit = {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
credentials: this.getCredentials(),
|
credentials: await this.getCredentials(),
|
||||||
method: method,
|
method: method,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1917,8 +1927,9 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
.join("&");
|
.join("&");
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCredentials(): RequestCredentials {
|
private async getCredentials(): Promise<RequestCredentials> {
|
||||||
if (!this.isWebClient || this.environmentService.hasBaseUrl()) {
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
if (!this.isWebClient || env.hasBaseUrl()) {
|
||||||
return "include";
|
return "include";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as signalR from "@microsoft/signalr";
|
import * as signalR from "@microsoft/signalr";
|
||||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../abstractions/api.service";
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||||
@ -38,7 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
) {
|
) {
|
||||||
this.environmentService.urls.subscribe(() => {
|
this.environmentService.environment$.subscribe(() => {
|
||||||
if (!this.inited) {
|
if (!this.inited) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -51,7 +52,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
this.inited = false;
|
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
|
// Set notifications server URL to `https://-` to effectively disable communication
|
||||||
// with the notifications server from the client app
|
// 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 { 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 { 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 { 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 { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
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";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 44;
|
export const CURRENT_VERSION = 45;
|
||||||
|
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@ -94,7 +96,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(EventCollectionMigrator, 40, 41)
|
.with(EventCollectionMigrator, 40, 41)
|
||||||
.with(EnableFaviconMigrator, 41, 42)
|
.with(EnableFaviconMigrator, 41, 42)
|
||||||
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
|
.with(AutoConfirmFingerPrintsMigrator, 42, 43)
|
||||||
.with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION);
|
.with(UserDecryptionOptionsMigrator, 43, 44)
|
||||||
|
.with(MergeEnvironmentState, 44, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
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({
|
this.oidcClient = new OidcClient({
|
||||||
authority: this.vault.userType.openIDConnectAuthorityBase,
|
authority: this.vault.userType.openIDConnectAuthorityBase,
|
||||||
client_id: this.vault.userType.openIDConnectClientId,
|
client_id: this.vault.userType.openIDConnectClientId,
|
||||||
redirect_uri: this.getOidcRedirectUrl(),
|
redirect_uri: await this.getOidcRedirectUrl(),
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: this.vault.userType.oidcScope,
|
scope: this.vault.userType.oidcScope,
|
||||||
response_mode: "query",
|
response_mode: "query",
|
||||||
@ -151,12 +151,13 @@ export class LastPassDirectImportService {
|
|||||||
return redirectUri + "&" + params;
|
return redirectUri + "&" + params;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOidcRedirectUrl() {
|
private async getOidcRedirectUrl() {
|
||||||
const clientType = this.platformUtilsService.getClientType();
|
const clientType = this.platformUtilsService.getClientType();
|
||||||
if (clientType === ClientType.Desktop) {
|
if (clientType === ClientType.Desktop) {
|
||||||
return "bitwarden://import-callback-lp";
|
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";
|
return webUrl + "/sso-connector.html?lp=1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user