diff --git a/frontend/src/components/AuthGate.tsx b/frontend/src/components/AuthGate.tsx index 12d386a..3ab5e3f 100644 --- a/frontend/src/components/AuthGate.tsx +++ b/frontend/src/components/AuthGate.tsx @@ -1,6 +1,13 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import type { User } from '../types' +const SHARED_TIMEOUT_MS = 10 * 60 * 1000 + +function isSharedDevice() { + return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1') +} + + interface Props { children: (user: User) => React.ReactNode } @@ -23,6 +30,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=hk-planner', { credentials: 'include' }) @@ -33,6 +41,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)