const API_URL = import.meta.env.VITE_EFC_API_URL; // e.g. http://localhost:8000/api/v1 const BASE_URL = API_URL.replace(/\/api\/v1\/?$/, ''); // e.g. http://localhost:8000 // Variable para controlar si ya hay una renovación de token en proceso let isRefreshing = false; let failedQueue = []; // Función para procesar la cola de peticiones fallidas después de renovar el token const processQueue = (error, token = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; /** * Intenta renovar el token. Estrategia: * 1. Nuevo endpoint cookie-based (SSO / hub-frontend): no requiere body, * lee el refresh_token desde la cookie HTTP-only. * 2. Fallback legacy (apps antiguas): lee refresh de localStorage y llama * al endpoint de SimpleJWT. */ const refreshToken = async () => { // ── Intento 1: SimpleJWT refresh (login directo EFC) ────────────────── // Usa el endpoint estándar de SimpleJWT con el token de localStorage. const refresh = localStorage.getItem('refresh'); if (refresh) { try { // Endpoint SimpleJWT estándar (login EFC directo) — campo "refresh" const res = await fetch(`${BASE_URL}/api/v1/token/refresh/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh }), }); if (res.ok) { const data = await res.json(); const newAccess = data.access; if (newAccess) { localStorage.setItem('access', newAccess); if (data.refresh) localStorage.setItem('refresh', data.refresh); return newAccess; } } } catch (_) { // continúa al fallback } try { // Fallback: endpoint SSO local (tokens HS256 de sesiones Hub) const res = await fetch(`${API_URL}/auth/session/refresh/`, { method: 'POST', credentials: 'include', }); if (res.ok) { const data = await res.json(); const newAccess = data.access_token || data.access; if (newAccess) { localStorage.setItem('access', newAccess); return newAccess; } } } catch (_) {} } // ── Fallo total: limpiar sesión y redirigir al Hub ───────────────────── localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); localStorage.removeItem('user_permissions'); const hubUrl = import.meta.env.VITE_HUB_URL || 'http://localhost:3001'; const returnTo = encodeURIComponent(`${window.location.origin}/auth/sso`); setTimeout(() => { window.location.href = `${hubUrl}/login?return_to=${returnTo}`; }, 1000); throw new Error('SESSION_EXPIRED'); }; // Función principal para hacer peticiones con manejo automático de tokens export const fetchWithAuth = async (url, options = {}) => { // Obtener el token actual let token = localStorage.getItem('access'); // Configurar headers por defecto let defaultHeaders = {}; if (token) defaultHeaders['Authorization'] = `Bearer ${token}`; // Solo poner Content-Type si no es GET o si hay body if ((options.method && options.method.toUpperCase() !== 'GET') || options.body) { defaultHeaders['Content-Type'] = 'application/json'; } // Combinar headers const finalOptions = { ...options, headers: { ...defaultHeaders, ...options.headers } }; // Helper: ejecutar fetch con reintento ante error de red (Fix E) const safeFetch = async (u, opts, retries = 1) => { for (let i = 0; i <= retries; i++) { try { return await fetch(u, opts); } catch (networkErr) { if (i === retries) throw networkErr; await new Promise(r => setTimeout(r, 300 * (i + 1))); // backoff 300ms } } }; try { let response = await safeFetch(url, finalOptions); // 401 → intentar renovar token if (response.status === 401) { if (!isRefreshing) { isRefreshing = true; try { const newToken = await refreshToken(); processQueue(null, newToken); finalOptions.headers['Authorization'] = `Bearer ${newToken}`; response = await safeFetch(url, finalOptions); } catch (refreshError) { processQueue(refreshError, null); throw refreshError; } finally { isRefreshing = false; } } else { // Otro refresh en curso — encolar y esperar return new Promise((resolve, reject) => { failedQueue.push({ resolve: (tok) => { finalOptions.headers['Authorization'] = `Bearer ${tok}`; safeFetch(url, finalOptions).then(resolve).catch(reject); }, reject, }); }); } } // 401 persistente después del retry → la sesión expiró definitivamente if (response.status === 401) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); localStorage.removeItem('user_permissions'); const hubUrl = import.meta.env.VITE_HUB_URL || 'http://localhost:3001'; const returnTo = encodeURIComponent(`${window.location.origin}/auth/sso`); setTimeout(() => { window.location.href = `${hubUrl}/login?return_to=${returnTo}`; }, 800); throw new Error('SESSION_EXPIRED'); } return response; } catch (error) { throw error; } }; // Función auxiliar para hacer peticiones GET con manejo de tokens export const getWithAuth = async (url) => { return fetchWithAuth(url, { method: 'GET' }); }; // Función auxiliar para hacer peticiones POST con manejo de tokens export const postWithAuth = async (url, data) => { return fetchWithAuth(url, { method: 'POST', body: JSON.stringify(data) }); }; // Función auxiliar para hacer peticiones PUT con manejo de tokens export const putWithAuth = async (url, data) => { return fetchWithAuth(url, { method: 'PUT', body: JSON.stringify(data) }); }; // Función auxiliar para hacer peticiones PATCH con manejo de tokens export const patchWithAuth = async (url, data) => { return fetchWithAuth(url, { method: 'PATCH', body: JSON.stringify(data) }); }; // Función auxiliar para hacer peticiones DELETE con manejo de tokens export const deleteWithAuth = async (url) => { return fetchWithAuth(url, { method: 'DELETE' }); }; // Función para hacer peticiones con FormData (para archivos) export const postFormDataWithAuth = async (url, formData) => { let token = localStorage.getItem('access'); const options = { method: 'POST', headers: { ...(token && { 'Authorization': `Bearer ${token}` }) }, body: formData }; try { let response = await fetch(url, options); if (response.status === 401) { if (!isRefreshing) { isRefreshing = true; try { const newToken = await refreshToken(); processQueue(null, newToken); options.headers['Authorization'] = `Bearer ${newToken}`; response = await fetch(url, options); } catch (refreshError) { processQueue(refreshError, null); throw refreshError; } finally { isRefreshing = false; } } else { // Ya hay un refresh en proceso, agregar esta petición a la cola return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token) => { options.headers['Authorization'] = `Bearer ${token}`; fetch(url, options) .then(resolve) .catch(reject); }, reject }); }); } } if (response.status === 401) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); setTimeout(() => { window.location.href = '/login'; }, 1000); throw new Error('Session expired'); } return response; } catch (error) { throw error; } }; // Función para hacer peticiones PUT con FormData (para archivos) export const putFormDataWithAuth = async (url, formData) => { let token = localStorage.getItem('access'); const options = { method: 'PUT', headers: { ...(token && { 'Authorization': `Bearer ${token}` }) }, body: formData }; try { let response = await fetch(url, options); if (response.status === 401) { if (!isRefreshing) { isRefreshing = true; try { const newToken = await refreshToken(); processQueue(null, newToken); options.headers['Authorization'] = `Bearer ${newToken}`; response = await fetch(url, options); } catch (refreshError) { processQueue(refreshError, null); throw refreshError; } finally { isRefreshing = false; } } else { // Ya hay un refresh en proceso, agregar esta petición a la cola return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token) => { options.headers['Authorization'] = `Bearer ${token}`; fetch(url, options) .then(resolve) .catch(reject); }, reject }); }); } } if (response.status === 401) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); setTimeout(() => { window.location.href = '/login'; }, 1000); throw new Error('Session expired'); } return response; } catch (error) { throw error; } }; // Función para hacer peticiones PATCH con FormData (para archivos) export const patchFormDataWithAuth = async (url, formData) => { let token = localStorage.getItem('access'); const options = { method: 'PATCH', headers: { ...(token && { 'Authorization': `Bearer ${token}` }) }, body: formData }; try { let response = await fetch(url, options); if (response.status === 401) { if (!isRefreshing) { isRefreshing = true; try { const newToken = await refreshToken(); processQueue(null, newToken); options.headers['Authorization'] = `Bearer ${newToken}`; response = await fetch(url, options); } catch (refreshError) { processQueue(refreshError, null); throw refreshError; } finally { isRefreshing = false; } } else { // Ya hay un refresh en proceso, agregar esta petición a la cola return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token) => { options.headers['Authorization'] = `Bearer ${token}`; fetch(url, options) .then(resolve) .catch(reject); }, reject }); }); } } if (response.status === 401) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('user_id'); localStorage.removeItem('user_is_importador'); setTimeout(() => { window.location.href = '/login'; }, 1000); throw new Error('Session expired'); } return response; } catch (error) { throw error; } }; export default fetchWithAuth;