From 020077503098e9b1db86f461e6810365d9d24b01 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Mon, 13 Jul 2026 11:53:04 +0000 Subject: [PATCH] Add shared device inactivity timeout to AuthGate --- frontend/src/components/AuthGate.tsx | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/AuthGate.tsx b/frontend/src/components/AuthGate.tsx index 82c5e04..0df2e33 100644 --- a/frontend/src/components/AuthGate.tsx +++ b/frontend/src/components/AuthGate.tsx @@ -1,4 +1,11 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' + +const SHARED_TIMEOUT_MS = 10 * 60 * 1000 + +function isSharedDevice() { + return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1') +} + interface User { email: string @@ -17,6 +24,7 @@ export function AuthGate({ children }: Props) { const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) + const timerRef = useRef | null>(null) useEffect(() => { fetch('/api/auth/verify?app=noticeboard', { credentials: 'include' }) @@ -32,6 +40,30 @@ export function AuthGate({ children }: Props) { .catch(() => setState('login')) }, []) + useEffect(() => { + if (state !== 'authed' || !isSharedDevice()) return + + async function forceLogout() { + await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}) + setUser(null) + setState('login') + } + + function reset() { + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(forceLogout, SHARED_TIMEOUT_MS) + } + + 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)) + } + }, [state]) + async function login(e: React.FormEvent) { e.preventDefault() setLoading(true)