implementar visualizacion de logs y seteo de usuario administrador
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AlertCircle, CheckCircle2, ChevronLeft, ChevronRight, FileSignature, LogIn, ShieldAlert, Trash2, UserPlus2, X } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { listMyAuditEvents, purgeMyAuditEvents } from '../../api/audit';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 15, 30, 50];
|
||||
|
||||
const EVENT_LABELS = {
|
||||
USER_REGISTERED: 'Registro inicial',
|
||||
LOGIN_SUCCESS: 'Acceso correcto',
|
||||
LOGIN_FAILED: 'Error de acceso',
|
||||
VERIFICATION_EMAIL_RESENT: 'Reenvío de validación',
|
||||
PASSWORD_CHANGE_REQUESTED: 'Solicitud de cambio',
|
||||
PASSWORD_CHANGED: 'Cambio correcto',
|
||||
ACCOUNT_UNLOCK_PASSWORD_CHANGE_REQUESTED: 'Desbloqueo solicitado',
|
||||
LEGAL_PACKAGE_SIGNED: 'Documentos legales firmados',
|
||||
DOCUMENT_UPLOAD: 'Documento cargado',
|
||||
CERTIFICATE_UPLOAD: 'Certificado cargado',
|
||||
CERTIFICATE_DELETE: 'Certificado eliminado',
|
||||
PROCESS_ERROR: 'Error de proceso',
|
||||
};
|
||||
|
||||
const EVENT_ICONS = {
|
||||
USER_REGISTERED: UserPlus2,
|
||||
LOGIN_SUCCESS: LogIn,
|
||||
LOGIN_FAILED: ShieldAlert,
|
||||
PROCESS_ERROR: AlertCircle,
|
||||
LEGAL_PACKAGE_SIGNED: FileSignature,
|
||||
};
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString('es-EC');
|
||||
};
|
||||
|
||||
const renderMetadata = (metadata) => {
|
||||
if (!metadata) return '—';
|
||||
if (metadata.documents?.length) {
|
||||
return metadata.documents.map((item) => item.title || item.slug).join(', ');
|
||||
}
|
||||
if (metadata.path || metadata.method || metadata.message) {
|
||||
return [metadata.method, metadata.path, metadata.message].filter(Boolean).join(' · ');
|
||||
}
|
||||
if (metadata.alias || metadata.subject) {
|
||||
return [metadata.alias, metadata.subject].filter(Boolean).join(' · ');
|
||||
}
|
||||
return Object.entries(metadata)
|
||||
.slice(0, 3)
|
||||
.map(([key, value]) => `${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`)
|
||||
.join(' · ');
|
||||
};
|
||||
|
||||
export default function AuditPage() {
|
||||
const { user } = useAuth();
|
||||
const [items, setItems] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [limit, setLimit] = useState(15);
|
||||
const [pagination, setPagination] = useState({ page: 1, totalPages: 1, total: 0 });
|
||||
const [showPurgeModal, setShowPurgeModal] = useState(false);
|
||||
const [certPassword, setCertPassword] = useState('');
|
||||
const [purging, setPurging] = useState(false);
|
||||
|
||||
const loadEvents = () => {
|
||||
setLoading(true);
|
||||
return listMyAuditEvents({ page, limit })
|
||||
.then(({ data }) => {
|
||||
setItems(data.data.rows || []);
|
||||
setPagination(data.data.pagination || { page: 1, totalPages: 1, total: 0 });
|
||||
})
|
||||
.catch(() => toast.error('No se pudieron cargar tus logs'))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadEvents();
|
||||
}, [page, limit]);
|
||||
|
||||
const handlePurgeLogs = async (event) => {
|
||||
event.preventDefault();
|
||||
setPurging(true);
|
||||
try {
|
||||
const { data } = await purgeMyAuditEvents(certPassword);
|
||||
toast.success(`Se eliminaron ${data.data.deletedCount} logs de tu cuenta.`);
|
||||
setCertPassword('');
|
||||
setShowPurgeModal(false);
|
||||
setPage(1);
|
||||
await loadEvents();
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.error || 'No se pudieron eliminar tus logs.');
|
||||
} finally {
|
||||
setPurging(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
{showPurgeModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowPurgeModal(false)}>
|
||||
<div className="modal-card" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title">
|
||||
<Trash2 size={18} />
|
||||
<span>Eliminar mis logs</span>
|
||||
</div>
|
||||
<button type="button" className="btn-icon" onClick={() => setShowPurgeModal(false)}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="modal-desc">
|
||||
Para eliminar tus logs debes validar la contraseña de tu firma registrada. Esta acción dejará un rastro mínimo visible solo para administración.
|
||||
</p>
|
||||
<form onSubmit={handlePurgeLogs} className="auth-form">
|
||||
<div className="field">
|
||||
<label>Contraseña de la firma</label>
|
||||
<input
|
||||
type="password"
|
||||
value={certPassword}
|
||||
onChange={(event) => setCertPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowPurgeModal(false)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={purging}>
|
||||
{purging ? 'Eliminando...' : 'Eliminar logs'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h2>Auditoría</h2>
|
||||
<p>
|
||||
{user?.role === 'admin'
|
||||
? 'Aquí se conservan únicamente los rastros administrativos de purga de logs.'
|
||||
: 'Tus eventos del sistema, del más nuevo al más antiguo, con IP, fecha y detalle técnico relevante.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-card audit-toolbar">
|
||||
<div className="audit-toolbar-group">
|
||||
<strong>Total de eventos:</strong>
|
||||
<span>{pagination.total}</span>
|
||||
</div>
|
||||
<div className="audit-toolbar-group">
|
||||
<label htmlFor="audit-limit">Mostrar</label>
|
||||
<select
|
||||
id="audit-limit"
|
||||
value={limit}
|
||||
onChange={(event) => {
|
||||
setPage(1);
|
||||
setLimit(Number(event.target.value));
|
||||
}}
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{user?.role !== 'admin' && (
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowPurgeModal(true)}>
|
||||
<Trash2 size={14} />
|
||||
Eliminar mis logs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="table-header audit-table-header">
|
||||
<span className="th">Evento</span>
|
||||
<span className="th">Fecha</span>
|
||||
<span className="th">IP</span>
|
||||
<span className="th">Detalle</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading-area"><div className="spinner" /></div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<CheckCircle2 size={44} />
|
||||
<p>Aún no hay eventos de auditoría para mostrar.</p>
|
||||
</div>
|
||||
) : (
|
||||
items.map((item) => {
|
||||
const Icon = EVENT_ICONS[item.type] || CheckCircle2;
|
||||
return (
|
||||
<div key={item.id} className="table-row audit-table-row">
|
||||
<div className="audit-event-cell">
|
||||
<div className={`audit-event-icon ${item.type === 'PROCESS_ERROR' || item.type === 'LOGIN_FAILED' ? 'is-error' : ''}`}>
|
||||
<Icon size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="doc-n">{EVENT_LABELS[item.type] || item.type}</div>
|
||||
<div className="doc-m">{item.type}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="td">{formatDateTime(item.created_at)}</span>
|
||||
<span className="td">{item.ip || '—'}</span>
|
||||
<span className="td audit-detail-cell">{renderMetadata(item.metadata)}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-card audit-pagination">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((current) => Math.max(1, current - 1))}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
Anterior
|
||||
</button>
|
||||
<span>Página {pagination.page} de {pagination.totalPages}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={page >= pagination.totalPages}
|
||||
onClick={() => setPage((current) => Math.min(pagination.totalPages, current + 1))}
|
||||
>
|
||||
Siguiente
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user