import { createContext, useContext, useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { api, setUnauthorizedHandler } from '../services/api.js'; import { tokenExpiresAt, isTokenExpired, tokenSubject } from '../utils/jwt.js'; import { saveReturnContext, markSessionExpired, consumeReturnContext, clearSessionExpiredFlag } from '../utils/session.js'; import { setGuardBypass, clearUserDrafts } from '../utils/drafts.js'; const AuthContext = createContext(null); const CHECK_EVERY_MS = 30 * 1000; const MAX_TIMEOUT_MS = 2 ** 31 - 1; export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [ready, setReady] = useState(false); const userRef = useRef(null); const lastPingRef = useRef(0); userRef.current = user; const logout = useCallback((options = {}) => { // Salida voluntaria o sesion invalida: la navegacion a /login no debe pedir confirmacion // (los borradores ya estan guardados en el navegador). setGuardBypass(true); if (!options.preserveDrafts) clearUserDrafts(userRef.current?.id); if (options.remote) api.post('/auth/logout').catch(() => {}); api.setToken(null); setUser(null); }, []); // Vencimiento: recuerda donde estaba el usuario (para volver ahi tras el login) y marca el // aviso que muestra la pantalla de login. Idempotente. const expireSession = useCallback((info = {}) => { if (!localStorage.getItem('plc_token')) return; saveReturnContext(userRef.current?.id || tokenSubject(localStorage.getItem('plc_token'))); markSessionExpired(info.code === 'SESSION_REVOKED'); logout({ preserveDrafts: true }); }, [logout]); useEffect(() => { setUnauthorizedHandler((info) => expireSession(info)); }, [expireSession]); useEffect(() => { const token = localStorage.getItem('plc_token'); if (!token) { setReady(true); return; } if (isTokenExpired(token)) { // Recarga con la sesion ya vencida (ej. pestaña dormida): igual se recuerda donde estaba. saveReturnContext(tokenSubject(token)); markSessionExpired(); api.setToken(null); setReady(true); return; } lastPingRef.current = Date.now(); api.get('/auth/me') .then((me) => setUser(me)) .catch((error) => { if (error.status !== 401) logout(); }) .finally(() => setReady(true)); }, [logout]); // Vigilancia del vencimiento aunque no haya peticiones: temporizador al instante exacto, un // chequeo periodico y, sobre todo, al volver a la pestaña/ventana -- el navegador congela los // temporizadores de pestañas en segundo plano, asi que no se puede confiar solo en ellos. useEffect(() => { if (!user) return undefined; const check = () => { if (isTokenExpired(localStorage.getItem('plc_token'), 0)) { expireSession(); return; } if (document.visibilityState === 'visible' && Date.now() - lastPingRef.current >= 60000) { lastPingRef.current = Date.now(); api.get('/auth/me').catch(() => {}); } }; const exp = tokenExpiresAt(localStorage.getItem('plc_token')); const timer = exp ? setTimeout(check, Math.min(Math.max(exp - Date.now(), 0) + 500, MAX_TIMEOUT_MS)) : null; const interval = setInterval(check, CHECK_EVERY_MS); const onVisible = () => { if (document.visibilityState === 'visible') check(); }; document.addEventListener('visibilitychange', onVisible); window.addEventListener('focus', check); window.addEventListener('pageshow', check); check(); return () => { if (timer) clearTimeout(timer); clearInterval(interval); document.removeEventListener('visibilitychange', onVisible); window.removeEventListener('focus', check); window.removeEventListener('pageshow', check); }; }, [user, expireSession]); // Devuelve la ruta a la que volver tras iniciar sesion (solo si la dejo este mismo usuario). const startSession = useCallback((token, sessionUser) => { api.setToken(token); lastPingRef.current = Date.now(); clearSessionExpiredFlag(); setGuardBypass(false); setUser(sessionUser); return consumeReturnContext(sessionUser.id); }, []); const login = useCallback(async (email, password) => { const result = await api.post('/auth/login', { email, password }); const returnTo = startSession(result.token, result.user); return { user: result.user, returnTo }; }, [startSession]); // Usado por RegisterPage cuando el registro devuelve sesion directa (primer usuario = superadmin). const setSession = useCallback((token, sessionUser) => { startSession(token, sessionUser); }, [startSession]); const value = useMemo(() => ({ user, ready, login, logout, setSession }), [user, ready, login, logout, setSession]); return {children}; } export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth debe usarse dentro de AuthProvider'); return ctx; }