Files
Soporte ZH Consultores 8ff56d3d78 refactor(users): eliminar columna orden, listar alfabéticamente
- Migración 012: ALTER TABLE users DROP COLUMN orden
- Lookups: abogadoNames y allUsers ordenan por name ASC
- API admin: create/update_user dejan de leer/escribir orden
- UI admin: removida la columna "Orden" de la tabla y el input
  "Orden" del modal de usuario
2026-06-04 21:31:02 -05:00

220 lines
9.5 KiB
PHP

<?php
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');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
const ROLES = ['admin','socio','abogado','usuario'];
function ok($data = null): void {
echo json_encode(['ok' => 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);
}