new funcionality

This commit is contained in:
2026-07-27 17:20:02 +02:00
parent c2b5291a74
commit c5ce8a7a36
101 changed files with 6889 additions and 176 deletions

View File

@@ -178,7 +178,7 @@
<p class="subtitle">Manual de uso: puesta en marcha, panel de administración, flujos OAuth2 y API REST completa para construir cualquier cliente propio (Angular, Flutter, NestJS, etc.).</p>
<div class="meta">
Laravel 12 · Passport 13 · Vue 3 + TypeScript · SCSS/BEM · spatie/laravel-permission · L5-Swagger<br>
Generado el 25 de julio de 2026
Generado el 27 de julio de 2026 (actualizado: webhooks salientes + protección anti-SSRF)
</div>
</div>
@@ -189,13 +189,16 @@
<li><span><span class="n">2.</span>Puesta en marcha</span></li>
<li><span><span class="n">3.</span>Usuarios por defecto (seed)</span></li>
<li><span><span class="n">4.</span>Uso de la interfaz web (panel de administración)</span></li>
<li><span><span class="n">5.</span>Analíticas: sesiones, logins y actividad</span></li>
<li><span><span class="n">6.</span>Roles, permisos y roles por app</span></li>
<li><span><span class="n">7.</span>Idioma (español / inglés / catalán)</span></li>
<li><span><span class="n">8.</span>Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</span></li>
<li><span><span class="n">9.</span>API REST completa: construí tu propio panel admin</span></li>
<li><span><span class="n">10.</span>Documentación interactiva (Swagger)</span></li>
<li><span><span class="n">11.</span>Referencia rápida de rutas</span></li>
<li><span><span class="n">5.</span>Verificación en dos pasos (2FA)</span></li>
<li><span><span class="n">6.</span>Analíticas: sesiones, logins y actividad</span></li>
<li><span><span class="n">7.</span>Audit log: quién cambió qué</span></li>
<li><span><span class="n">8.</span>Roles, permisos y roles por app</span></li>
<li><span><span class="n">9.</span>Idioma (español / inglés / catalán)</span></li>
<li><span><span class="n">10.</span>Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</span></li>
<li><span><span class="n">11.</span>API REST completa: construí tu propio panel admin</span></li>
<li><span><span class="n">12.</span>Webhooks: enterate sin hacer polling</span></li>
<li><span><span class="n">13.</span>Documentación interactiva (Swagger)</span></li>
<li><span><span class="n">14.</span>Referencia rápida de rutas</span></li>
</ul>
</section>
@@ -259,6 +262,14 @@ cola de trabajos y los logs. La app queda disponible en <code>http://localhost:8
vuelve a crear los usuarios/clientes de ejemplo descriptos en la sección 3. Los <code>client_secret</code>
de los clientes OAuth de ejemplo se imprimen en la consola en ese momento — solo se muestran una vez.
</div>
<div class="box">
<strong>Email:</strong> por defecto <code>MAIL_MAILER=log</code> — los emails (hoy, solo el de
recuperación de contraseña, ver sección 4) se escriben en <code>storage/logs/laravel.log</code> en vez de
enviarse de verdad, para no necesitar SMTP en desarrollo. Para producción, configurá un mailer real en
<code>.env</code> (<code>MAIL_MAILER</code>, <code>MAIL_HOST</code>, etc. — cualquiera soportado por
Laravel). El envío va encolado (<code>ShouldQueue</code>), así que también necesitás un worker corriendo
(<code>php artisan queue:work</code>, o <code>composer run dev</code> en desarrollo).
</div>
</section>
<section>
@@ -325,6 +336,33 @@ automáticamente, pero <strong>no loguea al usuario</strong>: queda pendiente de
un mensaje de "Cuenta creada" y el login devuelve error hasta que un admin la aprueba desde el panel de
usuarios (sección 4). <strong>Excepción:</strong> si la base de usuarios está vacía, esa primera cuenta se
crea como admin y queda logueada de una (ver sección 3).</p>
<div class="box warn">
<strong>Rate limiting:</strong> <code>POST /web-api/login</code> admite 5 intentos por minuto por
combinación de email + IP (frena fuerza bruta de contraseña sin afectar a otros usuarios que compartan
IP), y <code>POST /web-api/register</code> 5 por minuto por IP. Al superar el límite, la respuesta es
<code>429 Too Many Requests</code> con un header <code>Retry-After</code>.
</div>
<h3>Recuperación de contraseña</h3>
<p>
Desde <code>/login</code> → "¿Olvidaste tu contraseña?" (<code>/forgot-password</code>), cualquiera puede pedir
un enlace de restablecimiento con solo su email. La respuesta es <strong>siempre la misma</strong>, exista o
no una cuenta con ese email — lo contrario permitiría enumerar qué emails están registrados probando uno por
uno. Si la cuenta existe, se manda un email (ver la nota sobre <code>MAIL_MAILER</code> en la sección 2) con
un enlace a <code>/reset-password?token=...&amp;email=...</code>, válido durante <strong>60 minutos</strong> y
de un solo uso.
</p>
<div class="box">
<strong>Al restablecer la contraseña se cierran todas las sesiones activas</strong> (los tokens OAuth
vigentes de esa cuenta, igual que "Cerrar todas" en Sesiones activas — sección 4) y queda un
<code>user.password_reset</code> en el audit log (sección 7). El motivo más común para resetear una
contraseña es sospechar que se filtró, así que no tiene sentido dejar vigente lo que se emitió con la
vieja.
</div>
<div class="box warn">
<strong>Rate limiting:</strong> tanto pedir el enlace como confirmar el restablecimiento admiten 5
intentos por minuto por combinación de email + IP.
</div>
<h3>Dashboard</h3>
<p><code>/</code> — accesos rápidos a Clientes OAuth, Personal Access Tokens, Apps autorizadas y (si sos admin)
@@ -341,6 +379,17 @@ crear uno se elige:</p>
</ul>
<div class="box warn">El <code>client_secret</code> se muestra <strong>una sola vez</strong>, justo después de
crear el cliente. Copiarlo en ese momento.</div>
<p>Cada cliente tiene, además, un botón <strong>"Webhooks"</strong> para registrar endpoints que reciben avisos
cuando pasa algo relevante para esa app (ver sección 12) — sin tener que hacer polling.</p>
<div class="box">
<strong>Rotar secret.</strong> Los clientes confidenciales (con <code>client_secret</code>) tienen un botón
<strong>"Rotar secret"</strong>. Genera un secret nuevo y lo muestra una sola vez, igual que al crear el
cliente — pero el secret <strong>anterior sigue siendo válido durante 48 horas</strong> (grace period), así
que podés actualizar la config de tu app sin downtime: mientras dure el grace period, ambos secrets
(viejo y nuevo) funcionan contra <code>POST /oauth/token</code>. Pasadas las 48 horas, o si rotás de
nuevo antes, el secret anterior deja de servir. No aplica a clientes públicos (Authorization Code + PKCE sin
secret) — no hay nada que rotar.
</div>
<h3>Personal Access Tokens</h3>
<p><code>/tokens</code> — tokens tipo "API key" para el propio usuario, sin pasar por el flujo de consentimiento.
@@ -370,41 +419,167 @@ sesión.</p>
igual que un borrado normal — se puede restaurar). Un usuario dado de alta directamente por un admin
(el formulario "Nuevo usuario" de esta misma pantalla) queda aprobado automáticamente.
</div>
<div class="box">
<strong>Sesiones activas.</strong> Dentro de "Editar" (junto a "Roles por app") hay una sección "Sesiones
activas" con los tokens OAuth vigentes de ese usuario — qué apps tienen acceso a su cuenta ahora mismo, con
scope y fecha de emisión. Cada sesión se puede revocar individualmente, o todas de una con "Cerrar todas".
Pensado para incident response: si un usuario reporta una cuenta comprometida (o algo raro aparece en
Analíticas/Audit log), un admin puede cortarle el acceso a todo sin esperar a que expire solo.
</div>
<h3>Seguridad: verificación en dos pasos</h3>
<p><code>/security</code> — cualquier usuario (no solo admins) puede activar 2FA acá. Ver sección 5.</p>
<h3>Analíticas (solo admins)</h3>
<p><code>/admin/analytics</code> — ver sección 5.</p>
<p><code>/admin/analytics</code> — ver sección 6.</p>
<h3>Audit log (solo admins)</h3>
<p><code>/admin/audit-log</code> — ver sección 7.</p>
</section>
<section>
<h2>5. Analíticas: sesiones, logins y actividad</h2>
<h2>5. Verificación en dos pasos (2FA)</h2>
<p>
Cualquier usuario (no hace falta ser admin) puede activar 2FA con TOTP desde <code>/security</code> —
compatible con Google Authenticator, Authy, 1Password, o cualquier app que hable el estándar TOTP
(RFC 6238). Es una capa extra sobre la contraseña: una vez activo, el login pide además un código de 6
dígitos.
</p>
<h3>Activación</h3>
<ol>
<li>El servidor genera un secreto (todavía no activo) y un código QR para escanear.</li>
<li>El usuario escanea el QR con su app autenticadora (o copia el secreto a mano si no puede escanear).</li>
<li>Confirma con un código real generado por la app — recién en ese momento queda activo de verdad. Un
secreto generado pero nunca confirmado no habilita nada.</li>
<li>El servidor muestra <strong>8 códigos de recuperación</strong>, una sola vez. Cada uno sirve para un
único login — son la salida de emergencia si se pierde el teléfono con la app autenticadora.</li>
</ol>
<h3>Login con 2FA activo</h3>
<p>
El login pasa a ser de dos pasos: primero email + contraseña (esto <strong>no</strong> crea sesión todavía);
si el usuario tiene 2FA activo, el servidor responde pidiendo el código en vez de loguear directo. Recién con
un código válido (TOTP o de recuperación) se crea la sesión. Un código de recuperación usado se invalida al
instante — no sirve una segunda vez.
</p>
<div class="box warn">
<strong>Rate limiting:</strong> el segundo paso del login (<code>/web-api/login/2fa</code>), confirmar la
activación (<code>/2fa/confirm</code>), desactivar (<code>/2fa/disable</code>) y regenerar códigos de
recuperación (<code>/2fa/recovery-codes</code>) admiten 6 intentos por minuto — el espacio de un código
TOTP de 6 dígitos es chico como para no limitarlo.
</div>
<div class="box">
<strong>Desactivar 2FA o regenerar códigos de recuperación:</strong> ambas acciones, desde
<code>/security</code>, piden confirmar la contraseña actual. Desactivar borra el secreto y los códigos de
recuperación por completo; para volver a activarlo hay que escanear un QR nuevo (no se reutiliza el
secreto anterior). Regenerar invalida los 8 códigos viejos y muestra 8 nuevos.
</div>
<div class="box">
<strong>Password Grant y 2FA:</strong> si el usuario tiene 2FA activo, <code>POST /oauth/token</code> con
<code>grant_type=password</code> exige además un campo <code>otp</code> con un código TOTP o de
recuperación válido, en el mismo request — sin él, o con uno inválido, el servidor responde
<code>400 invalid_grant</code> igual que con una contraseña incorrecta (no delata si el problema fue la
contraseña o el 2FA). Los usuarios sin 2FA no notan ningún cambio.
<pre><code>curl -X POST /oauth/token -d grant_type=password \
-d client_id=... -d client_secret=... \
-d username=ana@example.com -d password=... \
-d otp=123456 -d scope="users:read"</code></pre>
</div>
<p>Cada activación, desactivación y regeneración de códigos queda registrada en el audit log (sección 7).</p>
</section>
<section>
<h2>6. Analíticas: sesiones, logins y actividad</h2>
<p>
Pantalla de monitoreo para el admin: cada vez que Passport emite un access token (cualquier grant type) o alguien
inicia sesión en el panel, queda un registro en <code>login_events</code>. Con eso se arman:
</p>
<ul>
<li><strong>KPIs</strong>: usuarios totales, sesiones activas (tokens OAuth vigentes, no revocados ni
expirados), logins de hoy y de los últimos 7 días.</li>
expirados), logins de hoy, logins de los últimos 7 días, tasa de fallos de login y tasa de aprobación
OAuth (últimos 7 días).</li>
<li><strong>Logins por día</strong> (últimos 7/30/90 días, elegible arriba a la derecha): barras apiladas —
logins de apps (OAuth) vs. logins al panel admin (sesión) — con tooltip al pasar el mouse sobre cada día.</li>
<li><strong>Logins por app</strong>: qué apps se usan más.</li>
<li><strong>Usuarios más activos</strong>: ranking por cantidad de logins en el período.</li>
<li><strong>Funnel de autorización OAuth</strong>: por cada cliente, cuántas veces se mostró la pantalla de
consentimiento (<code>/oauth/authorize</code>), cuántas se aprobaron y cuántas se denegaron, con la tasa de
aprobación resultante.</li>
<li><strong>Tokens activos por cliente</strong>: snapshot en vivo de tokens OAuth vigentes (no revocados,
no expirados), agrupados por app.</li>
<li><strong>Uso de scopes</strong>: qué scopes (<code>users:read</code>, <code>account:write</code>, etc.)
aparecen más entre los tokens emitidos en el período.</li>
<li><strong>Actividad reciente</strong>: tabla con los últimos eventos (usuario, app, tipo, IP, cuándo).</li>
</ul>
<div class="box">
<strong>Exportar PDF:</strong> el botón "Exportar PDF" arriba a la derecha abre el diálogo de impresión del
navegador con una hoja de estilos dedicada — oculta la barra de navegación y los controles, fuerza tema
claro y expande la tabla de actividad reciente completa. No genera el PDF en el servidor: es el navegador
el que lo arma ("Guardar como PDF" en el diálogo de impresión), así que no depende de ninguna librería ni
servicio adicional.
</div>
<div class="box">
<strong>Tasa de fallos de login</strong> solo cuenta credenciales inválidas en el login de sesión (panel
admin) — no cuenta intentos contra una cuenta desactivada o pendiente de aprobación, porque eso es un
estado de cuenta, no una señal de ataque de fuerza bruta.
</div>
<div class="box">
El seeder de demo genera 30 días de actividad sintética para que estos gráficos no arranquen vacíos. En un
despliegue real, los datos son 100% eventos reales — no hay nada que "activar".
despliegue real, los datos son 100% eventos reales — no hay nada que "activar". El funnel de autorización y
la tasa de fallos de login sí arrancan vacíos incluso con el seed (son eventos nuevos, no incluidos en la
actividad sintética) hasta que ocurran de verdad.
</div>
<p>Misma info disponible por API: <code>GET /api/analytics/summary?days=30</code> (scope
<code>users:read</code>, requiere el permiso <code>users.view</code> — igual que el resto del panel admin).</p>
</section>
<section>
<h2>6. Roles, permisos y roles por app</h2>
<h2>7. Audit log: quién cambió qué</h2>
<p>
Pantalla de auditoría para el admin (<code>/admin/audit-log</code>): registra automáticamente los cambios que
importan desde el punto de vista de seguridad/gobernanza — no hace falta activar nada, queda un renglón cada vez
que ocurre alguno de estos eventos:
</p>
<table>
<tr><th>Acción</th><th>Cuándo se registra</th><th>Qué guarda en "Detalles"</th></tr>
<tr><td><code>user.created</code></td><td>Un admin da de alta un usuario desde el panel.</td><td>Nombre, email y rol asignado.</td></tr>
<tr><td><code>user.updated</code></td><td>Un admin edita nombre, email, rol, estado activo/inactivo o contraseña de un usuario.</td><td>Solo los campos que efectivamente cambiaron (antes → después). La contraseña nunca se guarda en texto plano, solo si cambió o no.</td></tr>
<tr><td><code>user.deleted</code></td><td>Un admin borra (soft delete) un usuario.</td><td>—</td></tr>
<tr><td><code>user.restored</code></td><td>Un admin restaura un usuario desde la papelera.</td><td>—</td></tr>
<tr><td><code>user.force_deleted</code></td><td>Un admin borra definitivamente un usuario de la papelera.</td><td>—</td></tr>
<tr><td><code>user.approved</code></td><td>Un admin aprueba un registro pendiente.</td><td>—</td></tr>
<tr><td><code>user.app_role_assigned</code></td><td>Un admin asigna un rol por-app a un usuario.</td><td>Client y rol asignado.</td></tr>
<tr><td><code>user.app_role_revoked</code></td><td>Un admin quita un rol por-app a un usuario.</td><td>Client y rol quitado.</td></tr>
<tr><td><code>client.created</code></td><td>Se registra un cliente OAuth nuevo (panel o API).</td><td>Grant type y si es confidencial.</td></tr>
<tr><td><code>client.revoked</code></td><td>Se revoca un cliente OAuth (panel o API) — cascada a sus tokens.</td><td>Cuántos tokens quedaron revocados.</td></tr>
<tr><td><code>user.2fa_enabled</code></td><td>Un usuario activa 2FA.</td><td>—</td></tr>
<tr><td><code>user.2fa_disabled</code></td><td>Un usuario desactiva 2FA.</td><td>—</td></tr>
<tr><td><code>user.2fa_recovery_regenerated</code></td><td>Un usuario regenera sus códigos de recuperación.</td><td>—</td></tr>
<tr><td><code>user.session_revoked</code></td><td>Un admin revoca un token OAuth ("sesión") de un usuario.</td><td>—</td></tr>
<tr><td><code>user.sessions_revoked_all</code></td><td>Un admin cierra todas las sesiones activas de un usuario de una vez.</td><td>Cuántos tokens quedaron revocados.</td></tr>
<tr><td><code>client.webhook_created</code></td><td>Se registra un webhook nuevo en un cliente OAuth.</td><td>URL y eventos suscritos.</td></tr>
<tr><td><code>client.webhook_deleted</code></td><td>Se borra un webhook.</td><td>URL.</td></tr>
<tr><td><code>client.secret_rotated</code></td><td>El dueño de un cliente confidencial rota su <code>client_secret</code>.</td><td>Hasta cuándo sigue valiendo el secret anterior.</td></tr>
<tr><td><code>user.password_reset</code></td><td>Un usuario restablece su contraseña vía "¿Olvidaste tu contraseña?".</td><td>Cuántas sesiones quedaron revocadas.</td></tr>
</table>
<p>Cada evento guarda además quién lo hizo (o "(sistema)" si no hay actor), IP, y cuándo. Es filtrable por
sujeto (Usuarios / Clientes OAuth) y por acción, y está paginado.</p>
<div class="box warn">
<strong>Qué NO cubre todavía:</strong> cambios de <code>name</code>/<code>redirect_uris</code> en un cliente
OAuth existente (solo se audita creación y revocación), y acciones de auto-servicio de un usuario sobre sus
propios clientes/tokens/apps autorizadas (ese flujo no pasa por rutas de admin).
</div>
<p>Requiere el permiso <code>audit.view</code> (nuevo — separado de <code>users.*</code> porque también cubre
clientes OAuth, no solo usuarios). Se lo asigna automáticamente al rol <code>admin</code>. Misma info por API:
<code>GET /api/audit-log</code> (scope <code>users:read</code>).</p>
</section>
<section>
<h2>8. Roles, permisos y roles por app</h2>
<h3>Roles globales</h3>
<p>Gobiernan el panel de administración de esta app (<code>spatie/laravel-permission</code>):</p>
<table>
<tr><th>Rol</th><th>Permisos</th></tr>
<tr><td><span class="pill">admin</span></td><td><code>users.view</code>, <code>users.create</code>, <code>users.update</code>, <code>users.delete</code></td></tr>
<tr><td><span class="pill">admin</span></td><td><code>users.view</code>, <code>users.create</code>, <code>users.update</code>, <code>users.delete</code>, <code>audit.view</code></td></tr>
<tr><td><span class="pill">user</span></td><td>Ninguno extra — gestiona su propio perfil, sus clientes OAuth y sus tokens.</td></tr>
</table>
@@ -413,13 +588,13 @@ inicia sesión en el panel, queda un registro en <code>login_events</code>. Con
<code>user</code>) <strong>específico de una app registrada</strong>, desde el panel de edición de usuario
("Roles por app"). Sirve para que, por ejemplo, un usuario sea <code>admin</code> dentro de la app Angular
pero solo <code>user</code> dentro de la app NestJS — sin tocar su rol global.</p>
<p>Esa información viaja en la respuesta de la API (<code>app_roles</code>, ver sección 8): cada app ve
<p>Esa información viaja en la respuesta de la API (<code>app_roles</code>, ver sección 11): cada app ve
únicamente el rol que ese usuario tiene <em>en ella</em>, resuelto automáticamente según el <code>client_id</code>
del token usado en la request.</p>
</section>
<section>
<h2>7. Idioma (español / inglés / catalán)</h2>
<h2>9. Idioma (español / inglés / catalán)</h2>
<p>El selector está arriba a la derecha en la barra de navegación (<strong>ES / EN / CA</strong>). Cambiarlo
hace un POST a <code>/web-api/locale</code>, guarda el idioma en la sesión y recarga la página — el servidor
decide el idioma con el que renderiza esa carga (mensajes de validación/autenticación vía
@@ -437,7 +612,7 @@ sin parpadeo.</p>
</section>
<section>
<h2>8. Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</h2>
<h2>10. Cómo conectar tu app (Angular, NestJS, Flutter, etc.)</h2>
<h3>Paso 1 — Registrar la app</h3>
<p>Desde <code>/clients</code>, crear un cliente OAuth eligiendo el grant type según el tipo de app:</p>
<table>
@@ -479,7 +654,7 @@ con <code>?code=...&state=...</code>.</p>
</section>
<section>
<h2>9. API REST completa: construí tu propio panel admin</h2>
<h2>11. API REST completa: construí tu propio panel admin</h2>
<p>
Todo lo que hace la SPA Vue por sesión también está disponible por token OAuth2. Esto significa que un cliente
Angular, Flutter, o lo que sea, puede ser un <strong>admin completo</strong> — no solo consultar usuarios —
@@ -503,8 +678,12 @@ sin depender de la sesión de Laravel. Todos los endpoints requieren <code>Autho
<tr><td>GET</td><td><code>/api/users/{id}/app-roles</code></td><td>Roles del usuario por app (propio siempre permitido; otros requieren <code>users.view</code>).</td></tr>
<tr><td>POST</td><td><code>/api/users/{id}/app-roles</code></td><td>Asignar un rol en una app (requiere <code>users.update</code>, sin excepción para uno mismo).</td></tr>
<tr><td>DELETE</td><td><code>/api/users/{id}/app-roles/{clientId}/{role}</code></td><td>Quitar un rol en una app (requiere <code>users.update</code>).</td></tr>
<tr><td>GET</td><td><code>/api/users/{id}/sessions</code></td><td>Tokens OAuth activos de ese usuario ("sesiones") (requiere <code>users.view</code>).</td></tr>
<tr><td>DELETE</td><td><code>/api/users/{id}/sessions/{tokenId}</code></td><td>Revocar una sesión puntual (requiere <code>users.update</code>).</td></tr>
<tr><td>DELETE</td><td><code>/api/users/{id}/sessions</code></td><td>Revocar todas las sesiones activas de ese usuario (requiere <code>users.update</code>).</td></tr>
<tr><td>GET</td><td><code>/api/apps</code></td><td>Listar todas las apps registradas (requiere <code>users.view</code>) — para armar el selector de roles-por-app.</td></tr>
<tr><td>GET</td><td><code>/api/analytics/summary?days=30</code></td><td>KPIs, logins por día/app, usuarios más activos y actividad reciente (requiere <code>users.view</code>).</td></tr>
<tr><td>GET</td><td><code>/api/analytics/summary?days=30</code></td><td>KPIs, logins por día/app, usuarios más activos, funnel de autorización OAuth, tokens activos por cliente, uso de scopes y actividad reciente (requiere <code>users.view</code>).</td></tr>
<tr><td>GET</td><td><code>/api/audit-log?action=&subject_type=&per_page=25</code></td><td>Historial de auditoría paginado: altas/bajas de usuarios, cambios de rol, clientes OAuth creados/revocados (requiere <code>audit.view</code>).</td></tr>
</table>
<h3>Ejemplo de respuesta de usuario</h3>
@@ -532,11 +711,18 @@ client_credentials, que no tiene usuario asociado):</p>
<tr><td>POST</td><td><code>/api/clients</code></td><td>Registrar un cliente nuevo (devuelve <code>plain_secret</code> una sola vez).</td></tr>
<tr><td>PUT</td><td><code>/api/clients/{id}</code></td><td>Editar nombre/redirect URIs.</td></tr>
<tr><td>DELETE</td><td><code>/api/clients/{id}</code></td><td>Revocar el cliente y sus tokens.</td></tr>
<tr><td>POST</td><td><code>/api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> (solo clientes confidenciales). Devuelve <code>plain_secret</code> una sola vez; el secret anterior sigue valiendo 48h.</td></tr>
<tr><td>GET</td><td><code>/api/personal-access-tokens</code></td><td>Listar tus Personal Access Tokens.</td></tr>
<tr><td>POST</td><td><code>/api/personal-access-tokens</code></td><td>Crear uno (el token se muestra una sola vez).</td></tr>
<tr><td>DELETE</td><td><code>/api/personal-access-tokens/{id}</code></td><td>Revocarlo.</td></tr>
<tr><td>GET</td><td><code>/api/authorized-apps</code></td><td>Apps de terceros a las que le diste acceso.</td></tr>
<tr><td>DELETE</td><td><code>/api/authorized-apps/{tokenId}</code></td><td>Revocarle el acceso a una.</td></tr>
<tr><td>GET</td><td><code>/api/clients/{id}/webhooks</code></td><td>Listar los webhooks de uno de tus clientes (ver sección 12).</td></tr>
<tr><td>POST</td><td><code>/api/clients/{id}/webhooks</code></td><td>Registrar un webhook nuevo (devuelve <code>secret</code> una sola vez).</td></tr>
<tr><td>PUT</td><td><code>/api/clients/{id}/webhooks/{webhookId}</code></td><td>Editar URL, eventos o pausar/reanudar.</td></tr>
<tr><td>DELETE</td><td><code>/api/clients/{id}/webhooks/{webhookId}</code></td><td>Borrar un webhook.</td></tr>
<tr><td>GET</td><td><code>/api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de intentos de entrega.</td></tr>
<tr><td>POST</td><td><code>/api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega puntual.</td></tr>
</table>
<h3>Scopes disponibles</h3>
@@ -558,7 +744,80 @@ client_credentials, que no tiene usuario asociado):</p>
</section>
<section>
<h2>10. Documentación interactiva (Swagger)</h2>
<h2>12. Webhooks: enterate sin hacer polling</h2>
<p>
Cada cliente OAuth puede registrar uno o más endpoints webhook desde <code>/clients</code> → botón
"Webhooks" (o por API, sección 11). Cuando pasa algo relevante para ese cliente, este servidor le manda un
<code>POST</code> con el evento — no hace falta consultar la API cada tanto para enterarse.
</p>
<h3>Eventos disponibles</h3>
<table>
<tr><th>Evento</th><th>Cuándo se dispara</th><th>A quién le llega</th></tr>
<tr><td><code>token.revoked</code></td><td>Se revoca un token de ese cliente — por el usuario dueño (self-service), por un admin (Sesiones activas, sección 4), o en cascada al revocar el cliente entero.</td><td>Los webhooks de <strong>ese</strong> cliente.</td></tr>
<tr><td><code>user.deactivated</code></td><td>Un admin desactiva un usuario.</td><td>Los webhooks de todo cliente que tenga al menos un token vigente para ese usuario en ese momento.</td></tr>
</table>
<p>Con <code>token.revoked</code> tu app se entera al instante de que ya no tiene acceso a la cuenta de ese
usuario (por ejemplo, para cerrarle la sesión local), en vez de esperar a que el token expire solo.</p>
<h3>URLs permitidas</h3>
<div class="box warn">
<strong>Protección anti-SSRF:</strong> la URL de un webhook tiene que resolver a una dirección pública.
Se rechazan <code>localhost</code>, IPs loopback (<code>127.0.0.0/8</code>), privadas
(<code>10.0.0.0/8</code>, <code>172.16.0.0/12</code>, <code>192.168.0.0/16</code>) y de link-local
(<code>169.254.0.0/16</code> — incluye el endpoint de metadata de instancia en AWS/GCP/Azure,
<code>169.254.169.254</code>). También esquemas que no sean <code>http</code>/<code>https</code>.
Sin esto, cualquiera que registre un client podría hacer que este servidor le pegue a servicios internos
(Redis, la propia API, metadata de la nube) y leer la respuesta en el panel — el clásico ataque SSRF.
La validación se repite justo antes de cada envío, no solo al crear el webhook: un dominio público hoy
podría resolver a una IP interna mañana (DNS rebinding).
</div>
<h3>Verificar la firma</h3>
<p>
Cada entrega incluye el header <code>X-Webhook-Signature: sha256={firma}</code>, un HMAC-SHA256 del body crudo
(JSON) usando el <code>secret</code> que se te mostró al crear el webhook (una sola vez, igual que un
<code>client_secret</code> — si lo perdiste, borrá el webhook y creá uno nuevo). Verificalo antes de confiar en
el payload:
</p>
<pre><code>// Node.js
import crypto from 'crypto';
function isValid(rawBody: string, signatureHeader: string, secret: string): boolean {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}</code></pre>
<p>El body tiene forma <code>{"event": "token.revoked", "data": {...}}</code>. El header
<code>X-Webhook-Event</code> repite el nombre del evento, por si preferís ramificar por header sin parsear el body.</p>
<h3>Reintentos</h3>
<div class="box">
Si tu endpoint no responde <code>2xx</code> (o no responde en 5 segundos), se reintenta hasta 2 veces más
con backoff creciente (10s, 60s, 300s). Después de 3 intentos fallidos, se abandona esa entrega — no hay
cola de reintento infinita. Cada intento (éxito o fracaso) queda registrado; el panel muestra el estado del
último envío junto a cada webhook (código de respuesta o "pendiente").
</div>
<div class="box warn">
<strong>Pausar en vez de borrar:</strong> si tu endpoint va a estar caído un rato (deploy, mantenimiento),
pausalo desde el panel en vez de dejar que fallen los reintentos — un webhook pausado no genera intentos de
entrega hasta que lo reactivés.
</div>
<h3>Historial de entregas y reintento manual</h3>
<p>
El botón <strong>"Ver historial"</strong> de cada webhook (junto a Pausar/Borrar) abre el listado paginado de
todas las entregas — no solo la última. Cada fila muestra el código de respuesta (o "pendiente" si nunca
respondió), el evento, cuándo se creó y cuántos intentos acumula. Las entregas que no llegaron a un
<code>2xx</code> tienen un botón <strong>"Reintentar"</strong>: vuelve a encolar el mismo registro (no crea
uno nuevo — el contador de intentos sigue acumulando sobre esa entrega) para forzar un envío inmediato sin
esperar al backoff automático. Útil después de arreglar algo en tu endpoint y no querer esperar los 5 minutos
del backoff.
</p>
<p>Cada alta y baja de un webhook queda en el audit log (sección 7): <code>client.webhook_created</code> /
<code>client.webhook_deleted</code>.</p>
</section>
<section>
<h2>13. Documentación interactiva (Swagger)</h2>
<p>Disponible en <code>http://localhost:8000/api/documentation</code>. Incluye el botón <strong>Authorize</strong>
para probar los tres flujos (Authorization Code, Client Credentials, Password) directamente desde el navegador,
usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
@@ -566,16 +825,27 @@ usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
</section>
<section>
<h2>11. Referencia rápida de rutas</h2>
<h2>14. Referencia rápida de rutas</h2>
<h3>Interfaz web (sesión, uso interno de esta app)</h3>
<table>
<tr><th>Ruta</th><th>Qué hace</th></tr>
<tr><td><code>POST /web-api/register</code></td><td>Registro — crea la cuenta pendiente, no loguea.</td></tr>
<tr><td><code>POST /web-api/login</code> / <code>/logout</code></td><td>Autenticación por sesión.</td></tr>
<tr><td><code>GET /web-api/me</code></td><td>Usuario logueado (con roles/permisos).</td></tr>
<tr><td><code>POST /web-api/login</code> / <code>/logout</code></td><td>Autenticación por sesión. Si el usuario tiene 2FA, <code>/login</code> devuelve <code>{"two_factor":true}</code> en vez de loguear.</td></tr>
<tr><td><code>POST /web-api/login/2fa</code></td><td>Segundo paso del login con 2FA activo (código TOTP o de recuperación).</td></tr>
<tr><td><code>POST /web-api/forgot-password</code></td><td>Pedir el enlace de restablecimiento (respuesta genérica, exista o no la cuenta).</td></tr>
<tr><td><code>POST /web-api/reset-password</code></td><td>Confirmar el restablecimiento con el token del email — revoca todas las sesiones activas.</td></tr>
<tr><td><code>GET /web-api/me</code></td><td>Usuario logueado (con roles/permisos y <code>two_factor_enabled</code>).</td></tr>
<tr><td><code>POST /web-api/2fa/setup</code></td><td>Genera secreto pendiente + QR para activar 2FA.</td></tr>
<tr><td><code>POST /web-api/2fa/confirm</code></td><td>Confirma la activación con un código real — devuelve los códigos de recuperación una vez.</td></tr>
<tr><td><code>POST /web-api/2fa/disable</code></td><td>Desactiva 2FA (requiere contraseña actual).</td></tr>
<tr><td><code>POST /web-api/2fa/recovery-codes</code></td><td>Regenera los códigos de recuperación (requiere contraseña actual).</td></tr>
<tr><td><code>GET/POST/PUT/DELETE /web-api/clients</code></td><td>Clientes OAuth propios.</td></tr>
<tr><td><code>POST /web-api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> de un cliente confidencial propio (secret anterior válido 48h más).</td></tr>
<tr><td><code>GET/POST/DELETE /web-api/personal-access-tokens</code></td><td>Personal Access Tokens propios.</td></tr>
<tr><td><code>GET/DELETE /web-api/authorized-apps</code></td><td>Apps de terceros autorizadas.</td></tr>
<tr><td><code>GET/POST/PUT/DELETE /web-api/clients/{id}/webhooks</code></td><td>Webhooks de un cliente propio.</td></tr>
<tr><td><code>GET /web-api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de entregas de un webhook propio.</td></tr>
<tr><td><code>POST /web-api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega.</td></tr>
<tr><td><code>GET/POST/PUT/DELETE /web-api/admin/users</code></td><td>Administración de usuarios (admin).</td></tr>
<tr><td><code>GET /web-api/admin/users/trashed</code></td><td>Usuarios en la papelera (admin).</td></tr>
<tr><td><code>POST /web-api/admin/users/{id}/restore</code></td><td>Restaurar un usuario borrado (admin).</td></tr>
@@ -584,7 +854,12 @@ usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
<tr><td><code>POST /web-api/admin/users/{id}/approve</code></td><td>Aprobar un registro pendiente (admin).</td></tr>
<tr><td><code>GET /web-api/admin/apps</code></td><td>Listado de todas las apps registradas (admin).</td></tr>
<tr><td><code>GET/POST/DELETE /web-api/admin/users/{id}/app-roles</code></td><td>Roles por app de un usuario (admin).</td></tr>
<tr><td><code>GET /web-api/admin/analytics/summary</code></td><td>Sesiones, logins y actividad (admin).</td></tr>
<tr><td><code>GET /web-api/admin/users/{id}/sessions</code></td><td>Tokens OAuth activos de un usuario (admin).</td></tr>
<tr><td><code>DELETE /web-api/admin/users/{id}/sessions/{tokenId}</code></td><td>Revocar una sesión puntual (admin).</td></tr>
<tr><td><code>DELETE /web-api/admin/users/{id}/sessions</code></td><td>Revocar todas las sesiones activas de un usuario (admin).</td></tr>
<tr><td><code>GET /web-api/admin/analytics/summary</code></td><td>Sesiones, logins, funnel de autorización, tokens activos, uso de scopes y actividad (admin).</td></tr>
<tr><td><code>GET /web-api/admin/audit-log</code></td><td>Historial de auditoría paginado y filtrable (admin).</td></tr>
<tr><td><code>POST /web-api/oauth/authorize-events</code></td><td>Registro interno del funnel de autorización (llamado por la pantalla de consentimiento, no pensado para uso directo).</td></tr>
<tr><td><code>POST /web-api/locale</code></td><td>Cambiar idioma (es/en/ca).</td></tr>
</table>
@@ -594,6 +869,13 @@ usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
<tr><td><code>GET /oauth/authorize</code></td><td>Inicio del flujo Authorization Code (pantalla de consentimiento).</td></tr>
<tr><td><code>POST /oauth/token</code></td><td>Emisión/renovación de tokens (todos los grant types).</td></tr>
</table>
<div class="box warn">
<strong>Rate limiting:</strong> todas las rutas <code>/oauth/*</code> admiten 20 requests por minuto por
combinación de IP + <code>client_id</code> — protege especialmente al Password Grant (usuario/contraseña
directos contra <code>/oauth/token</code>) de fuerza bruta, sin penalizar a otros clientes si uno solo
está siendo atacado. Va además del <code>throttle</code> genérico (60/min por IP) que Passport ya trae
por defecto en <code>/oauth/token</code>.
</div>
<h3>API REST (Bearer token, para apps externas — Angular, Flutter, lo que sea)</h3>
<table>
@@ -606,11 +888,17 @@ usando el cliente de ejemplo <em>"Swagger UI"</em> sembrado (sección 3).</p>
<tr><td><code>GET /api/users/pending</code></td><td>Registros pendientes de aprobación.</td></tr>
<tr><td><code>POST /api/users/{id}/approve</code></td><td>Aprobar un registro pendiente.</td></tr>
<tr><td><code>GET/POST/DELETE /api/users/{id}/app-roles</code></td><td>Roles por app de un usuario.</td></tr>
<tr><td><code>GET/DELETE /api/users/{id}/sessions</code></td><td>Tokens OAuth activos de un usuario; revocar uno o todos.</td></tr>
<tr><td><code>GET /api/apps</code></td><td>Listado de apps registradas.</td></tr>
<tr><td><code>GET /api/analytics/summary</code></td><td>Sesiones, logins y actividad.</td></tr>
<tr><td><code>GET /api/analytics/summary</code></td><td>Sesiones, logins, funnel de autorización, tokens activos, uso de scopes y actividad.</td></tr>
<tr><td><code>GET /api/audit-log</code></td><td>Historial de auditoría paginado y filtrable.</td></tr>
<tr><td><code>GET/POST/PUT/DELETE /api/clients</code></td><td>Tus clientes OAuth.</td></tr>
<tr><td><code>POST /api/clients/{id}/rotate-secret</code></td><td>Rotar el <code>client_secret</code> de un cliente confidencial (secret anterior válido 48h más).</td></tr>
<tr><td><code>GET/POST/DELETE /api/personal-access-tokens</code></td><td>Tus Personal Access Tokens.</td></tr>
<tr><td><code>GET/DELETE /api/authorized-apps</code></td><td>Apps de terceros que autorizaste.</td></tr>
<tr><td><code>GET/POST/PUT/DELETE /api/clients/{id}/webhooks</code></td><td>Webhooks de uno de tus clientes.</td></tr>
<tr><td><code>GET /api/clients/{id}/webhooks/{webhookId}/deliveries</code></td><td>Historial paginado de entregas de un webhook.</td></tr>
<tr><td><code>POST /api/clients/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/retry</code></td><td>Reintentar manualmente una entrega.</td></tr>
</table>
<footer class="pagefoot">OAuth2 Identity Server — Manual de uso · generado automáticamente a partir del estado actual del proyecto.</footer>