Fix: Sesion 1 Hora y Refresh con Redirección

This commit is contained in:
2026-02-16 20:33:38 -03:00
parent bd45e89bd2
commit 5a7c3f62f1
3 changed files with 102 additions and 68 deletions

View File

@@ -33,7 +33,7 @@ public class TokenService : ITokenService
new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.UserType == 3 ? "Admin" : "User") new Claim(ClaimTypes.Role, user.UserType == 3 ? "Admin" : "User")
}), }),
Expires = DateTime.UtcNow.AddMinutes(15), Expires = DateTime.UtcNow.AddMinutes(60),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Issuer = _config["Jwt:Issuer"], Issuer = _config["Jwt:Issuer"],
Audience = _config["Jwt:Audience"] Audience = _config["Jwt:Audience"]

View File

@@ -1,6 +1,8 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; import { createContext, useContext, useState,
import { AuthService, type UserSession } from '../services/auth.service'; useEffect, useCallback, type ReactNode,
import { ChatService } from '../services/chat.service'; } from "react";
import { AuthService, type UserSession } from "../services/auth.service";
import { ChatService } from "../services/chat.service";
interface AuthContextType { interface AuthContextType {
user: UserSession | null; user: UserSession | null;
@@ -19,7 +21,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const fetchUnreadCount = async () => { // Función para cerrar sesión limpiamente
const logout = useCallback(() => {
AuthService.logout();
setUser(null);
setUnreadCount(0);
localStorage.removeItem("userProfile");
// Redirigir a home
if (window.location.pathname !== "/") {
window.location.href = "/";
}
}, []);
const fetchUnreadCount = useCallback(async () => {
const currentUser = AuthService.getCurrentUser(); const currentUser = AuthService.getCurrentUser();
if (currentUser) { if (currentUser) {
try { try {
@@ -29,64 +43,96 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUnreadCount(0); setUnreadCount(0);
} }
} }
}; }, []);
// Verificar sesión al cargar la app (Solo una vez) // Función centralizada para verificar la sesión
useEffect(() => { const verifySession = useCallback(async () => {
const initAuth = async () => {
try { try {
const sessionUser = await AuthService.checkSession(); const sessionUser = await AuthService.checkSession();
if (sessionUser) { if (sessionUser) {
// Si la sesión es válida, actualizamos el estado si es necesario
// Comparamos IDs para evitar re-renders innecesarios si es el mismo usuario
if (sessionUser.id !== user?.id) {
setUser(sessionUser); setUser(sessionUser);
await fetchUnreadCount(); // <--- 5. LLAMAR AL CARGAR LA APP }
await fetchUnreadCount();
} else { } else {
setUser(null); // Si el backend dice null (sesión inválida), sacamos al usuario
setUnreadCount(0); if (user) logout();
} }
} catch (error) { } catch (error) {
setUser(null); // Si hay error de red o 401 que no se pudo refrescar
setUnreadCount(0); if (user) logout();
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [user, logout, fetchUnreadCount]);
initAuth();
}, []);
const login = (userData: UserSession) => { const login = (userData: UserSession) => {
setUser(userData); setUser(userData);
localStorage.setItem('userProfile', JSON.stringify(userData)); localStorage.setItem("userProfile", JSON.stringify(userData));
fetchUnreadCount(); fetchUnreadCount();
}; };
const logout = () => { const refreshSession = async () => {
AuthService.logout(); await verifySession();
setUser(null);
setUnreadCount(0);
localStorage.removeItem('userProfile');
}; };
const refreshSession = async () => { // 1. Carga Inicial
const sessionUser = await AuthService.checkSession(); useEffect(() => {
setUser(sessionUser); verifySession();
if (sessionUser) { }, [verifySession]);
await fetchUnreadCount();
// 2. DETECTOR DE PESTAÑA ACTIVA
useEffect(() => {
const handleVisibilityChange = () => {
// Solo verificamos cuando el usuario VUELVE a la pestaña
if (document.visibilityState === "visible") {
console.log("🔄 Pestaña activa: Verificando sesión...");
verifySession();
} }
}; };
document.addEventListener("visibilitychange", handleVisibilityChange);
return () =>
document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [verifySession]);
// 3. HEARTBEAT - Verifica la sesión cada 5 minutos mientras la pestaña esté abierta
useEffect(() => {
if (!user) return; // No gastar recursos si no hay usuario
const interval = setInterval(
() => {
verifySession();
},
5 * 60 * 1000,
); // 5 minutos
return () => clearInterval(interval);
}, [user, verifySession]);
return ( return (
<AuthContext.Provider value={{ user, loading, unreadCount, login, logout, refreshSession, fetchUnreadCount }}> <AuthContext.Provider
value={{
user,
loading,
unreadCount,
login,
logout,
refreshSession,
fetchUnreadCount,
}}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
} }
// Hook personalizado para usar el contexto fácilmente
export function useAuth() { export function useAuth() {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error("useAuth must be used within an AuthProvider");
} }
return context; return context;
} }

View File

@@ -4,22 +4,18 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,
withCredentials: true, // Importante para enviar Cookies withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
// Interceptor de Respuesta (Manejo de Errores y Refresh) // Interceptor de Respuesta
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {
const originalRequest = error.config; const originalRequest = error.config;
// Condición para intentar refresh:
// 1. Es error 401 (Unauthorized)
// 2. No hemos reintentado ya esta petición (_retry no es true)
// 3. 🛑 IMPORTANTE: La URL que falló NO es la de refresh-token (evita bucle)
if ( if (
error.response?.status === 401 && error.response?.status === 401 &&
!originalRequest._retry && !originalRequest._retry &&
@@ -28,32 +24,24 @@ apiClient.interceptors.response.use(
originalRequest._retry = true; originalRequest._retry = true;
try { try {
// Intentamos renovar el token
await apiClient.post('/auth/refresh-token'); await apiClient.post('/auth/refresh-token');
// Si el refresh funciona, reintentamos la petición original
return apiClient(originalRequest); return apiClient(originalRequest);
} catch (refreshError) { } catch (refreshError) {
// Si el refresh falla (401 o cualquier otro), no hay nada que hacer. // SI FALLA EL REFRESH (Sesión caducada definitivamente)
// Forzamos logout en el cliente para limpiar basura
localStorage.removeItem('session');
localStorage.removeItem('userProfile');
// Opcional: Redirigir a login o recargar para limpiar estado // Limpiamos todo rastro de sesión local
// window.location.href = '/'; localStorage.removeItem('userProfile');
localStorage.removeItem('session');
// Redirigir a home y recargar para limpiar estado de React
// Esto asegura que visualmente se vea deslogueado
window.location.href = '/';
return Promise.reject(refreshError); return Promise.reject(refreshError);
} }
} }
return Promise.reject(error); return Promise.reject(error);
} }
); );
// Interceptor de Request (Para inyectar token si usáramos Headers,
// pero como usamos Cookies httpOnly para el AccessToken,
// este interceptor solo es necesario si tu backend espera algún header custom extra,
// de lo contrario las cookies viajan solas gracias a withCredentials: true).
// Lo dejamos limpio por ahora.
export default apiClient; export default apiClient;