Saltar a contenido

📚 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, JWT o JWT + RBAC.
  • No existe la entidad Permission: el permiso es la fila resource_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 --noEmit termina 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

  1. Ficha del ISS
  2. Mapa mental del ISS
  3. Mapa del backend
  4. Árbol de archivos
  5. Las tres modalidades de acceso
  6. Flujos
  7. Comandos explicados
  8. La matriz de pruebas de la fase
  9. Recorrido del ISS, paso a paso
  10. Diagnóstico
  11. Conexión con el resto del curso
  12. Glosario
  13. Criterios de aceptación
  14. Evaluación
  15. 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

(estado al terminar ISS-15: idéntico a la estructura después)

Archivos creados / modificados en este ISS

(ninguno)

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 en features/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 jsonwebtoken no 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_TTL y JWT_REFRESH_TTL_DAYS se añaden al .env en 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 es resource_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_users y resource_roles soportan asignar / retirar / reactivar y reconcileRole
  • [ ] Seeders deterministas: 2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones
  • [ ] Swagger /api/docs con bearerAuth, 401/403 y las 3 modalidades por operación
  • [ ] npx tsc --noEmit OK
  • [ ] 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 cruza routes/controller/service; el repository y el model no 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

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, el SeedersRunner (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 es resource_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_users y resource_roles soportan asignar / retirar / reactivar y reconcileRole
  • [ ] Seeders deterministas: 2 roles, 58 recursos, 2 usuarios, 2 asignaciones, 65 concesiones
  • [ ] Swagger /api/docs con bearerAuth, 401/403 y las 3 modalidades por operación
  • [ ] npx tsc --noEmit OK
  • [ ] Smoke test E2E de las tres modalidades en verde

Evaluación

Preguntas de comprensión

  1. ¿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.

  2. ¿Cuál es la diferencia entre una ruta JWT y una ruta JWT + RBAC? La ruta JWT solo exige authenticate (identidad válida): perfil, permisos y sesiones propias. La ruta JWT + RBAC exige además authorize, que comprueba una concesión sobre (method, path). Sin identidad → 401; con identidad pero sin concesión → 403.

  3. Si no existe la entidad Permission, ¿dónde vive el permiso? En la fila resource_roles (role_id, resource_id). Un rol agrupa concesiones sobre recursos; un usuario recibe roles vía role_users. El permiso es una relación, no una tabla propia.

  4. ¿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.

  5. ¿Qué significa exactamente deny by default y 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.

  6. ¿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_roles que reparten esos recursos entre los dos roles (admin con los 58 permisos y seller con 7). Un recurso puede estar concedido a varios roles.

  7. ¿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:

npx tsc --noEmit
npm run db:seed
npm run dev

Resultado esperado:

  • npx tsc --noEmit termina sin errores.
  • npm run db:seed siembra 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/docs muestra bearerAuth, las respuestas 401/403 y la modalidad de cada operación.

Después, ejecuta el smoke test E2E de las tres modalidades desde Swagger:

  1. OPEN: POST /api/sesion/login con admin / Admin123! y con seller / Seller123!; POST /api/sesion/refresh; POST /api/sesion/logout.
  2. JWT: con el access_token en Authorize, GET /api/sesion/perfil, GET /api/permisos y GET /api/sesiones.
  3. JWT + RBAC: GET /api/clientes con admin (200) y una ruta de negocio no concedida con seller (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 como resource_roles (role_id, resource_id)
  • [ ] deny by default verificado (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/docs con bearerAuth, 401/403 y modalidad por operación
  • [ ] npx tsc --noEmit OK
  • [ ] 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