diff --git a/client/src/components/Layout.scss b/client/src/components/Layout.scss index 7408757..25336ec 100644 --- a/client/src/components/Layout.scss +++ b/client/src/components/Layout.scss @@ -55,6 +55,11 @@ opacity: 0.85; } + &__admin { + color: var(--accent) !important; + font-weight: 500; + } + &__burger { display: none; width: 40px; diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 1bf59e4..9800c7c 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,17 +1,30 @@ import { useEffect, useState } from "react"; import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { api, type SiteSettings } from "../lib/api"; +import { canAccessAdmin } from "../lib/adminAccess"; import "./Layout.scss"; export function Layout() { const [site, setSite] = useState(null); const [menuOpen, setMenuOpen] = useState(false); + const [showAdmin, setShowAdmin] = useState(false); const location = useLocation(); useEffect(() => { api.site().then(setSite).catch(() => setSite({ name: "jmartgraphix" })); }, []); + // Only surface Admin when Traefik/Authelia already allows GET /admin + useEffect(() => { + let cancelled = false; + canAccessAdmin().then((ok) => { + if (!cancelled) setShowAdmin(ok); + }); + return () => { + cancelled = true; + }; + }, [location.pathname]); + useEffect(() => { setMenuOpen(false); }, [location.pathname]); @@ -47,6 +60,11 @@ export function Layout() { > ArtStation + {showAdmin && ( + + Admin + + )} diff --git a/client/src/lib/adminAccess.ts b/client/src/lib/adminAccess.ts new file mode 100644 index 0000000..9e8798f --- /dev/null +++ b/client/src/lib/adminAccess.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ +export async function canAccessAdmin(): Promise { + try { + const res = await fetch("/admin", { + method: "GET", + credentials: "include", + redirect: "manual", + 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 + if (res.type === "opaqueredirect") return false; + if (res.status >= 300 && res.status < 400) return false; + return res.status === 200; + } catch { + return false; + } +} diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index da97688..9a2bc96 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -90,8 +90,7 @@ export function HomePage() { {recent.length === 0 ? (

- Projects will appear here once published. Use the{" "} - admin panel to add work, or import from ArtStation. + Projects will appear here once published.

) : (