feat: version 3.0.0 -- Correcciones de auditoria de seguridad, accesibilidad y redundancia
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico, revocacion de JWT por version de sesion (028), control optimista de edicion (029), logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion de proyectos, accesibilidad de dialogos/controles e i18n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,42 +1,100 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, useCallback } from '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);
|
||||
userRef.current = user;
|
||||
|
||||
const logout = useCallback(() => {
|
||||
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);
|
||||
api.setToken(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => logout());
|
||||
// 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(() => {
|
||||
if (!localStorage.getItem('plc_token')) return;
|
||||
if (userRef.current) saveReturnContext(userRef.current.id);
|
||||
markSessionExpired();
|
||||
logout({ preserveDrafts: true });
|
||||
}, [logout]);
|
||||
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => expireSession());
|
||||
}, [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;
|
||||
}
|
||||
api.get('/auth/me')
|
||||
.then((me) => setUser(me))
|
||||
.catch(() => logout())
|
||||
.finally(() => setReady(true));
|
||||
}, [logout]);
|
||||
|
||||
const login = useCallback(async (email, password) => {
|
||||
const result = await api.post('/auth/login', { email, password });
|
||||
api.setToken(result.token);
|
||||
setUser(result.user);
|
||||
return result.user;
|
||||
// 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();
|
||||
};
|
||||
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);
|
||||
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) => {
|
||||
api.setToken(token);
|
||||
setUser(sessionUser);
|
||||
}, []);
|
||||
const setSession = useCallback((token, sessionUser) => { startSession(token, sessionUser); }, [startSession]);
|
||||
|
||||
const value = useMemo(() => ({ user, ready, login, logout, setSession }), [user, ready, login, logout, setSession]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user