feat(admin): Registro de transacciones — log de auditoría de todo el sistema
NUEVA INFRAESTRUCTURA * SQL 010: tabla audit_log (id, fecha, user_email, user_name, modulo, accion, entity_type, entity_id, entity_label, detalles JSON, ip_addr) con índices por fecha/user/modulo/accion/entity. * src/AuditLog.php: helper estático con dos métodos: - log() lee actor de $_SESSION (uso normal post-auth) - logAs() recibe email/name explícito (para login/logout y CLI) Falla silenciosamente si la DB no responde — no rompe la operación principal por un fallo de logging. HOOKS EN MUTACIONES * Auth.php: registra 'login' al completar OAuth y 'logout' antes de destruir la sesión. * escrituras/api.php: registra create/update/delete con id+nombre. * tramites/api.php: registra 8 mutaciones — create/update/delete del trámite, set_status (con nuevo estatus en detalles), add_obs, add_hours/update_hours/delete_hours (con abogado y horas en detalles), todos referenciados por codigo (ADM-001, etc.). * admin/api.php: registra create_user/update_user/delete_user, con diff de cambios en update_user. * GETs (list_*) no se loguean — serían ruido. LECTOR * admin/api.php nuevo endpoint GET ?action=list_log que devuelve entradas con filtros from/to/user/modulo/accion/search (LIKE) + catálogos para dropdowns. Límite 500 por query. UI EN PANEL DE CONTROL * admin.html ahora tiene tabs: "Usuarios" (existente) y "Registro de transacciones" (nuevo). * Tab Registro: toolbar de filtros (rango fechas, usuario, módulo, acción, búsqueda libre, atajos Hoy/Semana/Mes/Todo) + tabla con Fecha·Hora · Usuario · Módulo (pill coloreado) · Acción · Entidad con label y tipo · Detalles JSON resumido.
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../src/Auth.php';
|
||||
require_once __DIR__ . '/../../src/Db.php';
|
||||
require_once __DIR__ . '/../../src/Lookups.php';
|
||||
require_once __DIR__ . '/../../src/AuditLog.php';
|
||||
|
||||
Auth::requireSocio();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
@@ -72,6 +73,7 @@ try {
|
||||
$row['id'] = (int)$row['id'];
|
||||
$row['activo'] = (int)$row['activo'];
|
||||
$row['orden'] = (int)$row['orden'];
|
||||
AuditLog::log('admin', 'create_user', 'user', (string)$id, $email, ['role'=>$role, 'name'=>$name]);
|
||||
ok($row);
|
||||
}
|
||||
|
||||
@@ -133,6 +135,11 @@ try {
|
||||
$row['id'] = (int)$row['id'];
|
||||
$row['activo'] = (int)$row['activo'];
|
||||
$row['orden'] = (int)$row['orden'];
|
||||
$diff = [];
|
||||
if ($existing['role'] !== $newRole) $diff['role'] = ['from'=>$existing['role'], 'to'=>$newRole];
|
||||
if ((int)$existing['activo'] !== $newActivo) $diff['activo'] = ['from'=>(int)$existing['activo'], 'to'=>$newActivo];
|
||||
if ($existing['email'] !== $newEmail) $diff['email'] = ['from'=>$existing['email'], 'to'=>$newEmail];
|
||||
AuditLog::log('admin', 'update_user', 'user', (string)$id, $newEmail, $diff ?: null);
|
||||
ok($row);
|
||||
}
|
||||
|
||||
@@ -157,9 +164,54 @@ try {
|
||||
// hours.autor_email) permanecen como referencia muerta — eso es lo deseado para
|
||||
// preservar la trazabilidad de trámites antiguos.
|
||||
$pdo->prepare('DELETE FROM users WHERE id = :id')->execute([':id' => $id]);
|
||||
AuditLog::log('admin', 'delete_user', 'user', (string)$id, $u['email'], ['role'=>$u['role']]);
|
||||
ok(['deleted' => $id, 'email' => $u['email']]);
|
||||
}
|
||||
|
||||
if ($method === 'GET' && $action === 'list_log') {
|
||||
$from = $_GET['from'] ?? '';
|
||||
$to = $_GET['to'] ?? '';
|
||||
$uEmail = trim((string)($_GET['user'] ?? ''));
|
||||
$modulo = trim((string)($_GET['modulo'] ?? ''));
|
||||
$accion = trim((string)($_GET['accion'] ?? ''));
|
||||
$search = trim((string)($_GET['search'] ?? ''));
|
||||
$limit = min(1000, max(50, (int)($_GET['limit'] ?? 500)));
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) { $where[] = 'fecha >= :from'; $params[':from'] = $from . ' 00:00:00'; }
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) { $where[] = 'fecha <= :to'; $params[':to'] = $to . ' 23:59:59'; }
|
||||
if ($uEmail !== '') { $where[] = 'user_email = :ue'; $params[':ue'] = $uEmail; }
|
||||
if ($modulo !== '') { $where[] = 'modulo = :m'; $params[':m'] = $modulo; }
|
||||
if ($accion !== '') { $where[] = 'accion = :a'; $params[':a'] = $accion; }
|
||||
if ($search !== '') {
|
||||
$where[] = '(user_name LIKE :s OR user_email LIKE :s OR entity_label LIKE :s OR detalles LIKE :s)';
|
||||
$params[':s'] = '%' . $search . '%';
|
||||
}
|
||||
$sql = 'SELECT id, fecha, user_email, user_name, modulo, accion, entity_type, entity_id, entity_label, detalles, ip_addr
|
||||
FROM audit_log
|
||||
WHERE ' . implode(' AND ', $where) . '
|
||||
ORDER BY fecha DESC LIMIT ' . $limit;
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
foreach ($rows as &$r) { $r['id'] = (int)$r['id']; }
|
||||
|
||||
// Catálogos para los dropdowns de filtro
|
||||
$modulos = $pdo->query("SELECT DISTINCT modulo FROM audit_log ORDER BY modulo")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$acciones = $pdo->query("SELECT DISTINCT accion FROM audit_log ORDER BY accion")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$usuarios = $pdo->query("SELECT DISTINCT user_email, MAX(user_name) AS user_name
|
||||
FROM audit_log WHERE user_email != ''
|
||||
GROUP BY user_email ORDER BY user_email")->fetchAll();
|
||||
|
||||
ok([
|
||||
'entries' => $rows,
|
||||
'modulos' => $modulos,
|
||||
'acciones' => $acciones,
|
||||
'usuarios' => $usuarios,
|
||||
]);
|
||||
}
|
||||
|
||||
err('Acción no reconocida', 404);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../src/Auth.php';
|
||||
require_once __DIR__ . '/../../src/Db.php';
|
||||
require_once __DIR__ . '/../../src/AuditLog.php';
|
||||
|
||||
if (($_GET['action'] ?? '') === 'logout') {
|
||||
header('Location: /auth/logout.php');
|
||||
@@ -73,7 +74,9 @@ try {
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM escrituras_tramites WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok($row->fetch());
|
||||
$r = $row->fetch();
|
||||
AuditLog::log('escrituras', 'create', 'escritura', (string)$id, $r['tramite'] ?? '');
|
||||
ok($r);
|
||||
}
|
||||
|
||||
if ($method === 'PUT' && $action === 'update') {
|
||||
@@ -148,13 +151,20 @@ try {
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM escrituras_tramites WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok($row->fetch());
|
||||
$r = $row->fetch();
|
||||
AuditLog::log('escrituras', 'update', 'escritura', (string)$id, $r['tramite'] ?? '');
|
||||
ok($r);
|
||||
}
|
||||
|
||||
if ($method === 'DELETE' && $action === 'delete') {
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
if (!$id) err('ID requerido');
|
||||
// Fetch name before delete for the log
|
||||
$nameStmt = $pdo->prepare('SELECT tramite FROM escrituras_tramites WHERE id = :id');
|
||||
$nameStmt->execute([':id' => $id]);
|
||||
$tramiteName = (string)($nameStmt->fetchColumn() ?: '');
|
||||
$pdo->prepare('DELETE FROM escrituras_tramites WHERE id = :id')->execute([':id' => $id]);
|
||||
AuditLog::log('escrituras', 'delete', 'escritura', (string)$id, $tramiteName);
|
||||
ok(['deleted' => $id]);
|
||||
}
|
||||
|
||||
|
||||
+35
-3
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../../src/Auth.php';
|
||||
require_once __DIR__ . '/../../src/Db.php';
|
||||
require_once __DIR__ . '/../../src/AuditLog.php';
|
||||
|
||||
if (($_GET['action'] ?? '') === 'logout') {
|
||||
header('Location: /auth/logout.php');
|
||||
@@ -227,6 +228,8 @@ try {
|
||||
if ($myId > 0 && !in_array($myId, $involIds, true)) $involIds[] = $myId;
|
||||
syncInvolucrados($pdo, $id, $involIds, $user['email']);
|
||||
|
||||
AuditLog::log('tramites', 'create', 'tramite_legal', $codigo, $codigo, ['departamento'=>$dept, 'cliente'=>$b['cliente']??'']);
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok(hydrate($pdo, $row->fetch()));
|
||||
@@ -290,6 +293,8 @@ try {
|
||||
syncInvolucrados($pdo, $id, array_map('intval', $b['involucrados']), $user['email']);
|
||||
}
|
||||
|
||||
AuditLog::log('tramites', 'update', 'tramite_legal', $before['codigo'], $before['codigo']);
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok(hydrate($pdo, $row->fetch()));
|
||||
@@ -303,14 +308,16 @@ try {
|
||||
|
||||
if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403);
|
||||
|
||||
$cur = $pdo->prepare('SELECT departamento FROM tramites_legales WHERE id = :id');
|
||||
$cur = $pdo->prepare('SELECT departamento, codigo FROM tramites_legales WHERE id = :id');
|
||||
$cur->execute([':id' => $id]);
|
||||
$dept = $cur->fetchColumn();
|
||||
if (!$dept) err('Trámite no encontrado', 404);
|
||||
$curRow = $cur->fetch();
|
||||
if (!$curRow) err('Trámite no encontrado', 404);
|
||||
$dept = $curRow['departamento'];
|
||||
if (!in_array($val, STATUS_MAP[$dept], true)) err('Estatus inválido para el departamento');
|
||||
|
||||
$pdo->prepare('UPDATE tramites_legales SET estatus = :s, actualizado_por = :u WHERE id = :id')
|
||||
->execute([':s' => $val, ':u' => $user['email'], ':id' => $id]);
|
||||
AuditLog::log('tramites', 'set_status', 'tramite_legal', $curRow['codigo'], $curRow['codigo'], ['estatus'=>$val]);
|
||||
ok(['id' => $id, 'estatus' => $val]);
|
||||
}
|
||||
|
||||
@@ -330,6 +337,11 @@ try {
|
||||
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
|
||||
->execute([':u' => $user['email'], ':id' => $id]);
|
||||
|
||||
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
|
||||
$codSel->execute([':id' => $id]);
|
||||
$codigo = (string)$codSel->fetchColumn();
|
||||
AuditLog::log('tramites', 'add_obs', 'tramite_legal', $codigo, $codigo, ['abogado'=>$abog, 'texto'=>$texto]);
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok(hydrate($pdo, $row->fetch()));
|
||||
@@ -352,6 +364,11 @@ try {
|
||||
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
|
||||
->execute([':u' => $user['email'], ':id' => $id]);
|
||||
|
||||
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
|
||||
$codSel->execute([':id' => $id]);
|
||||
$codigo = (string)$codSel->fetchColumn();
|
||||
AuditLog::log('tramites', 'add_hours', 'tramite_legal', $codigo, $codigo, ['abogado'=>$abog, 'hrs'=>$hrs]);
|
||||
|
||||
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$row->execute([':id' => $id]);
|
||||
ok(hydrate($pdo, $row->fetch()));
|
||||
@@ -383,6 +400,11 @@ try {
|
||||
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
|
||||
->execute([':u' => $user['email'], ':id' => $tramiteId]);
|
||||
|
||||
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
|
||||
$codSel->execute([':id' => $tramiteId]);
|
||||
$codigo = (string)$codSel->fetchColumn();
|
||||
AuditLog::log('tramites', 'update_hours', 'hour', (string)$hourId, $codigo, ['abogado'=>$abog, 'hrs'=>$hrs]);
|
||||
|
||||
$r = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$r->execute([':id' => $tramiteId]);
|
||||
ok(hydrate($pdo, $r->fetch()));
|
||||
@@ -405,6 +427,11 @@ try {
|
||||
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
|
||||
->execute([':u' => $user['email'], ':id' => $tramiteId]);
|
||||
|
||||
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
|
||||
$codSel->execute([':id' => $tramiteId]);
|
||||
$codigo = (string)$codSel->fetchColumn();
|
||||
AuditLog::log('tramites', 'delete_hours', 'hour', (string)$hourId, $codigo);
|
||||
|
||||
$r = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
|
||||
$r->execute([':id' => $tramiteId]);
|
||||
ok(hydrate($pdo, $r->fetch()));
|
||||
@@ -418,7 +445,12 @@ try {
|
||||
|
||||
if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403);
|
||||
|
||||
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
|
||||
$codSel->execute([':id' => $id]);
|
||||
$codigo = (string)$codSel->fetchColumn();
|
||||
|
||||
$pdo->prepare('DELETE FROM tramites_legales WHERE id = :id')->execute([':id' => $id]);
|
||||
AuditLog::log('tramites', 'delete', 'tramite_legal', $codigo, $codigo);
|
||||
ok(['deleted' => $id]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user