feat(auth): nuevo rol 'admin' (programador, modo Dios oculto)

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.
This commit is contained in:
Soporte ZH Consultores
2026-06-05 00:27:54 +00:00
parent 25f3d7461b
commit 510304872c
6 changed files with 80 additions and 23 deletions
+5 -4
View File
@@ -68,10 +68,11 @@ final class Auth {
public static function role(): string { return (string)(self::userRow()['role'] ?? 'usuario'); }
public static function userName(): string { return (string)(self::userRow()['name'] ?? ''); }
public static function isSocio(): bool { return self::role() === 'socio'; }
public static function isAbogado(): bool { return in_array(self::role(), ['socio','abogado'], true); }
public static function canCreate(): bool { return self::isAbogado(); } // socio + abogado pueden crear trámites
public static function canDelete(): bool { return self::isAbogado(); } // socio + abogado pueden eliminar
public static function isAdmin(): bool { return self::role() === 'admin'; }
public static function isSocio(): bool { return in_array(self::role(), ['admin','socio'], true); }
public static function isAbogado(): bool { return in_array(self::role(), ['admin','socio','abogado'], true); }
public static function canCreate(): bool { return self::isAbogado(); } // admin + socio + abogado pueden crear trámites
public static function canDelete(): bool { return self::isAbogado(); } // admin + socio + abogado pueden eliminar
public static function requireSocio(): void {
self::requireAuth();
+1
View File
@@ -16,6 +16,7 @@ final class Bootstrap {
'me' => $user['email'],
'name' => $user['name'],
'role' => Auth::role(),
'isAdmin' => Auth::isAdmin(),
'isSocio' => Auth::isSocio(),
'isAbogado' => Auth::isAbogado(),
'canCreate' => Auth::canCreate(),
+17 -10
View File
@@ -8,7 +8,11 @@ require_once __DIR__ . '/Db.php';
* dropdowns y resolver visibilidad.
*/
final class Lookups {
/** Nombres completos para el dropdown "Abogado Solicitante" (socios + abogados activos). */
/**
* Nombres completos para el dropdown "Abogado Solicitante".
* Solo socios y abogados; admins quedan ocultos (el programador no debe
* aparecer en listas de abogados del firme).
*/
public static function abogadoNames(): array {
$rows = Db::pdo()->query(
"SELECT DISTINCT name FROM users
@@ -20,12 +24,13 @@ final class Lookups {
return array_map(fn($r) => $r['name'], $rows);
}
/** Todos los usuarios activos para el picker de Involucrados. */
/** Usuarios activos para el picker de Involucrados (sin admins ni placeholders). */
public static function involucrablesUsers(): array {
$rows = Db::pdo()->query(
"SELECT id, email, name, role
FROM users
WHERE activo = 1
AND role != 'admin'
AND email NOT LIKE 'placeholder-%'
ORDER BY name ASC, email ASC"
)->fetchAll();
@@ -33,14 +38,16 @@ final class Lookups {
return $rows;
}
/** Todos los users (incluye inactivos y placeholders) para el Panel de Control. */
public static function allUsers(): array {
$rows = Db::pdo()->query(
"SELECT id, email, name, role, activo, orden,
first_seen_at, last_login_at
FROM users
ORDER BY role DESC, name ASC, email ASC"
)->fetchAll();
/**
* Todos los users para el Panel de Control.
* Si $includeAdmins es false, oculta el rol admin (uso para socios).
*/
public static function allUsers(bool $includeAdmins = true): array {
$sql = "SELECT id, email, name, role, activo, orden, first_seen_at, last_login_at
FROM users";
if (!$includeAdmins) $sql .= " WHERE role != 'admin'";
$sql .= " ORDER BY FIELD(role,'admin','socio','abogado','usuario'), name ASC, email ASC";
$rows = Db::pdo()->query($sql)->fetchAll();
foreach ($rows as &$r) {
$r['id'] = (int)$r['id'];
$r['activo'] = (int)$r['activo'];