412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
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;
|