1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00: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:
Oscar Hinton
2024-03-21 17:09:44 +01:00
committed by GitHub
parent 7a42b4ebc6
commit e767295c86
88 changed files with 1710 additions and 1379 deletions

View File

@@ -1,4 +1,5 @@
import { Directive, Input } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -19,7 +20,8 @@ export abstract class CaptchaProtectedComponent {
) {}
async setupCaptcha() {
const webVaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
this.captcha = new CaptchaIFrame(
window,

View File

@@ -7,17 +7,15 @@
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
[ngSwitch]="selectedEnvironment"
>
<span *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{
"usDomain" | i18n
}}</span>
<span *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{
"euDomain" | i18n
}}</span>
<span *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
"selfHostedServer" | i18n
}}</span>
<span class="text-primary">
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
{{ selectedRegion.domain }}
</ng-container>
<ng-template #fallback>
{{ "selfHostedServer" | i18n }}
</ng-template>
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
@@ -41,40 +39,23 @@
role="dialog"
aria-modal="true"
>
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.US)"
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.US ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
"
></i>
<span>{{ "usDomain" | i18n }}</span>
</button>
<br />
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.EU)"
[attr.aria-pressed]="selectedEnvironment === ServerEnvironmentType.EU ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="
selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden'
"
></i>
<span>{{ "euDomain" | i18n }}</span>
</button>
<br />
<ng-container *ngFor="let region of availableRegions">
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(region.key)"
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
style="padding-bottom: 1px"
aria-hidden="true"
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'"
></i>
<span>{{ region.domain }}</span>
</button>
<br />
</ng-container>
<button
type="button"
class="environment-selector-dialog-item"

View File

@@ -1,13 +1,13 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
import { Component, EventEmitter, Output } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Observable, map } from "rxjs";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import {
EnvironmentService as EnvironmentServiceAbstraction,
EnvironmentService,
Region,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
@Component({
@@ -34,7 +34,7 @@ import {
]),
],
})
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
export class EnvironmentSelectorComponent {
@Output() onOpenSelfHostedSettings = new EventEmitter();
isOpen = false;
showingModal = false;
@@ -48,59 +48,34 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
overlayY: "top",
},
];
protected componentDestroyed$: Subject<void> = new Subject();
protected availableRegions = this.environmentService.availableRegions();
protected selectedRegion$: Observable<RegionConfig | undefined> =
this.environmentService.environment$.pipe(
map((e) => e.getRegion()),
map((r) => this.availableRegions.find((ar) => ar.key === r)),
);
constructor(
protected environmentService: EnvironmentServiceAbstraction,
protected configService: ConfigServiceAbstraction,
protected environmentService: EnvironmentService,
protected router: Router,
) {}
async ngOnInit() {
this.configService.serverConfig$.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateEnvironmentInfo();
});
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateEnvironmentInfo();
}
ngOnDestroy(): void {
this.componentDestroyed$.next();
this.componentDestroyed$.complete();
}
async toggle(option: Region) {
this.isOpen = !this.isOpen;
if (option === null) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateEnvironmentInfo();
if (option === Region.SelfHosted) {
this.onOpenSelfHostedSettings.emit();
return;
}
await this.environmentService.setRegion(option);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateEnvironmentInfo();
}
async updateEnvironmentInfo() {
this.selectedEnvironment = this.environmentService.selectedRegion;
await this.environmentService.setEnvironment(option);
}
close() {
this.isOpen = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateEnvironmentInfo();
}
}

View File

@@ -1,4 +1,5 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
EnvironmentService,
@@ -27,21 +28,29 @@ export class EnvironmentComponent {
protected i18nService: I18nService,
private modalService: ModalService,
) {
const urls = this.environmentService.getUrls();
if (this.environmentService.selectedRegion != Region.SelfHosted) {
return;
}
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
if (env.getRegion() !== Region.SelfHosted) {
this.baseUrl = "";
this.webVaultUrl = "";
this.apiUrl = "";
this.identityUrl = "";
this.iconsUrl = "";
this.notificationsUrl = "";
return;
}
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
const urls = env.getUrls();
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
});
}
async submit() {
const resUrls = await this.environmentService.setUrls({
await this.environmentService.setEnvironment(Region.SelfHosted, {
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
@@ -50,14 +59,6 @@ export class EnvironmentComponent {
notifications: this.notificationsUrl,
});
// re-set urls since service can change them, ex: prefixing https://
this.baseUrl = resUrls.base;
this.apiUrl = resUrls.api;
this.identityUrl = resUrls.identity;
this.webVaultUrl = resUrls.webVault;
this.iconsUrl = resUrls.icons;
this.notificationsUrl = resUrls.notifications;
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
this.saved();
}

View File

@@ -346,7 +346,7 @@ export class LockComponent implements OnInit, OnDestroy {
!this.platformUtilsService.supportsSecureStorage());
this.email = await this.stateService.getEmail();
this.webVaultHostname = await this.environmentService.getHost();
this.webVaultHostname = (await this.environmentService.getEnvironment()).getHostname();
}
/**

View File

@@ -1,7 +1,7 @@
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject } from "rxjs";
import { Subject, firstValueFrom } from "rxjs";
import { take, takeUntil } from "rxjs/operators";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
@@ -84,10 +84,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
super(environmentService, i18nService, platformUtilsService);
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
async ngOnInit() {
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (!params) {
@@ -245,7 +241,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
// Build URI
const webUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webUrl = env.getWebVaultUrl();
// Launch browser
this.platformUtilsService.launchUri(

View File

@@ -157,8 +157,10 @@ export class SsoComponent {
// Save state (regardless of new or existing)
await this.ssoLoginService.setSsoState(state);
const env = await firstValueFrom(this.environmentService.environment$);
let authorizeUrl =
this.environmentService.getIdentityUrl() +
env.getIdentityUrl() +
"/connect/authorize?" +
"client_id=" +
this.clientId +

View File

@@ -1,5 +1,6 @@
import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@@ -31,8 +32,9 @@ export class TwoFactorOptionsComponent implements OnInit {
this.onProviderSelected.emit(p.type);
}
recover() {
const webVault = this.environmentService.getWebVaultUrl();
async recover() {
const env = await firstValueFrom(this.environmentService.environment$);
const webVault = env.getWebVaultUrl();
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
this.onRecoverSelected.emit();
}

View File

@@ -116,7 +116,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
}
if (this.win != null && this.webAuthnSupported) {
const webVaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
this.webAuthn = new WebAuthnIFrame(
this.win,
webVaultUrl,
@@ -494,5 +495,5 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
}
// implemented in clients
launchDuoFrameless() {}
async launchDuoFrameless() {}
}

View File

@@ -115,7 +115,7 @@ import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstraction
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/platform/abstractions/environment.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@@ -140,7 +140,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@@ -363,7 +363,7 @@ const typesafeProviders: Array<SafeProvider> = [
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
@@ -477,8 +477,8 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, StateProvider],
}),
safeProvider({
provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService,
provide: EnvironmentService,
useClass: DefaultEnvironmentService,
deps: [StateProvider, AccountServiceAbstraction],
}),
safeProvider({
@@ -545,7 +545,7 @@ const typesafeProviders: Array<SafeProvider> = [
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
AppIdServiceAbstraction,
StateServiceAbstraction,
LOGOUT_CALLBACK,
@@ -647,7 +647,7 @@ const typesafeProviders: Array<SafeProvider> = [
LogService,
STATE_FACTORY,
AccountServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
TokenServiceAbstraction,
MigrationRunner,
STATE_SERVICE_USE_CACHE,
@@ -711,7 +711,7 @@ const typesafeProviders: Array<SafeProvider> = [
SyncServiceAbstraction,
AppIdServiceAbstraction,
ApiServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
LOGOUT_CALLBACK,
StateServiceAbstraction,
AuthServiceAbstraction,
@@ -853,8 +853,9 @@ const typesafeProviders: Array<SafeProvider> = [
StateServiceAbstraction,
ConfigApiServiceAbstraction,
AuthServiceAbstraction,
EnvironmentServiceAbstraction,
EnvironmentService,
LogService,
StateProvider,
],
}),
safeProvider({
@@ -869,7 +870,7 @@ const typesafeProviders: Array<SafeProvider> = [
safeProvider({
provide: AnonymousHubServiceAbstraction,
useClass: AnonymousHubService,
deps: [EnvironmentServiceAbstraction, LoginStrategyServiceAbstraction, LogService],
deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService],
}),
safeProvider({
provide: ValidationServiceAbstraction,
@@ -949,7 +950,7 @@ const typesafeProviders: Array<SafeProvider> = [
safeProvider({
provide: WebAuthnLoginApiServiceAbstraction,
useClass: WebAuthnLoginApiService,
deps: [ApiServiceAbstraction, EnvironmentServiceAbstraction],
deps: [ApiServiceAbstraction, EnvironmentService],
}),
safeProvider({
provide: WebAuthnLoginServiceAbstraction,

View File

@@ -1,7 +1,7 @@
import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { BehaviorSubject, Subject, concatMap, firstValueFrom, map, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -123,7 +123,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
{ name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true },
{ name: i18nService.t("sendTypeText"), value: SendType.Text, premium: false },
];
this.sendLinkBaseUrl = this.environmentService.getSendUrl();
}
get link(): string {
@@ -190,6 +189,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
});
const env = await firstValueFrom(this.environmentService.environment$);
this.sendLinkBaseUrl = env.getSendUrl();
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.destroy$))
.subscribe((hasPremiumFromAnySource) => {

View File

@@ -1,5 +1,5 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { Subject, firstValueFrom, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -198,9 +198,9 @@ export class SendComponent implements OnInit, OnDestroy {
return true;
}
copy(s: SendView) {
const sendLinkBaseUrl = this.environmentService.getSendUrl();
const link = sendLinkBaseUrl + s.accessId + "/" + s.urlB64Key;
async copy(s: SendView) {
const env = await firstValueFrom(this.environmentService.environment$);
const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key;
this.platformUtilsService.copyToClipboard(link);
this.platformUtilsService.showToast(
"success",

View File

@@ -39,11 +39,12 @@ export class IconComponent implements OnInit {
) {}
async ngOnInit() {
const iconsUrl = this.environmentService.getIconsUrl();
this.data$ = combineLatest([
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
this.cipher$.pipe(filter((c) => c !== undefined)),
]).pipe(map(([showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)));
]).pipe(
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
);
}
}

View File

@@ -1,5 +1,5 @@
import { Directive } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { OnInit, Directive } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -11,12 +11,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { DialogService } from "@bitwarden/components";
@Directive()
export class PremiumComponent {
export class PremiumComponent implements OnInit {
isPremium$: Observable<boolean>;
price = 10;
refreshPromise: Promise<any>;
cloudWebVaultUrl: string;
private directiveIsDestroyed$ = new Subject<boolean>();
constructor(
protected i18nService: I18nService,
@@ -25,13 +24,16 @@ export class PremiumComponent {
private logService: LogService,
protected stateService: StateService,
protected dialogService: DialogService,
environmentService: EnvironmentService,
private environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
this.cloudWebVaultUrl = environmentService.getCloudWebVaultUrl();
this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
}
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
}
async refresh() {
try {
this.refreshPromise = this.apiService.refreshIdentityToken();