Fixed ConsoleUi salt handling

This commit is contained in:
2026-10-24 18:09:59 +02:00
parent 25e251aa79
commit 17a4149a73
7 changed files with 56 additions and 29 deletions
@@ -24,3 +24,4 @@ VITE_ENABLE_HTTPS="true"
# HasherSettings for HashIds compatibility with backend pfm-api # HasherSettings for HashIds compatibility with backend pfm-api
VITE_HASHER_MIN_LENGTH="11" VITE_HASHER_MIN_LENGTH="11"
VITE_HASHER_SALT="TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ=="
@@ -30,16 +30,15 @@ OIDC_CLIENT_ID="${VITE_OIDC_CLIENT_ID:-${OIDC_CLIENT_ID:-${SecuritySettings__Cli
OIDC_SCOPE="${VITE_OIDC_SCOPE:-${OIDC_SCOPE:-${SecuritySettings__Scope:-${SecuritySettings_Scope:-${SCOPE:-}}}}}" OIDC_SCOPE="${VITE_OIDC_SCOPE:-${OIDC_SCOPE:-${SecuritySettings__Scope:-${SecuritySettings_Scope:-${SCOPE:-}}}}}"
# 6. Hasher Settings # 6. Hasher Settings
HASHER_MIN_LENGTH="${VITE_HASHER_MIN_LENGTH:-${HASHER_MIN_LENGTH:-${HasherSettings__MinLength:-${HasherSettings_MinLength:-}}}}}" HASHER_MIN_LENGTH="${VITE_HASHER_MIN_LENGTH:-${HASHER_MIN_LENGTH:-${HasherSettings__MinLength:-${HasherSettings_MinLength:-11}}}}}"
HASHER_SALT="${VITE_HASHER_SALT:-${HASHER_SALT:-${HasherSettings__Salt:-${HasherSettings_Salt:-TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==}}}}"
if [ -d "$TARGET_DIR" ]; then if [ -d "$TARGET_DIR" ]; then
echo "[docker-entrypoint] Generating runtime configuration files in $TARGET_DIR..." echo "[docker-entrypoint] Generating runtime configuration files in $TARGET_DIR..."
# Generate env-config.js for instant, synchronous window.__RUNTIME_CONFIG__ initialization # Generate env-config.js for instant, synchronous window.__RUNTIME_CONFIG__ initialization
# Public SPA clients use Authorization Code flow with PKCE; client secrets and hasher salts must NEVER be injected or exposed here.
cat <<EOF > "$TARGET_DIR/env-config.js" cat <<EOF > "$TARGET_DIR/env-config.js"
// Generated dynamically at container startup from deployment environment variables // Generated dynamically at container startup from deployment environment variables
// Note: Secrets such as client secrets, private keys, or hashing salts must NEVER be exposed here.
window.__RUNTIME_CONFIG__ = { window.__RUNTIME_CONFIG__ = {
API_BASE_URL: "${API_BASE_URL}", API_BASE_URL: "${API_BASE_URL}",
VITE_API_BASE_URL: "${API_BASE_URL}", VITE_API_BASE_URL: "${API_BASE_URL}",
@@ -53,6 +52,8 @@ window.__RUNTIME_CONFIG__ = {
VITE_OIDC_SCOPE: "${OIDC_SCOPE}", VITE_OIDC_SCOPE: "${OIDC_SCOPE}",
HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}", HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}",
VITE_HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}", VITE_HASHER_MIN_LENGTH: "${HASHER_MIN_LENGTH}",
HASHER_SALT: "${HASHER_SALT}",
VITE_HASHER_SALT: "${HASHER_SALT}",
ApiSettings: { ApiSettings: {
BaseUrl: "${API_BASE_URL}" BaseUrl: "${API_BASE_URL}"
}, },
@@ -63,6 +64,7 @@ window.__RUNTIME_CONFIG__ = {
Scope: "${OIDC_SCOPE}" Scope: "${OIDC_SCOPE}"
}, },
HasherSettings: { HasherSettings: {
Salt: "${HASHER_SALT}",
MinHashLength: "${HASHER_MIN_LENGTH}" MinHashLength: "${HASHER_MIN_LENGTH}"
} }
}; };
@@ -81,6 +83,7 @@ EOF
"Scope": "${OIDC_SCOPE}" "Scope": "${OIDC_SCOPE}"
}, },
"HasherSettings": { "HasherSettings": {
"Salt": "${HASHER_SALT}",
"MinHashLength": "${HASHER_MIN_LENGTH}" "MinHashLength": "${HASHER_MIN_LENGTH}"
} }
} }
@@ -9,6 +9,7 @@
"Scope": "" "Scope": ""
}, },
"HasherSettings": { "HasherSettings": {
"MinHashLength": "" "Salt": "TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==",
"MinHashLength": "11"
} }
} }
@@ -1,4 +1,13 @@
// Development and fallback runtime configuration. // Development and fallback runtime configuration.
// In Docker / Kubernetes / Cloud Run deployments, this file is dynamically // In Docker / Kubernetes / Cloud Run deployments, this file is dynamically
// populated at container startup by /docker-entrypoint.sh with deployment environment variables. // populated at container startup by /docker-entrypoint.sh with deployment environment variables.
window.__RUNTIME_CONFIG__ = window.__RUNTIME_CONFIG__ || {}; window.__RUNTIME_CONFIG__ = window.__RUNTIME_CONFIG__ || {
HASHER_SALT: "TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==",
VITE_HASHER_SALT: "TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==",
HASHER_MIN_LENGTH: "11",
VITE_HASHER_MIN_LENGTH: "11",
HasherSettings: {
Salt: "TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==",
MinHashLength: "11"
}
};
+2 -2
View File
@@ -44,7 +44,7 @@ export const defaultApiSettings: ApiSettings = {
}; };
export const defaultHasherSettings: HasherSettings = { export const defaultHasherSettings: HasherSettings = {
Salt: '', Salt: 'TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==',
MinHashLength: 11, MinHashLength: 11,
}; };
@@ -300,7 +300,7 @@ function resolveConfig(fetchedJson?: Record<string, any>): AppConfig {
BaseUrl: normalizeBaseUrl(baseUrl), BaseUrl: normalizeBaseUrl(baseUrl),
}, },
HasherSettings: { HasherSettings: {
Salt: salt || '', Salt: salt || defaultHasherSettings.Salt,
MinHashLength: minHashLength, MinHashLength: minHashLength,
}, },
ConfigSource: source, ConfigSource: source,
@@ -4,9 +4,13 @@ const env = (typeof import.meta !== 'undefined' && import.meta.env
? import.meta.env ? import.meta.env
: {}) as Record<string, string | undefined>; : {}) as Record<string, string | undefined>;
export const DEFAULT_HASHER_SALT =
'TGlnaHQgQ2hhcm1zLCBNaWRyYW5kQm9va3MgbWFrZSBsb3RzIG9mIG1vbmV5IGFuZCBhcmUgYSBzdWNjZXNzZnVsIHZpcmFsIHN0b3J5IGluIFNvdXRoIEFmcmljYQ==';
export const DEFAULT_MIN_HASH_LENGTH = 11;
export const HASHER_SETTINGS = { export const HASHER_SETTINGS = {
minHashLength: Number(env.VITE_HASHER_MIN_LENGTH) || 11, minHashLength: Number(env.VITE_HASHER_MIN_LENGTH) || DEFAULT_MIN_HASH_LENGTH,
salt: (env.VITE_HASHER_SALT as string) || '', salt: (env.VITE_HASHER_SALT as string) || DEFAULT_HASHER_SALT,
}; };
let hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength); let hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength);
@@ -33,14 +37,27 @@ export function updateHasherSettings(salt?: string, minLength?: number | string)
* Validates if candidate is already a HashID generated by this hasher. * Validates if candidate is already a HashID generated by this hasher.
*/ */
export function isHashId(candidate: string | null | undefined): boolean { export function isHashId(candidate: string | null | undefined): boolean {
if (!candidate || typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) { if (
!candidate ||
typeof candidate !== 'string' ||
candidate.length < HASHER_SETTINGS.minHashLength ||
/^\d+$/.test(candidate)
) {
return false; return false;
} }
try { try {
const decodedNumbers = hasher.decode(candidate); const decodedNumbers = hasher.decode(candidate);
if (Array.isArray(decodedNumbers) && decodedNumbers.length > 0) return true; if (Array.isArray(decodedNumbers) && decodedNumbers.length > 0) {
if (hasher.encode(decodedNumbers as number[]) === candidate) {
return true;
}
}
const decodedHex = hasher.decodeHex(candidate); const decodedHex = hasher.decodeHex(candidate);
if (decodedHex && decodedHex.length > 0) return true; if (decodedHex && decodedHex.length > 0) {
if (hasher.encodeHex(decodedHex) === candidate) {
return true;
}
}
} catch { } catch {
return false; return false;
} }
@@ -198,23 +215,7 @@ export function decodeEntityId(hashId: string | number | null | undefined): stri
* Prevents double-encoding already-hashed route parameters or IDs. * Prevents double-encoding already-hashed route parameters or IDs.
*/ */
export function isAlreadyHashId(candidate: string | null | undefined): boolean { export function isAlreadyHashId(candidate: string | null | undefined): boolean {
if ( return isHashId(candidate);
!candidate ||
typeof candidate !== 'string' ||
candidate.length < HASHER_SETTINGS.minHashLength ||
/^\d+$/.test(candidate)
) {
return false;
}
try {
const decoded = hasher.decode(candidate);
if (!Array.isArray(decoded) || decoded.length === 0) {
return false;
}
return hasher.encode(decoded as number[]) === candidate;
} catch {
return false;
}
} }
/** /**
@@ -246,6 +247,11 @@ export function decodeHashIdSync(hashId: string | number | null | undefined): nu
if (Array.isArray(decoded) && decoded.length > 0) { if (Array.isArray(decoded) && decoded.length > 0) {
return Number(decoded[0]); return Number(decoded[0]);
} }
const decodedHex = hasher.decodeHex(str);
if (decodedHex) {
const num = Number(BigInt('0x' + decodedHex));
if (!isNaN(num)) return Math.trunc(num);
}
} catch {} } catch {}
return str; return str;
@@ -270,6 +276,11 @@ export function decodeToNumberSync(hashId: number | string | null | undefined):
if (Array.isArray(decoded) && decoded.length > 0) { if (Array.isArray(decoded) && decoded.length > 0) {
return Number(decoded[0]); return Number(decoded[0]);
} }
const decodedHex = hasher.decodeHex(str);
if (decodedHex) {
const num = Number(BigInt('0x' + decodedHex));
if (!isNaN(num)) return Math.trunc(num);
}
} catch {} } catch {}
const fallback = Number(str); const fallback = Number(str);
return isNaN(fallback) ? 0 : Math.trunc(fallback); return isNaN(fallback) ? 0 : Math.trunc(fallback);
+2
View File
@@ -5,6 +5,8 @@ interface ImportMetaEnv {
readonly VITE_OIDC_AUDIENCE?: string; readonly VITE_OIDC_AUDIENCE?: string;
readonly VITE_OIDC_CLIENT_ID?: string; readonly VITE_OIDC_CLIENT_ID?: string;
readonly VITE_API_BASE_URL?: string; readonly VITE_API_BASE_URL?: string;
readonly VITE_HASHER_SALT?: string;
readonly VITE_HASHER_MIN_LENGTH?: string;
} }
interface ImportMeta { interface ImportMeta {