Files
frontend/src/fetchWithAuth.js
2025-08-15 13:45:12 -06:00

432 lines
12 KiB
JavaScript

const API_URL = import.meta.env.VITE_EFC_API_URL;
// 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 = [];
};
// Función para renovar el token usando el refresh token
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, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh: refresh
}),
});
}
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');
// Redirigir al login después de un pequeño delay
setTimeout(() => {
window.location.href = '/login';
}, 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
}
};
try {
// Hacer la petición inicial
let response = await fetch(url, finalOptions);
// Si la respuesta es 401 (Unauthorized), manejar renovación de 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);
} 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
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token) => {
finalOptions.headers['Authorization'] = `Bearer ${token}`;
fetch(url, finalOptions)
.then(resolve)
.catch(reject);
},
reject
});
});
}
}
// Si todavía hay un 401 después del intento de renovación, verificar si realmente es un problema de auth
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
}
}
return response;
} catch (error) {
// Si hay un error de red o cualquier otro error, propagarlo
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;