Se soluciono autenticacion

This commit is contained in:
2025-08-05 13:06:24 -06:00
parent c280afe646
commit c9df4e3ab2
21 changed files with 758 additions and 624 deletions

View File

@@ -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<PedimentoDocumentsResponse> {
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();
}

View File

@@ -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<DocumentsResponse> {
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<DocumentsResponse> {
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();
}

View File

@@ -1,15 +1,9 @@
import { fetchWithAuth, putWithAuth } from '../fetchWithAuth';
// PUT para marcar una notificación como vista
export async function marcarNotificacionComoVista(id: number): Promise<Notificacion> {
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<NotificacionesResponse> {
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<NotificacionesResponse>{
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();
}

View File

@@ -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<OrganizationUsage> {
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<OrganizationUsage> {
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');
}

View File

@@ -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<PedimentoDocumentsResponse> {
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();
}

View File

@@ -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<string, any> = {}
): Promise<ProcesamientoPedimentosResponse> {
const API_URL = import.meta.env.VITE_EFC_API_URL;
const headers: Record<string, string> = {};
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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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(
<StrictMode>
isDevelopment ? (
<NotificationProvider>
<App />
</NotificationProvider>
</StrictMode>,
) : (
<StrictMode>
<NotificationProvider>
<App />
</NotificationProvider>
</StrictMode>
),
)

View File

@@ -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);

View File

@@ -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,

View File

@@ -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

View File

@@ -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]);

View File

@@ -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);
}
};

View File

@@ -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);
}

View File

@@ -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 = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="current_password"
value={passwordData.current_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.current_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa tu contraseña actual"
/>
{passwordErrors.current_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.current_password}</p>
)}
</div>
<div className="space-y-2">
@@ -653,9 +755,19 @@ const Settings = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="new_password"
value={passwordData.new_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.new_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Ingresa una nueva contraseña"
/>
{passwordErrors.new_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.new_password}</p>
)}
</div>
<div className="space-y-2">
@@ -664,28 +776,52 @@ const Settings = () => {
</label>
<input
type="password"
className="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm transition-all duration-200 hover:border-gray-400"
name="confirm_password"
value={passwordData.confirm_password}
onChange={handlePasswordChange}
className={`w-full px-4 py-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2 text-sm transition-all duration-200 hover:border-gray-400 ${
passwordErrors.confirm_password
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
}`}
placeholder="Confirma tu nueva contraseña"
/>
{passwordErrors.confirm_password && (
<p className="mt-1 text-sm text-red-600">{passwordErrors.confirm_password}</p>
)}
</div>
</div>
<div className="mt-6">
<button
type="button"
className="inline-flex items-center px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"
onClick={handleChangePassword}
disabled={changingPassword}
className="inline-flex items-center px-6 py-3 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Actualizar contraseña
{changingPassword ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Cambiando contraseña...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Actualizar contraseña
</>
)}
</button>
</div>
</div>
{/* Información de seguridad adicional */}
<div className="bg-gray-50 rounded-xl p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4">Consejos de seguridad</h4>
<h4 className="text-lg font-semibold text-gray-900 mb-4">Requisitos de contraseña</h4>
<div className="space-y-3">
<div className="flex items-start">
<div className="flex-shrink-0">
@@ -694,7 +830,7 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Usa al menos 8 caracteres con una combinación de letras, números y símbolos
Mínimo 8 caracteres de longitud
</p>
</div>
<div className="flex items-start">
@@ -704,7 +840,7 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Evita usar información personal como nombres o fechas de nacimiento
Al menos una letra minúscula (a-z)
</p>
</div>
<div className="flex items-start">
@@ -714,9 +850,55 @@ const Settings = () => {
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
No reutilices contraseñas de otras cuentas
Al menos una letra mayúscula (A-Z)
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-600">
Al menos un número (0-9)
</p>
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-200">
<h5 className="text-sm font-semibold text-gray-900 mb-2">Consejos adicionales</h5>
<div className="space-y-2">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Evita usar información personal como nombres o fechas de nacimiento
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
No reutilices contraseñas de otras cuentas
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="ml-2 text-xs text-gray-600">
Considera usar símbolos especiales para mayor seguridad
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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() {
<button type="button" onClick={async () => {
if (!deleteVucem) return;
try {
const token = localStorage.getItem('access');
const res = await fetch(`${API_URL}/vucem/vucem/${deleteVucem.id}/`, {
method: 'DELETE',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
});
const res = await deleteWithAuth(`${API_URL}/vucem/vucem/${deleteVucem.id}/`);
if (!res.ok) throw new Error('Error al eliminar VUCEM');
await fetchVucem();
closeModals();