From 231eacc27c22ae7710642a6ffddd99af3f79b6f2 Mon Sep 17 00:00:00 2001 From: Falkan Date: Fri, 20 Mar 2026 01:57:06 -0400 Subject: [PATCH] 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. --- src/hooks.ts | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index ae14c8a..9be71cf 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -2,17 +2,29 @@ import fs from 'fs'; import path from 'path'; import type { Reroute } from '@sveltejs/kit'; -// Read adminPath once at startup. Changes require a server restart. -let adminPath = 'admin'; -try { - const config = JSON.parse(fs.readFileSync(path.resolve('data', 'config.json'), 'utf8')); - adminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin'; - console.log('[hooks] adminPath set to:', adminPath); -} catch { - // config.json missing or unreadable — fall back to 'admin' +const CONFIG_PATH = path.resolve('data', 'config.json'); + +// Cache adminPath with a 5-second TTL so changes take effect quickly +// but we don't hit disk on every request. +let cachedAdminPath = 'admin'; +let cacheExpiry = 0; + +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 //... to /admin/... before SvelteKit resolves the route. @@ -20,18 +32,17 @@ export { adminPath }; * so attackers cannot enumerate the admin URL. */ export const reroute: Reroute = ({ url }) => { + const adminPath = getAdminPath(); const prefix = `/${adminPath}`; // Public path → internal /admin 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); } // Block direct /admin access when a custom path is configured if (adminPath !== 'admin') { if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) { - console.log(`[reroute] blocking direct /admin access → /404`); return '/404'; } }