1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +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() {}
}