implementar visualizacion de logs y seteo de usuario administrador
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
const auditService = require('../modules/audit/audit.service');
|
||||||
const { sendError } = require('../utils/apiResponse');
|
const { sendError } = require('../utils/apiResponse');
|
||||||
|
|
||||||
const errorHandler = (err, req, res, next) => {
|
const errorHandler = (err, req, res, next) => {
|
||||||
@@ -14,6 +15,23 @@ const errorHandler = (err, req, res, next) => {
|
|||||||
? (err.message || 'Request error')
|
? (err.message || 'Request error')
|
||||||
: (statusCode >= 500 ? 'Internal server error' : err.message || 'Request error');
|
: (statusCode >= 500 ? 'Internal server error' : err.message || 'Request error');
|
||||||
|
|
||||||
|
if (req.path !== '/health' && req.path !== '/api/health') {
|
||||||
|
auditService.logEvent({
|
||||||
|
type: 'PROCESS_ERROR',
|
||||||
|
userId: req.user?.id || null,
|
||||||
|
email: req.user?.email || req.body?.email || null,
|
||||||
|
ip: req.ip?.replace?.('::ffff:', '') || null,
|
||||||
|
userAgent: req.headers['user-agent'] || null,
|
||||||
|
metadata: {
|
||||||
|
method: req.method,
|
||||||
|
path: req.originalUrl || req.path,
|
||||||
|
statusCode,
|
||||||
|
message: err.message || null,
|
||||||
|
code: err.code || null,
|
||||||
|
},
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
if (err.code) {
|
if (err.code) {
|
||||||
return res.status(statusCode).json({
|
return res.status(statusCode).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const auditService = require('./audit.service');
|
||||||
|
const { sendSuccess } = require('../../utils/apiResponse');
|
||||||
|
|
||||||
|
const listMine = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const result = await auditService.getEvents({
|
||||||
|
userId: req.user.id,
|
||||||
|
role: req.user.role,
|
||||||
|
page: req.query.page,
|
||||||
|
limit: req.query.limit,
|
||||||
|
type: req.query.type,
|
||||||
|
});
|
||||||
|
return sendSuccess(res, result, 200);
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const purgeMine = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!req.body?.certPassword) {
|
||||||
|
const error = new Error('Debes ingresar la contraseña de la firma registrada.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const result = await auditService.purgeUserEvents({
|
||||||
|
actorUserId: req.user.id,
|
||||||
|
actorEmail: req.user.email,
|
||||||
|
actorRole: req.user.role,
|
||||||
|
certPassword: req.body.certPassword,
|
||||||
|
ip: req.ip?.replace('::ffff:', '') || null,
|
||||||
|
userAgent: req.headers['user-agent'] || null,
|
||||||
|
});
|
||||||
|
return sendSuccess(res, result, 200);
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listMine,
|
||||||
|
purgeMine,
|
||||||
|
};
|
||||||
@@ -30,15 +30,39 @@ const saveEvent = async (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEvents = async ({ type, userId, limit = 100, offset = 0 } = {}) => {
|
const getEvents = async ({ type, userId, limit = 100, offset = 0, types = [] } = {}) => {
|
||||||
let sql = 'SELECT * FROM audit_logs WHERE 1=1';
|
let sql = 'SELECT * FROM audit_logs WHERE 1=1';
|
||||||
|
let countSql = 'SELECT COUNT(*) AS total FROM audit_logs WHERE 1=1';
|
||||||
const params = [];
|
const params = [];
|
||||||
|
const countParams = [];
|
||||||
if (type) { sql += ' AND type = ?'; params.push(type); }
|
if (type) { sql += ' AND type = ?'; params.push(type); }
|
||||||
if (userId) { sql += ' AND user_id = ?'; params.push(userId); }
|
if (type) { countSql += ' AND type = ?'; countParams.push(type); }
|
||||||
|
if (types.length > 0) {
|
||||||
|
const placeholders = types.map(() => '?').join(', ');
|
||||||
|
sql += ` AND type IN (${placeholders})`;
|
||||||
|
countSql += ` AND type IN (${placeholders})`;
|
||||||
|
params.push(...types);
|
||||||
|
countParams.push(...types);
|
||||||
|
}
|
||||||
|
if (userId) {
|
||||||
|
sql += ' AND user_id = ?';
|
||||||
|
countSql += ' AND user_id = ?';
|
||||||
|
params.push(userId);
|
||||||
|
countParams.push(userId);
|
||||||
|
}
|
||||||
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||||
params.push(limit, offset);
|
params.push(limit, offset);
|
||||||
const [rows] = await db.execute(sql, params);
|
const [rows] = await db.execute(sql, params);
|
||||||
return rows;
|
const [countRows] = await db.execute(countSql, countParams);
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
total: countRows[0]?.total || 0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { saveEvent, getEvents };
|
const deleteEventsByUserId = async (userId) => {
|
||||||
|
const [result] = await db.execute('DELETE FROM audit_logs WHERE user_id = ?', [userId]);
|
||||||
|
return result.affectedRows || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { saveEvent, getEvents, deleteEventsByUserId };
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const authenticate = require('../../middlewares/authenticate');
|
||||||
|
const auditController = require('./audit.controller');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/mine', authenticate, auditController.listMine);
|
||||||
|
router.post('/mine/purge', authenticate, auditController.purgeMine);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,11 +1,74 @@
|
|||||||
const auditRepository = require('./audit.repository');
|
const auditRepository = require('./audit.repository');
|
||||||
|
const certificatesService = require('../certificates/certificates.service');
|
||||||
|
|
||||||
const logEvent = async ({ type, userId = null, email = null, ip = null, userAgent = null, metadata = null } = {}) => {
|
const logEvent = async ({ type, userId = null, email = null, ip = null, userAgent = null, metadata = null } = {}) => {
|
||||||
await auditRepository.saveEvent({ type, userId, email, ip, userAgent, metadata });
|
await auditRepository.saveEvent({ type, userId, email, ip, userAgent, metadata });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEvents = async (filters) => {
|
const ADMIN_VISIBLE_TYPES = ['AUDIT_LOGS_PURGED'];
|
||||||
return auditRepository.getEvents(filters);
|
|
||||||
|
const getEvents = async (filters = {}) => {
|
||||||
|
const limit = Math.min(Math.max(Number(filters.limit) || 15, 1), 50);
|
||||||
|
const page = Math.max(Number(filters.page) || 1, 1);
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
const types = filters.role === 'admin' ? ADMIN_VISIBLE_TYPES : [];
|
||||||
|
|
||||||
|
const result = await auditRepository.getEvents({
|
||||||
|
type: filters.type || null,
|
||||||
|
userId: filters.role === 'admin' ? null : (filters.userId || null),
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
types,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: result.rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
||||||
|
})),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total: result.total,
|
||||||
|
totalPages: Math.max(1, Math.ceil(result.total / limit)),
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { logEvent, getEvents };
|
const purgeUserEvents = async ({ actorUserId, actorEmail, actorRole, certPassword, ip, userAgent }) => {
|
||||||
|
if (actorRole === 'admin') {
|
||||||
|
const error = new Error('La cuenta administradora no puede eliminar logs.');
|
||||||
|
error.statusCode = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeCertificate = await certificatesService.getLatestActiveForUser(actorUserId);
|
||||||
|
if (!activeCertificate) {
|
||||||
|
const error = new Error('No existe un certificado activo asociado a esta cuenta.');
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificatesService.loadForSigning(activeCertificate.id, actorUserId, certPassword);
|
||||||
|
|
||||||
|
const deletedCount = await auditRepository.deleteEventsByUserId(actorUserId);
|
||||||
|
|
||||||
|
await auditRepository.saveEvent({
|
||||||
|
type: 'AUDIT_LOGS_PURGED',
|
||||||
|
userId: null,
|
||||||
|
email: actorEmail,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
metadata: {
|
||||||
|
purgedUserId: actorUserId,
|
||||||
|
purgedEmail: actorEmail,
|
||||||
|
deletedCount,
|
||||||
|
certificateId: activeCertificate.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { deletedCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { logEvent, getEvents, purgeUserEvents };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const authRoutes = require('../modules/auth/auth.routes');
|
const authRoutes = require('../modules/auth/auth.routes');
|
||||||
|
const auditRoutes = require('../modules/audit/audit.routes');
|
||||||
const certificatesRoutes = require('../modules/certificates/certificates.routes');
|
const certificatesRoutes = require('../modules/certificates/certificates.routes');
|
||||||
const downloadsRoutes = require('../modules/downloads/downloads.routes');
|
const downloadsRoutes = require('../modules/downloads/downloads.routes');
|
||||||
const documentsRoutes = require('../modules/documents/documents.routes');
|
const documentsRoutes = require('../modules/documents/documents.routes');
|
||||||
@@ -11,6 +12,7 @@ const healthRoutes = require('../modules/health/health.routes');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use('/auth', authRoutes);
|
router.use('/auth', authRoutes);
|
||||||
|
router.use('/audit', auditRoutes);
|
||||||
router.use('/certificates', certificatesRoutes);
|
router.use('/certificates', certificatesRoutes);
|
||||||
router.use('/downloads', downloadsRoutes);
|
router.use('/downloads', downloadsRoutes);
|
||||||
router.use('/documents', documentsRoutes);
|
router.use('/documents', documentsRoutes);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SignatureStylePage from './pages/signature-style/SignatureStylePage';
|
|||||||
import SignaturesPage from './pages/signatures/SignaturesPage';
|
import SignaturesPage from './pages/signatures/SignaturesPage';
|
||||||
import VerifySignaturePage from './pages/verify/VerifySignaturePage';
|
import VerifySignaturePage from './pages/verify/VerifySignaturePage';
|
||||||
import LegalPage from './pages/legal/LegalPage';
|
import LegalPage from './pages/legal/LegalPage';
|
||||||
|
import AuditPage from './pages/audit/AuditPage';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -53,6 +54,10 @@ export default function App() {
|
|||||||
path="/legal"
|
path="/legal"
|
||||||
element={<PrivateRoute><Layout><LegalPage /></Layout></PrivateRoute>}
|
element={<PrivateRoute><Layout><LegalPage /></Layout></PrivateRoute>}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/audit"
|
||||||
|
element={<PrivateRoute><Layout><AuditPage /></Layout></PrivateRoute>}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
export const listMyAuditEvents = ({ page = 1, limit = 15, type } = {}) => {
|
||||||
|
const params = { page, limit };
|
||||||
|
if (type) params.type = type;
|
||||||
|
return client.get('/audit/mine', { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const purgeMyAuditEvents = (certPassword) =>
|
||||||
|
client.post('/audit/mine/purge', { certPassword });
|
||||||
@@ -18,6 +18,7 @@ const navSections = [
|
|||||||
label: 'Sistema',
|
label: 'Sistema',
|
||||||
items: [
|
items: [
|
||||||
{ to: '/signature-style', icon: Settings, label: 'Configuración' },
|
{ to: '/signature-style', icon: Settings, label: 'Configuración' },
|
||||||
|
{ to: '/audit', icon: ScrollText, label: 'Auditoría' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -29,6 +30,7 @@ const topbarTitles = {
|
|||||||
'/signature-style': 'Configuración',
|
'/signature-style': 'Configuración',
|
||||||
'/signatures': 'Firmar ahora',
|
'/signatures': 'Firmar ahora',
|
||||||
'/legal': 'Legal',
|
'/legal': 'Legal',
|
||||||
|
'/audit': 'Auditoría',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
@@ -113,12 +115,8 @@ export default function Layout({ children }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="sidebar-group sidebar-group--disabled">
|
<div className="sidebar-group">
|
||||||
<div className="nav-group-label">Extra</div>
|
<div className="nav-group-label">Extra</div>
|
||||||
<span className="nav-item nav-item--static">
|
|
||||||
<ScrollText size={15} />
|
|
||||||
<span>Auditoría</span>
|
|
||||||
</span>
|
|
||||||
<Link
|
<Link
|
||||||
to="/legal"
|
to="/legal"
|
||||||
className={`nav-item ${location.pathname.startsWith('/legal') ? 'active' : ''}`}
|
className={`nav-item ${location.pathname.startsWith('/legal') ? 'active' : ''}`}
|
||||||
|
|||||||
@@ -610,6 +610,63 @@ select {
|
|||||||
background: var(--gray-50);
|
background: var(--gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audit-toolbar,
|
||||||
|
.audit-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-toolbar-group select {
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-table-header,
|
||||||
|
.audit-table-row {
|
||||||
|
grid-template-columns: minmax(220px, 1.2fr) 180px 120px minmax(260px, 1.5fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-event-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-event-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--c-info-bg);
|
||||||
|
color: var(--c-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-event-icon.is-error {
|
||||||
|
background: var(--c-danger-bg);
|
||||||
|
color: var(--c-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-detail-cell {
|
||||||
|
color: var(--gray-600);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Buttons ────────────────────────────────────────────────── */
|
/* ─── Buttons ────────────────────────────────────────────────── */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex; align-items: center; gap: 6px;
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
|||||||
@@ -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