diff --git a/client/src/lib/adminAccess.ts b/client/src/lib/adminAccess.ts index 9e8798f..73afbc9 100644 --- a/client/src/lib/adminAccess.ts +++ b/client/src/lib/adminAccess.ts @@ -1,8 +1,11 @@ /** * Probe Traefik/Authelia protection on /admin. - * Returns true only when a same-origin GET succeeds (HTTP 200), - * meaning the visitor is already authenticated for admin. - * Failures (401/403/302/network) keep the public site admin-free. + * Returns true only when: + * 1) GET /admin succeeds (HTTP 200) — path is allowed through Authelia + * 2) GET /api/v1/admin/me succeeds — Remote-User is present and authorized + * + * (2) matters on LAN where Authelia may bypass auth but still leave no Remote-User, + * so the public site stays admin-free for anonymous visitors. */ export async function canAccessAdmin(): Promise { try { @@ -13,11 +16,22 @@ export async function canAccessAdmin(): Promise { cache: "no-store", headers: { Accept: "text/html" }, }); - // opaqueredirect (0) = browser blocked reading a cross-origin redirect - // 3xx with redirect:manual also means not authorized for the resource + // opaqueredirect = browser blocked reading a cross-origin redirect if (res.type === "opaqueredirect") return false; if (res.status >= 300 && res.status < 400) return false; - return res.status === 200; + if (res.status !== 200) return false; + + // Confirm real Authelia identity (not just network-level bypass of /admin) + const me = await fetch("/api/v1/admin/me", { + method: "GET", + credentials: "include", + redirect: "manual", + cache: "no-store", + headers: { Accept: "application/json" }, + }); + if (me.type === "opaqueredirect") return false; + if (me.status >= 300 && me.status < 400) return false; + return me.status === 200; } catch { return false; }