import { createContext, useContext, useEffect, useRef, useState } from 'react' import type { ReactNode } from 'react' import type { User } from '../types' function getInactivityMs(): number | null { if (window.matchMedia('(display-mode: standalone)').matches) return null const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('hnf_inactivity_mins=')) if (!c) return null const mins = parseInt(c.split('=')[1]) return isNaN(mins) || mins <= 0 ? null : mins * 60 * 1000 } // Only bounce to the central login when actually embedded in the portal shell. // A standalone PWA or a directly-opened browser tab must never navigate away // from its own start_url/scope — otherwise it loses its installed-app context. function isEmbedded() { return window.top !== window } function verify(): Promise { return fetch('/kitchen/api/auth/verify?app=kitchen', { credentials: 'include' }) .then(r => { if (!r.ok) throw new Error('unauth') return r.json() }) .then((data): User => ({ email: data.email || data.sub || '', name: data.name || data.display_name || '', is_admin: data.is_admin ?? false, caps: data.caps ?? [], })) } // Compatibility shim: archive components destructure token/login/logout/restrictedPages // from useAuth(). These stubs keep the TypeScript build clean. Runtime behaviour: // token = "__session__" (truthy so component guards pass; backends ignore the // Authorization header and use the hnf_session cookie — see log B5b for full migration). // restrictedPages = [] (replaced by cap-based access control). // logout = clears the session and returns to the sign-in screen. // login = no-op (cookie auth, no local token). interface AuthCtx { user: User token: string restrictedPages: string[] login: (t: string) => void logout: () => void } const Ctx = createContext(null) export function useAuth() { const ctx = useContext(Ctx) if (!ctx) throw new Error('useAuth outside AuthGate') return ctx } export default function AuthGate({ children }: { children: ReactNode }) { const [state, setState] = useState<'checking' | 'authed' | 'login'>('checking') const [user, setUser] = useState(null) const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const timerRef = useRef | null>(null) useEffect(() => { verify() .then(data => { setUser(data); setState('authed') }) .catch(() => { if (isEmbedded()) window.top!.location.href = `/login?from=${encodeURIComponent('/app/kitchen')}` else setState('login') }) }, []) // Inactivity auto-logout — disabled for installed PWAs; configurable per // device (Admin Settings → Device) for shared/front-desk browser sessions. useEffect(() => { const ms = getInactivityMs() if (state !== 'authed' || !ms) return const timeoutMs: number = ms async function forceLogout() { await fetch('/kitchen/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}) setUser(null) setState('login') } 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)) } }, [state]) async function login(e: React.FormEvent) { e.preventDefault() setLoading(true) setError('') try { const res = await fetch('/kitchen/api/auth/login', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) if (!res.ok) { setError('Invalid email or password'); return } setUser(await verify()) setState('authed') } catch { setError('Connection error — please try again') } finally { setLoading(false) } } if (state === 'checking') { return (
) } if (state === 'login') { return (

Kitchen

{import.meta.env.VITE_HOTEL_NAME}

setEmail(e.target.value)} placeholder="Email" required autoComplete="email" style={inputStyle} /> setPassword(e.target.value)} placeholder="Password" required autoComplete="current-password" style={inputStyle} /> {error &&

{error}

}
) } const ctx: AuthCtx = { user: user!, token: '__session__', restrictedPages: [], login: () => {}, logout: () => { fetch('/kitchen/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}) if (isEmbedded()) window.top!.location.href = '/login' else { setUser(null); setState('login') } }, } return {children} } const inputStyle: React.CSSProperties = { background: 'var(--navy-dark)', border: '1px solid var(--surface-2)', borderRadius: '6px', color: 'var(--text)', padding: '0.625rem 0.75rem', fontSize: '1rem', width: '100%', outline: 'none', }