From d2603bca994c14c0e4756316a122db7c191ffb30 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Fri, 24 Jul 2026 17:07:21 +0000 Subject: [PATCH] Fix session-expiry redirect losing return-to-app + wire up inactivity timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Login.tsx only read the return path from router state, which a hard cross-document navigation (a child app bouncing the whole shell here on session expiry) can never carry — so re-authenticating always landed on the Dashboard instead of back in whatever app you were using. Login.tsx now also accepts a ?from= query param for that case. Also wired up AuthGate's inactivity auto-logout timer, which was defined (getInactivityMs) but never actually called — so the per-device timeout configurable in Admin Settings had no effect. Disabled for installed PWAs as intended. Co-Authored-By: Claude Sonnet 5 --- src/components/AuthGate.tsx | 30 ++++++++++++++++++++++++++++++ src/pages/Login.tsx | 7 ++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/AuthGate.tsx b/src/components/AuthGate.tsx index 18ecb56..5c528a9 100644 --- a/src/components/AuthGate.tsx +++ b/src/components/AuthGate.tsx @@ -19,6 +19,7 @@ export function AuthGate({ children }: Props) { const navigate = useNavigate() const location = useLocation() const userRef = useRef(null) + const timerRef = useRef | null>(null) function fetchMe(isInitial = false) { fetch('/api/auth/me', { credentials: 'include' }) @@ -58,6 +59,35 @@ export function AuthGate({ children }: Props) { } }, []) + // Inactivity auto-logout — disabled for installed PWAs; configurable per + // device (Admin Settings → Device) for shared/front-desk browser sessions. + useEffect(() => { + const ms = getInactivityMs() + if (!user || !ms) return + const timeoutMs: number = ms + + async function forceLogout() { + await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}) + userRef.current = null + setUser(null) + navigate('/login', { replace: true, state: { from: location.pathname } }) + } + + function reset() { + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(forceLogout, timeoutMs) + } + + const events = ['mousemove', 'keydown', 'click', 'touchstart'] as const + events.forEach(e => window.addEventListener(e, reset, { passive: true })) + reset() + + return () => { + if (timerRef.current) clearTimeout(timerRef.current) + events.forEach(e => window.removeEventListener(e, reset)) + } + }, [user]) + if (checking) { return (
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 94dd43d..79dd3b1 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -9,7 +9,12 @@ export function Login() { const [loading, setLoading] = useState(false) const navigate = useNavigate() const location = useLocation() - const from = (location.state as { from?: string })?.from || '/' + // `state.from` covers portal-internal redirects (client-side nav preserves + // router state); `?from=` covers a child app bouncing the whole shell here + // via a hard page navigation, which can't carry router state. + const from = (location.state as { from?: string })?.from + || new URLSearchParams(location.search).get('from') + || '/' async function submit(e: React.FormEvent) { e.preventDefault()