322 lines
9.0 KiB
TypeScript
322 lines
9.0 KiB
TypeScript
import { updateHasherSettings as updateToolsHasherSettings } from './services/toolsService';
|
|
import { updateHasherSettings as updateUtilsHasherSettings } from './utils/hashids';
|
|
|
|
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-console-dev',
|
|
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: '',
|
|
MinHashLength: 11,
|
|
};
|
|
|
|
function syncAllHasherSettings(salt: string | undefined, minHashLength: number): void {
|
|
updateToolsHasherSettings(salt, minHashLength);
|
|
updateUtilsHasherSettings(salt, minHashLength);
|
|
}
|
|
|
|
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>): {
|
|
authority?: string;
|
|
audience?: string;
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
scope?: string;
|
|
baseUrl?: string;
|
|
hasherSalt?: string;
|
|
hasherMinLength?: number;
|
|
} {
|
|
if (!src || typeof src !== 'object') return {};
|
|
|
|
const sec = src.SecuritySettings || src.securitySettings || {};
|
|
const api = src.ApiSettings || src.apiSettings || {};
|
|
const hasher = src.HasherSettings || src.hasherSettings || {};
|
|
|
|
const minLenRaw = pickFirstNonEmpty(
|
|
src.VITE_HASHER_MIN_LENGTH,
|
|
src.HASHER_MIN_LENGTH,
|
|
src.HasherSettings__MinHashLength,
|
|
hasher.MinHashLength,
|
|
hasher.minHashLength,
|
|
src.MinHashLength,
|
|
src.minHashLength
|
|
);
|
|
const parsedMinLen = minLenRaw !== undefined ? Number(minLenRaw) : undefined;
|
|
|
|
return {
|
|
authority: pickFirstNonEmpty(
|
|
src.VITE_OIDC_AUTHORITY,
|
|
src.OIDC_AUTHORITY,
|
|
src.SecuritySettings__Authority,
|
|
sec.Authority,
|
|
sec.authority,
|
|
src.Authority,
|
|
src.authority
|
|
),
|
|
audience: pickFirstNonEmpty(
|
|
src.VITE_OIDC_AUDIENCE,
|
|
src.OIDC_AUDIENCE,
|
|
src.SecuritySettings__Audience,
|
|
sec.Audience,
|
|
sec.audience,
|
|
src.Audience,
|
|
src.audience
|
|
),
|
|
clientId: pickFirstNonEmpty(
|
|
src.VITE_OIDC_CLIENT_ID,
|
|
src.OIDC_CLIENT_ID,
|
|
src.SecuritySettings__ClientId,
|
|
sec.ClientId,
|
|
sec.clientId,
|
|
src.ClientId,
|
|
src.clientId
|
|
),
|
|
clientSecret: pickFirstNonEmpty(
|
|
src.VITE_OIDC_CLIENT_SECRET,
|
|
src.OIDC_CLIENT_SECRET,
|
|
src.SecuritySettings__ClientSecret,
|
|
sec.ClientSecret,
|
|
sec.clientSecret,
|
|
src.ClientSecret,
|
|
src.clientSecret
|
|
),
|
|
scope: pickFirstNonEmpty(
|
|
src.VITE_OIDC_SCOPE,
|
|
src.OIDC_SCOPE,
|
|
src.SecuritySettings__Scope,
|
|
sec.Scope,
|
|
sec.scope,
|
|
src.Scope,
|
|
src.scope
|
|
),
|
|
baseUrl: pickFirstNonEmpty(
|
|
src.VITE_API_BASE_URL,
|
|
src.API_BASE_URL,
|
|
src.ApiSettings__BaseUrl,
|
|
api.BaseUrl,
|
|
api.baseUrl,
|
|
src.BaseUrl,
|
|
src.baseUrl
|
|
),
|
|
hasherSalt: pickFirstNonEmpty(
|
|
src.VITE_HASHER_SALT,
|
|
src.HASHER_SALT,
|
|
src.HasherSettings__Salt,
|
|
hasher.Salt,
|
|
hasher.salt,
|
|
src.Salt,
|
|
src.salt
|
|
),
|
|
hasherMinLength:
|
|
parsedMinLen !== undefined && !isNaN(parsedMinLen) && parsedMinLen > 0
|
|
? parsedMinLen
|
|
: 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>
|
|
);
|
|
|
|
const hasWin = Object.values(winCfg).some((v) => v !== undefined);
|
|
const hasFetch = Object.values(fetchCfg).some((v) => v !== undefined);
|
|
const hasEnv = Object.values(envCfg).some((v) => v !== undefined);
|
|
|
|
let source: ConfigSource = 'fallback-defaults';
|
|
if (hasWin) source = 'runtime-injected';
|
|
else if (hasFetch) source = 'runtime-fetched';
|
|
else if (hasEnv) source = 'build-env';
|
|
|
|
const authority = normalizeAuthorityUrl(
|
|
pickFirstNonEmpty(winCfg.authority, fetchCfg.authority, envCfg.authority, defaultSecuritySettings.Authority)!
|
|
);
|
|
const audience = pickFirstNonEmpty(
|
|
winCfg.audience,
|
|
fetchCfg.audience,
|
|
envCfg.audience,
|
|
defaultSecuritySettings.Audience
|
|
)!;
|
|
const clientId = pickFirstNonEmpty(
|
|
winCfg.clientId,
|
|
fetchCfg.clientId,
|
|
envCfg.clientId,
|
|
defaultSecuritySettings.ClientId
|
|
)!;
|
|
const clientSecret = pickFirstNonEmpty(
|
|
winCfg.clientSecret,
|
|
fetchCfg.clientSecret,
|
|
envCfg.clientSecret
|
|
);
|
|
const scope = pickFirstNonEmpty(
|
|
winCfg.scope,
|
|
fetchCfg.scope,
|
|
envCfg.scope,
|
|
defaultSecuritySettings.Scope
|
|
);
|
|
const baseUrl = normalizeBaseUrl(
|
|
pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!
|
|
);
|
|
const hasherSalt = pickFirstNonEmpty(
|
|
winCfg.hasherSalt,
|
|
fetchCfg.hasherSalt,
|
|
envCfg.hasherSalt,
|
|
defaultHasherSettings.Salt
|
|
);
|
|
const hasherMinLength =
|
|
winCfg.hasherMinLength ||
|
|
fetchCfg.hasherMinLength ||
|
|
envCfg.hasherMinLength ||
|
|
defaultHasherSettings.MinHashLength;
|
|
|
|
return {
|
|
SecuritySettings: {
|
|
Authority: authority,
|
|
Audience: audience,
|
|
ClientId: clientId,
|
|
...(clientSecret ? { ClientSecret: clientSecret } : {}),
|
|
Scope: scope,
|
|
},
|
|
ApiSettings: {
|
|
BaseUrl: baseUrl,
|
|
},
|
|
HasherSettings: {
|
|
Salt: hasherSalt || '',
|
|
MinHashLength: hasherMinLength,
|
|
},
|
|
ConfigSource: source,
|
|
LoadedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
let runtimeConfig: AppConfig = resolveConfig();
|
|
syncAllHasherSettings(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);
|
|
};
|
|
}
|
|
|
|
let loadPromise: Promise<AppConfig> | null = null;
|
|
|
|
export async function loadRuntimeConfig(forceReload = false): Promise<AppConfig> {
|
|
if (loadPromise && !forceReload) {
|
|
return loadPromise;
|
|
}
|
|
|
|
loadPromise = (async () => {
|
|
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) {
|
|
const contentType = res.headers.get('content-type') || '';
|
|
if (contentType.includes('application/json') || contentType.includes('text/plain')) {
|
|
fetchedJson = await res.json().catch(() => undefined);
|
|
} else {
|
|
const text = await res.text().catch(() => '');
|
|
if (text.trim().startsWith('{')) {
|
|
fetchedJson = JSON.parse(text);
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Fallback if /config.json is unreachable
|
|
}
|
|
}
|
|
|
|
runtimeConfig = resolveConfig(fetchedJson);
|
|
syncAllHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
|
configListeners.forEach((fn) => fn(runtimeConfig));
|
|
return runtimeConfig;
|
|
})();
|
|
|
|
return loadPromise;
|
|
}
|
|
|
|
export const getAppConfig = (): AppConfig => runtimeConfig;
|
|
export const getSecuritySettings = (): SecuritySettings => runtimeConfig.SecuritySettings;
|
|
export const getApiBaseUrl = (): string => runtimeConfig.ApiSettings.BaseUrl;
|
|
export const getHasherSettings = (): HasherSettings => runtimeConfig.HasherSettings;
|