fix: adminPath TTL cache in hooks.ts instead of read-once at startup

Previous approach read config.json once at module load time. If the PVC
wasn't fully mounted yet, it fell back to 'admin' and stayed there for
the lifetime of the process. New approach caches for 5 seconds and re-reads
on expiry, so it recovers from startup races and picks up changes without
requiring a restart.
This commit is contained in:
Falkan
2026-03-20 01:57:06 -04:00
parent 7239ca6d5a
commit 231eacc27c

View File

@@ -2,17 +2,29 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { Reroute } from '@sveltejs/kit'; import type { Reroute } from '@sveltejs/kit';
// Read adminPath once at startup. Changes require a server restart. const CONFIG_PATH = path.resolve('data', 'config.json');
let adminPath = 'admin';
try { // Cache adminPath with a 5-second TTL so changes take effect quickly
const config = JSON.parse(fs.readFileSync(path.resolve('data', 'config.json'), 'utf8')); // but we don't hit disk on every request.
adminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin'; let cachedAdminPath = 'admin';
console.log('[hooks] adminPath set to:', adminPath); let cacheExpiry = 0;
} catch {
// config.json missing or unreadable — fall back to 'admin' function getAdminPath(): string {
const now = Date.now();
if (now < cacheExpiry) return cachedAdminPath;
try {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
cachedAdminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin';
} catch {
// config.json missing or unreadable — keep current cached value
}
cacheExpiry = now + 5000;
return cachedAdminPath;
} }
export { adminPath }; export { getAdminPath as adminPath };
/** /**
* Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route. * Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route.
@@ -20,18 +32,17 @@ export { adminPath };
* so attackers cannot enumerate the admin URL. * so attackers cannot enumerate the admin URL.
*/ */
export const reroute: Reroute = ({ url }) => { export const reroute: Reroute = ({ url }) => {
const adminPath = getAdminPath();
const prefix = `/${adminPath}`; const prefix = `/${adminPath}`;
// Public path → internal /admin // Public path → internal /admin
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) { if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
console.log(`[reroute] ${url.pathname} → /admin${url.pathname.slice(prefix.length)}`);
return '/admin' + url.pathname.slice(prefix.length); return '/admin' + url.pathname.slice(prefix.length);
} }
// Block direct /admin access when a custom path is configured // Block direct /admin access when a custom path is configured
if (adminPath !== 'admin') { if (adminPath !== 'admin') {
if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) { if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
console.log(`[reroute] blocking direct /admin access → /404`);
return '/404'; return '/404';
} }
} }