# Roles y permisos
Quién puede ver y hacer qué. Es el punto donde más seguido se cometen errores, así que conviene entenderlo completo.
## Las dos columnas de un usuario
En `admin_users` conviven dos campos que parecen redundantes y **no lo son**:
| Columna | Para qué se usa |
|---|---|
| `role` | Texto del rol (`admin`, `bacteriologo`, …). Lo consultan las vistas para decidir qué mostrar |
| `role_id` | Apunta a `roles.id`. Es de donde se **cargan los módulos** al iniciar sesión |
> **Hay que mantener las dos sincronizadas.** Cambiar solo `role` deja al usuario con los permisos viejos, porque el acceso real sale de `role_id`. Este error ya ocurrió: un usuario cambió de rol, la interfaz mostraba el rol nuevo y los módulos seguían siendo los anteriores.
Al cambiar el rol de alguien, actualice las dos a la vez:
```sql
UPDATE admin_users
SET role = 'lab_recepcion',
role_id = (SELECT id FROM roles WHERE slug = 'lab_recepcion')
WHERE id = 12;
```
## Cómo se arma el acceso al iniciar sesión
`authenticateUser()` (`config/config.php`) valida la contraseña y arma la sesión:
```
admin_users.role_id
└── role_modules → lista de module_slug + permission
└── $_SESSION['admin_user']['modules'] (qué módulos ve)
$_SESSION['admin_user']['module_permissions'] (read o write en cada uno)
```
**Esto ocurre una sola vez, al entrar.** Cualquier cambio en `role_modules` no afecta a las sesiones abiertas: el usuario tiene que cerrar sesión y volver a entrar.
## Las dos preguntas del control de acceso
```php
hasModule('lab_domicilios') // ¿puede entrar al módulo?
hasModuleWrite('lab_domicilios') // ¿puede modificar, o solo mirar?
```
- `hasModule()` mira si el slug está en la lista de módulos de la sesión.
- `hasModuleWrite()` mira `module_permissions[slug] === 'write'`. Los administradores siempre pueden escribir.
Una vista típica lo usa así:
```php
$puedeEscribir = hasModuleWrite('lab_domicilios');
...
```
## La columna que manda es `permission`
`role_modules` tiene dos formas de expresar lo mismo, y solo una se usa:
| Columnas | ¿Se usan? |
|---|---|
| `permission` (`read` / `write`) | **Sí.** Es lo que lee `hasModuleWrite()` |
| `can_view`, `can_create`, `can_edit`, `can_delete`, `can_export` | No las lee el control de acceso |
> Poner `can_edit = 0` **no impide editar**. Para dejar un módulo en solo lectura hay que fijar `permission = 'read'`. Las columnas `can_*` quedaron de un diseño anterior; conviene mantenerlas coherentes por prolijidad, pero no protegen nada.
Solo lectura de verdad:
```sql
UPDATE role_modules SET permission = 'read'
WHERE role_id = 1030 AND module_slug IN ('lab_domicilios', 'lab_ordenes');
```
## Roles actuales
{{roles}}
## Sesiones sin `role_id`
Hay dos casos heredados que siguen contemplados en el código:
- **`modules` nulo y rol `admin`** → acceso total. Cubre usuarios anteriores al sistema de roles.
- **Rol `enfermero` sin `role_id`** → recibe `enfermero_portal` y `lab_formularios` de forma fija.
## Verificaciones adicionales
El control por módulo no siempre alcanza. Varias pantallas agregan sus propias reglas:
| Dónde | Regla |
|---|---|
| `enfermero_portal.php` | Solo `admin`, `superadmin` y `enfermero` |
| `api/lab/save_domicilio.php` | Un enfermero solo edita domicilios que creó **o** que tiene asignados |
| `api/lab/firmar_profesional.php` | Un enfermero solo firma envíos propios |
| `modules/turnero/api/_helpers.php` | `requireTurnero()` en todos los endpoints del turnero |
| `modules/turnero/module.php` | El menú cambia según rol y según la IP del equipo |
Al agregar un endpoint que modifica datos, **no alcanza con confiar en que la vista ocultó el botón**: el endpoint tiene que verificar por su cuenta.
## Diagnóstico rápido
Alguien reporta que no ve un módulo o que puede editar lo que no debería:
```sql
-- Qué rol tiene realmente y si las dos columnas coinciden
SELECT u.id, u.username, u.role, u.role_id, r.slug AS rol_real
FROM admin_users u LEFT JOIN roles r ON r.id = u.role_id
WHERE u.username = 'usuario';
-- Qué módulos le da ese rol
SELECT module_slug, permission FROM role_modules WHERE role_id = ;
```
Si los datos se ven bien y el usuario sigue sin acceso: **no ha vuelto a iniciar sesión**.