157 lines
6.6 KiB
TypeScript
157 lines
6.6 KiB
TypeScript
import { updateHasherSettings } from './services/toolsService';
|
|
|
|
export interface SecuritySettings {
|
|
Authority: string;
|
|
Audience: string;
|
|
ClientId: string;
|
|
ClientSecret: string;
|
|
Scope?: string;
|
|
}
|
|
|
|
export interface ApiSettings {
|
|
BaseUrl: string;
|
|
}
|
|
|
|
export interface HasherSettings {
|
|
Salt: string;
|
|
MinHashLength: number;
|
|
}
|
|
|
|
export type ConfigSource = 'runtime-injected' | 'runtime-fetched' | 'build-env' | 'fallback-defaults';
|
|
|
|
export interface AppConfig {
|
|
SecuritySettings: SecuritySettings;
|
|
ApiSettings: ApiSettings;
|
|
HasherSettings: HasherSettings;
|
|
ConfigSource: ConfigSource;
|
|
LoadedAt?: string;
|
|
}
|
|
|
|
export const defaultSecuritySettings: SecuritySettings = {
|
|
Authority: 'https://sts.security.khongisa.co.za',
|
|
Audience: 'pfm-api-dev',
|
|
ClientId: 'pfm-scalar-dev',
|
|
ClientSecret: 'secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27',
|
|
Scope: 'openid profile roles email address pfm-api-dev',
|
|
};
|
|
|
|
export const defaultApiSettings: ApiSettings = {
|
|
BaseUrl: 'https://api.ifri.khongisa.co.za/',
|
|
};
|
|
|
|
export const defaultHasherSettings: HasherSettings = {
|
|
Salt: 'TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==',
|
|
MinHashLength: 11,
|
|
};
|
|
|
|
function pickFirstNonEmpty(...candidates: any[]): string | undefined {
|
|
for (const c of candidates) {
|
|
if (c !== undefined && c !== null) {
|
|
const s = String(c).trim();
|
|
if (s !== '' && s !== 'undefined' && s !== 'null') return s;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeBaseUrl(url: string): string {
|
|
let trimmed = url.trim();
|
|
if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`;
|
|
return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
|
|
}
|
|
|
|
function normalizeAuthorityUrl(url: string): string {
|
|
let trimmed = url.trim();
|
|
if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`;
|
|
return trimmed.replace(/\/+$/, '');
|
|
}
|
|
|
|
function getWindowRuntimeConfig(): Record<string, any> {
|
|
if (typeof window === 'undefined') return {};
|
|
const win = window as any;
|
|
return win.__RUNTIME_CONFIG__ || win.APP_CONFIG || win._env_ || {};
|
|
}
|
|
|
|
function extractSourceConfig(src: Record<string, any>) {
|
|
if (!src || typeof src !== 'object') return {};
|
|
return {
|
|
authority: pickFirstNonEmpty(src.VITE_OIDC_AUTHORITY, src.OIDC_AUTHORITY, src.SecuritySettings?.Authority, src.SecuritySettings__Authority),
|
|
audience: pickFirstNonEmpty(src.VITE_OIDC_AUDIENCE, src.OIDC_AUDIENCE, src.SecuritySettings?.Audience, src.SecuritySettings__Audience),
|
|
clientId: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_ID, src.OIDC_CLIENT_ID, src.SecuritySettings?.ClientId, src.SecuritySettings__ClientId),
|
|
clientSecret: pickFirstNonEmpty(src.VITE_OIDC_CLIENT_SECRET, src.OIDC_CLIENT_SECRET, src.SecuritySettings?.ClientSecret, src.SecuritySettings__ClientSecret),
|
|
scope: pickFirstNonEmpty(src.VITE_OIDC_SCOPE, src.OIDC_SCOPE, src.SecuritySettings?.Scope, src.SecuritySettings__Scope),
|
|
baseUrl: pickFirstNonEmpty(src.VITE_API_BASE_URL, src.API_BASE_URL, src.ApiSettings?.BaseUrl, src.ApiSettings__BaseUrl),
|
|
hasherSalt: pickFirstNonEmpty(src.VITE_HASHER_SALT, src.HASHER_SALT, src.HasherSettings?.Salt, src.HasherSettings__Salt),
|
|
hasherMinLength: Number(pickFirstNonEmpty(src.VITE_HASHER_MIN_LENGTH, src.HASHER_MIN_LENGTH, src.HasherSettings?.MinHashLength)) || undefined,
|
|
};
|
|
}
|
|
|
|
function resolveConfig(fetchedJson?: Record<string, any>): AppConfig {
|
|
const winCfg = extractSourceConfig(getWindowRuntimeConfig());
|
|
const fetchCfg = fetchedJson ? extractSourceConfig(fetchedJson) : {};
|
|
const envCfg = extractSourceConfig((typeof import.meta !== 'undefined' && (import.meta as any).env ? (import.meta as any).env : {}) as Record<string, any>);
|
|
|
|
let source: ConfigSource = 'fallback-defaults';
|
|
if (Object.values(winCfg).some((v) => v !== undefined)) source = 'runtime-injected';
|
|
else if (Object.values(fetchCfg).some((v) => v !== undefined)) source = 'runtime-fetched';
|
|
else if (Object.values(envCfg).some((v) => v !== undefined)) source = 'build-env';
|
|
|
|
return {
|
|
SecuritySettings: {
|
|
Authority: normalizeAuthorityUrl(pickFirstNonEmpty(winCfg.authority, fetchCfg.authority, envCfg.authority, defaultSecuritySettings.Authority)!),
|
|
Audience: pickFirstNonEmpty(winCfg.audience, fetchCfg.audience, envCfg.audience, defaultSecuritySettings.Audience)!,
|
|
ClientId: pickFirstNonEmpty(winCfg.clientId, fetchCfg.clientId, envCfg.clientId, defaultSecuritySettings.ClientId)!,
|
|
ClientSecret: pickFirstNonEmpty(winCfg.clientSecret, fetchCfg.clientSecret, envCfg.clientSecret, defaultSecuritySettings.ClientSecret)!,
|
|
Scope: pickFirstNonEmpty(winCfg.scope, fetchCfg.scope, envCfg.scope, defaultSecuritySettings.Scope),
|
|
},
|
|
ApiSettings: {
|
|
BaseUrl: normalizeBaseUrl(pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!),
|
|
},
|
|
HasherSettings: {
|
|
Salt: pickFirstNonEmpty(winCfg.hasherSalt, fetchCfg.hasherSalt, envCfg.hasherSalt, defaultHasherSettings.Salt)!,
|
|
MinHashLength: winCfg.hasherMinLength || fetchCfg.hasherMinLength || envCfg.hasherMinLength || defaultHasherSettings.MinHashLength,
|
|
},
|
|
ConfigSource: source,
|
|
LoadedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
let runtimeConfig: AppConfig = resolveConfig();
|
|
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
|
|
|
const configListeners: Array<(config: AppConfig) => void> = [];
|
|
|
|
export function onConfigChanged(listener: (config: AppConfig) => void): () => void {
|
|
configListeners.push(listener);
|
|
return () => {
|
|
const idx = configListeners.indexOf(listener);
|
|
if (idx !== -1) configListeners.splice(idx, 1);
|
|
};
|
|
}
|
|
|
|
export async function loadRuntimeConfig(): Promise<AppConfig> {
|
|
let fetchedJson: Record<string, any> | undefined;
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
const res = await fetch(`/config.json?_t=${Date.now()}`, {
|
|
cache: 'no-store',
|
|
headers: { Accept: 'application/json' },
|
|
}).catch(() => null);
|
|
if (res && res.ok) {
|
|
fetchedJson = await res.json().catch(() => undefined);
|
|
}
|
|
} catch {
|
|
// Safe fallback
|
|
}
|
|
}
|
|
runtimeConfig = resolveConfig(fetchedJson);
|
|
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
|
configListeners.forEach((fn) => fn(runtimeConfig));
|
|
return runtimeConfig;
|
|
}
|
|
|
|
export const getAppConfig = () => runtimeConfig;
|
|
export const getSecuritySettings = () => runtimeConfig.SecuritySettings;
|
|
export const getApiBaseUrl = () => runtimeConfig.ApiSettings.BaseUrl;
|
|
export const getHasherSettings = () => runtimeConfig.HasherSettings;
|