feature/implementacion de hub en EFC
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
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;
|
||||
@@ -17,72 +18,69 @@ const processQueue = (error, token = null) => {
|
||||
failedQueue = [];
|
||||
};
|
||||
|
||||
// Función para renovar el token usando el refresh token
|
||||
/**
|
||||
* 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 () => {
|
||||
try {
|
||||
const refresh = localStorage.getItem('refresh');
|
||||
if (!refresh) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
// Intentar primero con el endpoint completo
|
||||
let refreshEndpoint = `${API_URL}/token/refresh/`;
|
||||
|
||||
let response = await fetch(refreshEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refresh: refresh
|
||||
}),
|
||||
});
|
||||
|
||||
// Si falla con 404, intentar con el endpoint alternativo
|
||||
if (response.status === 404) {
|
||||
refreshEndpoint = `${API_URL}/token/refresh/`;
|
||||
response = await fetch(refreshEndpoint, {
|
||||
// ── 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: refresh
|
||||
}),
|
||||
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
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to refresh token: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Guardar el nuevo access token
|
||||
localStorage.setItem('access', data.access);
|
||||
|
||||
// Si viene un nuevo refresh token, guardarlo también
|
||||
if (data.refresh) {
|
||||
localStorage.setItem('refresh', data.refresh);
|
||||
}
|
||||
|
||||
return data.access;
|
||||
} catch (error) {
|
||||
// Si falla la renovación, limpiar tokens y redirigir al login
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
localStorage.removeItem('user_id');
|
||||
localStorage.removeItem('user_is_importador');
|
||||
localStorage.removeItem('user_permissions');
|
||||
|
||||
// Redirigir al login después de un pequeño delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1000);
|
||||
|
||||
throw new Error('SESSION_EXPIRED');
|
||||
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
|
||||
@@ -108,85 +106,66 @@ export const fetchWithAuth = async (url, options = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Hacer la petición inicial
|
||||
let response = await fetch(url, finalOptions);
|
||||
// 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Si la respuesta es 401 (Unauthorized), manejar renovación de token
|
||||
try {
|
||||
let response = await safeFetch(url, finalOptions);
|
||||
|
||||
// 401 → intentar renovar token
|
||||
if (response.status === 401) {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
// Renovar el token
|
||||
const newToken = await refreshToken();
|
||||
|
||||
// Procesar la cola de peticiones pendientes
|
||||
processQueue(null, newToken);
|
||||
|
||||
// Actualizar el header de autorización y reintentar la petición original
|
||||
finalOptions.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
response = await fetch(url, finalOptions);
|
||||
|
||||
response = await safeFetch(url, finalOptions);
|
||||
} catch (refreshError) {
|
||||
// Si falla la renovación, procesar la cola con error
|
||||
processQueue(refreshError, null);
|
||||
throw refreshError;
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
} else {
|
||||
// Ya hay un refresh en proceso, agregar esta petición a la cola
|
||||
// Otro refresh en curso — encolar y esperar
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({
|
||||
resolve: (token) => {
|
||||
finalOptions.headers['Authorization'] = `Bearer ${token}`;
|
||||
fetch(url, finalOptions)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
resolve: (tok) => {
|
||||
finalOptions.headers['Authorization'] = `Bearer ${tok}`;
|
||||
safeFetch(url, finalOptions).then(resolve).catch(reject);
|
||||
},
|
||||
reject
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Si todavía hay un 401 después del intento de renovación, verificar si realmente es un problema de auth
|
||||
|
||||
// 401 persistente después del retry → la sesión expiró definitivamente
|
||||
if (response.status === 401) {
|
||||
// Verificar si tenemos un token válido en localStorage
|
||||
const currentToken = localStorage.getItem('access');
|
||||
|
||||
// Intentar obtener más información del error
|
||||
let errorDetails = '';
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
errorDetails = errorText;
|
||||
} catch (e) {
|
||||
// Error al leer respuesta
|
||||
}
|
||||
|
||||
// Si no hay token o el error indica problema de autenticación, limpiar sesión
|
||||
if (!currentToken || errorDetails.includes('token') || errorDetails.includes('auth')) {
|
||||
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');
|
||||
} else {
|
||||
// Si hay token pero sigue fallando, podría ser un problema del servidor
|
||||
// No limpiar la sesión, dejar que el error se propague
|
||||
}
|
||||
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) {
|
||||
// Si hay un error de red o cualquier otro error, propagarlo
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user