true, 'data' => $data, 'user' => Auth::user()], JSON_UNESCAPED_UNICODE); exit; } function err(string $msg, int $code = 400): void { http_response_code($code); echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE); exit; } function body(): array { return json_decode((string) file_get_contents('php://input'), true) ?? []; } $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; $me = Auth::userId(); $pdo = Db::pdo(); try { if ($method === 'GET' && $action === 'list_users') { // Los socios no ven a los admins; los admins ven todo ok(Lookups::allUsers(Auth::isAdmin())); } if ($method === 'POST' && $action === 'create_user') { $b = body(); $email = strtolower(trim((string)($b['email'] ?? ''))); if ($email === '') err('email requerido'); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) err('email inválido'); $name = trim((string)($b['name'] ?? '')); $role = in_array(($b['role'] ?? ''), ROLES, true) ? $b['role'] : 'usuario'; $activo = (int)($b['activo'] ?? 1) === 1 ? 1 : 0; // Solo admins pueden crear nuevos admins if ($role === 'admin' && !Auth::isAdmin()) { err('Solo los admins pueden crear nuevos admins', 403); } $exists = $pdo->prepare('SELECT id FROM users WHERE email = :e'); $exists->execute([':e' => $email]); if ($exists->fetch()) err('Ya existe un usuario con ese email', 409); $ins = $pdo->prepare( 'INSERT INTO users (email, name, role, activo, first_seen_at, last_login_at) VALUES (:email, :name, :role, :activo, NULL, NULL)' ); $ins->execute([ ':email' => $email, ':name' => $name, ':role' => $role, ':activo' => $activo, ]); $id = (int)$pdo->lastInsertId(); $sel = $pdo->prepare('SELECT id, email, name, role, activo, first_seen_at, last_login_at FROM users WHERE id = :id'); $sel->execute([':id' => $id]); $row = $sel->fetch(); $row['id'] = (int)$row['id']; $row['activo'] = (int)$row['activo']; AuditLog::log('admin', 'create_user', 'user', (string)$id, $email, ['role'=>$role, 'name'=>$name]); ok($row); } if ($method === 'PUT' && $action === 'update_user') { $b = body(); $id = (int)($b['_id'] ?? 0); if (!$id) err('id requerido'); $sel = $pdo->prepare('SELECT id, email, role, activo FROM users WHERE id = :id'); $sel->execute([':id' => $id]); $existing = $sel->fetch(); if (!$existing) err('Usuario no encontrado', 404); $newRole = in_array(($b['role'] ?? $existing['role']), ROLES, true) ? $b['role'] : $existing['role']; $newActivo = isset($b['activo']) ? ((int)$b['activo'] === 1 ? 1 : 0) : (int)$existing['activo']; // Solo admins pueden editar a otros admins if ($existing['role'] === 'admin' && !Auth::isAdmin()) { err('Solo los admins pueden editar a otros admins', 403); } // Solo admins pueden asignar el rol admin if ($newRole === 'admin' && !Auth::isAdmin()) { err('Solo los admins pueden asignar el rol admin', 403); } // Salvaguardas: no puedes quitarte tu modo Dios ni desactivarte if ((int)$existing['id'] === $me) { if (!in_array($newRole, ['admin','socio'], true)) { err('No puedes quitarte el modo Dios a ti mismo (debe quedar Admin o Socio)', 400); } if ($newActivo === 0) err('No puedes desactivarte a ti mismo', 400); } $newEmail = isset($b['email']) ? strtolower(trim((string)$b['email'])) : $existing['email']; if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) err('email inválido'); if ($newEmail !== $existing['email']) { $chk = $pdo->prepare('SELECT id FROM users WHERE email = :e AND id != :id'); $chk->execute([':e' => $newEmail, ':id' => $id]); if ($chk->fetch()) err('Otro usuario ya tiene ese email', 409); } $name = trim((string)($b['name'] ?? '')); $upd = $pdo->prepare( 'UPDATE users SET email = :email, name = :name, role = :role, activo = :activo WHERE id = :id' ); $upd->execute([ ':email' => $newEmail, ':name' => $name, ':role' => $newRole, ':activo' => $newActivo, ':id' => $id, ]); $sel = $pdo->prepare('SELECT id, email, name, role, activo, first_seen_at, last_login_at FROM users WHERE id = :id'); $sel->execute([':id' => $id]); $row = $sel->fetch(); $row['id'] = (int)$row['id']; $row['activo'] = (int)$row['activo']; $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); } if ($method === 'DELETE' && $action === 'delete_user') { $id = (int)($_GET['id'] ?? 0); if (!$id) err('id requerido'); if ($id === $me) err('No puedes eliminarte a ti mismo', 400); $sel = $pdo->prepare('SELECT id, email, role FROM users WHERE id = :id'); $sel->execute([':id' => $id]); $u = $sel->fetch(); if (!$u) err('Usuario no encontrado', 404); if ($u['role'] === 'admin' && !Auth::isAdmin()) { err('Solo los admins pueden eliminar a otros admins', 403); } if (in_array($u['role'], ['admin','socio'], true)) { err('No puedes eliminar a un Admin o Socio. Cambia su rol a Abogado o Usuario primero.', 400); } // CASCADE de tramites_involucrados limpia los vínculos automáticamente. // Los strings históricos (tramite.creado_por, tramite.abogado, obs.autor_email, // 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) { error_log('admin db error: ' . $e->getMessage()); err('Error de base de datos', 500); } catch (Throwable $e) { error_log('admin error: ' . $e->getMessage()); err('Error interno', 500); }