From ca2c5a4ef6f16fe47b4c8041e2da347b68a7099d Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Sun, 19 Jul 2026 00:03:57 +0530 Subject: [PATCH] feat(admin): show a dialog when the session expires instead of failing silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin access tokens expire after 15 minutes with no refresh flow. Until now, once the token expired, every subsequent action (approve, save, etc.) just 403/401'd with no visible feedback, or an error banner easy to miss inside a modal — it looked like the button "didn't work." Add a global window.fetch wrapper (installSessionExpiryWatcher, installed once from AdminShell) that flips a shared signal whenever an authenticated request comes back 401. AdminShell renders a "You have been logged out" dialog on top of everything when that fires, with a button that clears the stale session and sends the admin back to /login?from=. Co-Authored-By: Claude Sonnet 5 --- src/components/AdminShell.tsx | 50 +++++++++++++++++++++++++++++++++++ src/lib/session-expired.ts | 42 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/lib/session-expired.ts diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index 470f346..bb27423 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -14,6 +14,7 @@ import AdminSidebar from "./AdminSidebar"; import { isExternalIdentity } from "~/lib/admin-auth"; import { clearAdminSession, hasAdminSession, setAdminSession } from "~/lib/admin-session"; import { normalizeAllowedModules } from "~/lib/admin/module-access"; +import { sessionExpired, clearSessionExpired, installSessionExpiryWatcher } from "~/lib/session-expired"; type Tab = { href: string; label: string; exact?: boolean }; type SearchResult = { id: string; title: string; subtitle: string; href: string }; @@ -476,7 +477,21 @@ export default function AdminShell(props: { children: JSX.Element }) { } }); + const goToLoginAfterExpiry = () => { + clearSessionExpired(); + if (typeof sessionStorage !== "undefined") { + sessionStorage.removeItem("nxtgauge_admin_access_token"); + sessionStorage.removeItem("nxtgauge_admin_preview"); + } + clearAdminSession(); + navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { + replace: true, + }); + }; + onMount(() => { + installSessionExpiryWatcher(); + const savedTheme = ( typeof localStorage !== "undefined" ? localStorage.getItem("nxtgauge_admin_theme") : null ) as "light" | "dark" | null; @@ -667,6 +682,41 @@ export default function AdminShell(props: { children: JSX.Element }) { color: isDark() ? "#E5E7EB" : "#0D0D2A", }} > + +
+
+

+ You have been logged out +

+

+ Your session has expired for security reasons. Please log in again to continue. +

+ +
+
+
+ ) => { + const response = await originalFetch(...args); + if (response.status === 401) { + const hadToken = + typeof sessionStorage !== 'undefined' && + Boolean(sessionStorage.getItem('nxtgauge_admin_access_token')); + // Only treat this as a session expiry if we were actually holding a token — + // otherwise this is just a normal pre-login 401 (e.g. a failed login attempt). + if (hadToken) markSessionExpired(); + } + return response; + }; +}