Files
project.linkedcorp.net/frontend/src/context/auth-context.jsx
T
2026-10-26 16:42:19 -05:00

117 lines
4.9 KiB
React

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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth debe usarse dentro de AuthProvider');
return ctx;
}