Fixed sync handshake
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { updateHasherSettings } from './services/toolsService';
|
||||
import { updateHasherSettings as updateToolsHasherSettings } from './services/toolsService';
|
||||
import { updateHasherSettings as updateUtilsHasherSettings } from './utils/hashids';
|
||||
|
||||
export interface SecuritySettings {
|
||||
Authority: string;
|
||||
@@ -30,8 +31,8 @@ export interface AppConfig {
|
||||
export const defaultSecuritySettings: SecuritySettings = {
|
||||
Authority: 'https://sts.security.khongisa.co.za',
|
||||
Audience: 'pfm-api-dev',
|
||||
ClientId: 'pfm-scalar-dev',
|
||||
ClientSecret: 'secret_71f88570a46eb22eb5270b47f69e3332c84d8db85d008f0276cb7431c8872d27',
|
||||
ClientId: 'pfm-console-dev',
|
||||
ClientSecret: 'secret_dcaeaa28097d9ba5b06ec324d9eac9d9c81de96e0a203cf2c6154c6dd7c89e94',
|
||||
Scope: 'openid profile roles email address pfm-api-dev',
|
||||
};
|
||||
|
||||
@@ -44,11 +45,18 @@ export const defaultHasherSettings: HasherSettings = {
|
||||
MinHashLength: 11,
|
||||
};
|
||||
|
||||
function syncAllHasherSettings(salt: string, 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;
|
||||
if (s !== '' && s !== 'undefined' && s !== 'null') {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -56,13 +64,17 @@ function pickFirstNonEmpty(...candidates: any[]): string | undefined {
|
||||
|
||||
function normalizeBaseUrl(url: string): string {
|
||||
let trimmed = url.trim();
|
||||
if (!/^https?:\/\//i.test(trimmed)) trimmed = `https://${trimmed}`;
|
||||
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}`;
|
||||
if (!/^https?:\/\//i.test(trimmed)) {
|
||||
trimmed = `https://${trimmed}`;
|
||||
}
|
||||
return trimmed.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
@@ -72,44 +84,178 @@ function getWindowRuntimeConfig(): Record<string, any> {
|
||||
return win.__RUNTIME_CONFIG__ || win.APP_CONFIG || win._env_ || {};
|
||||
}
|
||||
|
||||
function extractSourceConfig(src: Record<string, any>) {
|
||||
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, 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,
|
||||
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 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 (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';
|
||||
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,
|
||||
defaultSecuritySettings.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: 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),
|
||||
Authority: authority,
|
||||
Audience: audience,
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Scope: scope,
|
||||
},
|
||||
ApiSettings: {
|
||||
BaseUrl: normalizeBaseUrl(pickFirstNonEmpty(winCfg.baseUrl, fetchCfg.baseUrl, envCfg.baseUrl, defaultApiSettings.BaseUrl)!),
|
||||
BaseUrl: baseUrl,
|
||||
},
|
||||
HasherSettings: {
|
||||
Salt: pickFirstNonEmpty(winCfg.hasherSalt, fetchCfg.hasherSalt, envCfg.hasherSalt, defaultHasherSettings.Salt)!,
|
||||
MinHashLength: winCfg.hasherMinLength || fetchCfg.hasherMinLength || envCfg.hasherMinLength || defaultHasherSettings.MinHashLength,
|
||||
Salt: hasherSalt,
|
||||
MinHashLength: hasherMinLength,
|
||||
},
|
||||
ConfigSource: source,
|
||||
LoadedAt: new Date().toISOString(),
|
||||
@@ -117,7 +263,7 @@ function resolveConfig(fetchedJson?: Record<string, any>): AppConfig {
|
||||
}
|
||||
|
||||
let runtimeConfig: AppConfig = resolveConfig();
|
||||
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
syncAllHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
|
||||
const configListeners: Array<(config: AppConfig) => void> = [];
|
||||
|
||||
@@ -129,28 +275,49 @@ export function onConfigChanged(listener: (config: AppConfig) => void): () => vo
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
let loadPromise: Promise<AppConfig> | null = null;
|
||||
|
||||
export async function loadRuntimeConfig(forceReload = false): Promise<AppConfig> {
|
||||
if (loadPromise && !forceReload) {
|
||||
return loadPromise;
|
||||
}
|
||||
runtimeConfig = resolveConfig(fetchedJson);
|
||||
updateHasherSettings(runtimeConfig.HasherSettings.Salt, runtimeConfig.HasherSettings.MinHashLength);
|
||||
configListeners.forEach((fn) => fn(runtimeConfig));
|
||||
return runtimeConfig;
|
||||
|
||||
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 = () => runtimeConfig;
|
||||
export const getSecuritySettings = () => runtimeConfig.SecuritySettings;
|
||||
export const getApiBaseUrl = () => runtimeConfig.ApiSettings.BaseUrl;
|
||||
export const getHasherSettings = () => runtimeConfig.HasherSettings;
|
||||
export const getAppConfig = (): AppConfig => runtimeConfig;
|
||||
export const getSecuritySettings = (): SecuritySettings => runtimeConfig.SecuritySettings;
|
||||
export const getApiBaseUrl = (): string => runtimeConfig.ApiSettings.BaseUrl;
|
||||
export const getHasherSettings = (): HasherSettings => runtimeConfig.HasherSettings;
|
||||
|
||||
Reference in New Issue
Block a user