1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

SM-90: Add Server Version to Browser About Page (#3223)

* Add structure to display server version on browser

* Add getConfig to State Service interface

* Clean up settings component code

* Switch to ServerConfig, use Observables in the ConfigService, and more

* Fix runtime error

* Sm 90 addison (#3275)

* Use await instead of then

* Rename stateServerConfig -> storedServerConfig

* Move config validation logic to the model

* Use implied check for undefined

* Rename getStateServicerServerConfig -> buildServerConfig

* Rename getApiServiceServerConfig -> pollServerConfig

* Build server config in async

* small fixes and add last seen text

* Move config server to /config folder

* Update with concatMap and other changes

* Config project updates

* Rename fileds to convention and remove unneeded migration

* Update libs/common/src/services/state.service.ts

Update based on Oscar's recommendation

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Update options for Oscar's rec

* Rename abstractions to abstracitons

* Fix null issues and add options

* Combine classes into one file, per Oscar's rec

* Add null checking

* Fix dependency issue

* Add null checks, await, and fix date issue

* Remove unneeded null check

* In progress commit, unsuitable for for more than dev env, just backing up changes made with Oscar

* Fix temp code to force last seen state

* Add localization and escapes in the browser about section

* Call complete on destroy subject rather than unsubscribe

* use mediumDate and formatDate for the last seen date messaging

* Add ThirdPartyServerName in example

* Add deprecated note per Oscar's comment

* [SM-90] Change to using a modal for browser about (#3417)

* Fix inconsistent constructor null checking

* ServerConfig can be null, fixes this

* Switch to call super first, as required

* remove unneeded null checks

* Remove null checks from server-config.data.ts class

* Update via PR comments and add back needed null check in server conf obj

* Remove type annotation from serverConfig$

* Update self-hosted to be <small> per design decision

* Re-fetch config every hour

* Make third party server version <small> and change wording per Oscar's PR comment

* Add expiresSoon function and re-fetch if the serverConfig will expire soon (older than 18 hours)

* Fix misaligned small third party server message text

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Colton Hurst
2022-09-08 08:27:19 -04:00
committed by GitHub
parent cb31a71e8d
commit 3b69a60511
18 changed files with 398 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
import { ServerConfigResponse } from "@bitwarden/common/models/response/server-config-response";
export abstract class ConfigApiServiceAbstraction {
get: () => Promise<ServerConfigResponse>;
}

View File

@@ -0,0 +1,7 @@
import { Observable } from "rxjs";
import { ServerConfig } from "./server-config";
export abstract class ConfigServiceAbstraction {
serverConfig$: Observable<ServerConfig | null>;
}

View File

@@ -0,0 +1,40 @@
import {
ServerConfigData,
ThirdPartyServerConfigData,
EnvironmentServerConfigData,
} from "@bitwarden/common/models/data/server-config.data";
const dayInMilliseconds = 24 * 3600 * 1000;
const eighteenHoursInMilliseconds = 18 * 3600 * 1000;
export class ServerConfig {
version: string;
gitHash: string;
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: Date;
constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version;
this.gitHash = serverConfigData.gitHash;
this.server = serverConfigData.server;
this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment;
if (this.server?.name == null && this.server?.url == null) {
this.server = null;
}
}
private getAgeInMilliseconds(): number {
return new Date().getTime() - this.utcDate?.getTime();
}
isValid(): boolean {
return this.getAgeInMilliseconds() <= dayInMilliseconds;
}
expiresSoon(): boolean {
return this.getAgeInMilliseconds() >= eighteenHoursInMilliseconds;
}
}

View File

@@ -33,4 +33,5 @@ export abstract class EnvironmentService {
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
getUrls: () => Urls;
isCloud: () => boolean;
}

View File

@@ -13,6 +13,7 @@ import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
import { EncString } from "../models/domain/encString";
import { EnvironmentUrls } from "../models/domain/environmentUrls";
@@ -319,4 +320,12 @@ export abstract class StateService<T extends Account = Account> {
setStateVersion: (value: number) => Promise<void>;
getWindow: () => Promise<WindowState>;
setWindow: (value: WindowState) => Promise<void>;
/**
* @deprecated Do not call this directly, use ConfigService
*/
getServerConfig: (options?: StorageOptions) => Promise<ServerConfigData>;
/**
* @deprecated Do not call this directly, use ConfigService
*/
setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise<void>;
}

View File

@@ -0,0 +1,53 @@
import {
ServerConfigResponse,
ThirdPartyServerConfigResponse,
EnvironmentServerConfigResponse,
} from "../response/server-config-response";
export class ServerConfigData {
version: string;
gitHash: string;
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: string;
constructor(serverConfigReponse: ServerConfigResponse) {
this.version = serverConfigReponse?.version;
this.gitHash = serverConfigReponse?.gitHash;
this.server = serverConfigReponse?.server
? new ThirdPartyServerConfigData(serverConfigReponse.server)
: null;
this.utcDate = new Date().toISOString();
this.environment = serverConfigReponse?.environment
? new EnvironmentServerConfigData(serverConfigReponse.environment)
: null;
}
}
export class ThirdPartyServerConfigData {
name: string;
url: string;
constructor(response: ThirdPartyServerConfigResponse) {
this.name = response.name;
this.url = response.url;
}
}
export class EnvironmentServerConfigData {
vault: string;
api: string;
identity: string;
admin: string;
notifications: string;
sso: string;
constructor(response: EnvironmentServerConfigResponse) {
this.vault = response.vault;
this.api = response.api;
this.identity = response.identity;
this.admin = response.admin;
this.notifications = response.notifications;
this.sso = response.sso;
}
}

View File

@@ -10,6 +10,7 @@ import { OrganizationData } from "../data/organizationData";
import { PolicyData } from "../data/policyData";
import { ProviderData } from "../data/providerData";
import { SendData } from "../data/sendData";
import { ServerConfigData } from "../data/server-config.data";
import { CipherView } from "../view/cipherView";
import { CollectionView } from "../view/collectionView";
import { SendView } from "../view/sendView";
@@ -140,6 +141,7 @@ export class AccountSettings {
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData;
}
export type AccountSettingsSettings = {

View File

@@ -0,0 +1,61 @@
import { BaseResponse } from "./baseResponse";
export class ServerConfigResponse extends BaseResponse {
version: string;
gitHash: string;
server: ThirdPartyServerConfigResponse;
environment: EnvironmentServerConfigResponse;
constructor(response: any) {
super(response);
if (response == null) {
return;
}
this.version = this.getResponseProperty("Version");
this.gitHash = this.getResponseProperty("GitHash");
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
}
}
export class EnvironmentServerConfigResponse extends BaseResponse {
vault: string;
api: string;
identity: string;
admin: string;
notifications: string;
sso: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.vault = this.getResponseProperty("Vault");
this.api = this.getResponseProperty("Api");
this.identity = this.getResponseProperty("Identity");
this.admin = this.getResponseProperty("Admin");
this.notifications = this.getResponseProperty("Notifications");
this.sso = this.getResponseProperty("Sso");
}
}
export class ThirdPartyServerConfigResponse extends BaseResponse {
name: string;
url: string;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.name = this.getResponseProperty("Name");
this.url = this.getResponseProperty("Url");
}
}

View File

@@ -0,0 +1,12 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ConfigApiServiceAbstraction as ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/config/config-api.service.abstraction";
import { ServerConfigResponse } from "@bitwarden/common/models/response/server-config-response";
export class ConfigApiService implements ConfigApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async get(): Promise<ServerConfigResponse> {
const r = await this.apiService.send("GET", "/config", null, true, true);
return new ServerConfigResponse(r);
}
}

View File

@@ -0,0 +1,61 @@
import { BehaviorSubject, concatMap, map, switchMap, timer, EMPTY } from "rxjs";
import { ServerConfigData } from "@bitwarden/common/models/data/server-config.data";
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";
export class ConfigService implements ConfigServiceAbstraction {
private _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
serverConfig$ = this._serverConfig.asObservable();
constructor(
private stateService: StateService,
private configApiService: ConfigApiServiceAbstraction
) {
this.stateService.activeAccountUnlocked$
.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;
})
)
.subscribe((serverConfig) => {
this._serverConfig.next(serverConfig);
});
}
private async buildServerConfig(): Promise<ServerConfig> {
const data = await this.stateService.getServerConfig();
const domain = data ? new ServerConfig(data) : null;
if (domain == null || !domain.isValid() || domain.expiresSoon()) {
const value = await this.fetchServerConfig();
return value ?? domain;
}
return domain;
}
private async fetchServerConfig(): Promise<ServerConfig> {
const response = await this.configApiService.get();
const data = new ServerConfigData(response);
if (data != null) {
await this.stateService.setServerConfig(data);
return new ServerConfig(data);
}
return null;
}
}

View File

@@ -207,4 +207,10 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return url.trim();
}
isCloud(): boolean {
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
this.getApiUrl()
);
}
}

View File

@@ -21,6 +21,7 @@ import { OrganizationData } from "../models/data/organizationData";
import { PolicyData } from "../models/data/policyData";
import { ProviderData } from "../models/data/providerData";
import { SendData } from "../models/data/sendData";
import { ServerConfigData } from "../models/data/server-config.data";
import {
Account,
AccountData,
@@ -2277,6 +2278,23 @@ export class StateService<
);
}
async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
account.settings.serverConfig = value;
return await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
);
}
async getServerConfig(options: StorageOptions): Promise<ServerConfigData> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
)?.settings?.serverConfig;
}
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
let globals: TGlobalState;
if (this.useMemory(options.storageLocation)) {