new funcionality
This commit is contained in:
@@ -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=...&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>
|
||||
|
||||
Reference in New Issue
Block a user