1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-1349] Update client service to retrieve feature flags from API (#5064)

* [PM-1351] Add property to server-config.response. Change config to be able to fetch without being authed.

* [PM-1351] fetch every hour.

* [PM-1351] fetch on vault sync.

* [PM-1351] browser desktop fetch configs on sync complete.

* [PM-1351] Add methods to retrieve feature flags

* [PM-1351] Add enum to use as key to get values feature flag values

* [PM-1351] Remove debug code

* [PM-1351] Get flags when unauthed. Add enums as params. Hourly always fetch.

* [PM-1351] add check for authed user using auth service

* [PM-1351] remove unnecessary timer on account unlock
This commit is contained in:
André Bispo
2023-04-26 15:30:39 +01:00
committed by GitHub
parent dfe69f77f5
commit cfc380c697
12 changed files with 95 additions and 35 deletions

View File

@@ -615,7 +615,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
{
provide: ConfigServiceAbstraction,
useClass: ConfigService,
deps: [StateServiceAbstraction, ConfigApiServiceAbstraction],
deps: [StateServiceAbstraction, ConfigApiServiceAbstraction, AuthServiceAbstraction],
},
{
provide: ConfigApiServiceAbstraction,

View File

@@ -1,7 +1,13 @@
import { Observable } from "rxjs";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { ServerConfig } from "./server-config";
export abstract class ConfigServiceAbstraction {
serverConfig$: Observable<ServerConfig | null>;
fetchServerConfig: () => Promise<ServerConfig>;
getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise<boolean>;
getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise<string>;
getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise<number>;
}

View File

@@ -15,6 +15,7 @@ export class ServerConfig {
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: Date;
featureStates: { [key: string]: string } = {};
constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version;
@@ -22,6 +23,7 @@ export class ServerConfig {
this.server = serverConfigData.server;
this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment;
this.featureStates = serverConfigData.featureStates;
if (this.server?.name == null && this.server?.url == null) {
this.server = null;

View File

@@ -0,0 +1,3 @@
export enum FeatureFlag {
DisplayEuEnvironmentFlag = "display-eu-environment",
}

View File

@@ -12,6 +12,7 @@ export class ServerConfigData {
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: string;
featureStates: { [key: string]: string } = {};
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
this.version = serverConfigResponse?.version;
@@ -23,6 +24,7 @@ export class ServerConfigData {
this.environment = serverConfigResponse?.environment
? new EnvironmentServerConfigData(serverConfigResponse.environment)
: null;
this.featureStates = serverConfigResponse?.featureStates;
}
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {

View File

@@ -5,6 +5,7 @@ export class ServerConfigResponse extends BaseResponse {
gitHash: string;
server: ThirdPartyServerConfigResponse;
environment: EnvironmentServerConfigResponse;
featureStates: { [key: string]: string } = {};
constructor(response: any) {
super(response);
@@ -17,6 +18,7 @@ export class ServerConfigResponse extends BaseResponse {
this.gitHash = this.getResponseProperty("GitHash");
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
this.featureStates = this.getResponseProperty("FeatureStates");
}
}

View File

@@ -6,7 +6,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async get(): Promise<ServerConfigResponse> {
const r = await this.apiService.send("GET", "/config", null, true, true);
const r = await this.apiService.send("GET", "/config", null, false, true);
return new ServerConfigResponse(r);
}
}

View File

@@ -1,9 +1,12 @@
import { BehaviorSubject, concatMap, map, switchMap, timer, EMPTY } from "rxjs";
import { BehaviorSubject, concatMap, timer } from "rxjs";
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 { StateService } from "../../abstractions/state.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { ServerConfigData } from "../../models/data/server-config.data";
export class ConfigService implements ConfigServiceAbstraction {
@@ -12,21 +15,14 @@ export class ConfigService implements ConfigServiceAbstraction {
constructor(
private stateService: StateService,
private configApiService: ConfigApiServiceAbstraction
private configApiService: ConfigApiServiceAbstraction,
private authService: AuthService
) {
this.stateService.activeAccountUnlocked$
// Re-fetch the server config every hour
timer(0, 1000 * 3600)
.pipe(
switchMap((unlocked) => {
if (!unlocked) {
this._serverConfig.next(null);
return EMPTY;
}
// Re-fetch the server config every hour
return timer(0, 3600 * 1000).pipe(map(() => unlocked));
}),
concatMap(async (unlocked) => {
return unlocked ? await this.buildServerConfig() : null;
concatMap(async () => {
return await this.fetchServerConfig();
})
)
.subscribe((serverConfig) => {
@@ -34,9 +30,51 @@ export class ConfigService implements ConfigServiceAbstraction {
});
}
async fetchServerConfig(): Promise<ServerConfig> {
try {
const response = await this.configApiService.get();
if (response != null) {
const data = new ServerConfigData(response);
const serverConfig = new ServerConfig(data);
this._serverConfig.next(serverConfig);
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
return serverConfig;
}
await this.stateService.setServerConfig(data);
}
} catch {
return null;
}
}
async getFeatureFlagBool(key: FeatureFlag, defaultValue = false): Promise<boolean> {
return await this.getFeatureFlag(key, defaultValue);
}
async getFeatureFlagString(key: FeatureFlag, defaultValue = ""): Promise<string> {
return await this.getFeatureFlag(key, defaultValue);
}
async getFeatureFlagNumber(key: FeatureFlag, defaultValue = 0): Promise<number> {
return await this.getFeatureFlag(key, defaultValue);
}
private async getFeatureFlag<T>(key: FeatureFlag, defaultValue: T): Promise<T> {
const serverConfig = await this.buildServerConfig();
if (
serverConfig == null ||
serverConfig.featureStates == null ||
serverConfig.featureStates[key] == null
) {
return defaultValue;
}
return serverConfig.featureStates[key] as T;
}
private async buildServerConfig(): Promise<ServerConfig> {
const data = await this.stateService.getServerConfig();
const domain = data ? new ServerConfig(data) : null;
const domain = data ? new ServerConfig(data) : this._serverConfig.getValue();
if (domain == null || !domain.isValid() || domain.expiresSoon()) {
const value = await this.fetchServerConfig();
@@ -45,18 +83,4 @@ export class ConfigService implements ConfigServiceAbstraction {
return domain;
}
private async fetchServerConfig(): Promise<ServerConfig> {
try {
const response = await this.configApiService.get();
if (response != null) {
const data = new ServerConfigData(response);
await this.stateService.setServerConfig(data);
return new ServerConfig(data);
}
} catch {
return null;
}
}
}