185 lines
6.7 KiB
TypeScript
185 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Header from '@/components/Header';
|
|
import { useLocale } from '@/lib/i18n/LocaleContext';
|
|
import { useUser } from '@/lib/auth/UserContext';
|
|
import styles from './page.module.css';
|
|
|
|
interface AuditLog {
|
|
id: number;
|
|
userId: number;
|
|
action: 'CREATE' | 'UPDATE' | 'DELETE' | 'APPROVE' | 'REJECT' | 'LOGIN';
|
|
targetTable: string;
|
|
targetId: number;
|
|
previousValue: any;
|
|
newValue: any;
|
|
ipAddress: string | null;
|
|
createdAt: string;
|
|
user: {
|
|
username: string;
|
|
email: string;
|
|
role: string;
|
|
};
|
|
}
|
|
|
|
export default function AuditLogsPage() {
|
|
const router = useRouter();
|
|
const { t, locale } = useLocale();
|
|
const { role, isLoading: authLoading } = useUser();
|
|
const [logs, setLogs] = useState<AuditLog[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
|
|
|
const fetchLogs = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await fetch('/api/audit-logs');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setLogs(data.logs || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch audit logs', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && role !== 'admin') {
|
|
router.push('/unauthorized');
|
|
}
|
|
}, [role, authLoading, router]);
|
|
|
|
useEffect(() => {
|
|
if (role === 'admin') {
|
|
fetchLogs();
|
|
}
|
|
}, [role]);
|
|
|
|
const handleRowClick = (log: AuditLog) => {
|
|
setSelectedLog(log);
|
|
};
|
|
|
|
if (authLoading || (role !== 'admin' && role !== null)) {
|
|
return <div className={styles.loading}>Verificando credenciales...</div>;
|
|
}
|
|
|
|
if (role !== 'admin') {
|
|
return null; // Redirecting...
|
|
}
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<Header activeTab="audit" />
|
|
<main className={styles.main} id="audit-logs-main-view">
|
|
<div className={styles.titleSection}>
|
|
<h1 className={styles.title}>{t('audit.title')}</h1>
|
|
</div>
|
|
|
|
<div className={styles.layoutGrid}>
|
|
{/* Audit Logs Table */}
|
|
<div className={styles.tableContainer}>
|
|
{isLoading ? (
|
|
<div className={styles.loading}>Cargando registros...</div>
|
|
) : logs.length === 0 ? (
|
|
<div className={styles.emptyState}>{t('audit.empty')}</div>
|
|
) : (
|
|
<table className={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
<th className={styles.th}>{t('audit.timestamp')}</th>
|
|
<th className={styles.th}>{t('audit.actor')}</th>
|
|
<th className={styles.th}>{t('audit.action')}</th>
|
|
<th className={styles.th}>{t('audit.target_table')}</th>
|
|
<th className={styles.th}>ID</th>
|
|
<th className={styles.th}>{t('audit.ip_address')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map((log) => (
|
|
<tr
|
|
key={log.id}
|
|
onClick={() => handleRowClick(log)}
|
|
className={`${styles.tr} ${selectedLog?.id === log.id ? styles.trSelected : ''}`}
|
|
data-audit-id={log.id}
|
|
>
|
|
<td className={styles.td}>
|
|
{new Date(log.createdAt).toLocaleString(locale === 'es' ? 'es-CO' : 'en-US')}
|
|
</td>
|
|
<td className={styles.td}>
|
|
<div><strong>{log.user.username}</strong></div>
|
|
<div style={{ fontSize: '0.75rem', opacity: 0.7 }}>{log.user.role}</div>
|
|
</td>
|
|
<td className={styles.td}>
|
|
<span className={`${styles.badge} ${styles['badge' + log.action]}`}>
|
|
{log.action}
|
|
</span>
|
|
</td>
|
|
<td className={styles.td}>{log.targetTable}</td>
|
|
<td className={styles.td}>{log.targetId}</td>
|
|
<td className={styles.td}>{log.ipAddress || '-'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
|
|
{/* Details Side Panel */}
|
|
<div className={styles.detailsPanel}>
|
|
<h3 className={styles.panelTitle}>{t('audit.details')}</h3>
|
|
{selectedLog ? (
|
|
<div>
|
|
<div className={styles.panelHeaderInfo}>
|
|
<div><strong>ID:</strong> {selectedLog.id}</div>
|
|
<div><strong>Actor:</strong> {selectedLog.user.username} ({selectedLog.user.email})</div>
|
|
<div><strong>Acción:</strong> {selectedLog.action}</div>
|
|
<div><strong>Tabla:</strong> {selectedLog.targetTable} (ID: {selectedLog.targetId})</div>
|
|
<div><strong>Fecha:</strong> {new Date(selectedLog.createdAt).toLocaleString()}</div>
|
|
<div><strong>IP:</strong> {selectedLog.ipAddress || '-'}</div>
|
|
</div>
|
|
|
|
<div className={styles.diffContainer}>
|
|
<h4 style={{ margin: '0 0 10px 0', fontSize: '0.875rem' }}>{t('audit.diff_panel')}</h4>
|
|
|
|
<div className={styles.diffBox}>
|
|
<span className={styles.diffTitle}>{t('audit.previous')}</span>
|
|
{selectedLog.previousValue ? (
|
|
<pre className={styles.jsonPre}>
|
|
{JSON.stringify(selectedLog.previousValue, null, 2)}
|
|
</pre>
|
|
) : (
|
|
<div className={styles.jsonPre} style={{ opacity: 0.5 }}>
|
|
{t('audit.no_diff')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.diffBox}>
|
|
<span className={styles.diffTitle}>{t('audit.new')}</span>
|
|
{selectedLog.newValue ? (
|
|
<pre className={styles.jsonPre}>
|
|
{JSON.stringify(selectedLog.newValue, null, 2)}
|
|
</pre>
|
|
) : (
|
|
<div className={styles.jsonPre} style={{ opacity: 0.5 }}>
|
|
{t('audit.no_diff')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={styles.emptyDetails}>
|
|
Seleccione una fila para ver el JSON Diff
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|