📚 Unidad CIERRE-AUTH · Cierre Fase II — Auth con RBAC (backend completo) — capa 🧠 APRENDER
🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación
Capa Página Para qué 🧠 Aprender esta página comprender, explicar y relacionar 🛠 Construir Cierre Fase II — Auth con RBAC (backend completo) ejecutar, programar y verificar ✅ GATE Cierre de la unidad condición para pasar al bloque siguiente
Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas de la unidad; los enlaces apuntan a secciones reales del material (anclas de MkDocs, sin acentos).
| Tema | 🧠 Aprender (esta página) | 🛠 Construir (ISS técnico) |
|---|---|---|
| Ruta y ficha de la unidad | Ruta de aprendizaje · Ficha del ISS | Contenido de la unidad |
| Mapas y estructura | Mapa mental · Mapa del backend · Árbol de archivos | Contenido de la unidad |
| Recorrido y comandos | Comandos explicados · Recorrido paso a paso | Contenido de la unidad |
| Diagnóstico | Diagnóstico | Criterios de aceptación |
| Evaluación y cierre | Criterios · Evaluación · GATE | Condiciones de cierre · Cierre |
🎬 Video explicativo
Recorrido audiovisual de la unidad. Este video audita el laboratorio completo. No hay código nuevo: doce features, once tablas, tres modalidades y la diferencia entre 58 recursos y 65 concesiones.
3:53 · narración en español · subtítulos activables desde el reproductor.
CIERRE-AUTH — Cuaderno de aprendizaje visual
Tema
Cierre de la Fase II — Auth con RBAC: la definición de terminado (DoD) del backend completo (Business + Auth) y la verificación integral de las tres modalidades de acceso.
Fuente técnica autoritativa
| Archivo fuente | ../manual/18-cierre-auth.md |
| Estado | Solo lectura — este cuaderno no modifica el documento de cierre |
| Alcance | 640 líneas: cableado final, mapa definitivo de las tres modalidades, DoD de la Fase I + Fase II, cómo repetir el patrón, referencia rápida de paquetes y fuentes |
Este cuaderno es una capa pedagógica sobre ese archivo: el documento de cierre manda y aquí solo se explica cómo comprobar que el backend quedó completo. Todo su contenido técnico (cableado, modalidades, DoD, versiones) se reproduce íntegro y verbatim en la sección Recorrido del ISS, paso a paso.
Es el último documento del recorrido: no construye nada, cierra el laboratorio entero. Su valor está en el mapa definitivo de rutas y en el DoD que fusiona las dos fases.
Pregunta que responde: ¿qué documento exacto define el final del laboratorio y qué alcance tiene?
Regla del ISS
Objetivo: dejar el backend completo y funcional: 12 features, 11 tablas, 3 modalidades, seguridad stateless con revocación inmediata, y todo verificable con TypeScript, seeders y Swagger. Bloqueado por: ISS-15 — Feature Session.
La condición de cierre es el DoD del laboratorio (Fase I + Fase II). Se cierra el laboratorio solo si se cumplen, a la vez:
- Existen las 12 features (5 de negocio + 7 de auth) y las 11 tablas.
- Cada ruta declara su modalidad de acceso:
OPEN,JWToJWT + RBAC. - No existe la entidad
Permission: el permiso es la filaresource_roles (role_id, resource_id). - La autorización aplica
deny by default: sin concesión activa → 403. - Los seeders son deterministas (2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones).
npx tsc --noEmittermina sin errores y el smoke test E2E de las tres modalidades está en verde.
Cómo leer este cuaderno
Cada concepto se presenta tres veces, desde tres ángulos distintos:
CONCEPTO
│
┌───────────┼───────────┐
▼ ▼ ▼
EXPLICACIÓN CÓDIGO VISUAL
│ │ │
¿qué es? ¿dónde está? ¿cómo lo
¿por qué? ¿qué hace? visualizo?
¿para qué? ¿cómo opera? ¿con qué
se relaciona?
Pregunta que responde: ¿cómo está organizado este cuaderno y qué espero encontrar en cada parte?
Un cuaderno de cierre cambia el acento: aquí el «código» que manda no es el que tú escribes, sino el código que auditas. Seis componentes guían esa auditoría:
| Componente | Dónde vive | Para qué sirve |
|---|---|---|
| Texto | todas las secciones | entender el por qué del cierre |
| Código | Recorrido del ISS |
ver el qué exacto que se audita (cableado final) |
| Diagramas | Mapa mental, Mapa del backend, Árbol de archivos, Flujos |
ver el cómo se conecta la seguridad |
| Preguntas | Evaluación y bajo cada diagrama |
comprobar que entendiste |
| Matriz de pruebas | La matriz de pruebas de la fase |
ejecutar y comprobar las tres modalidades |
| GATE | GATE |
saber si el laboratorio está realmente cerrado |
Ruta de aprendizaje
Esta ruta es específica del cierre de Fase II: primero se repasa el cableado final, luego el mapa de las tres modalidades, después se ejecuta y, por último, se audita el cierre completo.
Repaso del cableado final (config → routes → swagger → seeders)
↓
Las tres modalidades de acceso (OPEN · JWT · JWT + RBAC)
↓
La matriz RBAC (roles · resources · role_users · resource_roles)
↓
Matriz de pruebas de la fase (caso por modalidad)
↓
Ejecución (tsc, seeders, servidor, smoke test E2E)
↓
Auditoría de cierre (DoD Fase I + Fase II en verde)
↓
GATE del laboratorio
Pregunta que responde: ¿cuál es el camino ordenado para dar por cerrado el laboratorio completo?
Fíjate en lo que no aparece: no hay «crear feature», ni «escribir middleware», ni «emitir un token». Todo eso se hizo entre ISS-09 y ISS-15. Aquí solo se verifica.
Índice
- Ficha del ISS
- Mapa mental del ISS
- Mapa del backend
- Árbol de archivos
- Las tres modalidades de acceso
- Flujos
- Comandos explicados
- La matriz de pruebas de la fase
- Recorrido del ISS, paso a paso
- Diagnóstico
- Conexión con el resto del curso
- Glosario
- Criterios de aceptación
- Evaluación
- GATE
Ficha del ISS
| Campo | Valor |
|---|---|
| ISS | CIERRE Fase II — Auth con RBAC |
| Título | Cierre del laboratorio (backend completo con Auth + RBAC) |
| Objetivo | Dejar el backend completo y funcional: 12 features, 11 tablas, 3 modalidades, seguridad stateless con revocación inmediata, todo verificable con TypeScript, seeders y Swagger |
| Fase | Fase II — Auth con RBAC (cierre del laboratorio) |
| Tecnología principal | Express 5 + TypeScript + Sequelize + JWT (HS256) + RBAC (resource_roles) |
| Depende de | ISS-15 — Feature Session |
| Habilita | — (es el último documento del laboratorio) |
| Archivos creados | Ninguno (documento de cierre: no escribe código) |
| Archivos parcheados | Ninguno |
| Componentes incorporados | Ninguno: se auditan los componentes de ISS-09 a ISS-15 |
| Verificación principal | npx tsc --noEmit sin errores + smoke test E2E de las tres modalidades |
| Resultado esperado | 12 features, 11 tablas, 3 modalidades por ruta, deny by default (403), 65 concesiones sembradas y Swagger con bearerAuth |
| GATE | El checklist del DoD (Fase I + Fase II) completo en verde |
Qué implementamos AHORA
Nada de código nuevo. Este cierre consolida y verifica la seguridad de la Fase II sobre el negocio ya cerrado en la Fase I: comprueba el cableado final (config, routes, swagger, seeders), el mapa definitivo de las tres modalidades y la matriz RBAC, y confirma que todo el sistema es verificable de punta a punta.
Qué todavía NO implementamos
Este es el final del recorrido: no queda fase posterior. Aun así conviene fijar lo que el laboratorio deja deliberadamente fuera del código.
| No se implementa aquí | Explicación |
|---|---|
| Código de seguridad nuevo | Este cierre solo audita lo construido en ISS-09…ISS-15; no añade ni parchea archivos |
Entidad Permission |
No existe por diseño: el permiso es la fila resource_roles (role_id, resource_id) |
Seeder de refresh_tokens |
No tiene seeder: lo puebla el login |
| Ningún ISS posterior | Es el último documento; el recorrido de construcción termina en ISS-15 |
Ojo con la trampa habitual: que existan 58 recursos sembrados no significa que todos estén concedidos. La matriz RBAC tiene exactamente 65 concesiones repartidas entre los dos roles; lo no concedido da 403.
Mapa mental del ISS
mindmap
root((CIERRE<br/>Fase II<br/>Auth y RBAC))
Cableado final
config
routes
swagger
seeders
Tres modalidades
OPEN
JWT
JWT mas RBAC
Matriz RBAC
roles
resources
role_users
resource_roles
Estado
Doce features
Once tablas
Deny by default
Verificacion
tsc sin errores
Seeders deterministas
Smoke test E2E
Cierre
Auditoria final
Fin del laboratorio
Pregunta que responde: ¿de qué trata este cierre y qué piezas lo componen?
Mapa del backend
Esta representación responde siempre a la misma pregunta: ¿en qué parte del backend estamos? Aquí es un mapa de balance de fase: todo está terminado, incluidas las tres modalidades de acceso.
IMPLEMENTADO HASTA ESTE CIERRE (Fase I + Fase II)
─────────────────────────────────────────────────
FASE I — BUSINESS
HTTP
↓
Routes → Controller → Service → Repository → Model → Sequelize → BD
✅ ✅ ✅ ✅ ✅ ✅
Features (5): clients ✅ · product-types ✅ · products ✅ · sales ✅ · product-sales ✅
SeedersRunner ✅ · Swagger ✅
FASE II — AUTH CON RBAC
HTTP
↓
(authenticate · authorize) → Controller → Service → Repository → Model
✅ ✅
Features (7): access ✅ · users ✅ · roles ✅ · resources ✅
role-users ✅ · resource-roles ✅ · refresh-tokens ✅ · session ✅
Matriz RBAC: roles(2) ✅ · resources(58) ✅ · role_users(2) ✅ · resource_roles(65) ✅
LAS TRES MODALIDADES DE ACCESO (por ruta)
├── OPEN ✅ (login · refresh · logout · docs)
├── JWT ✅ (perfil · permisos · sesiones propias)
└── JWT + RBAC ✅ (todo el CRUD de negocio y de administración)
Garantías transversales
├── Tuerca de seguridad stateless con revocación inmediata ✅
├── deny by default: sin concesión → 403 ✅
├── Access corto (HS256) + refresh opaco, hasheado y rotativo ✅
└── npx tsc --noEmit en verde ✅
OBJETIVO DE ARQUITECTURA
────────────────────────
✅ Completado: no queda nada pendiente en el laboratorio.
FUERA DE ALCANCE EN ESTE LAB
────────────────────────────
⬜ No hay capas futuras: el backend está cerrado.
Pregunta que responde: ¿qué capas del backend existen ya al cerrar la Fase II y queda algo pendiente?
Mira el contraste con la Fase I: lo que en el cierre de Business era 🎯 (toda la seguridad) ahora está ✅. No queda ningún 🎯.
Árbol de archivos
En un cierre no hay archivos nuevos: el documento no crea ni parchea nada. Por eso aquí se muestra el árbol consolidado de la Fase II, con la leyenda de lo construido entre ISS-09 y ISS-15.
Leyenda: ★ = creado en la Fase II · △ = existente parcheado en la Fase II · ✅ = pieza verificada en este cierre.
Estructura antes
Archivos creados / modificados en este ISS
Estructura después
src/
├── config/index.ts ✅ △ arranque: 11 modelos → asociaciones → rutas → Swagger
├── database/seeders/{counts,index}.ts ✅ △ seguridad primero, después negocio
├── routes/index.ts ✅ △ agregador: 5 features de negocio + 7 de auth
├── shared/
│ ├── auth/{password,jwt,resource-match,auth-user}.ts ✅ ★ utilidades de seguridad
│ ├── http/{base-controller,error-response,swagger-security}.ts ✅ ★ errores y esquema bearerAuth
│ ├── database/with-transaction.ts ✅ transacciones (Fase I)
│ └── errors/app-error.ts ✅ errores de dominio (Fase I)
├── features/
│ ├── business/ ✅ Fase I
│ │ ├── clients/ product-types/ products/ sales/ product-sales/
│ │ └── (cada feature: model, dto/, repository, service, controller, routes, seeder, swagger, http/)
│ └── auth/ ✅ Fase II
│ ├── access/{authenticate,authorize}.middleware.ts ✅ ★ las tres modalidades
│ ├── rbac.associations.ts ✅ ★ asociaciones de la matriz RBAC
│ ├── users/ roles/ resources/ ✅ ★ identidades y catálogo
│ ├── role-users/ resource-roles/ ✅ ★ la matriz RBAC
│ ├── refresh-tokens/ session/ ✅ ★ sesiones y sesión
│ └── (cada feature: dto/, repository, service, controller, routes, ...[.seeder,.swagger], http/)
├── swagger/index.ts ✅ △ registry de 12 módulos + bearerAuth
└── server.ts ✅ punto de entrada
El árbol no cambia con este cierre: su único efecto es que confirmas, capa por capa, que la seguridad completa existe y convive con el negocio.
Pregunta que responde: ¿el cierre modifica la estructura del proyecto y dónde vive la seguridad en el árbol? — No la modifica; la seguridad vive en
shared/auth/y enfeatures/auth/.
Las tres modalidades de acceso
Cada operación de la API decide su modalidad middleware a middleware. Este es el mapa definitivo del documento de cierre:
| Modalidad | Middlewares | Rutas |
|---|---|---|
| OPEN | — | POST /api/sesion/login · /refresh · /logout · GET /api/docs · /api/docs.json |
| JWT | authenticate |
GET /api/sesion/perfil · /api/permisos · GET /api/sesiones · /:id · PATCH /api/sesiones/:id/deactivate · /deactivate-all · DELETE /api/sesiones |
| JWT + RBAC | authenticate, authorize |
/api/usuarios… · /api/roles… · /api/recursos… · /api/asignaciones-rol… · /api/concesiones-rol… · /api/clientes… · /api/tipos-producto… · /api/productos… · /api/ventas… · /api/detalle-ventas… |
Así decide el sistema, petición a petición:
flowchart TD
A["Peticion entrante"] --> B{"Ruta OPEN?"}
B -- "si" --> C["Ejecuta sin identidad"]
B -- "no" --> D{"Token valido?"}
D -- "no" --> E["401 Unauthorized"]
D -- "si" --> F{"Ruta JWT?"}
F -- "si" --> G["Ejecuta con identidad"]
F -- "no" --> H{"Concesion activa de method y path?"}
H -- "no" --> I["403 Forbidden"]
H -- "si" --> J["Ejecuta con identidad y permiso"]
Pregunta que responde: ¿en qué orden se aplican OPEN, authenticate y authorize, y por qué el 401 y el 403 no son lo mismo?
El 401 significa «no sé quién eres» (falta o falla el token); el 403 significa «sé quién eres, pero no tienes concesión para esto». authorize consulta la matriz en cada petición y no cachea, así que un cambio de permisos surte efecto de inmediato.
Flujos
Ciclo de sesión: login → refresh → logout
sequenceDiagram
participant U as Usuario
participant API as API Express
participant DB as Base de datos
U->>API: POST /api/sesion/login con credenciales
API->>DB: verifica usuario y contrasena
DB-->>API: usuario valido
API-->>U: access_token JWT y refresh_token opaco
U->>API: POST /api/sesion/refresh con refresh_token
API->>DB: valida hash y rota el token
API-->>U: nuevo par de tokens
U->>API: DELETE /api/sesion/logout
API->>DB: revoca refresh_token
API-->>U: sesion cerrada
Pregunta que responde: ¿por qué el refresh token se rota en cada uso y qué aporta la revocación inmediata?
Autorización con deny by default
sequenceDiagram
participant C as Cliente
participant A as authenticate
participant Z as authorize
participant M as Matriz RBAC
C->>A: peticion con Bearer token
A->>A: valida firma HS256 y exp
A->>Z: identidad valida
Z->>M: consulta role_id y resource_id para method y path
M-->>Z: sin concesion
Z-->>C: 403 Forbidden
Pregunta que responde: ¿por qué una identidad válida puede aun así recibir 403?
La matriz RBAC
erDiagram
USERS ||--o{ ROLE_USERS : "recibe rol"
ROLES ||--o{ ROLE_USERS : "agrupa usuarios"
ROLES ||--o{ RESOURCE_ROLES : "recibe concesiones"
RESOURCES ||--o{ RESOURCE_ROLES : "es concedido"
USERS ||--o{ REFRESH_TOKENS : "abre sesiones"
CLIENTS ||--o{ SALES : "compra"
PRODUCTS ||--o{ PRODUCT_SALES : "aparece en"
SALES ||--o{ PRODUCT_SALES : "contiene"
Pregunta que responde: ¿dónde vive el permiso en el modelo de datos?
El permiso no es una tabla: es la fila resource_roles (role_id, resource_id). Un rol recibe concesiones sobre recursos; un usuario recibe roles. Por eso el sistema puede conceder un permiso en caliente sin tocar código.
Comandos explicados
El cierre no trae comandos nuevos: reutiliza los del laboratorio. Estos son los que demuestran que el backend completo está cerrado.
npx tsc --noEmit
COMANDO
↓
npx tsc --noEmit
↓
QUÉ HACE
Compila todo el proyecto con TypeScript y comprueba los tipos sin
generar archivos de salida.
↓
POR QUÉ SE NECESITA
Es el criterio de aceptación transversal de las dos fases: 12 features,
DTOs, middlewares y asociaciones deben tipar sin un solo error.
↓
QUÉ CREA O MODIFICA
Nada. Es una verificación de solo lectura.
↓
RESULTADO ESPERADO
Sin salida y con código de salida 0.
↓
CÓMO VERIFICARLO
Si aparece cualquier error TS, el cierre falla: hay que corregir el
tipo antes de dar el laboratorio por terminado.
npm run db:seed
COMANDO
↓
npm run db:seed
↓
QUÉ HACE
Ejecuta el SeedersRunner: seguridad primero (roles → resources → users
→ role_users → resource_roles) y después negocio.
↓
POR QUÉ SE NECESITA
El smoke test necesita la matriz RBAC sembrada: los dos usuarios
canónicos, sus roles y sus 65 concesiones.
↓
QUÉ CREA O MODIFICA
Puebla las 11 tablas. Los catálogos de seguridad son deterministas y
reconciliadores: reejecutar deja la matriz exacta.
↓
RESULTADO ESPERADO
El runner imprime los conteos, siembra en orden y termina con
«SeedersRunner finalizado».
↓
CÓMO VERIFICARLO
Los defaults son users=2, clients=10, product_types=25, products=15,
sales=5, product_sales=12. Puedes variarlos con CLI:
`npm run db:seed -- --users=5 --clients=20 --products=15`.
npm run dev
COMANDO
↓
npm run dev
↓
QUÉ HACE
Arranca el servidor: primero la base de datos (conexión + `sync`),
después abre el puerto.
↓
POR QUÉ SE NECESITA
Es la superficie donde se ejecuta el smoke test E2E de las tres
modalidades y donde se abre Swagger.
↓
QUÉ CREA O MODIFICA
Sincroniza el esquema desde los modelos y queda escuchando en el
puerto configurado.
↓
RESULTADO ESPERADO
«Base de datos sincronizada exitosamente» y el servidor en marcha;
Swagger anuncia `/api/docs` y `/api/docs.json`.
↓
CÓMO VERIFICARLO
Abre `/api/docs`: debe mostrar `bearerAuth` y las operaciones con su
modalidad (OPEN, JWT o JWT + RBAC).
Dependencias que añade la Fase II
No hay comando nuevo más allá de instalar los paquetes con npm install; lo que cambia es el inventario de dependencias respecto a la Fase I:
| Paquete | Versión | Para qué |
|---|---|---|
jsonwebtoken |
^9.0.3 |
Firmar y verificar el access token (HS256) |
@types/jsonwebtoken |
^9.0.10 |
Tipos de desarrollo para JWT |
@types/node |
^22.20.4 |
Tipos actualizados (la Fase I usaba ^22.20.3) |
Además, las variables JWT_SECRET, JWT_ACCESS_TTL y JWT_REFRESH_TTL_DAYS se añaden al .env en ISS-09 §14.1.
Pregunta que responde: ¿por qué instalar
jsonwebtokenno es suficiente para que las rutas queden protegidas?
La matriz de pruebas de la fase
Esta matriz se deriva del DoD y del mapa de rutas del documento de cierre. Cada fila es un caso que debes ejecutar antes de dar el GATE por bueno.
| # | Caso | Petición | Resultado esperado |
|---|---|---|---|
| 1 | Login admin (OPEN) | POST /api/sesion/login con admin / Admin123! |
200 con access_token y refresh_token |
| 2 | Login seller (OPEN) | POST /api/sesion/login con seller / Seller123! |
200 con el par de tokens |
| 3 | Refresh (OPEN) | POST /api/sesion/refresh con el refresh token |
200 con un nuevo par (rotación) |
| 4 | Reúso de refresh | Reutilizar un refresh ya rotado | Rechazo y detección de reúso |
| 5 | Logout (OPEN) | POST /api/sesion/logout |
200; el refresh queda revocado |
| 6 | Perfil (JWT) | GET /api/sesion/perfil con Bearer |
200 con la identidad |
| 7 | Permisos (JWT) | GET /api/permisos con Bearer |
200 con los permisos efectivos |
| 8 | Sesiones (JWT) | GET /api/sesiones y GET /api/sesiones/:id |
200 con las sesiones propias |
| 9 | Desactivar sesión (JWT) | PATCH /api/sesiones/:id/deactivate · /deactivate-all |
200; la sesión deja de servir |
| 10 | Cerrar sesiones (JWT) | DELETE /api/sesiones |
200; revocación en bloque |
| 11 | Negocio con permiso (JWT + RBAC) | GET /api/clientes con el token de admin |
200 (admin tiene los 58 permisos) |
| 12 | Negocio sin permiso (JWT + RBAC) | Ruta de negocio con seller sin concesión |
403 |
| 13 | Sin token | Cualquier ruta JWT o JWT + RBAC sin Authorization |
401 |
| 14 | Admin de seguridad (JWT + RBAC) | /api/usuarios… · /api/roles… · /api/recursos… |
200 con admin; 403 sin concesión |
| 15 | Alta en caliente | POST /api/recursos y luego POST /api/concesiones-rol |
El permiso tiene efecto inmediato en la siguiente petición |
| 16 | Documentación | GET /api/docs y GET /api/docs.json |
UI con bearerAuth, 401/403 y la modalidad por operación |
| 17 | Tipado global | npx tsc --noEmit |
Sin errores |
Pregunta que responde: ¿qué casos mínimos demuestran que las tres modalidades funcionan de verdad?
Los casos 1–5 cubren OPEN; los 6–10, JWT; y los 11–15, JWT + RBAC incluido el deny by default y el alta en caliente. Los casos 16–17 son los criterios transversales del DoD.
Recorrido del ISS, paso a paso
Lo que sigue es la reproducción íntegra y verbatim del documento de cierre 18-cierre-auth.md. Se inserta automáticamente al construir el cuaderno: los encabezados aparecen degradados un nivel para anidar bajo esta sección y los enlaces relativos se reescriben a ../manual/ para que abran correctamente desde docs/aprendizaje/. No se resume ni se reformatea nada; ahí encontrarás el cableado final completo y el DoD textual.
Fase II: Auth con RBAC — Cierre del laboratorio (backend completo)
Contexto mínimo (autosuficiente). Si abres este archivo aislado —o lo llevas a otra IA—, esto es lo que hay que saber antes de ejecutarlo. - Proyecto:
app-storelab-express-ii— Express 5 + TypeScript + Sequelize, arquitectura por features. - Alcance final: Fase I (Business) + Fase II (Auth con RBAC). Todas las rutas de negocio pasan de SIN AUTH a JWT + RBAC. - Recorrido obligatorio de una petición:HTTP → (middlewares) → Controller → Service → Repository → Model → Sequelize → BD. Ninguna capa se salta a la siguiente. - Diseño de datos:../bd-storelab.md— Fase I (§4–§11) y Fase II (§12–§22). - Capas, convenciones y reglas transversales:00-contexto.md.
Este cierre Título Cierre del laboratorio (backend completo con Auth + RBAC) Feature / tablas las 11 tablas API todas Depende de ISS-15 — Feature Session Habilita —
Contenido de este cierre
- Cableado final (
config,routes,swagger, seeders) - Las tres modalidades — mapa definitivo de rutas
- DoD del laboratorio
- Cómo repetir el patrón (otra entidad)
- Referencia rápida de paquetes
- Fuentes
Objetivo: dejar el backend completo y funcional: 12 features, 11 tablas, 3 modalidades, seguridad stateless con revocación inmediata, y todo verificable con TypeScript, seeders y Swagger.
Cableado final
src/config/index.ts — arranque: modelos → asociaciones → rutas → Swagger.
: > src/config/index.ts
cat >> src/config/index.ts << 'EOF'
import dotenv from "dotenv";
import express, { Application, ErrorRequestHandler } from "express";
import morgan from "morgan";
var cors = require("cors");
import { sequelize, getDatabaseInfo, testConnection } from "../database/db";
import "../features/business/clients/client.model";
import "../features/business/product-types/product-type.model";
import "../features/business/products/product.model";
import "../features/business/sales/sale.model";
import "../features/business/product-sales/product-sale.model";
import "../features/business/products/products.associations";
import "../features/business/sales/sales.associations";
import "../features/business/product-sales/product-sales.associations";
// Fase II — Auth con RBAC: primero los seis modelos, después las asociaciones
// (las asociaciones referencian los modelos, no al revés).
import "../features/auth/users/user.model";
import "../features/auth/roles/role.model";
import "../features/auth/resources/resource.model";
import "../features/auth/role-users/role-user.model";
import "../features/auth/resource-roles/resource-role.model";
import "../features/auth/refresh-tokens/refresh-token.model";
import "../features/auth/rbac.associations";
import { Routes } from "../routes/index";
import { setupSwagger } from "../swagger/index";
dotenv.config();
export class App {
public app: Application;
public routePrv: Routes = new Routes();
constructor(private port?: number | string) {
this.app = express();
this.settings();
this.middlewares();
this.routes();
this.docs();
this.errorHandling();
}
private settings(): void {
this.app.set('port', this.port || process.env.PORT || 4000);
}
private middlewares(): void {
this.app.use(morgan('dev'));
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: false }));
}
private routes(): void {
// Fase I — Business (cada operación, modalidad JWT + RBAC)
this.routePrv.clientsRoutes.routes(this.app);
this.routePrv.productTypesRoutes.routes(this.app);
this.routePrv.productsRoutes.routes(this.app);
this.routePrv.salesRoutes.routes(this.app);
this.routePrv.productSalesRoutes.routes(this.app);
// Fase II — Auth con RBAC
// `sessionRoutes` registra los endpoints OPEN/JWT (login, refresh, logout,
// perfil, permisos); el resto son modalidad JWT + RBAC.
this.routePrv.sessionRoutes.routes(this.app);
this.routePrv.refreshTokensRoutes.routes(this.app);
this.routePrv.usersRoutes.routes(this.app);
this.routePrv.rolesRoutes.routes(this.app);
this.routePrv.resourcesRoutes.routes(this.app);
this.routePrv.roleUsersRoutes.routes(this.app);
this.routePrv.resourceRolesRoutes.routes(this.app);
}
private docs(): void {
setupSwagger(this.app);
}
/**
* Errores que ocurren **antes** de llegar a un controller o middleware.
*
* El caso típico es un cuerpo JSON malformado: `express.json()` lanza un
* `SyntaxError` que, sin manejador, cae en el de Express por defecto y responde
* 400 con un HTML que incluye el **stack trace y rutas absolutas del servidor**
* (fuga de información). Aquí se traduce a un 400 JSON limpio.
*
* Debe registrarse **después** de las rutas: Express reconoce un middleware de
* error por su aridad de 4 argumentos.
*/
private errorHandling(): void {
const bodyErrorHandler: ErrorRequestHandler = (err, _req, res, next) => {
if (err instanceof SyntaxError && "body" in err) {
res.status(400).json({ error: "Malformed JSON body" });
return;
}
next(err);
};
this.app.use(bodyErrorHandler);
}
private async dbConnection(): Promise<void> {
try {
const dbInfo = getDatabaseInfo();
console.log(`🔗 Intentando conectar a: ${dbInfo.engine.toUpperCase()}`);
const isConnected = await testConnection();
if (!isConnected) {
throw new Error(`No se pudo conectar a la base de datos ${dbInfo.engine.toUpperCase()}`);
}
// Lab: sync crea/altera tablas desde los modelos (BD limpia → snake_case desde cero).
const force = process.env.DB_SYNC_FORCE === "true";
const isMysql =
sequelize.getDialect() === "mysql" || sequelize.getDialect() === "mariadb";
if (isMysql) {
await sequelize.query("SET FOREIGN_KEY_CHECKS = 0");
}
try {
await sequelize.sync({ force, alter: !force });
} finally {
if (isMysql) {
await sequelize.query("SET FOREIGN_KEY_CHECKS = 1");
}
}
console.log(
force
? "📦 Base de datos recreada (DB_SYNC_FORCE=true)"
: "📦 Base de datos sincronizada exitosamente"
);
} catch (error) {
console.error("❌ Error al conectar con la base de datos:", error);
process.exit(1);
}
}
async listen() {
// Orden de arranque: primero la BD (conexión + `sync`), después abrir el puerto.
// Si se abre el puerto antes de terminar `sync({ alter: true })`, las sentencias
// DDL (ALTER TABLE, DROP/ADD FOREIGN KEY) compiten con las peticiones que ya
// están entrando y provocan deadlocks y errores de FK intermitentes.
await this.dbConnection();
await this.app.listen(this.app.get('port'));
console.log(`🚀 Servidor ejecutándose en puerto ${this.app.get('port')}`);
}
}
EOF
src/routes/index.ts — agregador de features (5 de negocio + 7 de auth). Cada feature decide su modalidad de acceso middleware a middleware.
: > src/routes/index.ts
cat >> src/routes/index.ts << 'EOF'
import { ClientsRoutes } from "../features/business/clients/clients.routes";
import { ProductTypesRoutes } from "../features/business/product-types/product-types.routes";
import { ProductsRoutes } from "../features/business/products/products.routes";
import { SalesRoutes } from "../features/business/sales/sales.routes";
import { ProductSalesRoutes } from "../features/business/product-sales/product-sales.routes";
import { SessionRoutes } from "../features/auth/session/session.routes";
import { RefreshTokensRoutes } from "../features/auth/refresh-tokens/refresh-tokens.routes";
import { UsersRoutes } from "../features/auth/users/users.routes";
import { RolesRoutes } from "../features/auth/roles/roles.routes";
import { ResourcesRoutes } from "../features/auth/resources/resources.routes";
import { RoleUsersRoutes } from "../features/auth/role-users/role-users.routes";
import { ResourceRolesRoutes } from "../features/auth/resource-roles/resource-roles.routes";
/**
* Registro de features. Cada feature expone sus propias rutas y decide su
* **modalidad de acceso** (OPEN, JWT o JWT + RBAC) middleware a middleware.
*
* Fase I — Business: 5 features (JWT + RBAC).
* Fase II — Auth: 7 features: sesión (OPEN/JWT), sesiones propias (JWT),
* usuarios, roles, recursos, asignaciones y concesiones (JWT + RBAC).
*/
export class Routes {
// Fase I — Business
public clientsRoutes: ClientsRoutes = new ClientsRoutes();
public productTypesRoutes: ProductTypesRoutes = new ProductTypesRoutes();
public productsRoutes: ProductsRoutes = new ProductsRoutes();
public salesRoutes: SalesRoutes = new SalesRoutes();
public productSalesRoutes: ProductSalesRoutes = new ProductSalesRoutes();
// Fase II — Auth con RBAC
public sessionRoutes: SessionRoutes = new SessionRoutes();
public refreshTokensRoutes: RefreshTokensRoutes = new RefreshTokensRoutes();
public usersRoutes: UsersRoutes = new UsersRoutes();
public rolesRoutes: RolesRoutes = new RolesRoutes();
public resourcesRoutes: ResourcesRoutes = new ResourcesRoutes();
public roleUsersRoutes: RoleUsersRoutes = new RoleUsersRoutes();
public resourceRolesRoutes: ResourceRolesRoutes = new ResourceRolesRoutes();
}
EOF
src/swagger/index.ts — registry OpenAPI: fusiona 12 módulos, declara bearerAuth y fija la postura secure by default (security: [{ bearerAuth: [] }]), que las operaciones OPEN anulan con security: [].
: > src/swagger/index.ts
cat >> src/swagger/index.ts << 'EOF'
import { Application } from "express";
import swaggerUi from "swagger-ui-express";
import { clientsSwagger } from "../features/business/clients/clients.swagger";
import { productTypesSwagger } from "../features/business/product-types/product-types.swagger";
import { productsSwagger } from "../features/business/products/products.swagger";
import { salesSwagger } from "../features/business/sales/sales.swagger";
import { productSalesSwagger } from "../features/business/product-sales/product-sales.swagger";
import { sessionSwagger } from "../features/auth/session/session.swagger";
import { refreshTokensSwagger } from "../features/auth/refresh-tokens/refresh-tokens.swagger";
import { usersSwagger } from "../features/auth/users/users.swagger";
import { rolesSwagger } from "../features/auth/roles/roles.swagger";
import { resourcesSwagger } from "../features/auth/resources/resources.swagger";
import { roleUsersSwagger } from "../features/auth/role-users/role-users.swagger";
import { resourceRolesSwagger } from "../features/auth/resource-roles/resource-roles.swagger";
import { bearerSecurityScheme } from "../shared/http/swagger-security";
import { unauthorizedResponse, forbiddenResponse } from "../shared/http/swagger-security";
export type FeatureSwaggerModule = {
tags: unknown[];
paths: Record<string, unknown>;
components?: { schemas?: Record<string, unknown> };
};
/**
* Registry externo: importa la documentación OpenAPI de cada feature
* (mismo patrón que SeedersRunner).
*
* Orden: primero los módulos de seguridad (que definen el esquema `bearerAuth` y
* los endpoints de sesión), después los de negocio.
*/
const featureSwaggerModules: FeatureSwaggerModule[] = [
sessionSwagger,
refreshTokensSwagger,
usersSwagger,
rolesSwagger,
resourcesSwagger,
roleUsersSwagger,
resourceRolesSwagger,
clientsSwagger,
productTypesSwagger,
productsSwagger,
salesSwagger,
productSalesSwagger,
];
export function buildOpenApiDocument() {
const tags: unknown[] = [];
const paths: Record<string, unknown> = {};
const schemas: Record<string, unknown> = {};
for (const mod of featureSwaggerModules) {
tags.push(...mod.tags);
Object.assign(paths, mod.paths);
if (mod.components?.schemas) {
Object.assign(schemas, mod.components.schemas);
}
}
return {
openapi: "3.0.3",
info: {
title: "StoreLab API",
version: "2.0.0",
description: [
"API StoreLab (Express + Sequelize) con **Auth con RBAC**.",
"",
"**Las tres modalidades de acceso** (se declaran por operación, no globalmente):",
"",
"- **OPEN** — sin identidad previa: `POST /api/sesion/login`, `/refresh`, `/logout`.",
"- **JWT** — token de acceso válido: `/api/sesion/perfil`, `/api/permisos`, `/api/sesiones/*`.",
"- **JWT + RBAC** — token válido **y** concesión activa de `(method, path)`: todo el CRUD de negocio y de administración de seguridad.",
"",
"Autenticación: obtener el `access_token` en `POST /api/sesion/login` y pulsar **Authorize** con " +
"`Bearer <access_token>`. La autorización aplica **deny by default**: sin concesión explícita, 403.",
"",
"Credenciales de laboratorio: `admin / Admin123!` y `seller / Seller123!`.",
].join("\n"),
},
servers: [
{
url: `http://localhost:${process.env.PORT || 4000}`,
description: "Local",
},
],
tags,
paths,
// Postura *secure by default*: cualquier operación que no declare su propio
// `security` exige el access token. Los endpoints OPEN (login/refresh/logout)
// lo anulan explícitamente con `security: []`.
security: [{ bearerAuth: [] }],
components: {
// Esquema único de seguridad: `Authorization: Bearer <access_token>` (RFC 6750).
securitySchemes: bearerSecurityScheme,
// Respuestas reutilizables (referenciables con `$ref`).
responses: {
Unauthorized: unauthorizedResponse,
Forbidden: forbiddenResponse,
},
schemas,
},
};
}
/** Monta Swagger UI y el JSON OpenAPI */
export function setupSwagger(app: Application): void {
const document = buildOpenApiDocument();
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(document));
app.get("/api/docs.json", (_req, res) => {
res.json(document);
});
console.log("📘 Swagger UI: /api/docs | OpenAPI JSON: /api/docs.json");
}
EOF
src/database/seeders/index.ts — SeedersRunner: seguridad primero (roles → resources → users → role_users → resource_roles) y después negocio.
: > src/database/seeders/index.ts
cat >> src/database/seeders/index.ts << 'EOF'
import dotenv from "dotenv";
import { sequelize, testConnection } from "../db";
import "../../features/business/clients/client.model";
import "../../features/business/product-types/product-type.model";
import "../../features/business/products/product.model";
import "../../features/business/sales/sale.model";
import "../../features/business/product-sales/product-sale.model";
import "../../features/business/products/products.associations";
import "../../features/business/sales/sales.associations";
import "../../features/business/product-sales/product-sales.associations";
import "../../features/auth/users/user.model";
import "../../features/auth/roles/role.model";
import "../../features/auth/resources/resource.model";
import "../../features/auth/role-users/role-user.model";
import "../../features/auth/resource-roles/resource-role.model";
import "../../features/auth/refresh-tokens/refresh-token.model";
import "../../features/auth/rbac.associations";
import { seedRoles } from "../../features/auth/roles/roles.seeder";
import { seedResources } from "../../features/auth/resources/resources.seeder";
import { seedUsers } from "../../features/auth/users/users.seeder";
import { seedRoleUsers } from "../../features/auth/role-users/role-users.seeder";
import { seedResourceRoles } from "../../features/auth/resource-roles/resource-roles.seeder";
import { seedClients } from "../../features/business/clients/clients.seeder";
import { seedProductTypes } from "../../features/business/product-types/product-types.seeder";
import { seedProducts } from "../../features/business/products/products.seeder";
import { seedSales } from "../../features/business/sales/sales.seeder";
import { seedProductSales } from "../../features/business/product-sales/product-sales.seeder";
import { resolveSeedCounts } from "./counts";
dotenv.config();
/**
* SeedersRunner — ejecuta los seeders de TODAS las tablas (features).
*
* Orden: seguridad (Fase II) → negocio (Fase I).
*
* ```text
* Fase II roles → resources → users → role_users → resource_roles
* Fase I clients → product_types → products → sales → product_sales
* ```
*
* La seguridad va primero porque deja el sistema **operable de inmediato**: los
* dos usuarios canónicos con sus roles y sus concesiones, es decir, con la matriz
* RBAC completa. A partir de ahí, cualquier petición de negocio ya tiene sentido.
*
* Ejecutar seeders de todas las tablas:
* npm run db:seed
*
* Variar cantidades (CLI o env; claves = nombre de tabla):
* npm run db:seed -- --users=5 --clients=20 --products=15
* SEED_USERS=2 SEED_CLIENTS=5 npm run db:seed
*
* Defaults: ver `counts.ts`. Los seeders de catálogo (roles, resources, ...) son
* deterministas y **reconciliadores**: reejecutarlos deja la matriz exacta.
* Ubicación de cada seeder: `src/features/.../<plural>.seeder.ts`
* Este archivo solo orquesta; no define datos.
*/
export async function runAllSeeders(): Promise<void> {
const counts = resolveSeedCounts();
console.log("🌱 Iniciando SeedersRunner...");
console.log("📊 Conteos:", counts);
const ok = await testConnection();
if (!ok) {
throw new Error("No hay conexión a la base de datos");
}
const isMysql =
sequelize.getDialect() === "mysql" || sequelize.getDialect() === "mariadb";
if (isMysql) {
await sequelize.query("SET FOREIGN_KEY_CHECKS = 0");
}
try {
await sequelize.sync({ force: false, alter: true });
} finally {
if (isMysql) {
await sequelize.query("SET FOREIGN_KEY_CHECKS = 1");
}
}
// Fase II — Auth con RBAC (el orden respeta las dependencias de la cadena)
await seedRoles();
await seedResources();
await seedUsers(counts.users);
await seedRoleUsers();
await seedResourceRoles();
// Fase I — Business (padres → hijos)
await seedClients(counts.clients);
await seedProductTypes(counts.product_types);
await seedProducts(counts.products);
await seedSales(counts.sales);
await seedProductSales(counts.product_sales);
console.log("🌱 SeedersRunner finalizado");
}
if (require.main === module) {
runAllSeeders()
.then(async () => {
await sequelize.close();
process.exit(0);
})
.catch(async (err) => {
console.error("❌ Error en seeders:", err);
await sequelize.close();
process.exit(1);
});
}
EOF
src/database/seeders/counts.ts — cantidades variables; los catálogos de seguridad son deterministas y no tienen conteo.
: > src/database/seeders/counts.ts
cat >> src/database/seeders/counts.ts << 'EOF'
/**
* Cantidad de registros por tabla (snake_case = nombre de tabla BD).
* Prioridad: CLI (--clients=N) > env (SEED_CLIENTS) > default de este archivo.
*
* Solo las tablas con datos **variables** tienen conteo. Los catálogos de
* seguridad (`roles`, `resources`, `role_users`, `resource_roles`) son
* deterministas: su contenido vive en el código
* (`resource-catalog.ts` y los `*.seeder.ts` de cada feature de auth), no en un
* número. `refresh_tokens` no tiene seeder: lo puebla el login.
*
* Cuando agregues features, suma aquí la clave (nombre de tabla) y léela en el runner.
*/
export type SeedCounts = {
users: number;
clients: number;
product_types: number;
products: number;
sales: number;
product_sales: number;
};
export const DEFAULT_SEED_COUNTS: SeedCounts = {
// 2 usuarios canónicos: admin (ADMIN, 58 permisos) y seller (SELLER, 7 permisos).
users: 2,
clients: 10,
product_types: 25,
products: 15,
sales: 5,
product_sales: 12,
};
export function resolveSeedCounts(argv: string[] = process.argv.slice(2)): SeedCounts {
const counts: SeedCounts = { ...DEFAULT_SEED_COUNTS };
const envMap: Array<[keyof SeedCounts, string | undefined]> = [
["users", process.env.SEED_USERS],
["clients", process.env.SEED_CLIENTS],
["product_types", process.env.SEED_PRODUCT_TYPES],
["products", process.env.SEED_PRODUCTS],
["sales", process.env.SEED_SALES],
["product_sales", process.env.SEED_PRODUCT_SALES],
];
for (const [key, value] of envMap) {
if (value !== undefined && value !== "") {
counts[key] = Number(value);
}
}
for (const arg of argv) {
const m = arg.match(/^--([a-zA-Z_]+)=(\d+)$/);
if (!m) continue;
const key = m[1] as keyof SeedCounts;
const value = Number(m[2]);
if (key in counts) {
counts[key] = value;
}
}
return counts;
}
EOF
Las variables
JWT_SECRET,JWT_ACCESS_TTLyJWT_REFRESH_TTL_DAYSse añaden al.enven ISS-09 §14.1.
Las tres modalidades — mapa definitivo de rutas
| Modalidad | Middlewares | Rutas |
|---|---|---|
| OPEN | — | POST /api/sesion/login · /refresh · /logout · GET /api/docs · /api/docs.json |
| JWT | authenticate |
GET /api/sesion/perfil · /api/permisos · GET /api/sesiones · /:id · PATCH /api/sesiones/:id/deactivate · /deactivate-all · DELETE /api/sesiones |
| JWT + RBAC | authenticate, authorize |
/api/usuarios… · /api/roles… · /api/recursos… · /api/asignaciones-rol… · /api/concesiones-rol… · /api/clientes… · /api/tipos-producto… · /api/productos… · /api/ventas… · /api/detalle-ventas… |
Alta de un permiso en caliente (sin desplegar código):
1. POST /api/recursos { method, path, description } → alta del punto de acceso
2. POST /api/concesiones-rol { role_id, resource_id } → concesión a un rol
3. GET /api/roles/:id/... (o GET /api/concesiones-rol?role_id=) → verificación
El efecto es inmediato: authorize consulta la matriz en cada petición y no cachea.
Estructura final (Fase II en negrita)
src/
├── config/index.ts
├── database/seeders/{counts,index}.ts
├── routes/index.ts
├── shared/
│ ├── auth/{password,jwt,resource-match,auth-user}.ts # ← Fase II
│ ├── http/{base-controller,error-response,swagger-security}.ts
│ ├── database/with-transaction.ts
│ └── errors/app-error.ts
├── features/
│ ├── business/ # Fase I
│ │ ├── clients/ product-types/ products/ sales/ product-sales/
│ │ └── (cada feature: model, dto/, repository, service, controller, routes, seeder, swagger, http/)
│ └── auth/ # ← Fase II
│ ├── access/{authenticate,authorize}.middleware.ts
│ ├── rbac.associations.ts
│ ├── users/ roles/ resources/ role-users/ resource-roles/ refresh-tokens/ session/
│ └── (cada feature: dto/, repository, service, controller, routes, ...[.seeder,.swagger], http/)
├── swagger/index.ts
└── server.ts
DoD del laboratorio (Fase I + Fase II)
- [ ] 12 features (5 business + 7 auth), cada uno con sus capas
controller → service → repository → model - [ ] 11 tablas:
clients,product_types,products,sales,product_sales,users,roles,resources,role_users,resource_roles,refresh_tokens - [ ] 3 modalidades aplicadas por ruta: OPEN, JWT, JWT + RBAC
- [ ] No existe entidad
Permission; el permiso esresource_roles (role_id, resource_id) - [ ]
deny by default: sin concesión activa → 403 - [ ] Access token corto (HS256,
iss/aud/exp/jti), refresh token opaco, hasheado, rotativo y revocable - [ ]
role_usersyresource_rolessoportan asignar / retirar / reactivar yreconcileRole - [ ] Seeders deterministas: 2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones
- [ ] Swagger
/api/docsconbearerAuth, 401/403 y las 3 modalidades por operación - [ ]
npx tsc --noEmitOK - [ ] Smoke test E2E de las tres modalidades en verde
Cómo repetir el patrón (otra entidad)
ISS-n-A modelo + DTO + esqueletos repository/service/controller/routes + cableado
ISS-n-B…E CRUD en orden: getAll, getOne, create, update PUT/PATCH, delete físico/lógico
(cada paso toca las 3 capas: repository -> service -> controller) + http
ISS-n-F seeder + PARCHE counts/runner
ISS-n-G swagger + PARCHE registry
ISS-n-R si hay FK `tabla_singular_id`: associations.ts + PARCHE config
ISS-n-S si el recurso debe protegerse: alta en `resource-catalog.ts` + concesión por rol
Regla de oro del lab:
routes -> controller -> service -> repository -> model. Ninguna capa se salta a la siguiente ni consulta el modelo directamente. El DTO es el contrato que cruzaroutes/controller/service; elrepositoryy elmodelno lo conocen.
Referencia rápida de paquetes
npm install express@^5.2.1 cors@^2.8.6 dotenv@^17.4.2 morgan@^1.12.1 \
sequelize@^6.37.8 mysql2@^3.24.4 pg@^8.23.0 pg-hstore@^2.3.4 \
tedious@^20.0.0 oracledb@^7.0.1 bcryptjs@^3.0.3 \
jsonwebtoken@^9.0.3 swagger-ui-express@^5.0.1
npm install -D typescript@~5.9.2 ts-node@^10.9.2 nodemon@^3.1.14 \
@types/node@^22.20.4 @types/express@^5.0.6 \
@types/cors@^2.8.19 @types/morgan@^1.9.10 \
@types/sequelize@^6.12.0 @types/bcryptjs@^3.0.0 \
@types/jsonwebtoken@^9.0.10 @types/swagger-ui-express@^4.1.8 \
@faker-js/faker@^10.6.0
Fuentes
- Este manual es la única guía de construcción (ISS,
cat >>, PARCHE). bd-storelab.md— solo consulta: entidades y campos (Fase I y Fase II).- RFC 7519 — JSON Web Token
- RFC 8725 — JWT Best Current Practices
- RFC 6749 — OAuth 2.0 · RFC 6750 — Bearer Token Usage
- OWASP — Authentication Cheat Sheet · Session Management · Authorization
- NIST SP 800-63B — Digital Identity Guidelines
- Express — Middleware
Diagnóstico
| Síntoma | Causa probable | Solución |
|---|---|---|
Una ruta JWT + RBAC responde 401 |
Falta la cabecera Authorization: Bearer <access_token> o el token expiró |
Hacer login, copiar el access_token y pulsar Authorize en Swagger |
Una ruta JWT + RBAC responde 403 con token válido |
El rol no tiene concesión sobre (method, path) |
Revisar la matriz con GET /api/concesiones-rol y conceder el recurso al rol |
| El token se rechaza por firma | JWT_SECRET ausente o distinto entre emisiones |
Fijar JWT_SECRET en el .env (se añade en ISS-09 §14.1) |
| El access token dura demasiado | JWT_ACCESS_TTL mal configurado |
Ajustar JWT_ACCESS_TTL; el access debe ser corto |
| El refresh token no rota | Se está reutilizando un token ya consumido | Volver a hacer login; el refresh es rotativo y detecta el reúso |
| Tras sembrar, un rol no tiene permisos | Orden de seeders alterado (negocio antes que seguridad) | Respetar roles → resources → users → role_users → resource_roles y reejecutar npm run db:seed |
/api/docs no pide token |
La operación declara security: [] |
Es correcto si es OPEN (login/refresh/logout/docs); revisa la modalidad de la operación |
npx tsc --noEmit falla en shared/auth |
Firma de tipos de JWT desalineada | Corregir el tipo; @types/jsonwebtoken debe estar instalado |
Pregunta que responde: si el smoke test no queda en verde, ¿por dónde empiezo a mirar?
Conexión con el resto del curso
flowchart LR
A["CIERRE-BUSINESS<br/>Fase I verificada"] --> B["ISS-09 ... ISS-15<br/>Auth con RBAC"]
B --> C["CIERRE-AUTH<br/>verificacion Fase II"]
C --> D["Backend completo<br/>fin del laboratorio"]
Pregunta que responde: ¿qué cierra este documento y qué relación guarda con el cierre de la Fase I?
- Lo habilita: ISS-15, que cierra la última feature de seguridad (sesión).
- Lo que cierra: el laboratorio entero: el negocio de la Fase I más la seguridad de la Fase II.
- Piezas reutilizadas: el mismo recorrido
routes → controller → service → repository → model, elSeedersRunner(que ahora siembra seguridad y negocio) y el registry de Swagger (que ahora fusiona 12 módulos). - Qué cambió respecto a la Fase I: las rutas de negocio dejaron de ser abiertas y pasaron a exigir JWT + RBAC; antes respondían sin token, ahora exigen identidad y concesión.
Glosario
| Término | Significado en este cierre |
|---|---|
| Modalidad de acceso | Nivel de exigencia de una ruta: OPEN, JWT o JWT + RBAC |
authenticate |
Middleware que valida el access token y construye la identidad |
authorize |
Middleware que consulta la matriz RBAC y aplica deny by default |
| JWT / HS256 | Token firmado con iss/aud/exp/jti; el algoritmo del access token |
| Access token | Token corto que viaja en Authorization: Bearer <token> |
| Refresh token | Token opaco, hasheado, rotativo y revocable; permite nueva sesión sin reescribir la clave |
| Rotación | Cada refresh emite un token nuevo; reutilizar uno viejo delata el reúso |
| Revocación inmediata | authorize relee la matriz en cada petición: retirar una concesión surte efecto ya |
| Recurso | Punto de acceso identificado por (method, path) en resources |
| Concesión | Fila resource_roles (role_id, resource_id): el permiso, sin entidad Permission |
deny by default |
Sin concesión explícita → 403 |
| 401 vs 403 | 401 = no sé quién eres; 403 = sé quién eres, pero no tienes concesión |
bearerAuth |
Esquema de seguridad declarado en Swagger: Bearer <access_token> |
reconcileRole |
Reconciliador de la matriz: role_users y resource_roles permiten asignar, retirar y reactivar |
Pregunta que responde: ¿qué vocabulario de seguridad debo dominar para leer el DoD sin ambigüedad?
Criterios de aceptación
Los del documento fuente, textuales:
- [ ] 12 features (5 business + 7 auth), cada uno con sus capas
controller → service → repository → model - [ ] 11 tablas:
clients,product_types,products,sales,product_sales,users,roles,resources,role_users,resource_roles,refresh_tokens - [ ] 3 modalidades aplicadas por ruta: OPEN, JWT, JWT + RBAC
- [ ] No existe entidad
Permission; el permiso esresource_roles (role_id, resource_id) - [ ]
deny by default: sin concesión activa → 403 - [ ] Access token corto (HS256,
iss/aud/exp/jti), refresh token opaco, hasheado, rotativo y revocable - [ ]
role_usersyresource_rolessoportan asignar / retirar / reactivar yreconcileRole - [ ] Seeders deterministas: 2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones
- [ ] Swagger
/api/docsconbearerAuth, 401/403 y las 3 modalidades por operación - [ ]
npx tsc --noEmitOK - [ ] Smoke test E2E de las tres modalidades en verde
Evaluación
Preguntas de comprensión
-
¿Por qué el cierre de la Fase II tampoco crea archivos? Porque es un documento de verificación, no de construcción: define el DoD final y audita lo hecho entre ISS-09 e ISS-15. El código ya existe; lo que falta es demostrar que funciona como sistema completo.
-
¿Cuál es la diferencia entre una ruta
JWTy una rutaJWT + RBAC? La rutaJWTsolo exigeauthenticate(identidad válida): perfil, permisos y sesiones propias. La rutaJWT + RBACexige ademásauthorize, que comprueba una concesión sobre(method, path). Sin identidad → 401; con identidad pero sin concesión → 403. -
Si no existe la entidad
Permission, ¿dónde vive el permiso? En la filaresource_roles (role_id, resource_id). Un rol agrupa concesiones sobre recursos; un usuario recibe roles víarole_users. El permiso es una relación, no una tabla propia. -
¿Por qué el refresh token es «opaco, hasheado, rotativo y revocable»? Porque es una credencial de larga vida: si fuera un JWT legible y reutilizable, un robo daría acceso indefinido. Al ser opaco y guardarse hasheado, no se puede leer la base y usarlo; al rotar en cada refresh, reutilizar uno viejo se detecta; y al ser revocable, el logout lo invalida de inmediato.
-
¿Qué significa exactamente
deny by defaulty por qué es una decisión de diseño? Significa que, si no hay una concesión explícita, la petición se rechaza con 403. Es secure by default: olvidar conceder un permiso produce un bloqueo visible (seguro), no un acceso accidental (inseguro). El error se detecta en pruebas, no en producción. -
¿Por qué el mapa RBAC dice 58 recursos pero 65 concesiones? Porque recursos y concesiones son cosas distintas: 58 son los puntos de acceso del catálogo y 65 son las filas de
resource_rolesque reparten esos recursos entre los dos roles (admin con los 58 permisos y seller con 7). Un recurso puede estar concedido a varios roles. -
¿Qué demuestra el estado «todo ✅» del mapa del backend frente al cierre de la Fase I? Que la parte que en la Fase I era 🎯 (toda la seguridad: auth, RBAC y sesiones) ya está construida y verificada. El backend deja de tener capas pendientes: negocio y seguridad conviven con las tres modalidades por ruta.
Ejercicios
Ejercicio 1 — Clasificar rutas por modalidad.
Toma estas operaciones y asígnales su modalidad: POST /api/sesion/login, GET /api/sesion/perfil, GET /api/clientes, GET /api/docs, PATCH /api/sesiones/:id/deactivate. Justifica cada una.
Respuesta razonada
- `POST /api/sesion/login` → **OPEN**: es la puerta de entrada, no puede exigir token previo. - `GET /api/sesion/perfil` → **JWT**: necesitas identidad, pero no una concesión (es tu propio perfil). - `GET /api/clientes` → **JWT + RBAC**: negocio protegido; exige identidad **y** concesión `(GET, /api/clientes)`. - `GET /api/docs` → **OPEN**: la documentación es pública. - `PATCH /api/sesiones/:id/deactivate` → **JWT**: gestiona tus propias sesiones; basta con identidad válida. La regla: si no hay identidad previa posible → OPEN; si basta con saber quién eres → JWT; si además hay que tener permiso sobre el recurso → JWT + RBAC.Ejercicio 2 — Trazar un 403.
Explica paso a paso cómo una petición con un token válido de seller termina en 403 al llamar a una ruta de negocio no concedida.
Respuesta razonada
1. `authenticate` valida la firma HS256, el `exp` y construye la identidad de `seller` (token válido, no hay 401). 2. `authorize` calcula el recurso `(method, path)` y consulta la matriz `(role_id, resource_id)` en cada petición, sin caché. 3. No encuentra concesión activa para el rol `SELLER` sobre ese recurso. 4. Aplica `deny by default` y responde **403**. El punto clave es que el 403 **no** viene de un token malo sino de la ausencia de concesión, y que el efecto de conceder el recurso sería inmediato.Ejercicio 3 — Auditar el orden de los seeders. El runner siembra la seguridad antes que el negocio. Explica por qué ese orden y qué pasaría si se invirtiera, pensando en el smoke test.
Respuesta razonada
El orden es `roles → resources → users → role_users → resource_roles` y después `clients → product_types → products → sales → product_sales`. La seguridad va primero porque deja el sistema **operable de inmediato**: usuarios canónicos con sus roles y sus 65 concesiones, es decir, la matriz RBAC completa. Si se sembrara el negocio primero, el smoke test no podría hacer login ni autorizar (no habría usuarios ni concesiones), así que las rutas protegidas devolverían 401/403. Además, dentro de cada bloque se respeta el orden padre → hijo para no violar FKs.GATE
Para cerrar el laboratorio completo, ejecuta en orden:
Resultado esperado:
npx tsc --noEmittermina sin errores.npm run db:seedsiembra la seguridad (2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones) y el negocio, y termina con «SeedersRunner finalizado».- El servidor arranca y
/api/docsmuestrabearerAuth, las respuestas 401/403 y la modalidad de cada operación.
Después, ejecuta el smoke test E2E de las tres modalidades desde Swagger:
- OPEN:
POST /api/sesion/loginconadmin / Admin123!y conseller / Seller123!;POST /api/sesion/refresh;POST /api/sesion/logout. - JWT: con el
access_tokenen Authorize,GET /api/sesion/perfil,GET /api/permisosyGET /api/sesiones. - JWT + RBAC:
GET /api/clientesconadmin(200) y una ruta de negocio no concedida conseller(403); comprueba también que sin token la respuesta es 401.
Checklist de cierre:
- [ ] 12 features (5 business + 7 auth) con sus 4 capas
- [ ] 11 tablas presentes y sembradas
- [ ] 3 modalidades aplicadas por ruta
- [ ] Sin entidad
Permission; permiso comoresource_roles (role_id, resource_id) - [ ]
deny by defaultverificado (403 sin concesión) - [ ] Access corto + refresh opaco, hasheado, rotativo y revocable
- [ ] Seeders deterministas (2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones)
- [ ] Swagger
/api/docsconbearerAuth, 401/403 y modalidad por operación - [ ]
npx tsc --noEmitOK - [ ] Smoke test E2E de las tres modalidades en verde
Con el checklist en verde, el laboratorio está cerrado: negocio y seguridad funcionan juntos. Puedes volver al cierre de la Fase I para comparar el estado de partida con el estado final.
Navegación de la ruta: ← ISS-15 · 🛠 Construir · ↑ Ruta Express · → CIERRE-AUTH · 🛠 Construir