Initial commit: OAuth IAM Laravel application
Laravel-based OAuth/IAM service with Passport, Vue 3 frontend, and Docker Compose setup for local and production deployment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
108
docs/despliegue.md
Normal file
108
docs/despliegue.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Despliegue en producción (Docker)
|
||||
|
||||
Stack: `app` (PHP-FPM 8.3) + `nginx` (estáticos y proxy) + `db` (PostgreSQL 16) + `redis`
|
||||
(cache/sesión/colas) + `queue` (worker) + `scheduler`.
|
||||
|
||||
## 1. Preparar el `.env`
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env
|
||||
```
|
||||
|
||||
Editar `.env` y completar como mínimo:
|
||||
|
||||
- `APP_URL` — dominio real, con `https://`.
|
||||
- `DB_PASSWORD` — contraseña de PostgreSQL (usada también por el servicio `db`).
|
||||
- `SESSION_DOMAIN` — dominio (o `.tu-dominio.com` si hay subdominios).
|
||||
- `APP_KEY` — generar uno nuevo, no reutilizar el de desarrollo:
|
||||
|
||||
```bash
|
||||
docker run --rm -v "$PWD":/app -w /app composer:2 sh -c \
|
||||
"php -r \"echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;\""
|
||||
```
|
||||
|
||||
Pegar el resultado en `APP_KEY=`.
|
||||
|
||||
## 2. Levantar el stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
En el primer arranque, el contenedor `app` (ver `docker/php/entrypoint.sh`):
|
||||
|
||||
1. Genera las claves RSA de Passport (`storage/oauth-private.key` / `.pub`) si no existen —
|
||||
quedan persistidas en el volumen `app_storage`, **no se regeneran en cada deploy** (regenerarlas
|
||||
invalidaría todos los tokens ya emitidos).
|
||||
2. Corre `php artisan migrate --force`.
|
||||
3. Cachea config/rutas/vistas y regenera la documentación de Swagger.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml logs -f app
|
||||
```
|
||||
|
||||
## 3. Crear el primer usuario admin
|
||||
|
||||
**Recomendado para producción:** no correr `db:seed` (ese seeder crea datos de demo con
|
||||
contraseñas conocidas). En su lugar, con la base de usuarios vacía, andá a `/register` y creá una
|
||||
cuenta — al ser la primera, el sistema la marca automáticamente como **admin**, aprobada y con la
|
||||
sesión iniciada (no hace falta que nadie la apruebe, porque todavía no existe ningún admin). Cualquier
|
||||
registro posterior vuelve a requerir aprobación manual desde el panel de usuarios.
|
||||
|
||||
Alternativa solo para entornos de prueba/demo, con datos de ejemplo (usuarios, clientes OAuth, historial
|
||||
de logins falso):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml exec app php artisan db:seed --force
|
||||
```
|
||||
|
||||
Esto sí crea `admin@example.com` / `password` — **cambiar la contraseña de inmediato** si se usa
|
||||
fuera de un entorno descartable.
|
||||
|
||||
## 4. TLS
|
||||
|
||||
Por defecto `nginx` sirve solo HTTP en el puerto 80 (`docker/nginx/default.conf`). Para HTTPS:
|
||||
|
||||
1. Conseguir certificado (por ejemplo con `certbot --webroot` apuntando a
|
||||
`docker/nginx/certs`, o cualquier CA propia).
|
||||
2. Colocar `fullchain.pem` y `privkey.pem` en `docker/nginx/certs/`.
|
||||
3. Descomentar el bloque `server { listen 443 ... }` en `docker/nginx/default.conf` y el
|
||||
`location /` que redirige a HTTPS.
|
||||
4. Descomentar `"443:443"` en `docker-compose.prod.yml` (servicio `nginx`).
|
||||
5. `docker compose -f docker-compose.prod.yml up -d --build nginx`.
|
||||
|
||||
## 5. Desplegar una actualización
|
||||
|
||||
```bash
|
||||
git pull # o el mecanismo que uses para traer código nuevo
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
Las claves de Passport, la base de datos y Redis viven en volúmenes con nombre
|
||||
(`app_storage`, `db_data`, `redis_data`) — sobreviven a los rebuilds. El código y los assets del
|
||||
frontend, en cambio, se hornean dentro de la imagen en cada build (no hay bind mounts de código en
|
||||
producción).
|
||||
|
||||
## 6. Backups
|
||||
|
||||
Mínimo indispensable a respaldar:
|
||||
|
||||
- Volumen `db_data` (o mejor, `pg_dump` periódico contra el servicio `db`).
|
||||
- Volumen `app_storage` — contiene, entre otras cosas, las claves de Passport. Perderlas invalida
|
||||
todos los tokens emitidos hasta ese momento.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml exec db \
|
||||
pg_dump -U "$DB_USERNAME" "$DB_DATABASE" > backup-$(date +%F).sql
|
||||
```
|
||||
|
||||
## Servicios del compose
|
||||
|
||||
| Servicio | Rol |
|
||||
|-------------|-----|
|
||||
| `app` | PHP-FPM. Corre migraciones y cacheo al arrancar (`CONTAINER_ROLE=app`). |
|
||||
| `queue` | `php artisan queue:work redis` — misma imagen que `app`, sin el paso de migración. |
|
||||
| `scheduler` | `php artisan schedule:work` — reemplaza al cron del sistema. |
|
||||
| `nginx` | Sirve `public/build` y hace proxy de `*.php` a `app:9000`. |
|
||||
| `db` | PostgreSQL 16, datos en el volumen `db_data`. |
|
||||
| `redis` | Cache, sesiones y colas, datos en el volumen `redis_data` (AOF activado). |
|
||||
602
docs/integracion-iam.html
Normal file
602
docs/integracion-iam.html
Normal file
@@ -0,0 +1,602 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Integrar tu app con este servidor de identidad (IAM)</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 22mm 18mm;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||
color: #1f2430;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.cover {
|
||||
height: 245mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
page-break-after: always;
|
||||
}
|
||||
.cover .badge {
|
||||
display: inline-block;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 18px;
|
||||
width: fit-content;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 32px;
|
||||
margin: 0 0 10px;
|
||||
color: #111827;
|
||||
}
|
||||
.cover p.subtitle {
|
||||
font-size: 15px;
|
||||
color: #4b5563;
|
||||
max-width: 440px;
|
||||
margin: 0 0 28px;
|
||||
}
|
||||
.cover .meta {
|
||||
font-size: 11.5px;
|
||||
color: #9ca3af;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 12px;
|
||||
width: fit-content;
|
||||
}
|
||||
h1, h2, h3 { color: #111827; }
|
||||
h2 {
|
||||
font-size: 19px;
|
||||
margin-top: 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #4f46e5;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14.5px;
|
||||
margin-bottom: 6px;
|
||||
color: #4338ca;
|
||||
}
|
||||
section {
|
||||
page-break-before: always;
|
||||
padding-top: 4mm;
|
||||
}
|
||||
section:first-of-type { page-break-before: avoid; }
|
||||
p { margin: 6px 0 10px; }
|
||||
ul, ol { margin: 6px 0 12px; padding-left: 20px; }
|
||||
li { margin-bottom: 4px; }
|
||||
code {
|
||||
background: #f1f0fb;
|
||||
color: #4338ca;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
pre {
|
||||
background: #1e1e2e;
|
||||
color: #e2e2f5;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 10.3px;
|
||||
line-height: 1.5;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
pre code { background: none; color: inherit; padding: 0; }
|
||||
.lang-tag {
|
||||
display: inline-block;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .03em;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 16px;
|
||||
font-size: 11.5px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 6px 9px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f5fb;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) td { background: #fafafa; }
|
||||
.box {
|
||||
background: #f5f5fb;
|
||||
border-left: 4px solid #4f46e5;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
margin: 12px 0;
|
||||
font-size: 11.8px;
|
||||
}
|
||||
.box.warn {
|
||||
background: #fffbeb;
|
||||
border-left-color: #d97706;
|
||||
}
|
||||
.box.danger {
|
||||
background: #fef2f2;
|
||||
border-left-color: #dc2626;
|
||||
}
|
||||
.box strong { color: #111827; }
|
||||
.toc { list-style: none; padding-left: 0; }
|
||||
.toc li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px dotted #d1d5db;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.toc .n { color: #4f46e5; font-weight: 600; margin-right: 8px; }
|
||||
.pill {
|
||||
display: inline-block;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
border-radius: 4px;
|
||||
padding: 1px 7px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.stack-grid {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 12px 0 18px;
|
||||
}
|
||||
.stack-card {
|
||||
flex: 1;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.stack-card h4 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #4338ca;
|
||||
}
|
||||
.stack-card p {
|
||||
margin: 0;
|
||||
font-size: 10.8px;
|
||||
color: #6b7280;
|
||||
}
|
||||
footer.pagefoot {
|
||||
margin-top: 22px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="cover">
|
||||
<span class="badge">Guía para desarrolladores</span>
|
||||
<h1>Integrar tu app con este servidor de identidad</h1>
|
||||
<p class="subtitle">Cómo conectar Angular, NestJS, Flutter (o cualquier otro stack) a este servidor OAuth2
|
||||
para usarlo como IAM centralizado: login, usuarios, roles globales y roles por-app.</p>
|
||||
<div class="meta">
|
||||
Laravel 12 · Passport 13 (OAuth2, tokens JWT firmados RS256)<br>
|
||||
Generado el 25 de julio de 2026
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Índice</h2>
|
||||
<ul class="toc">
|
||||
<li><span><span class="n">1.</span>Qué te da este servidor</span></li>
|
||||
<li><span><span class="n">2.</span>Paso 1 — Registrar tu app como cliente OAuth</span></li>
|
||||
<li><span><span class="n">3.</span>Elegí el flujo según tu arquitectura</span></li>
|
||||
<li><span><span class="n">4.</span>Angular (SPA) — Authorization Code + PKCE</span></li>
|
||||
<li><span><span class="n">5.</span>NestJS — backend con login de usuarios</span></li>
|
||||
<li><span><span class="n">6.</span>NestJS — servicio backend-a-backend (Client Credentials)</span></li>
|
||||
<li><span><span class="n">7.</span>Flutter / apps móviles</span></li>
|
||||
<li><span><span class="n">8.</span>Validar el token en tu backend</span></li>
|
||||
<li><span><span class="n">9.</span>Roles: global vs. por-app</span></li>
|
||||
<li><span><span class="n">10.</span>Refrescar y revocar tokens</span></li>
|
||||
<li><span><span class="n">11.</span>Checklist de seguridad</span></li>
|
||||
<li><span><span class="n">12.</span>Referencia rápida</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Qué te da este servidor</h2>
|
||||
<p>
|
||||
Es un <strong>servidor de identidad OAuth2</strong> (Laravel Passport). En vez de que cada app tuya tenga su
|
||||
propia tabla de usuarios y su propio login, todas hablan con este servidor: acá vive el usuario, la
|
||||
contraseña, y quién es qué.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Login centralizado</strong> — un solo lugar donde el usuario pone su contraseña. Tu app nunca
|
||||
la ve: recibe un <em>access token</em> después de que el usuario aprueba el acceso.</li>
|
||||
<li><strong>Datos de usuario vía API</strong> — <code>GET /api/user</code> con el token te devuelve nombre,
|
||||
email, y sus roles.</li>
|
||||
<li><strong>Roles globales y roles por-app</strong> — un usuario puede ser <code>admin</code> en el sistema
|
||||
en general, pero además tener un rol <em>específico de tu app</em> (por ejemplo <code>editor</code> en la
|
||||
tuya y nada en la de otro equipo). Ver sección 9.</li>
|
||||
<li><strong>Gestión completa por API</strong> — si querés armar tu propio panel de administración de
|
||||
usuarios (en vez de usar el que trae este servidor), toda la funcionalidad está expuesta en
|
||||
<code>/api/*</code> con Bearer token. No hace falta sesión ni cookies de este servidor.</li>
|
||||
</ul>
|
||||
<div class="box">
|
||||
<strong>Dos capas de permisos, no confundirlas:</strong> los <em>scopes</em> del token OAuth (
|
||||
<code>users:read</code>, <code>users:write</code>...) definen qué puede hacer <em>ese token</em> contra la
|
||||
API de este servidor. Los <em>roles por-app</em> definen qué puede hacer <em>ese usuario dentro de tu app</em>
|
||||
— son datos que vos leés e interpretás como quieras, este servidor no te impone nada sobre cómo usarlos.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Paso 1 — Registrar tu app como cliente OAuth</h2>
|
||||
<p>Cada aplicación que se conecte necesita ser un <em>cliente</em> registrado. Se hace una sola vez, desde el
|
||||
panel (<code>/clients</code>, con cualquier cuenta) o por API:</p>
|
||||
<pre><code>curl -X POST http://localhost:8000/api/clients \
|
||||
-H "Authorization: Bearer {tu_access_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Mi App Angular",
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uris": ["https://mi-app.com/callback"],
|
||||
"confidential": false
|
||||
}'</code></pre>
|
||||
<p>La respuesta trae <code>client.id</code> (el <code>client_id</code>) y, si es confidencial,
|
||||
<code>plain_secret</code> — <strong>se muestra una sola vez</strong>, guardalo ya.</p>
|
||||
<table>
|
||||
<tr><th>Campo</th><th>Qué significa</th></tr>
|
||||
<tr><td><code>grant_type</code></td><td><code>authorization_code</code>, <code>client_credentials</code> o <code>password</code> — ver sección 3 para elegir.</td></tr>
|
||||
<tr><td><code>redirect_uris</code></td><td>A dónde te devuelve el navegador después del login. Tiene que matchear exacto con lo que mandes en <code>/oauth/authorize</code>.</td></tr>
|
||||
<tr><td><code>confidential</code></td><td><code>false</code> = cliente público, sin secret (SPA, apps móviles — usan PKCE en su lugar). <code>true</code> = confidencial, con secret (backends que pueden guardar un secreto de forma segura).</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Elegí el flujo según tu arquitectura</h2>
|
||||
<div class="stack-grid">
|
||||
<div class="stack-card">
|
||||
<h4>SPA pura (Angular, React, Vue)</h4>
|
||||
<p>Sin backend propio, corre 100% en el navegador. <strong>Authorization Code + PKCE</strong>, cliente
|
||||
público. El token queda en el navegador.</p>
|
||||
</div>
|
||||
<div class="stack-card">
|
||||
<h4>App con backend (NestJS, Express)</h4>
|
||||
<p>El backend maneja el login por vos (patrón BFF). <strong>Authorization Code</strong>, cliente
|
||||
confidencial — el secret vive en el backend, nunca llega al navegador.</p>
|
||||
</div>
|
||||
<div class="stack-card">
|
||||
<h4>App móvil (Flutter, RN)</h4>
|
||||
<p><strong>Authorization Code + PKCE</strong>, cliente público, con un redirect por deep link / custom
|
||||
scheme.</p>
|
||||
</div>
|
||||
<div class="stack-card">
|
||||
<h4>Servicio sin usuario final</h4>
|
||||
<p>Un cron, un worker, otro backend que llama a la API por su cuenta. <strong>Client Credentials</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<strong>Password Grant existe pero evitalo</strong> salvo que sea estrictamente first-party (una app que vos
|
||||
mismo controlás de punta a punta): tu app le pide el usuario y contraseña directamente al usuario y se los
|
||||
manda a este servidor. Ningún cliente de terceros debería tocar la contraseña del usuario.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Angular (SPA) — Authorization Code + PKCE</h2>
|
||||
<p>Ejemplo con <code>angular-oauth2-oidc</code> (también podés implementarlo a mano, son cuatro pasos: generar
|
||||
verifier/challenge, redirigir, recibir el <code>code</code>, canjearlo).</p>
|
||||
|
||||
<span class="lang-tag">app.config.ts</span>
|
||||
<pre><code>import { provideOAuthClient, OAuthModule } from 'angular-oauth2-oidc';
|
||||
|
||||
export const authConfig = {
|
||||
issuer: 'http://localhost:8000',
|
||||
loginUrl: 'http://localhost:8000/oauth/authorize',
|
||||
tokenEndpoint: 'http://localhost:8000/oauth/token',
|
||||
redirectUri: window.location.origin + '/callback',
|
||||
clientId: '{tu_client_id}',
|
||||
responseType: 'code',
|
||||
scope: 'users:read account:read',
|
||||
useHttpBasicAuth: false,
|
||||
disablePKCE: false, // clave: PKCE prendido, es cliente público
|
||||
oidc: false, // este servidor es OAuth2 puro, no expone id_token/OIDC
|
||||
};</code></pre>
|
||||
|
||||
<span class="lang-tag">auth.service.ts</span>
|
||||
<pre><code>import { Injectable } from '@angular/core';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
constructor(private oauth: OAuthService) {}
|
||||
|
||||
login() {
|
||||
this.oauth.initCodeFlow(); // redirige a /oauth/authorize con PKCE
|
||||
}
|
||||
|
||||
async me() {
|
||||
const token = this.oauth.getAccessToken();
|
||||
const res = await fetch('http://localhost:8000/api/user', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return res.json(); // { id, name, email, roles, app_roles, ... }
|
||||
}
|
||||
}</code></pre>
|
||||
<p>Si preferís no traer una librería, el flujo manual es: generar <code>code_verifier</code> (random) y
|
||||
<code>code_challenge</code> (SHA-256 + base64url del verifier), armar la URL de
|
||||
<code>/oauth/authorize</code> con esos parámetros + <code>state</code>, y en el callback hacer
|
||||
<code>POST /oauth/token</code> con <code>grant_type=authorization_code</code>, el <code>code</code> recibido y
|
||||
el <code>code_verifier</code> original.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. NestJS — backend con login de usuarios</h2>
|
||||
<p>Tu backend actúa de cliente <strong>confidencial</strong> (guarda el secret) y maneja el flujo Authorization
|
||||
Code por el usuario — patrón "backend for frontend": el navegador nunca ve el access token, solo una cookie de
|
||||
sesión propia de tu app.</p>
|
||||
|
||||
<span class="lang-tag">auth.controller.ts</span>
|
||||
<pre><code>@Controller('auth')
|
||||
export class AuthController {
|
||||
private readonly IAM = 'http://localhost:8000';
|
||||
private readonly CLIENT_ID = process.env.IAM_CLIENT_ID;
|
||||
private readonly CLIENT_SECRET = process.env.IAM_CLIENT_SECRET;
|
||||
|
||||
@Get('login')
|
||||
login(@Res() res: Response) {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.CLIENT_ID,
|
||||
redirect_uri: 'https://mi-backend.com/auth/callback',
|
||||
response_type: 'code',
|
||||
scope: 'users:read',
|
||||
state: crypto.randomUUID(),
|
||||
});
|
||||
res.redirect(`${this.IAM}/oauth/authorize?${params}`);
|
||||
}
|
||||
|
||||
@Get('callback')
|
||||
async callback(@Query('code') code: string, @Session() session: Record<string, any>) {
|
||||
const { data } = await axios.post(`${this.IAM}/oauth/token`, {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.CLIENT_ID,
|
||||
client_secret: this.CLIENT_SECRET,
|
||||
code,
|
||||
redirect_uri: 'https://mi-backend.com/auth/callback',
|
||||
});
|
||||
|
||||
session.accessToken = data.access_token; // guardalo del lado del servidor
|
||||
session.refreshToken = data.refresh_token;
|
||||
}
|
||||
}</code></pre>
|
||||
<p>Como es cliente confidencial, no hace falta PKCE (aunque no molesta agregarlo igual). El token queda
|
||||
guardado en la sesión del backend, nunca expuesto al navegador.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. NestJS — servicio backend-a-backend (Client Credentials)</h2>
|
||||
<p>Para un servicio que consulta/gestiona usuarios por su cuenta, sin un usuario logueado de por medio —
|
||||
scheduler, worker, integración entre sistemas.</p>
|
||||
|
||||
<span class="lang-tag">iam-client.service.ts</span>
|
||||
<pre><code>@Injectable()
|
||||
export class IamClientService {
|
||||
private token: string | null = null;
|
||||
private expiresAt = 0;
|
||||
|
||||
private async getToken(): Promise<string> {
|
||||
if (this.token && Date.now() < this.expiresAt) return this.token;
|
||||
|
||||
const { data } = await axios.post('http://localhost:8000/oauth/token', {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: process.env.IAM_SERVICE_CLIENT_ID,
|
||||
client_secret: process.env.IAM_SERVICE_CLIENT_SECRET,
|
||||
scope: 'users:read users:write',
|
||||
});
|
||||
|
||||
this.token = data.access_token;
|
||||
this.expiresAt = Date.now() + (data.expires_in - 60) * 1000; // renovar 1 min antes
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async listUsers() {
|
||||
const token = await this.getToken();
|
||||
const { data } = await axios.get('http://localhost:8000/api/users', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}</code></pre>
|
||||
<div class="box">
|
||||
Un token de <code>client_credentials</code> no tiene usuario asociado: actúa como "servicio de confianza".
|
||||
En este servidor eso significa que puede usar <code>/api/users</code> y <code>/api/apps</code> con el
|
||||
scope como único chequeo (sin necesitar un permiso de usuario admin) — pero <strong>no</strong> puede tocar
|
||||
recursos "de cuenta propia" (<code>/api/clients</code>, personal access tokens, apps autorizadas), porque
|
||||
esos son inherentemente de un usuario y no tiene ninguno asociado.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Flutter / apps móviles</h2>
|
||||
<p>Con el paquete <code>flutter_appauth</code> (implementa Authorization Code + PKCE con el navegador del
|
||||
sistema, que es lo recomendado — evita usar un WebView embebido).</p>
|
||||
|
||||
<span class="lang-tag">auth_service.dart</span>
|
||||
<pre><code>final appAuth = FlutterAppAuth();
|
||||
|
||||
Future<String?> login() async {
|
||||
final result = await appAuth.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
'{tu_client_id}',
|
||||
'com.miapp://callback', // custom scheme registrado en tu app
|
||||
serviceConfiguration: const AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint: 'http://localhost:8000/oauth/authorize',
|
||||
tokenEndpoint: 'http://localhost:8000/oauth/token',
|
||||
),
|
||||
scopes: ['users:read'],
|
||||
),
|
||||
);
|
||||
|
||||
return result.accessToken;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> me(String token) async {
|
||||
final res = await http.get(
|
||||
Uri.parse('http://localhost:8000/api/user'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}</code></pre>
|
||||
<p>El <code>redirect_uri</code> (<code>com.miapp://callback</code>) tiene que estar registrado exactamente
|
||||
igual en el cliente OAuth (sección 2) y configurado en tu app nativa (Android: intent filter; iOS: URL scheme
|
||||
en el <code>Info.plist</code>).</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. Validar el token en tu backend</h2>
|
||||
<p>Cuando tu backend recibe un request con <code>Authorization: Bearer {token}</code> y necesita confirmar que
|
||||
es válido, hay dos caminos:</p>
|
||||
|
||||
<h3>Opción A — Preguntarle a este servidor (más simple)</h3>
|
||||
<pre><code>GET /api/user
|
||||
Authorization: Bearer {token}</code></pre>
|
||||
<p>Si responde <code>200</code> con los datos del usuario, el token es válido. Es un request de red extra por
|
||||
cada llamada, pero es siempre exacto: refleja revocaciones al instante.</p>
|
||||
|
||||
<h3>Opción B — Validar el JWT localmente (sin red)</h3>
|
||||
<p>Los access tokens de este servidor son <strong>JWT firmados con RS256</strong> (no opacos). Tu backend puede
|
||||
verificar la firma con la clave pública de Passport (<code>storage/oauth-public.key</code> en este proyecto —
|
||||
pedísela al equipo que administra este servidor) sin llamarlo por red:</p>
|
||||
<pre><code>// Node.js, con la librería "jsonwebtoken"
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
|
||||
const publicKey = fs.readFileSync('oauth-public.key');
|
||||
|
||||
function verify(token: string) {
|
||||
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
|
||||
// -> { aud, jti, iat, nbf, exp, sub, scopes: string[] }
|
||||
}</code></pre>
|
||||
<p>Claims relevantes: <code>sub</code> = id del usuario (ausente en tokens de <code>client_credentials</code>),
|
||||
<code>aud</code> = <code>client_id</code> que lo pidió, <code>scopes</code> = array de scopes otorgados,
|
||||
<code>exp</code> = expiración (unix timestamp).</p>
|
||||
<div class="box warn">
|
||||
<strong>Trade-off:</strong> la validación local es más rápida (sin red) pero no se entera si el token fue
|
||||
<em>revocado</em> antes de su expiración natural — solo valida firma y expiración. Para casos sensibles
|
||||
(borrar acceso ya mismo), usá la Opción A o combiná: validación local + chequeo periódico contra el
|
||||
servidor.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Roles: global vs. por-app</h2>
|
||||
<p><code>GET /api/user</code> (o <code>/api/users/{id}</code>) devuelve, entre otros campos:</p>
|
||||
<pre><code>{
|
||||
"id": 2,
|
||||
"name": "Carla Gómez",
|
||||
"email": "carla@example.com",
|
||||
"roles": ["user"],
|
||||
"app_roles": ["editor"]
|
||||
}</code></pre>
|
||||
<ul>
|
||||
<li><code>roles</code> — el rol <strong>global</strong> del usuario en este servidor (<code>admin</code> o
|
||||
<code>user</code>). No dice nada sobre qué puede hacer en <em>tu</em> app.</li>
|
||||
<li><code>app_roles</code> — los roles que un admin le asignó a ese usuario <strong>específicamente para el
|
||||
cliente OAuth que hizo la request</strong> (o sea: tu app). Se resuelve solo, según el <code>client_id</code>
|
||||
del token usado — no hace falta pedir nada extra.</li>
|
||||
</ul>
|
||||
<p>Un admin asigna estos roles desde <code>/admin/users</code> → editar usuario → "Roles por app", eligiendo tu
|
||||
app de la lista. Interpretalos como quieras del lado tuyo (un guard de NestJS, un route guard de Angular, etc.)
|
||||
— este servidor solo los guarda y te los devuelve.</p>
|
||||
<span class="lang-tag">nestjs guard (ejemplo)</span>
|
||||
<pre><code>@Injectable()
|
||||
export class AppRoleGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const user = await this.iam.me(req.headers.authorization);
|
||||
return user.app_roles.includes('editor');
|
||||
}
|
||||
}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Refrescar y revocar tokens</h2>
|
||||
<p>Los access tokens expiran (15 días por defecto en este servidor). Si tu cliente pidió el scope
|
||||
<code>refresh_token</code> implícito del grant (viene incluido en <code>authorization_code</code> y
|
||||
<code>password</code>), la respuesta de <code>/oauth/token</code> trae un <code>refresh_token</code> — usalo
|
||||
así, sin volver a pasar por el login:</p>
|
||||
<pre><code>curl -X POST http://localhost:8000/oauth/token \
|
||||
-d grant_type=refresh_token \
|
||||
-d refresh_token={tu_refresh_token} \
|
||||
-d client_id={tu_client_id} \
|
||||
-d client_secret={tu_client_secret} # si tu cliente es confidencial</code></pre>
|
||||
<p><code>client_credentials</code> no tiene refresh token — simplemente pedís uno nuevo cuando el anterior
|
||||
expira (ver el ejemplo de NestJS en la sección 6).</p>
|
||||
<p>El usuario puede revocarte el acceso en cualquier momento desde <code>/authorized-apps</code> en el panel de
|
||||
este servidor — a partir de ahí tus requests con ese token empiezan a devolver <code>401</code>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>11. Checklist de seguridad</h2>
|
||||
<ul>
|
||||
<li>☐ Clientes públicos (SPA, móvil) — <strong>siempre PKCE</strong>, nunca les asignes un secret que
|
||||
tendrían que embeber en código que corre en el dispositivo del usuario.</li>
|
||||
<li>☐ Pedí solo los <strong>scopes que realmente necesitás</strong> — no hay scope por defecto en este
|
||||
servidor, es intencional.</li>
|
||||
<li>☐ En una SPA, si podés, no guardes el access token en <code>localStorage</code> (vulnerable a XSS) —
|
||||
preferí el patrón backend-for-frontend (sección 5) con cookie <code>httpOnly</code>.</li>
|
||||
<li>☐ Verificá siempre el parámetro <code>state</code> en el callback de Authorization Code (protección
|
||||
CSRF del flujo).</li>
|
||||
<li>☐ Los <code>client_secret</code> de clientes confidenciales van en variables de entorno del backend,
|
||||
nunca en el repo ni en el bundle del frontend.</li>
|
||||
<li>☐ Si tu servicio backend-a-backend solo necesita leer, pedile a un admin que te dé un cliente con scope
|
||||
<code>users:read</code> nada más (no <code>users:write</code>).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>12. Referencia rápida</h2>
|
||||
<table>
|
||||
<tr><th>Endpoint</th><th>Para qué</th></tr>
|
||||
<tr><td><code>GET /oauth/authorize</code></td><td>Inicio del flujo Authorization Code (redirige al login/consentimiento).</td></tr>
|
||||
<tr><td><code>POST /oauth/token</code></td><td>Canjear código, credenciales o refresh token por un access token.</td></tr>
|
||||
<tr><td><code>GET /api/user</code></td><td>Whoami — usuario del token actual + roles global y por-app.</td></tr>
|
||||
<tr><td><code>GET/POST/PUT/DELETE /api/users</code></td><td>Directorio de usuarios (scope <code>users:read</code>/<code>write</code>).</td></tr>
|
||||
<tr><td><code>GET/POST/DELETE /api/users/{id}/app-roles</code></td><td>Roles por-app de un usuario (para asignarlos vos mismo por API, si tenés permiso).</td></tr>
|
||||
<tr><td><code>GET /api/apps</code></td><td>Listado de apps (clientes OAuth) registradas.</td></tr>
|
||||
<tr><td><code>GET/POST/PUT/DELETE /api/clients</code></td><td>Gestionar tus propios clientes OAuth por API.</td></tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr><th>Scope</th><th>Da acceso a</th></tr>
|
||||
<tr><td><code>users:read</code></td><td>Leer usuarios y sus roles (global + por-app).</td></tr>
|
||||
<tr><td><code>users:write</code></td><td>Crear/editar/borrar usuarios, asignar roles por-app.</td></tr>
|
||||
<tr><td><code>account:read</code></td><td>Leer tus propios clientes OAuth, tokens y apps autorizadas.</td></tr>
|
||||
<tr><td><code>account:write</code></td><td>Gestionar tus propios clientes OAuth, tokens y apps autorizadas.</td></tr>
|
||||
</table>
|
||||
<p>Documentación interactiva (Swagger, para probar todo esto desde el navegador) en
|
||||
<code>/api/documentation</code>. Manual completo del panel de administración en
|
||||
<code>docs/manual-usuario.pdf</code>.</p>
|
||||
|
||||
<footer class="pagefoot">Guía de integración IAM — generada automáticamente a partir del estado actual del proyecto.</footer>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/integracion-iam.pdf
Normal file
BIN
docs/integracion-iam.pdf
Normal file
Binary file not shown.
620
docs/manual-usuario.html
Normal file
620
docs/manual-usuario.html
Normal file
@@ -0,0 +1,620 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OAuth2 Server — Manual de uso</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 22mm 18mm;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||
color: #1f2430;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.cover {
|
||||
height: 245mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
page-break-after: always;
|
||||
}
|
||||
.cover .badge {
|
||||
display: inline-block;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 18px;
|
||||
width: fit-content;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 34px;
|
||||
margin: 0 0 10px;
|
||||
color: #111827;
|
||||
}
|
||||
.cover p.subtitle {
|
||||
font-size: 15px;
|
||||
color: #4b5563;
|
||||
max-width: 420px;
|
||||
margin: 0 0 28px;
|
||||
}
|
||||
.cover .meta {
|
||||
font-size: 11.5px;
|
||||
color: #9ca3af;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 12px;
|
||||
width: fit-content;
|
||||
}
|
||||
h1, h2, h3 { color: #111827; }
|
||||
h2 {
|
||||
font-size: 19px;
|
||||
margin-top: 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #4f46e5;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14.5px;
|
||||
margin-bottom: 6px;
|
||||
color: #4338ca;
|
||||
}
|
||||
section {
|
||||
page-break-before: always;
|
||||
padding-top: 4mm;
|
||||
}
|
||||
section:first-of-type { page-break-before: avoid; }
|
||||
p { margin: 6px 0 10px; }
|
||||
ul, ol { margin: 6px 0 12px; padding-left: 20px; }
|
||||
li { margin-bottom: 4px; }
|
||||
code {
|
||||
background: #f1f0fb;
|
||||
color: #4338ca;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
pre {
|
||||
background: #1e1e2e;
|
||||
color: #e2e2f5;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 10.8px;
|
||||
line-height: 1.5;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
pre code { background: none; color: inherit; padding: 0; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 16px;
|
||||
font-size: 11.5px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 6px 9px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f5fb;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) td { background: #fafafa; }
|
||||
.box {
|
||||
background: #f5f5fb;
|
||||
border-left: 4px solid #4f46e5;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
margin: 12px 0;
|
||||
font-size: 11.8px;
|
||||
}
|
||||
.box.warn {
|
||||
background: #fffbeb;
|
||||
border-left-color: #d97706;
|
||||
}
|
||||
.box strong { color: #111827; }
|
||||
.credentials {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 12px 0 18px;
|
||||
}
|
||||
.cred-card {
|
||||
flex: 1;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.cred-card h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12.5px;
|
||||
color: #4338ca;
|
||||
}
|
||||
.cred-card div { margin-bottom: 3px; font-size: 11.5px; }
|
||||
.cred-card .label { color: #6b7280; display: inline-block; width: 62px; }
|
||||
.toc { list-style: none; padding-left: 0; }
|
||||
.toc li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px dotted #d1d5db;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.toc .n { color: #4f46e5; font-weight: 600; margin-right: 8px; }
|
||||
.pill {
|
||||
display: inline-block;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
border-radius: 4px;
|
||||
padding: 1px 7px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
footer.pagefoot {
|
||||
margin-top: 22px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="cover">
|
||||
<span class="badge">Documentación técnica</span>
|
||||
<h1>OAuth2 Identity Server</h1>
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Índice</h2>
|
||||
<ul class="toc">
|
||||
<li><span><span class="n">1.</span>Qué es esta app</span></li>
|
||||
<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>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Qué es esta app</h2>
|
||||
<p>
|
||||
Es un <strong>servidor de identidad / OAuth2</strong> (Laravel Passport) pensado para ser el centro único de
|
||||
usuarios, autenticación y permisos de un conjunto de aplicaciones propias. No es una app de negocio con datos
|
||||
propios: su único "recurso" son los <strong>usuarios</strong> y sus <strong>roles</strong>.
|
||||
</p>
|
||||
<p>Provee tres piezas:</p>
|
||||
<ul>
|
||||
<li><strong>Servidor de autorización OAuth2</strong> — login, pantalla de consentimiento, y emisión de tokens
|
||||
con los cuatro flujos estándar: Authorization Code + PKCE, Client Credentials, Password Grant y Personal
|
||||
Access Tokens.</li>
|
||||
<li><strong>Panel de administración en Vue 3 + TypeScript</strong> — la UI que trae la app "de fábrica",
|
||||
servida por el propio Laravel, autenticada por sesión. Estilos en SCSS con nomenclatura BEM
|
||||
(<code>resources/scss/</code>), sin frameworks de utility-classes.</li>
|
||||
<li><strong>API REST completa</strong> (<code>/api/*</code>) — el <em>mismo</em> conjunto de funciones del
|
||||
panel Vue (usuarios, roles por app, clientes OAuth, personal access tokens, apps autorizadas), pero
|
||||
autenticada 100% por token OAuth2 (sin sesión ni cookies). Con esto se puede construir <strong>otro admin
|
||||
entero</strong> en Angular, Flutter, o cualquier otro cliente — no hace falta usar la SPA Vue.</li>
|
||||
</ul>
|
||||
<div class="box">
|
||||
<strong>Idea central:</strong> cada aplicación externa que quiera loguear usuarios se registra acá como un
|
||||
<em>cliente OAuth</em> ("app"). Un mismo usuario puede tener un rol global (admin/user) y, además, un rol
|
||||
distinto <em>dentro de cada app</em> (por ejemplo: admin en la app de Angular, usuario normal en la de NestJS).
|
||||
</div>
|
||||
<div class="box">
|
||||
<strong>Dos frentes, misma API subyacente:</strong> la SPA Vue (sesión + cookies, pensada para uso interno
|
||||
en el navegador de este mismo servidor) y la API REST (Bearer token, pensada para cualquier cliente externo)
|
||||
exponen las <em>mismas</em> capacidades. Elegí el que corresponda: sesión para la UI que ya viene incluida,
|
||||
token para construir la tuya propia.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Puesta en marcha</h2>
|
||||
<h3>Requisitos</h3>
|
||||
<ul>
|
||||
<li>PHP 8.4+, Composer</li>
|
||||
<li>Node.js + npm</li>
|
||||
<li>SQLite (por defecto, cero configuración — se puede cambiar en <code>.env</code>)</li>
|
||||
</ul>
|
||||
<h3>Instalación (primera vez)</h3>
|
||||
<pre><code>composer install
|
||||
npm install
|
||||
php artisan migrate --seed
|
||||
npm run build</code></pre>
|
||||
<div class="box">
|
||||
<strong>Frontend:</strong> TypeScript (<code>npm run type-check</code> corre <code>vue-tsc</code> sin
|
||||
emitir archivos) y SCSS compilado por Vite (paquete <code>sass</code>) desde
|
||||
<code>resources/scss/app.scss</code> — no hay ningún framework de CSS utility-first instalado.
|
||||
</div>
|
||||
<h3>Levantar todo en desarrollo</h3>
|
||||
<pre><code>composer run dev</code></pre>
|
||||
<p>Este comando levanta en paralelo: el servidor PHP (<code>php artisan serve</code>), Vite en modo watch, la
|
||||
cola de trabajos y los logs. La app queda disponible en <code>http://localhost:8000</code>.</p>
|
||||
<div class="box warn">
|
||||
<strong>Reiniciar todo de cero:</strong> <code>php artisan migrate:fresh --seed</code> borra la base y
|
||||
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>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Usuarios por defecto (seed)</h2>
|
||||
<p>El seeder (<code>database/seeders/DatabaseSeeder.php</code>) crea una cuenta admin y algunos usuarios de
|
||||
ejemplo para poder probar la app de inmediato:</p>
|
||||
<div class="credentials">
|
||||
<div class="cred-card">
|
||||
<h4>🛡 Administrador</h4>
|
||||
<div><span class="label">Email:</span> <code>admin@example.com</code></div>
|
||||
<div><span class="label">Password:</span> <code>password</code></div>
|
||||
<div><span class="label">Rol global:</span> <span class="pill">admin</span></div>
|
||||
<div style="margin-top:6px; color:#6b7280;">Acceso al panel de administración de usuarios.</div>
|
||||
</div>
|
||||
<div class="cred-card">
|
||||
<h4>👤 Usuario normal</h4>
|
||||
<div><span class="label">Email:</span> <code>carla@example.com</code></div>
|
||||
<div><span class="label">Password:</span> <code>password</code></div>
|
||||
<div><span class="label">Rol global:</span> <span class="pill">user</span></div>
|
||||
<div style="margin-top:6px; color:#6b7280;">Sin acceso al panel admin. Es <strong>admin</strong> dentro de la app "Demo SPA" (rol por-app de ejemplo). Junto con <code>martin@example.com</code> y <code>bea@example.com</code>, solo están para que la sección de analíticas tenga datos de entrada.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
El seeder también crea <code>nuevo@example.com</code> / <code>password</code> sin aprobar, para que la
|
||||
pestaña <strong>"Pendientes"</strong> del panel de usuarios (sección 4) no arranque vacía.
|
||||
</div>
|
||||
<div class="box">
|
||||
<strong>Instalación sin seed (producción):</strong> si nunca corriste <code>db:seed</code>, no hay ningún
|
||||
usuario en la base. En ese caso, la <strong>primera</strong> cuenta que se registre por <code>/register</code>
|
||||
queda automáticamente como <strong>admin</strong>, aprobada y con la sesión iniciada — no hace falta que
|
||||
nadie más la apruebe, porque todavía no existe ningún admin que pudiera hacerlo. La pantalla de registro
|
||||
avisa de esto con un mensaje cuando detecta que la base está vacía. A partir del segundo registro, vuelve
|
||||
a regir el flujo normal (pendiente de aprobación por un admin).
|
||||
</div>
|
||||
|
||||
<h3>Clientes OAuth (apps) de ejemplo</h3>
|
||||
<p>El seeder también registra apps de prueba, una por cada flujo soportado. El <code>client_id</code> es fijo
|
||||
por ejecución (UUID nuevo cada <code>migrate:fresh</code>); el <code>client_secret</code> solo se muestra por
|
||||
consola al sembrar — si lo perdiste, hay que crear un cliente nuevo desde el panel (sección 4) o volver a
|
||||
sembrar.</p>
|
||||
<table>
|
||||
<tr><th>Nombre</th><th>Grant type</th><th>Uso</th></tr>
|
||||
<tr><td>Demo SPA (Authorization Code + PKCE)</td><td><code>authorization_code</code></td><td>Cliente público (sin secret), para apps tipo SPA/móvil.</td></tr>
|
||||
<tr><td>Demo Service (Client Credentials)</td><td><code>client_credentials</code></td><td>Comunicación máquina-a-máquina, sin usuario.</td></tr>
|
||||
<tr><td>Demo First-Party App (Password Grant)</td><td><code>password</code></td><td>Apps propias de confianza, login directo usuario/contraseña.</td></tr>
|
||||
<tr><td>Swagger UI</td><td><code>authorization_code</code>, <code>client_credentials</code></td><td>Para probar el botón "Authorize" de la documentación interactiva.</td></tr>
|
||||
<tr><td>Personal Access Client</td><td><code>personal_access</code></td><td>Interno — necesario para poder emitir Personal Access Tokens.</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Uso de la interfaz web (panel de administración)</h2>
|
||||
<p>Todo se maneja desde <code>http://localhost:8000</code> (Vue Router, sin recargar página).</p>
|
||||
<div class="box">
|
||||
<strong>Modo oscuro:</strong> el ícono de sol/luna en la barra de navegación alterna entre claro y oscuro.
|
||||
La preferencia se guarda en <code>localStorage</code> (por navegador, no por cuenta) y, si nunca la tocaste,
|
||||
arranca siguiendo el modo del sistema operativo. El cambio se aplica antes del primer render — no hay
|
||||
parpadeo del tema equivocado al cargar la página.
|
||||
</div>
|
||||
|
||||
<h3>Login / Registro</h3>
|
||||
<p><code>/login</code> y <code>/register</code>. El registro crea la cuenta con rol global <code>user</code>
|
||||
automáticamente, pero <strong>no loguea al usuario</strong>: queda pendiente de aprobación. La pantalla muestra
|
||||
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>
|
||||
|
||||
<h3>Dashboard</h3>
|
||||
<p><code>/</code> — accesos rápidos a Clientes OAuth, Personal Access Tokens, Apps autorizadas y (si sos admin)
|
||||
Usuarios.</p>
|
||||
|
||||
<h3>Clientes OAuth</h3>
|
||||
<p><code>/clients</code> — cada usuario ve y gestiona <em>sus propios</em> clientes (apps que registró). Al
|
||||
crear uno se elige:</p>
|
||||
<ul>
|
||||
<li><strong>Authorization Code + PKCE</strong>: hay que indicar redirect URIs; opcionalmente "confidencial"
|
||||
(con secret) o público (solo PKCE, sin secret).</li>
|
||||
<li><strong>Client Credentials</strong>: siempre confidencial, sin redirect URI.</li>
|
||||
<li><strong>Password Grant</strong>: para apps propias de confianza.</li>
|
||||
</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>
|
||||
|
||||
<h3>Personal Access Tokens</h3>
|
||||
<p><code>/tokens</code> — tokens tipo "API key" para el propio usuario, sin pasar por el flujo de consentimiento.
|
||||
Se elige nombre y scopes. El token completo también se muestra una única vez.</p>
|
||||
|
||||
<h3>Apps autorizadas</h3>
|
||||
<p><code>/authorized-apps</code> — lista de apps de terceros a las que el usuario le dio acceso a su cuenta vía
|
||||
<code>/oauth/authorize</code>, con opción de revocar el acceso en cualquier momento.</p>
|
||||
|
||||
<h3>Usuarios (solo admins)</h3>
|
||||
<p><code>/admin/users</code> — alta, edición (nombre, email, contraseña, rol global, activo/desactivado) y baja
|
||||
de cualquier usuario del sistema. Protecciones incluidas: no podés borrarte a vos mismo, no podés quitarte el
|
||||
rol admin a vos mismo, no se puede borrar al último administrador. Un usuario desactivado no puede iniciar
|
||||
sesión.</p>
|
||||
<div class="box">
|
||||
<strong>Borrado = papelera (soft delete).</strong> "Borrar" un usuario no lo elimina de la base: lo oculta
|
||||
(desaparece de logins, listados y de cualquier chequeo de permisos) pero queda recuperable. El botón
|
||||
<strong>"Ver papelera"</strong> arriba a la derecha del listado muestra los usuarios borrados, con dos
|
||||
acciones por fila: <strong>Restaurar</strong> (vuelve a aparecer tal cual estaba) o <strong>Borrar
|
||||
definitivamente</strong> (ahí sí, sin vuelta atrás).
|
||||
</div>
|
||||
<div class="box">
|
||||
<strong>Registros pendientes de aprobación.</strong> Arriba a la derecha del listado hay tres pestañas:
|
||||
<em>Ver activos</em>, <strong>Pendientes</strong> (con un contador si hay alguno) y <em>Ver papelera</em>.
|
||||
Todo el que se registra por <code>/register</code> aparece en "Pendientes" hasta que un admin lo
|
||||
<strong>aprueba</strong> (ahí ya puede iniciar sesión) o lo <strong>rechaza</strong> (va a la papelera,
|
||||
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>
|
||||
|
||||
<h3>Analíticas (solo admins)</h3>
|
||||
<p><code>/admin/analytics</code> — ver sección 5.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. 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>
|
||||
<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>Actividad reciente</strong>: tabla con los últimos eventos (usuario, app, tipo, IP, cuándo).</li>
|
||||
</ul>
|
||||
<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".
|
||||
</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>
|
||||
<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">user</span></td><td>Ninguno extra — gestiona su propio perfil, sus clientes OAuth y sus tokens.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Roles por app</h3>
|
||||
<p>Independientes del rol global: un admin le puede asignar a cualquier usuario un rol (<code>admin</code> o
|
||||
<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
|
||||
ú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>
|
||||
<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
|
||||
<code>laravel-lang</code>, más los textos propios de la app en <code>lang/{es,en,ca}/messages.php</code>).</p>
|
||||
<table>
|
||||
<tr><th>Idioma</th><th>Mensajes del servidor</th><th>Interfaz (SPA Vue)</th></tr>
|
||||
<tr><td><strong>ES</strong> (español, por defecto)</td><td>✓</td><td>✓</td></tr>
|
||||
<tr><td><strong>EN</strong> (inglés)</td><td>✓</td><td>✓ — traducción completa (nav, botones, formularios, tablas, gráficos)</td></tr>
|
||||
<tr><td><strong>CA</strong> (catalán)</td><td>✓</td><td>✓ — traducción completa</td></tr>
|
||||
</table>
|
||||
<p>La interfaz usa <code>vue-i18n</code> con diccionarios en <code>resources/js/i18n/{es,en,ca}.js</code>
|
||||
(168 claves cada uno). El locale de arranque del frontend se inyecta desde el servidor
|
||||
(<code>window.__LOCALE__</code> en <code>app.blade.php</code>), así que la SPA nace ya en el idioma correcto
|
||||
sin parpadeo.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. 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>
|
||||
<tr><th>Tipo de app</th><th>Grant recomendado</th></tr>
|
||||
<tr><td>SPA (Angular, React) servida al navegador</td><td>Authorization Code + PKCE, cliente <strong>público</strong></td></tr>
|
||||
<tr><td>Backend con secret propio (NestJS, Express)</td><td>Authorization Code, cliente <strong>confidencial</strong>, o Client Credentials si no hay usuario final</td></tr>
|
||||
<tr><td>Servicio interno máquina-a-máquina</td><td>Client Credentials</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Paso 2 — Login del usuario (Authorization Code + PKCE, ej. Angular)</h3>
|
||||
<p>Redirigir el navegador a:</p>
|
||||
<pre><code>GET /oauth/authorize
|
||||
?client_id={client_id}
|
||||
&redirect_uri={tu_callback}
|
||||
&response_type=code
|
||||
&scope=users:read users:write
|
||||
&state={valor_random}
|
||||
&code_challenge={challenge_pkce}
|
||||
&code_challenge_method=S256</code></pre>
|
||||
<p>El usuario loguea (o ya tiene sesión) y aprueba el acceso. Passport redirige a <code>redirect_uri</code>
|
||||
con <code>?code=...&state=...</code>.</p>
|
||||
|
||||
<h3>Paso 3 — Canjear el código por un token</h3>
|
||||
<pre><code>curl -X POST http://localhost:8000/oauth/token \
|
||||
-d grant_type=authorization_code \
|
||||
-d client_id={client_id} \
|
||||
-d code={code} \
|
||||
-d redirect_uri={tu_callback} \
|
||||
-d code_verifier={verifier_pkce}</code></pre>
|
||||
|
||||
<h3>Alternativa — Client Credentials (ej. NestJS backend-a-backend)</h3>
|
||||
<pre><code>curl -X POST http://localhost:8000/oauth/token \
|
||||
-d grant_type=client_credentials \
|
||||
-d client_id={client_id} \
|
||||
-d client_secret={client_secret} \
|
||||
-d scope="users:read"</code></pre>
|
||||
<div class="box">Un token de <code>client_credentials</code> no tiene usuario asociado: actúa como
|
||||
"servicio de confianza" y puede listar/crear/editar usuarios sin más chequeo que el scope del token.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. 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 —
|
||||
sin depender de la sesión de Laravel. Todos los endpoints requieren <code>Authorization: Bearer {access_token}</code>.
|
||||
</p>
|
||||
|
||||
<h3>Usuarios y roles (scopes <code>users:read</code> / <code>users:write</code>)</h3>
|
||||
<table>
|
||||
<tr><th>Método</th><th>Ruta</th><th>Descripción</th></tr>
|
||||
<tr><td>GET</td><td><code>/api/user</code></td><td>Whoami: usuario del token actual, con <code>roles</code> y <code>app_roles</code>.</td></tr>
|
||||
<tr><td>GET</td><td><code>/api/users</code></td><td>Listar usuarios (requiere <code>users.view</code>, salvo client_credentials).</td></tr>
|
||||
<tr><td>POST</td><td><code>/api/users</code></td><td>Crear usuario (requiere <code>users.create</code>).</td></tr>
|
||||
<tr><td>GET</td><td><code>/api/users/{id}</code></td><td>Ver un usuario. Siempre permitido sobre uno mismo.</td></tr>
|
||||
<tr><td>PUT</td><td><code>/api/users/{id}</code></td><td>Editar. Uno mismo puede cambiar nombre/email/contraseña; rol/estado y otros usuarios requieren <code>users.update</code>.</td></tr>
|
||||
<tr><td>DELETE</td><td><code>/api/users/{id}</code></td><td>Borrar — soft delete, va a la papelera (requiere <code>users.delete</code>; no se puede autoeliminar).</td></tr>
|
||||
<tr><td>GET</td><td><code>/api/users/trashed</code></td><td>Listar usuarios en la papelera (requiere <code>users.view</code>).</td></tr>
|
||||
<tr><td>POST</td><td><code>/api/users/{id}/restore</code></td><td>Restaurar un usuario borrado (requiere <code>users.update</code>).</td></tr>
|
||||
<tr><td>DELETE</td><td><code>/api/users/{id}/force</code></td><td>Borrado definitivo, sin vuelta atrás (requiere <code>users.delete</code>; el usuario tiene que estar en la papelera).</td></tr>
|
||||
<tr><td>GET</td><td><code>/api/users/pending</code></td><td>Listar registros pendientes de aprobación (requiere <code>users.view</code>).</td></tr>
|
||||
<tr><td>POST</td><td><code>/api/users/{id}/approve</code></td><td>Aprobar un registro pendiente (requiere <code>users.update</code>).</td></tr>
|
||||
<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/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>
|
||||
</table>
|
||||
|
||||
<h3>Ejemplo de respuesta de usuario</h3>
|
||||
<pre><code>{
|
||||
"id": 2,
|
||||
"name": "Carla Gómez",
|
||||
"email": "carla@example.com",
|
||||
"is_active": true,
|
||||
"roles": ["user"],
|
||||
"app_roles": ["admin"],
|
||||
"created_at": "2026-07-25T08:52:39.000000Z"
|
||||
}</code></pre>
|
||||
<p><code>roles</code> es el rol global; <code>app_roles</code> es el rol de ese usuario específicamente en la
|
||||
app dueña del token usado para la request (útil para que cada app decida qué mostrar/permitir sin tener su
|
||||
propio sistema de permisos). El campo <code>deleted_at</code> solo aparece (no-null) en las respuestas de
|
||||
<code>/api/users/trashed</code>.</p>
|
||||
|
||||
<h3>Cuenta propia: clientes, tokens y apps autorizadas (scopes <code>account:read</code> / <code>account:write</code>)</h3>
|
||||
<p>Equivalente por token a <code>/web-api/clients</code>, <code>/personal-access-tokens</code> y
|
||||
<code>/authorized-apps</code> de la sección 4 — siempre sobre el usuario dueño del token (no aplica a
|
||||
client_credentials, que no tiene usuario asociado):</p>
|
||||
<table>
|
||||
<tr><th>Método</th><th>Ruta</th><th>Descripción</th></tr>
|
||||
<tr><td>GET</td><td><code>/api/clients</code></td><td>Listar tus clientes OAuth (apps que registraste).</td></tr>
|
||||
<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>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>
|
||||
</table>
|
||||
|
||||
<h3>Scopes disponibles</h3>
|
||||
<table>
|
||||
<tr><th>Scope</th><th>Permite</th></tr>
|
||||
<tr><td><code>users:read</code></td><td>Leer usuarios y roles por app</td></tr>
|
||||
<tr><td><code>users:write</code></td><td>Crear, editar y borrar usuarios; asignar roles por app</td></tr>
|
||||
<tr><td><code>account:read</code></td><td>Leer tus clientes OAuth, tokens y apps autorizadas</td></tr>
|
||||
<tr><td><code>account:write</code></td><td>Gestionar tus clientes OAuth, tokens y apps autorizadas</td></tr>
|
||||
</table>
|
||||
<p>No hay scope por defecto: cada app debe pedir explícitamente los que necesita al iniciar el flujo OAuth
|
||||
(ej. <code>scope=users:read users:write account:read account:write</code> para un admin completo).</p>
|
||||
<div class="box warn">
|
||||
<strong>Client credentials y recursos self-service:</strong> un token <code>client_credentials</code> no
|
||||
tiene usuario asociado, así que <code>/api/clients</code>, <code>/api/personal-access-tokens</code> y
|
||||
<code>/api/authorized-apps</code> le devuelven <code>422</code> (no hay "cuenta propia" que gestionar).
|
||||
Sí puede usar <code>/api/users</code> y <code>/api/apps</code> como servicio de confianza.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. 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>
|
||||
<p>El JSON crudo de la especificación OpenAPI está en <code>/docs</code>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>11. 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>GET/POST/PUT/DELETE /web-api/clients</code></td><td>Clientes OAuth propios.</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/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>
|
||||
<tr><td><code>DELETE /web-api/admin/users/{id}/force</code></td><td>Borrado definitivo (admin).</td></tr>
|
||||
<tr><td><code>GET /web-api/admin/users/pending</code></td><td>Registros pendientes de aprobación (admin).</td></tr>
|
||||
<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>POST /web-api/locale</code></td><td>Cambiar idioma (es/en/ca).</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>OAuth2 (Passport)</h3>
|
||||
<table>
|
||||
<tr><th>Ruta</th><th>Qué hace</th></tr>
|
||||
<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>
|
||||
|
||||
<h3>API REST (Bearer token, para apps externas — Angular, Flutter, lo que sea)</h3>
|
||||
<table>
|
||||
<tr><th>Ruta</th><th>Qué hace</th></tr>
|
||||
<tr><td><code>GET /api/user</code></td><td>Whoami.</td></tr>
|
||||
<tr><td><code>GET/POST/PUT/DELETE /api/users</code></td><td>Gestión de usuarios (DELETE = papelera).</td></tr>
|
||||
<tr><td><code>GET /api/users/trashed</code></td><td>Usuarios en la papelera.</td></tr>
|
||||
<tr><td><code>POST /api/users/{id}/restore</code></td><td>Restaurar un usuario borrado.</td></tr>
|
||||
<tr><td><code>DELETE /api/users/{id}/force</code></td><td>Borrado definitivo.</td></tr>
|
||||
<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 /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/POST/PUT/DELETE /api/clients</code></td><td>Tus clientes OAuth.</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>
|
||||
</table>
|
||||
|
||||
<footer class="pagefoot">OAuth2 Identity Server — Manual de uso · generado automáticamente a partir del estado actual del proyecto.</footer>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/manual-usuario.pdf
Normal file
BIN
docs/manual-usuario.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user