bitwarden-estensione-browser/libs/common/src/platform/services/config/config.service.ts

131 lines
4.6 KiB
TypeScript

import {
ReplaySubject,
Subject,
catchError,
concatMap,
defer,
delayWhen,
firstValueFrom,
map,
merge,
timer,
} from "rxjs";
import { SemVer } from "semver";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum";
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
import { ServerConfig } from "../../abstractions/config/server-config";
import { EnvironmentService, Region } from "../../abstractions/environment.service";
import { LogService } from "../../abstractions/log.service";
import { StateService } from "../../abstractions/state.service";
import { ServerConfigData } from "../../models/data/server-config.data";
import { StateProvider } from "../../state";
const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600;
export class ConfigService implements ConfigServiceAbstraction {
private inited = false;
protected _serverConfig = new ReplaySubject<ServerConfig | null>(1);
serverConfig$ = this._serverConfig.asObservable();
private _forceFetchConfig = new Subject<void>();
protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour
cloudRegion$ = this.serverConfig$.pipe(
map((config) => config?.environment?.cloudRegion ?? Region.US),
);
constructor(
private stateService: StateService,
private configApiService: ConfigApiServiceAbstraction,
private authService: AuthService,
private environmentService: EnvironmentService,
private logService: LogService,
private stateProvider: StateProvider,
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
private subscribe = true,
) {}
init() {
if (!this.subscribe || this.inited) {
return;
}
const latestServerConfig$ = defer(() => this.configApiService.get()).pipe(
map((response) => new ServerConfigData(response)),
delayWhen((data) => this.saveConfig(data)),
catchError((e: unknown) => {
// fall back to stored ServerConfig (if any)
this.logService.error("Unable to fetch ServerConfig: " + (e as Error)?.message);
return this.stateService.getServerConfig();
}),
);
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here
merge(
this.refreshTimer$, // an overridable interval
this.environmentService.environment$, // when environment URLs change (including when app is started)
this._forceFetchConfig, // manual
)
.pipe(
concatMap(() => latestServerConfig$),
map((data) => (data == null ? null : new ServerConfig(data))),
)
.subscribe((config) => this._serverConfig.next(config));
this.inited = true;
}
getFeatureFlag$<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
return this.serverConfig$.pipe(
map((serverConfig) => {
if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) {
return defaultValue;
}
return serverConfig.featureStates[key] as T;
}),
);
}
async getFeatureFlag<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) {
return await firstValueFrom(this.getFeatureFlag$(key, defaultValue));
}
triggerServerConfigFetch() {
this._forceFetchConfig.next();
}
private async saveConfig(data: ServerConfigData) {
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
return;
}
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
await this.stateService.setServerConfig(data);
await this.environmentService.setCloudRegion(userId, data.environment?.cloudRegion);
}
/**
* Verifies whether the server version meets the minimum required version
* @param minimumRequiredServerVersion The minimum version required
* @returns True if the server version is greater than or equal to the minimum required version
*/
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) {
return this.serverConfig$.pipe(
map((serverConfig) => {
if (serverConfig == null) {
return false;
}
const serverVersion = new SemVer(serverConfig.version);
return serverVersion.compare(minimumRequiredServerVersion) >= 0;
}),
);
}
}