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.Role, user.UserType == 3 ? "Admin" : "User")
}),
Expires = DateTime.UtcNow.AddMinutes(15),
Expires = DateTime.UtcNow.AddMinutes(60),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Issuer = _config["Jwt:Issuer"],
Audience = _config["Jwt:Audience"]

View File

@@ -1,6 +1,8 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { AuthService, type UserSession } from '../services/auth.service';
import { ChatService } from '../services/chat.service';
import { createContext, useContext, useState,
useEffect, useCallback, type ReactNode,
} from "react";
import { AuthService, type UserSession } from "../services/auth.service";
import { ChatService } from "../services/chat.service";
interface AuthContextType {
user: UserSession | null;
@@ -19,7 +21,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [loading, setLoading] = useState(true);
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();
if (currentUser) {
try {
@@ -29,64 +43,96 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUnreadCount(0);
}
}
};
// Verificar sesión al cargar la app (Solo una vez)
useEffect(() => {
const initAuth = async () => {
try {
const sessionUser = await AuthService.checkSession();
if (sessionUser) {
setUser(sessionUser);
await fetchUnreadCount(); // <--- 5. LLAMAR AL CARGAR LA APP
} else {
setUser(null);
setUnreadCount(0);
}
} catch (error) {
setUser(null);
setUnreadCount(0);
} finally {
setLoading(false);
}
};
initAuth();
}, []);
// Función centralizada para verificar la sesión
const verifySession = useCallback(async () => {
try {
const sessionUser = await AuthService.checkSession();
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);
}
await fetchUnreadCount();
} else {
// Si el backend dice null (sesión inválida), sacamos al usuario
if (user) logout();
}
} catch (error) {
// Si hay error de red o 401 que no se pudo refrescar
if (user) logout();
} finally {
setLoading(false);
}
}, [user, logout, fetchUnreadCount]);
const login = (userData: UserSession) => {
setUser(userData);
localStorage.setItem('userProfile', JSON.stringify(userData));
localStorage.setItem("userProfile", JSON.stringify(userData));
fetchUnreadCount();
};
const logout = () => {
AuthService.logout();
setUser(null);
setUnreadCount(0);
localStorage.removeItem('userProfile');
const refreshSession = async () => {
await verifySession();
};
const refreshSession = async () => {
const sessionUser = await AuthService.checkSession();
setUser(sessionUser);
if (sessionUser) {
await fetchUnreadCount();
}
};
// 1. Carga Inicial
useEffect(() => {
verifySession();
}, [verifySession]);
// 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 (
<AuthContext.Provider value={{ user, loading, unreadCount, login, logout, refreshSession, fetchUnreadCount }}>
<AuthContext.Provider
value={{
user,
loading,
unreadCount,
login,
logout,
refreshSession,
fetchUnreadCount,
}}
>
{children}
</AuthContext.Provider>
);
}
// Hook personalizado para usar el contexto fácilmente
export function useAuth() {
const context = useContext(AuthContext);
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;
}
}

View File

@@ -4,22 +4,18 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL;
const apiClient = axios.create({
baseURL: BASE_URL,
withCredentials: true, // Importante para enviar Cookies
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});
// Interceptor de Respuesta (Manejo de Errores y Refresh)
// Interceptor de Respuesta
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
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 (
error.response?.status === 401 &&
!originalRequest._retry &&
@@ -28,32 +24,24 @@ apiClient.interceptors.response.use(
originalRequest._retry = true;
try {
// Intentamos renovar el token
await apiClient.post('/auth/refresh-token');
// Si el refresh funciona, reintentamos la petición original
return apiClient(originalRequest);
} catch (refreshError) {
// Si el refresh falla (401 o cualquier otro), no hay nada que hacer.
// Forzamos logout en el cliente para limpiar basura
localStorage.removeItem('session');
// SI FALLA EL REFRESH (Sesión caducada definitivamente)
// Limpiamos todo rastro de sesión local
localStorage.removeItem('userProfile');
// Opcional: Redirigir a login o recargar para limpiar estado
// window.location.href = '/';
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(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;