JERARQUÍA DE ROLES
admin = programador. Modo Dios. Oculto de dropdowns y del Panel
de Control para non-admins. Acceso total.
socio = modo Dios visible. Acceso al Panel, ve y edita todo.
abogado = crea casos, ve los suyos + donde es involucrado.
usuario = solo ve/edita donde es involucrado.
* Migración 008: agrega 'admin' al ENUM users.role. Promueve a
ozerpa@aal-law.com como admin.
* Auth.php: isAdmin() nuevo. isSocio() y isAbogado() ahora incluyen
admin (admin hereda todos los poderes de socio). canCreate/canDelete
también heredan.
* Lookups.php:
- abogadoNames() ya filtraba a role IN (socio, abogado) → admins no
aparecen en el dropdown "Abogado Solicitante".
- involucrablesUsers() agrega filtro role != 'admin' → admins no
aparecen en el picker de Involucrados.
- allUsers($includeAdmins) parametrizada: socios reciben la lista
sin admins, admins reciben todo.
* Bootstrap.php: window.DONNA.isAdmin disponible al frontend.
* admin/api.php:
- list_users pasa Auth::isAdmin() para filtrar.
- create_user / update_user: rechazan role='admin' si quien llama no
es admin. Solo admins pueden editar a otros admins.
- delete_user: protege admins (extends al check existente de socio).
- Self-protection extendida: admin no puede demoverse por debajo de
socio (queda en god-mode tier).
* admin.html: opción 'admin' en role dropdown solo visible si el
usuario actual es admin. Role pill navy/blanca para admin.
172 lines
6.8 KiB
PHP
172 lines
6.8 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';
|
|
|
|
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;
|
|
$orden = (int)($b['orden'] ?? 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, orden, first_seen_at, last_login_at)
|
|
VALUES (:email, :name, :role, :activo, :orden, NULL, NULL)'
|
|
);
|
|
$ins->execute([
|
|
':email' => $email, ':name' => $name, ':role' => $role,
|
|
':activo' => $activo, ':orden' => $orden,
|
|
]);
|
|
$id = (int)$pdo->lastInsertId();
|
|
|
|
$sel = $pdo->prepare('SELECT id, email, name, role, activo, orden, 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'];
|
|
$row['orden'] = (int)$row['orden'];
|
|
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'] ?? ''));
|
|
$orden = (int)($b['orden'] ?? 0);
|
|
|
|
$upd = $pdo->prepare(
|
|
'UPDATE users SET email = :email, name = :name, role = :role,
|
|
activo = :activo, orden = :orden
|
|
WHERE id = :id'
|
|
);
|
|
$upd->execute([
|
|
':email' => $newEmail, ':name' => $name, ':role' => $newRole,
|
|
':activo' => $newActivo, ':orden' => $orden,
|
|
':id' => $id,
|
|
]);
|
|
|
|
$sel = $pdo->prepare('SELECT id, email, name, role, activo, orden, 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'];
|
|
$row['orden'] = (int)$row['orden'];
|
|
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]);
|
|
ok(['deleted' => $id, 'email' => $u['email']]);
|
|
}
|
|
|
|
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);
|
|
}
|