From c9df4e3ab25e284f9f7a1220491a3f209bdfac48 Mon Sep 17 00:00:00 2001 From: Kevin Rosales Date: Tue, 5 Aug 2025 13:06:24 -0600 Subject: [PATCH] Se soluciono autenticacion --- src/api/documentos.ts | 20 +-- src/api/expedientes.ts | 63 +-------- src/api/notificaciones.ts | 28 +--- src/api/organization.ts | 14 +- src/api/pedimentoDocuments.ts | 28 ++-- src/api/procesos.ts | 38 ++++-- src/api/users.js | 49 ++----- src/api/users.ts | 56 ++------ src/context/UserContext.jsx | 58 +++----- src/fetchWithAuth.js | 226 +++++++++++++++++++++++++++++++ src/main.jsx | 12 +- src/pages/Admin.jsx | 13 +- src/pages/Documents.jsx | 109 +++++++-------- src/pages/Expedientes.jsx | 53 ++++---- src/pages/Organization.jsx | 20 +-- src/pages/PedimentoDetail.jsx | 166 ++++++++++------------- src/pages/Procesos.jsx | 78 +++++------ src/pages/Settings.jsx | 244 +++++++++++++++++++++++++++++----- src/pages/Users.jsx | 49 ++----- src/pages/UsersNew.jsx | 22 +-- src/pages/Vucem.jsx | 36 +---- 21 files changed, 758 insertions(+), 624 deletions(-) diff --git a/src/api/documentos.ts b/src/api/documentos.ts index 3da516e..19c9496 100644 --- a/src/api/documentos.ts +++ b/src/api/documentos.ts @@ -1,4 +1,5 @@ // src/api/pedimentoDocuments.ts +import { fetchWithAuth } from '../fetchWithAuth'; export interface PedimentoDocument { id: string; @@ -23,8 +24,6 @@ export interface PedimentoDocumentsResponse { const API_URL = import.meta.env.VITE_EFC_API_URL; export async function fetchPedimentoDocuments( - token: string, - pedimentoId: string = '', page: number = 1, pageSize: number = 10, filters: { @@ -32,7 +31,8 @@ export async function fetchPedimentoDocuments( extension?: string; document_type?: string | number; created_at?: string; - } = {} + } = {}, + pedimentoId: string = '' ): Promise { const params = new URLSearchParams(); params.append('page', String(page)); @@ -43,18 +43,10 @@ export async function fetchPedimentoDocuments( if (filters.document_type) params.append('document_type', String(filters.document_type)); if (filters.created_at) params.append('created_at', filters.created_at); - const res = await fetch( - `${API_URL}/record/documents/?${params.toString()}`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - } + const res = await fetchWithAuth( + `${API_URL}/record/documents/?${params.toString()}` ); - if (res.status === 401) { - throw new Error('SESSION_EXPIRED'); - } + if (!res.ok) throw new Error('No autorizado o error en la petición'); return res.json(); } diff --git a/src/api/expedientes.ts b/src/api/expedientes.ts index d5692e3..7d4245f 100644 --- a/src/api/expedientes.ts +++ b/src/api/expedientes.ts @@ -1,4 +1,5 @@ +import { fetchWithAuth } from '../fetchWithAuth'; export interface Document { id: string; @@ -19,8 +20,6 @@ export interface DocumentsResponse { results: Document[]; } -import { refreshToken } from './auth'; - const API_URL = import.meta.env.VITE_EFC_API_URL; // Obtiene la lista de documentos (pedimentos) export interface PedimentosFilters { @@ -38,7 +37,6 @@ export interface PedimentosFilters { } export async function fetchDocuments( - token: string, page: number = 1, pageSize: number = 10, filters: PedimentosFilters = {} @@ -51,65 +49,14 @@ export async function fetchDocuments( params.append(key, String(value)); } }); - let res = await fetch(`${API_URL}/customs/pedimentos/?${params.toString()}`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - if (res.status === 401) { - // Intentar refrescar el token - const refresh = localStorage.getItem('refresh'); - if (refresh) { - try { - const data = await refreshToken(refresh); - localStorage.setItem('access', data.access); - // Reintenta la petición con el nuevo access token - res = await fetch(`${API_URL}/customs/pedimentos/?page=${page}&page_size=${pageSize}`, { - headers: { - 'Authorization': `Bearer ${data.access}`, - 'Content-Type': 'application/json', - }, - }); - } catch (err) { - throw new Error('SESSION_EXPIRED'); - } - } else { - throw new Error('SESSION_EXPIRED'); - } - } + + const res = await fetchWithAuth(`${API_URL}/customs/pedimentos/?${params.toString()}`); if (!res.ok) throw new Error('No autorizado o error en la petición'); return res.json(); } // Obtiene los documentos por id de pedimento -export async function fetchDocumentById(token: string, id: string): Promise { - let res = await fetch(`${API_URL}/record/documents/?page=1&page_size=10&pedimento=${id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - if (res.status === 401) { - // Intentar refrescar el token - const refresh = localStorage.getItem('refresh'); - if (refresh) { - try { - const data = await refreshToken(refresh); - localStorage.setItem('access', data.access); - // Reintenta la petición con el nuevo access token - res = await fetch(`${API_URL}/record/documents/?page=1&page_size=10&pedimento=${id}/`, { - headers: { - 'Authorization': `Bearer ${data.access}`, - 'Content-Type': 'application/json', - }, - }); - } catch (err) { - throw new Error('SESSION_EXPIRED'); - } - } else { - throw new Error('SESSION_EXPIRED'); - } - } +export async function fetchDocumentById(id: string): Promise { + const res = await fetchWithAuth(`${API_URL}/record/documents/?page=1&page_size=10&pedimento=${id}/`); if (!res.ok) throw new Error('No autorizado o error en la petición'); return res.json(); } diff --git a/src/api/notificaciones.ts b/src/api/notificaciones.ts index fc2aa19..7744687 100644 --- a/src/api/notificaciones.ts +++ b/src/api/notificaciones.ts @@ -1,15 +1,9 @@ +import { fetchWithAuth, putWithAuth } from '../fetchWithAuth'; + // PUT para marcar una notificación como vista export async function marcarNotificacionComoVista(id: number): Promise { - const token = localStorage.getItem('access'); const url = `${API_URL}/notificaciones/notificaciones/${id}/`; - const headers = new Headers(); - if (token) headers.append('Authorization', `Bearer ${token}`); - headers.append('Content-Type', 'application/json'); - const res = await fetch(url, { - method: 'PUT', - headers, - body: JSON.stringify({ visto: true }) - }); + const res = await putWithAuth(url, { visto: true }); if (!res.ok) throw new Error('Error al actualizar notificación'); return await res.json(); } @@ -41,28 +35,16 @@ export interface NotificacionesResponse { const API_URL = import.meta.env.VITE_EFC_API_URL; export async function fetchNotificaciones({ page = 1, pageSize = 10, visto = false } = {}): Promise { - const token = localStorage.getItem('access'); const url = `${API_URL}/notificaciones/notificaciones/?page=${page}&page_size=${pageSize}&visto=${visto}`; - const headers = new Headers(); - if (token) headers.append('Authorization', `Bearer ${token}`); - headers.append('Content-Type', 'application/json'); - const res = await fetch(url, { - headers, - }); + const res = await fetchWithAuth(url); if (!res.ok) throw new Error('Error al obtener notificaciones'); return await res.json(); } export async function fetchAllNotifications({page = 1, page_size=10}): Promise{ - const token = localStorage.getItem('access'); const url = `${API_URL}/notificaciones/notificaciones/?page=${page}&page_size=${page_size}`; - const headers = new Headers(); - if (token) headers.append('Authorization', `Bearer ${token}`); - headers.append('Content-Type', 'application/json'); - const res = await fetch(url, { - headers, - }); + const res = await fetchWithAuth(url); if (!res.ok) throw new Error('Error al obtener notificaciones'); return await res.json(); } \ No newline at end of file diff --git a/src/api/organization.ts b/src/api/organization.ts index 62c60da..8e1e5e9 100644 --- a/src/api/organization.ts +++ b/src/api/organization.ts @@ -1,5 +1,6 @@ // organization.ts // Tipos para la respuesta del endpoint de uso de almacenamiento de organización +import { fetchWithAuth } from '../fetchWithAuth'; export interface OrganizationUsage { organizacion: string; @@ -16,16 +17,9 @@ export interface OrganizationUsage { const API_URL = import.meta.env.VITE_EFC_API_URL; // Ejemplo de función para obtener la información tipada -export async function fetchOrganizationUsage(token: string): Promise { - const res = await fetch(`${API_URL}/organization/uso-almacenamiento/mi_organizacion/`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - if (res.status === 401) { - throw new Error('SESSION_EXPIRED'); - } +export async function fetchOrganizationUsage(): Promise { + const res = await fetchWithAuth(`${API_URL}/organization/uso-almacenamiento/mi_organizacion/`); + if (!res.ok) { throw new Error('Error al obtener información de la organización'); } diff --git a/src/api/pedimentoDocuments.ts b/src/api/pedimentoDocuments.ts index c6218c0..16992c2 100644 --- a/src/api/pedimentoDocuments.ts +++ b/src/api/pedimentoDocuments.ts @@ -1,4 +1,5 @@ // src/api/pedimentoDocuments.ts +import { fetchWithAuth } from '../fetchWithAuth'; export interface PedimentoDocument { id: string; @@ -19,26 +20,25 @@ export interface PedimentoDocumentsResponse { results: PedimentoDocument[]; } -const API_URL = import.meta.env.VITE_EFC_API_URL; +const API_URL = (import.meta as any).env.VITE_EFC_API_URL; export async function fetchPedimentoDocuments( - token: string, pedimentoId: string, page: number = 1, pageSize: number = 10 ): Promise { - const res = await fetch( - `${API_URL}/record/documents/?page=${page}&page_size=${pageSize}&pedimento=${pedimentoId}`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + try { + const res = await fetchWithAuth( + `${API_URL}/record/documents/?page=${page}&page_size=${pageSize}&pedimento=${pedimentoId}` + ); + + if (!res.ok) { + throw new Error('No autorizado o error en la petición'); } - ); - if (res.status === 401) { - throw new Error('SESSION_EXPIRED'); + + return res.json(); + } catch (error) { + console.error('Error in fetchPedimentoDocuments:', error); + throw error; } - if (!res.ok) throw new Error('No autorizado o error en la petición'); - return res.json(); } diff --git a/src/api/procesos.ts b/src/api/procesos.ts index be42417..943b64e 100644 --- a/src/api/procesos.ts +++ b/src/api/procesos.ts @@ -1,3 +1,5 @@ +import { fetchWithAuth } from '../fetchWithAuth'; + // Tipos para la respuesta y registros export interface ProcesamientoPedimento { id: number; @@ -20,14 +22,34 @@ export interface ProcesamientoPedimentosResponse { // API para customs/procesamientopedimentos/ export async function fetchProcesamientoPedimentos( - token: string | null, page: number = 1, - pageSize: number = 20 + pageSize: number = 20, + filters: Record = {} ): Promise { - const API_URL = import.meta.env.VITE_EFC_API_URL; - const headers: Record = {}; - if (token) headers['Authorization'] = `Bearer ${token}`; - const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?page=${page}&page_size=${pageSize}`, { headers }); - if (!res.ok) throw new Error('Error al obtener procesamiento de pedimentos'); - return await res.json(); + try { + const API_URL = (import.meta as any).env.VITE_EFC_API_URL; + + // Construir query params + const params = new URLSearchParams(); + params.append('page', String(page)); + params.append('page_size', String(pageSize)); + + // Agregar filtros + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, String(value)); + } + }); + + const res = await fetchWithAuth(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`); + + if (!res.ok) { + throw new Error('Error al obtener procesamiento de pedimentos'); + } + + return await res.json(); + } catch (error) { + console.error('Error in fetchProcesamientoPedimentos:', error); + throw error; + } } diff --git a/src/api/users.js b/src/api/users.js index cb30ca9..03798d0 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,4 +1,7 @@ const API_URL = import.meta.env.VITE_EFC_API_URL || 'http://localhost:8000'; +import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth } from '../fetchWithAuth'; + +// Función helper para manejar respuestas async function handleResponse(response, operation = 'operación') { if (response.status === 401) { @@ -15,57 +18,27 @@ async function handleResponse(response, operation = 'operación') { return response.json(); } -export async function fetchUsers(token) { +export async function fetchUsers() { const url = `${API_URL}/user/users/`; - const res = await fetch(url, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); + const res = await fetchWithAuth(url); return handleResponse(res, 'Fetch Users'); } -export async function createUser(token, userData) { +export async function createUser(userData) { const url = `${API_URL}/user/users/`; - const res = await fetch(url, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(userData), - }); + const res = await postWithAuth(url, userData); return handleResponse(res, 'Create User'); } -export async function updateUser(token, id, userData) { +export async function updateUser(id, userData) { const url = `${API_URL}/user/users/${id}/`; - const res = await fetch(url, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(userData), - }); + const res = await putWithAuth(url, userData); return handleResponse(res, 'Update User'); } -export async function deleteUser(token, id) { +export async function deleteUser(id) { const url = `${API_URL}/user/users/${id}/`; - const res = await fetch(url, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); - if (res.status === 401) throw new Error('SESSION_EXPIRED'); + const res = await deleteWithAuth(url); if (!res.ok) throw new Error(`Error ${res.status}: ${res.statusText}`); return true; } diff --git a/src/api/users.ts b/src/api/users.ts index 760382e..580d3a4 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,4 +1,5 @@ const API_URL = import.meta.env.VITE_EFC_API_URL || 'http://localhost:8000'; +import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth } from '../fetchWithAuth'; // Función helper para manejar respuestas async function handleResponse(response, operation = 'operación') { @@ -26,18 +27,12 @@ async function handleResponse(response, operation = 'operación') { return response.json(); } -export async function fetchUsers(token) { +export async function fetchUsers() { try { const url = `${API_URL}/user/users/`; console.log('👥 Fetching users from:', url); - const res = await fetch(url, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); + const res = await fetchWithAuth(url); const data = await handleResponse(res, 'Fetch Users'); console.log('✅ Users data received'); @@ -52,20 +47,12 @@ export async function fetchUsers(token) { } } -export async function createUser(token, userData) { +export async function createUser(userData) { try { const url = `${API_URL}/user/users/`; console.log('➕ Creating user at:', url); - const res = await fetch(url, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(userData), - }); + const res = await postWithAuth(url, userData); const data = await handleResponse(res, 'Create User'); console.log('✅ User created successfully'); @@ -80,20 +67,12 @@ export async function createUser(token, userData) { } } -export async function updateUser(token, id, userData) { +export async function updateUser(id, userData) { try { const url = `${API_URL}/user/users/${id}/`; console.log('✏️ Updating user at:', url); - const res = await fetch(url, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(userData), - }); + const res = await putWithAuth(url, userData); const data = await handleResponse(res, 'Update User'); console.log('✅ User updated successfully'); @@ -108,19 +87,12 @@ export async function updateUser(token, id, userData) { } } -export async function deleteUser(token, id) { +export async function deleteUser(id) { try { const url = `${API_URL}/user/users/${id}/`; console.log('🗑️ Deleting user at:', url); - const res = await fetch(url, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); + const res = await deleteWithAuth(url); if (res.status === 401) { console.error('❌ Unauthorized - session expired'); @@ -145,18 +117,12 @@ export async function deleteUser(token, id) { } } -export async function getCurrentUser(token) { +export async function getCurrentUser() { try { const url = `${API_URL}/user/users/me/`; console.log('👤 Fetching current user from:', url); - const res = await fetch(url, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }); + const res = await fetchWithAuth(url); const data = await handleResponse(res, 'Get Current User'); console.log('✅ Current user data received:', data); diff --git a/src/context/UserContext.jsx b/src/context/UserContext.jsx index d5eb4b0..c0774e8 100644 --- a/src/context/UserContext.jsx +++ b/src/context/UserContext.jsx @@ -1,6 +1,5 @@ import React, { createContext, useContext, useEffect, useState, useRef } from 'react'; -import { getCurrentUser } from '../api/users.ts'; -import { refreshToken } from '../api/auth.js'; +import { fetchWithAuth } from '../fetchWithAuth'; const UserContext = createContext({ user: null, @@ -19,51 +18,28 @@ export function UserProvider({ children }) { if (fetchedOnce.current && loading) return; setLoading(true); setError(null); - let token = localStorage.getItem('access'); - let triedRefresh = false; - while (true) { - try { - if (token) { - const userData = await getCurrentUser(token); + + try { + const token = localStorage.getItem('access'); + if (token) { + const API_URL = import.meta.env.VITE_EFC_API_URL; + const response = await fetchWithAuth(`${API_URL}/user/users/me/`); + + if (response.ok) { + const userData = await response.json(); setUser(userData); } else { setUser(null); } - break; - } catch (err) { - // Si el token expiró, intenta refrescarlo una vez - if (!triedRefresh && (err.message === 'SESSION_EXPIRED' || err.message.includes('401'))) { - triedRefresh = true; - const refresh = localStorage.getItem('refresh'); - if (refresh) { - try { - const data = await refreshToken(refresh); - if (data.access) { - localStorage.setItem('access', data.access); - token = data.access; - continue; // Reintenta con el nuevo token - } else { - throw new Error('No se pudo refrescar el token'); - } - } catch (refreshErr) { - setError(refreshErr); - setUser(null); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - window.dispatchEvent(new CustomEvent('authStateChanged')); - break; - } - } else { - setUser(null); - break; - } - } else { - setError(err); - setUser(null); - break; - } + } else { + setUser(null); } + } catch (err) { + console.error('Error fetching user:', err); + setError(err); + setUser(null); } + setLoading(false); fetchedOnce.current = true; }; diff --git a/src/fetchWithAuth.js b/src/fetchWithAuth.js index e69de29..7a56731 100644 --- a/src/fetchWithAuth.js +++ b/src/fetchWithAuth.js @@ -0,0 +1,226 @@ +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'); + } + + const response = await fetch(`${API_URL}/auth/token/refresh/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refresh: refresh + }), + }); + + if (!response.ok) { + throw new Error('Failed to refresh token'); + } + + 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 error; + } +}; + +// 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 + const defaultHeaders = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }) + }; + + // 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), intentar renovar el token + if (response.status === 401 && !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; + } + } + + // Si todavía hay un 401 después del intento de renovación, redirigir al login + 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) { + // 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 && !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; + } + } + + 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; diff --git a/src/main.jsx b/src/main.jsx index 4a64a0d..14f92e9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -10,10 +10,18 @@ import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; +const isDevelopment = import.meta.env.DEV; + createRoot(document.getElementById('root')).render( - + isDevelopment ? ( - , + ) : ( + + + + + + ), ) diff --git a/src/pages/Admin.jsx b/src/pages/Admin.jsx index aa979f9..d7c0af7 100644 --- a/src/pages/Admin.jsx +++ b/src/pages/Admin.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { fetchWithAuth } from '../fetchWithAuth'; // Animación fade-in/slide-up para cards const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } @@ -45,33 +46,31 @@ export default function Admin() { setLoading(true); setError(''); try { - const token = localStorage.getItem('access'); - const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; - // Servicios - const resServices = await fetch(`${API_URL}/cards/services-util-information/`, { headers }); + const resServices = await fetchWithAuth(`${API_URL}/cards/services-util-information/`); if (!resServices.ok) throw new Error('Error al obtener estados de servicios'); const dataServices = await resServices.json(); setServices(dataServices); // Descargas - const resDownloads = await fetch(`${API_URL}/cards/document-util-information/`, { headers }); + const resDownloads = await fetchWithAuth(`${API_URL}/cards/document-util-information/`); if (!resDownloads.ok) throw new Error('Error al obtener información de descargas'); const dataDownloads = await resDownloads.json(); setDownloads(dataDownloads); // Últimos documentos - const resDocs = await fetch(`${API_URL}/cards/downloaded-documents/`, { headers }); + const resDocs = await fetchWithAuth(`${API_URL}/cards/downloaded-documents/`); if (!resDocs.ok) throw new Error('Error al obtener últimos documentos'); const dataDocs = await resDocs.json(); setLatestDocs(dataDocs.documentos); // Análisis de actividad de usuario - const resUserActivity = await fetch(`${API_URL}/cards/user-activity-analysis/`, { headers }); + const resUserActivity = await fetchWithAuth(`${API_URL}/cards/user-activity-analysis/`); if (!resUserActivity.ok) throw new Error('Error al obtener análisis de actividad de usuario'); const dataUserActivity = await resUserActivity.json(); setUserActivity(dataUserActivity); } catch (err) { + console.error('Error fetching admin data:', err); setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); diff --git a/src/pages/Documents.jsx b/src/pages/Documents.jsx index 8486520..5a0d87b 100644 --- a/src/pages/Documents.jsx +++ b/src/pages/Documents.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; import SuccessModal from '../components/SuccessModal.jsx'; +import { fetchWithAuth, postWithAuth } from '../fetchWithAuth'; // Animación fade-in/slide-up para bloques const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-documents')) { @@ -17,35 +18,28 @@ const API_URL = import.meta.env.VITE_EFC_API_URL; // Descarga individual const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - if (res.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return; + try { + const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); + + if (!res.ok) { + alert('No autorizado o error en la descarga'); + return; + } + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + if (setSuccess) setSuccess('Descarga exitosa'); + } catch (error) { + console.error('Error downloading file:', error); + showMessage('Error al descargar el archivo', 'error'); } - if (!res.ok) { - alert('No autorizado o error en la descarga'); - return; - } - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - if (setSuccess) setSuccess('Descarga exitosa'); }; // Descarga masiva (bulk) @@ -54,38 +48,32 @@ const downloadBulkZip = async (ids, showMessage, setSuccess, nombreZip = 'docume showMessage('Selecciona al menos un documento.', 'error'); return; } - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/bulk-download/`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ document_ids: ids, pedimento_nombre: nombreZip }), - }); - if (res.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return; + + try { + const res = await postWithAuth(`${API_URL}/record/documents/bulk-download/`, { + document_ids: ids, + pedimento_nombre: nombreZip + }); + + if (!res.ok) { + showMessage('No autorizado o error en la descarga masiva', 'error'); + return; + } + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${nombreZip || 'documentos'}.zip`; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + if (setSuccess) setSuccess('Descarga(s) completada(s)'); + } catch (error) { + console.error('Error in bulk download:', error); + showMessage('Error en la descarga masiva', 'error'); } - if (!res.ok) { - showMessage('No autorizado o error en la descarga masiva', 'error'); - return; - } - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${nombreZip || 'documentos'}.zip`; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - if (setSuccess) setSuccess('Descarga(s) completada(s)'); }; export default function Documents() { @@ -128,8 +116,7 @@ export default function Documents() { setLoading(true); setError(null); try { - const token = localStorage.getItem('access'); - const data = await fetchPedimentoDocuments(token, '', currentPage, itemsPerPage, { + const data = await fetchPedimentoDocuments(currentPage, itemsPerPage, { pedimento_numero: pedimentoNumeroFilter, extension: extensionFilter, document_type: documentTypeFilter, diff --git a/src/pages/Expedientes.jsx b/src/pages/Expedientes.jsx index ed20f21..61c4dbe 100644 --- a/src/pages/Expedientes.jsx +++ b/src/pages/Expedientes.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useLayoutEffect, useRef } from 'react'; +import { fetchWithAuth, postWithAuth } from '../fetchWithAuth'; // Animación fade-in/slide-up para bloques const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-documents')) { @@ -15,35 +16,28 @@ import { Link } from 'react-router-dom'; const API_URL = import.meta.env.VITE_EFC_API_URL; const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - if (res.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return; + try { + const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); + + if (!res.ok) { + alert('No autorizado o error en la descarga'); + return; + } + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + if (setSuccess) setSuccess('Descarga exitosa'); + } catch (error) { + console.error('Error downloading file:', error); + showMessage('Error al descargar el archivo', 'error'); } - if (!res.ok) { - alert('No autorizado o error en la descarga'); - return; - } - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - if (setSuccess) setSuccess('Descarga exitosa');useEffect }; export default function Documents() { @@ -83,7 +77,6 @@ export default function Documents() { // Fetching usando la función tipada de TypeScript const fetchPedimentosData = async (page = currentPage, pageSize = itemsPerPage) => { - const token = localStorage.getItem('access'); // Construir objeto de filtros const filters = { search: searchFilter || undefined, @@ -98,7 +91,7 @@ export default function Documents() { tipo_operacion: tipoOperacionFilter || undefined, clave_pedimento: clavePedimentoFilter || undefined, }; - return await fetchDocuments(token, page, pageSize, filters); + return await fetchDocuments(page, pageSize, filters); }; // Hook de polling que se ejecuta cada 30 segundos diff --git a/src/pages/Organization.jsx b/src/pages/Organization.jsx index f3c0e57..e7e589d 100644 --- a/src/pages/Organization.jsx +++ b/src/pages/Organization.jsx @@ -12,28 +12,14 @@ export default function Organization() { const [animatedPercent, setAnimatedPercent] = useState(0); useEffect(() => { - const token = localStorage.getItem('access'); - if (!token) { - setError('No se encontró el token de acceso.'); - setLoading(false); - return; - } - fetchOrganizationUsage(token) + fetchOrganizationUsage() .then(data => { setInfo(data); setLoading(false); }) .catch(err => { - if (err.message === 'SESSION_EXPIRED') { - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - } else { - setError(err.message); - } + console.error('Error fetching organization data:', err); + setError('Error al cargar la información de la organización.'); setLoading(false); }); }, [showMessage]); diff --git a/src/pages/PedimentoDetail.jsx b/src/pages/PedimentoDetail.jsx index 4cf7877..d52f300 100644 --- a/src/pages/PedimentoDetail.jsx +++ b/src/pages/PedimentoDetail.jsx @@ -16,40 +16,38 @@ import 'highlight.js/styles/github.css'; hljs.registerLanguage('xml', xml); // import type removed for JSX compatibility import { fetchPedimentoDocuments } from '../api/pedimentoDocuments'; +import { fetchWithAuth, postWithAuth } from '../fetchWithAuth'; import { useParams, Link } from 'react-router-dom'; import { useNotification } from '../context/NotificationContext'; const API_URL = import.meta.env.VITE_EFC_API_URL; const downloadFile = async (id, filename = 'archivo', showMessage) => { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - if (res.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return; + try { + const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${id}/`); + + if (!res.ok) { + showMessage('Error en la descarga del archivo', 'error'); + return; + } + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading file:', error); + if (error.message === 'SESSION_EXPIRED') { + showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); + } else { + showMessage('Error al descargar el archivo', 'error'); + } } - if (!res.ok) { - alert('No autorizado o error en la descarga'); - return; - } - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); }; const downloadBulkZip = async (ids, showMessage, pedimentoNombre) => { @@ -57,37 +55,35 @@ const downloadBulkZip = async (ids, showMessage, pedimentoNombre) => { showMessage('Selecciona al menos un documento.', 'error'); return; } - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/bulk-download/`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ document_ids: ids, pedimento_nombre: pedimentoNombre }), - }); - if (res.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return; + + try { + const res = await postWithAuth(`${API_URL}/record/documents/bulk-download/`, { + document_ids: ids, + pedimento_nombre: pedimentoNombre + }); + + if (!res.ok) { + showMessage('Error en la descarga masiva', 'error'); + return; + } + + const blob = await res.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${pedimentoNombre || 'documentos'}.zip`; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error in bulk download:', error); + if (error.message === 'SESSION_EXPIRED') { + showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); + } else { + showMessage('Error en la descarga masiva', 'error'); + } } - if (!res.ok) { - showMessage('No autorizado o error en la descarga masiva', 'error'); - return; - } - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${pedimentoNombre || 'documentos'}.zip`; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); }; import { useRef, useLayoutEffect } from 'react'; @@ -176,23 +172,8 @@ const [docsPrev, setDocsPrev] = useState(null); const { showMessage } = useNotification(); useEffect(() => { - const token = localStorage.getItem('access'); - fetch(`${API_URL}/customs/pedimentos/${id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }) + fetchWithAuth(`${API_URL}/customs/pedimentos/${id}/`) .then(res => { - if (res.status === 401) { - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return null; - } if (!res.ok) throw new Error('No autorizado o error en la petición'); return res.json(); }) @@ -201,7 +182,12 @@ const [docsPrev, setDocsPrev] = useState(null); setLoading(false); }) .catch(err => { - setError(err.message); + console.error('Error fetching pedimento:', err); + if (err.message === 'SESSION_EXPIRED') { + showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); + } else { + setError(err.message); + } setLoading(false); }); }, [id, showMessage]); @@ -209,10 +195,9 @@ const [docsPrev, setDocsPrev] = useState(null); // Fetch paginated documents useEffect(() => { if (!id) return; - const token = localStorage.getItem('access'); setDocsLoading(true); setDocsError(''); - fetchPedimentoDocuments(token, id, page, pageSize) + fetchPedimentoDocuments(id, page, pageSize) .then((data) => { setDocuments(data.results); setDocsCount(data.count); @@ -221,13 +206,9 @@ const [docsPrev, setDocsPrev] = useState(null); setDocsLoading(false); }) .catch(err => { + console.error('Error fetching documents:', err); if (err.message === 'SESSION_EXPIRED') { - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); } else { setDocsError(err.message); } @@ -288,22 +269,14 @@ const [docsPrev, setDocsPrev] = useState(null); setPreviewXml(''); setPreviewOpen(true); try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/record/documents/descargar/${doc.id}/`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - if (res.status === 401) { - setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.'); - setPreviewLoading(false); - return; - } + const res = await fetchWithAuth(`${API_URL}/record/documents/descargar/${doc.id}/`); + if (!res.ok) { - setPreviewError('No autorizado o error en la descarga'); + setPreviewError('Error al obtener el archivo'); setPreviewLoading(false); return; } + // Detectar tipo de archivo let type = ''; if (doc.extension) { @@ -332,7 +305,12 @@ const [docsPrev, setDocsPrev] = useState(null); setPreviewLoading(false); } } catch (err) { - setPreviewError('Error al obtener el archivo'); + console.error('Error in preview:', err); + if (err.message === 'SESSION_EXPIRED') { + setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.'); + } else { + setPreviewError('Error al obtener el archivo'); + } setPreviewLoading(false); } }; diff --git a/src/pages/Procesos.jsx b/src/pages/Procesos.jsx index 930baba..f835a67 100644 --- a/src/pages/Procesos.jsx +++ b/src/pages/Procesos.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { fetchProcesamientoPedimentos } from '../api/procesos.ts'; +import { postWithAuth } from '../fetchWithAuth'; const API_URL = import.meta.env.VITE_EFC_API_URL; const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL; @@ -62,31 +63,26 @@ export default function Procesos() { setExecutingId(null); return; } + try { - const token = localStorage.getItem('access'); - if (!token) { - alert('No hay token de autenticación. Por favor, inicia sesión nuevamente.'); - setExecutingId(null); - return; - } - const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }; - const body = JSON.stringify({ + const body = { pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, - }); - const res = await fetch(`${MICROSERVICE_URL}${endpoint}`, { - method: 'POST', - headers, - body, - }); + }; + + const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); + if (!res.ok) throw new Error('Error al ejecutar el servicio'); + alert('Servicio ejecutado correctamente'); setOpenDropdownId(null); } catch (err) { - alert('Error al ejecutar el servicio: ' + (err instanceof Error ? err.message : String(err))); + console.error('Error executing service:', err); + if (err.message === 'SESSION_EXPIRED') { + alert('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); + } else { + alert('Error al ejecutar el servicio: ' + (err instanceof Error ? err.message : String(err))); + } } finally { setExecutingId(null); } @@ -115,41 +111,29 @@ export default function Procesos() { setLoading(true); setError(''); try { - const token = localStorage.getItem('access'); - if (!token) { - setError('No hay token de autenticación. Por favor, inicia sesión nuevamente.'); - return; - } - // Construir query params - const params = new URLSearchParams(); - params.append('page', String(page)); - params.append('page_size', String(itemsPerPage)); - if (pedimentoPedimentoFilter) params.append('pedimento__pedimento', pedimentoPedimentoFilter); - if (estadoFilter) params.append('estado', estadoFilter); - if (servicioFilter) params.append('servicio', servicioFilter); + // Construir filtros + const filters = {}; + if (pedimentoPedimentoFilter) filters['pedimento__pedimento'] = pedimentoPedimentoFilter; + if (estadoFilter) filters['estado'] = estadoFilter; + if (servicioFilter) filters['servicio'] = servicioFilter; if (sortField) { - params.append('ordering', (sortOrder === 'desc' ? '-' : '') + sortField); + filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + sortField; } - const API_URL = import.meta.env.VITE_EFC_API_URL; - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - console.log('Fetching procesos with token:', token ? 'Token present' : 'No token'); - console.log('URL:', `${API_URL}/customs/procesamientopedimentos/?${params.toString()}`); - const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers }); - console.log('Response status:', res.status); - if (!res.ok) { - const errorText = await res.text(); - console.log('Error response:', errorText); - throw new Error(`Error al obtener procesamiento de pedimentos: ${res.status} - ${errorText}`); - } - const data = await res.json(); + + console.log('Fetching procesos with filters:', filters); + + const data = await fetchProcesamientoPedimentos(page, itemsPerPage, filters); + console.log('Data received:', data); setProcesos(data.results || []); setCount(data.count || 0); } catch (err) { - setError(err instanceof Error ? err.message : String(err)); + console.error('Error fetching procesos:', err); + if (err.message === 'SESSION_EXPIRED') { + setError('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); + } else { + setError(err instanceof Error ? err.message : String(err)); + } } finally { setLoading(false); } diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index fd172de..373290e 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { getCurrentUser } from '../api/users.ts'; import { useNotification } from '../context/NotificationContext'; +import { fetchWithAuth, patchWithAuth, postWithAuth, postFormDataWithAuth } from '../fetchWithAuth'; // Animación fade-in/slide-up para el componente const fadeInSlideUp = `@keyframes fadein-slideup { 0% { opacity: 0; transform: translateY(40px); } 100% { opacity: 1; transform: translateY(0); } }`; @@ -27,6 +28,16 @@ const Settings = () => { rfc: '' }); + // Estados para cambio de contraseña + const [passwordData, setPasswordData] = useState({ + current_password: '', + new_password: '', + confirm_password: '' + }); + + const [passwordErrors, setPasswordErrors] = useState({}); + const [changingPassword, setChangingPassword] = useState(false); + // Estado para controlar la animación de entrada const [showAnimation, setShowAnimation] = useState(false); const [hasAnimated, setHasAnimated] = useState(false); @@ -79,28 +90,10 @@ const Settings = () => { // Función para actualizar el usuario const updateUser = async (userData) => { - const token = localStorage.getItem('access'); const API_URL = import.meta.env.VITE_EFC_API_URL; try { - const response = await fetch(`${API_URL}/user/users/${currentUser.id}/`, { - method: 'PATCH', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData), - }); - - if (response.status === 401) { - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - return null; - } + const response = await patchWithAuth(`${API_URL}/user/users/${currentUser.id}/`, userData); if (!response.ok) { const errorData = await response.json(); @@ -123,6 +116,105 @@ const Settings = () => { })); }; + // Manejar cambios en el formulario de contraseña + const handlePasswordChange = (e) => { + const { name, value } = e.target; + setPasswordData(prev => ({ + ...prev, + [name]: value + })); + // Limpiar errores cuando el usuario empiece a escribir + if (passwordErrors[name]) { + setPasswordErrors(prev => ({ + ...prev, + [name]: '' + })); + } + }; + + // Validar contraseña + const validatePassword = (password) => { + const errors = []; + if (password.length < 8) { + errors.push('Debe tener al menos 8 caracteres'); + } + if (!/(?=.*[a-z])/.test(password)) { + errors.push('Debe contener al menos una letra minúscula'); + } + if (!/(?=.*[A-Z])/.test(password)) { + errors.push('Debe contener al menos una letra mayúscula'); + } + if (!/(?=.*\d)/.test(password)) { + errors.push('Debe contener al menos un número'); + } + return errors; + }; + + // Cambiar contraseña + const handleChangePassword = async () => { + const errors = {}; + + // Validaciones + if (!passwordData.current_password.trim()) { + errors.current_password = 'La contraseña actual es requerida'; + } + + if (!passwordData.new_password.trim()) { + errors.new_password = 'La nueva contraseña es requerida'; + } else { + const passwordValidationErrors = validatePassword(passwordData.new_password); + if (passwordValidationErrors.length > 0) { + errors.new_password = passwordValidationErrors[0]; // Mostrar el primer error + } + } + + if (!passwordData.confirm_password.trim()) { + errors.confirm_password = 'Confirma tu nueva contraseña'; + } else if (passwordData.new_password !== passwordData.confirm_password) { + errors.confirm_password = 'Las contraseñas no coinciden'; + } + + if (Object.keys(errors).length > 0) { + setPasswordErrors(errors); + return; + } + + setChangingPassword(true); + try { + const API_URL = import.meta.env.VITE_EFC_API_URL; + + const response = await postWithAuth(`${API_URL}/user/users/${currentUser.id}/change_password/`, { + old_password: passwordData.current_password, + new_password: passwordData.new_password + }); + + if (!response.ok) { + const errorData = await response.json(); + if (errorData.old_password) { + setPasswordErrors({ current_password: 'La contraseña actual es incorrecta' }); + } else { + throw new Error(errorData.detail || 'Error al cambiar la contraseña'); + } + return; + } + + // Limpiar formulario y mostrar mensaje de éxito + setPasswordData({ + current_password: '', + new_password: '', + confirm_password: '' + }); + setPasswordErrors({}); + showMessage('Contraseña cambiada exitosamente', 'success'); + + } catch (error) { + console.error('Error changing password:', error); + showMessage(error.message || 'Error al cambiar la contraseña', 'error'); + } finally { + setChangingPassword(false); + } + }; + // Guardar cambios del perfil const handleSaveProfile = async () => { if (!currentUser) return; @@ -642,9 +734,19 @@ const Settings = () => { + {passwordErrors.current_password && ( +

{passwordErrors.current_password}

+ )}
@@ -653,9 +755,19 @@ const Settings = () => { + {passwordErrors.new_password && ( +

{passwordErrors.new_password}

+ )}
@@ -664,28 +776,52 @@ const Settings = () => { + {passwordErrors.confirm_password && ( +

{passwordErrors.confirm_password}

+ )}
{/* Información de seguridad adicional */}
-

Consejos de seguridad

+

Requisitos de contraseña

@@ -694,7 +830,7 @@ const Settings = () => {

- Usa al menos 8 caracteres con una combinación de letras, números y símbolos + Mínimo 8 caracteres de longitud

@@ -704,7 +840,7 @@ const Settings = () => {

- Evita usar información personal como nombres o fechas de nacimiento + Al menos una letra minúscula (a-z)

@@ -714,9 +850,55 @@ const Settings = () => {

- No reutilices contraseñas de otras cuentas + Al menos una letra mayúscula (A-Z)

+
+
+ + + +
+

+ Al menos un número (0-9) +

+
+ + +
+
Consejos adicionales
+
+
+
+ + + +
+

+ Evita usar información personal como nombres o fechas de nacimiento +

+
+
+
+ + + +
+

+ No reutilices contraseñas de otras cuentas +

+
+
+
+ + + +
+

+ Considera usar símbolos especiales para mayor seguridad +

+
+
diff --git a/src/pages/Users.jsx b/src/pages/Users.jsx index 04be22d..7347a9e 100644 --- a/src/pages/Users.jsx +++ b/src/pages/Users.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { fetchUsers, createUser, updateUser, deleteUser } from '../api/users.ts'; +import { fetchUsers, createUser, updateUser, deleteUser, getCurrentUser } from '../api/users.ts'; import { useNotification } from '../context/NotificationContext'; const initialForm = { @@ -44,33 +44,24 @@ export default function Users() { const [itemsPerPage, setItemsPerPage] = useState(10); const { showMessage } = useNotification(); - const token = localStorage.getItem('access'); - const loadUsers = () => { setLoading(true); - fetchUsers(token) + fetchUsers() .then(data => { setUsers(data); setLoading(false); }) .catch(err => { - if (err.message === 'SESSION_EXPIRED') { - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - } else { - setError(err.message); - } + console.error('Error loading users:', err); + setError('Error al cargar usuarios'); setLoading(false); }); }; useEffect(() => { // Si no hay token, limpiar y redirigir a login inmediatamente - if (!token) { + const accessToken = localStorage.getItem('access'); + if (!accessToken) { localStorage.removeItem('access'); localStorage.removeItem('refresh'); localStorage.removeItem('username'); @@ -86,27 +77,7 @@ export default function Users() { loadUsers(); // Siempre sincroniza la información del usuario autenticado en localStorage - fetch(`${import.meta.env.VITE_EFC_API_URL}/user/users/me/`, { - headers: { 'Authorization': `Bearer ${token}` } - }) - .then(async res => { - if (!res.ok) { - // Token inválido o expirado, limpiar localStorage y redirigir a login - console.log('Token inválido o expirado'); - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - localStorage.removeItem('username'); - localStorage.removeItem('user_email'); - localStorage.removeItem('user_id'); - localStorage.removeItem('user_groups'); - localStorage.removeItem('user_first_name'); - localStorage.removeItem('user_last_name'); - localStorage.removeItem('user_is_importador'); - window.location.href = '/login'; - return null; - } - return res.json(); - }) + getCurrentUser() .then(data => { console.log('Respuesta de /api/users/me/:', data); if (data && data.username) { @@ -146,13 +117,13 @@ export default function Users() { setSubmitting(true); try { if (editingId) { - await updateUser(token, editingId, form); + await updateUser(editingId, form); showMessage('Usuario actualizado exitosamente', 'success'); setShowEditModal(false); } else { const groups = createType === 'importador' ? [3, 5] : [4, 3]; const extra = createType === 'importador' ? { is_importador: true } : {}; - await createUser(token, { ...form, groups, ...extra }); + await createUser({ ...form, groups, ...extra }); showMessage(createType === 'importador' ? 'Importador creado exitosamente' : 'Usuario creado exitosamente', 'success'); setShowCreateModal(false); } @@ -170,7 +141,7 @@ export default function Users() { if (!userToDelete) return; setSubmitting(true); try { - await deleteUser(token, userToDelete.id); + await deleteUser(userToDelete.id); showMessage('Usuario eliminado exitosamente', 'success'); setShowDeleteModal(false); setUserToDelete(null); diff --git a/src/pages/UsersNew.jsx b/src/pages/UsersNew.jsx index f72bab9..392ff6c 100644 --- a/src/pages/UsersNew.jsx +++ b/src/pages/UsersNew.jsx @@ -24,26 +24,16 @@ export default function Users() { const [searchTerm, setSearchTerm] = useState(''); const { showMessage } = useNotification(); - const token = localStorage.getItem('access'); - const loadUsers = () => { setLoading(true); - fetchUsers(token) + fetchUsers() .then(data => { setUsers(data); setLoading(false); }) .catch(err => { - if (err.message === 'SESSION_EXPIRED') { - localStorage.removeItem('access'); - localStorage.removeItem('refresh'); - showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 2000); - } else { - setError(err.message); - } + console.error('Error loading users:', err); + setError('Error al cargar usuarios'); setLoading(false); }); }; @@ -62,11 +52,11 @@ export default function Users() { setSubmitting(true); try { if (editingId) { - await updateUser(token, editingId, form); + await updateUser(editingId, form); showMessage('Usuario actualizado exitosamente', 'success'); setShowEditModal(false); } else { - await createUser(token, form); + await createUser(form); showMessage('Usuario creado exitosamente', 'success'); setShowCreateModal(false); } @@ -101,7 +91,7 @@ export default function Users() { if (!userToDelete) return; setSubmitting(true); try { - await deleteUser(token, userToDelete.id); + await deleteUser(userToDelete.id); showMessage('Usuario eliminado exitosamente', 'success'); setShowDeleteModal(false); setUserToDelete(null); diff --git a/src/pages/Vucem.jsx b/src/pages/Vucem.jsx index bee9261..047e0a4 100644 --- a/src/pages/Vucem.jsx +++ b/src/pages/Vucem.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth } from '../fetchWithAuth'; const API_URL = import.meta.env.VITE_EFC_API_URL; export default function Vucem() { @@ -118,10 +119,7 @@ export default function Vucem() { const fetchVucem = async () => { setLoading(true); try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/vucem/vucem/`, { - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - }); + const res = await fetchWithAuth(`${API_URL}/vucem/vucem/`); if (!res.ok) throw new Error('Error al cargar VUCEM'); const data = await res.json(); setVucemList(data); @@ -135,10 +133,7 @@ export default function Vucem() { // Funciones de descarga const downloadCertificate = async (id, usuario) => { try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/vucem/vucem/${id}/download_cer/`, { - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - }); + const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_cer/`); if (!res.ok) { if (res.status === 404) { @@ -171,10 +166,7 @@ export default function Vucem() { const downloadKey = async (id, usuario) => { try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/vucem/vucem/${id}/download_key/`, { - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - }); + const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_key/`); if (!res.ok) { if (res.status === 404) { @@ -939,12 +931,7 @@ export default function Vucem() { formData.append('acuseedocument', form.acuseedocument); formData.append('is_active', form.is_active); try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/vucem/vucem/`, { - method: 'POST', - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - body: formData, - }); + const res = await postWithAuth(`${API_URL}/vucem/vucem/`, formData); if (!res.ok) throw new Error('Error al crear VUCEM'); await fetchVucem(); closeModals(); @@ -1108,12 +1095,7 @@ export default function Vucem() { formData.append('acuseedocument', form.acuseedocument); formData.append('is_active', form.is_active); try { - const token = localStorage.getItem('access'); - const res = await fetch(`${API_URL}/vucem/vucem/${editVucem.id}/`, { - method: 'PATCH', - headers: token ? { 'Authorization': `Bearer ${token}` } : {}, - body: formData, - }); + const res = await putWithAuth(`${API_URL}/vucem/vucem/${editVucem.id}/`, formData); if (!res.ok) throw new Error('Error al actualizar VUCEM'); await fetchVucem(); closeModals(); @@ -1273,11 +1255,7 @@ export default function Vucem() {