diff --git a/Backend/MotoresArgentinosV2.Infrastructure/Services/TokenService.cs b/Backend/MotoresArgentinosV2.Infrastructure/Services/TokenService.cs index 0d46e53..45b3a4d 100644 --- a/Backend/MotoresArgentinosV2.Infrastructure/Services/TokenService.cs +++ b/Backend/MotoresArgentinosV2.Infrastructure/Services/TokenService.cs @@ -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"] diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx index 73913eb..5071280 100644 --- a/Frontend/src/context/AuthContext.tsx +++ b/Frontend/src/context/AuthContext.tsx @@ -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 ( - + {children} ); } -// 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; -} \ No newline at end of file +} diff --git a/Frontend/src/services/axios.client.ts b/Frontend/src/services/axios.client.ts index a7c3ef3..c60666a 100644 --- a/Frontend/src/services/axios.client.ts +++ b/Frontend/src/services/axios.client.ts @@ -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; \ No newline at end of file