Sesión 5 · chuleta
En el orden de las slides. Copia con el botón de cada bloque; si la slide dice «teclea», esas líneas van a mano.
📋 S5·01 · receta «Cambiar de rama» (terminal)
Sección titulada «📋 S5·01 · receta «Cambiar de rama» (terminal)»git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s5 origin/s5-start # checkpoint: mi-s5-cp origin/s5-checkpointnpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed📋 S5·02 · variable API (terminal)
Sección titulada «📋 S5·02 · variable API (terminal)»export API=http://localhost:3000/api/v1 # export: si no, los checks no la ven📋 S5·03 · login de Luis (terminal)
Sección titulada «📋 S5·03 · login de Luis (terminal)»# Luis (STUDENT) hace login y copia su tokencurl -s -H 'Content-Type: application/json' \ -d '{"email":"luis@academia.dev","password":"Passw0rd!"}' $API/auth/login📋 S5·04 · inscripciones de otro usuario (terminal)
Sección titulada «📋 S5·04 · inscripciones de otro usuario (terminal)»# …y pide las inscripciones de OTRO usuario (ids correlativos: prueba 4, 5, 6…)curl -s -H "Authorization: Bearer $TOKEN" $API/users/6/enrollments# → 200 [{"id":2,"userId":6,"courseId":1,"status":"COMPLETED","grade":9, …# "course":{"id":1,"title":"TypeScript desde cero"}}]📋 S5·05 · s5-patch-course.ia.ts (código de la IA, para leer)
Sección titulada «📋 S5·05 · s5-patch-course.ia.ts (código de la IA, para leer)»// ─────────────────────────────────────────────────────────────────────────// Generado por un asistente de IA · Sesión 5// Prompt: "Añade a mi API: PATCH /courses/:id para editar un curso, una// búsqueda de cursos por título, y configura CORS y rate limiting"// Instalación sugerida por el asistente:// npm install cors express-rate-limiter// Tu tarea: mapea cada fallo a su categoría del OWASP API Top 10 (API1…API10).// Antes de instalar nada: npm view <paquete> (autor, descargas, repo).// ─────────────────────────────────────────────────────────────────────────import cors from 'cors';import express, { type Request, type Response } from 'express';import { prisma } from '../db.js';import { requireAuth } from '../middlewares/require-auth.js';
export const app = express();app.use(cors({ origin: '*', credentials: true }));app.use(express.json());
app.patch('/api/v1/courses/:id', requireAuth, async (req: Request, res: Response) => { const course = await prisma.course.update({ where: { id: Number(req.params.id) }, data: req.body, include: { instructor: true }, }); res.json(course);});
app.get('/api/v1/courses/search', async (req: Request, res: Response) => { const results = await prisma.$queryRawUnsafe( `SELECT * FROM "Course" WHERE title ILIKE '%${req.query.q}%'`, ); res.json(results);});📋 S5·06 · npm view de los dos paquetes (terminal)
Sección titulada «📋 S5·06 · npm view de los dos paquetes (terminal)»npm view express-rate-limiternpm view express-rate-limit📋 S5·07 · Minilab 1 (1/2) · antes (terminal)
Sección titulada «📋 S5·07 · Minilab 1 (1/2) · antes (terminal)»curl -s "$API/courses?pageSize=1"# → …"instructor":{"id":1,# "email":"marta@academia.dev",# "name":"Marta",# "password":"$argon2id$v=19$m=65536…",# "role":"INSTRUCTOR", …}📋 S5·08 · Minilab 1 (1/2) · después (terminal)
Sección titulada «📋 S5·08 · Minilab 1 (1/2) · después (terminal)»curl -s "$API/courses?pageSize=1"# → …"instructor":{"id":1,"name":"Marta"}}]📋 S5·09 · Minilab 1 (2/2) · login de Marta (terminal)
Sección titulada «📋 S5·09 · Minilab 1 (2/2) · login de Marta (terminal)»curl -s -H 'Content-Type: application/json' \ -d '{"email":"marta@academia.dev","password":"Passw0rd!"}' \ $API/auth/login📋 S5·10 · Minilab 1 (2/2) · PATCH con instructorId (terminal)
Sección titulada «📋 S5·10 · Minilab 1 (2/2) · PATCH con instructorId (terminal)»curl -i -X PATCH \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $TOKEN" \ -d '{"price":20,"instructorId":999}' \ $API/courses/$ID# → 409 {"error":"Foreign key constraint failed…"}📋 S5·11 · searchQuerySchema (src/schemas/course.schema.ts)
Sección titulada «📋 S5·11 · searchQuerySchema (src/schemas/course.schema.ts)»// src/schemas/course.schema.ts (añade, junto a listCoursesQuerySchema)export const searchQuerySchema = z.object({ q: z.string().trim().min(2).max(50),});📋 S5·12 · searchCourses (src/services/courses.service.ts)
Sección titulada «📋 S5·12 · searchCourses (src/services/courses.service.ts)»// src/services/courses.service.ts (dentro de coursesService)searchCourses: (q: string) => coursesRepository.search(q),📋 S5·13 · search en el controlador (src/controllers/courses.controller.ts)
Sección titulada «📋 S5·13 · search en el controlador (src/controllers/courses.controller.ts)»// src/controllers/courses.controller.ts (dentro de coursesController)search: async (req: Request, res: Response) => { const { q } = searchQuerySchema.parse(req.query); res.status(200).json(await coursesService.searchCourses(q));},📋 S5·14 · ruta /search (src/routes/courses.routes.ts)
Sección titulada «📋 S5·14 · ruta /search (src/routes/courses.routes.ts)»// src/routes/courses.routes.ts: ANTES de la ruta '/:id'coursesRouter.get('/search', coursesController.search);📋 S5·15 · Minilab 2 · antes (terminal)
Sección titulada «📋 S5·15 · Minilab 2 · antes (terminal)»curl -s --get --data-urlencode "q=' OR '1'='1" \ $API/courses/search# → []curl -s --get --data-urlencode "q=%%" \ $API/courses/search# → [{"id":1,"title":"TypeScript desde cero"},# {"id":2,"title":"Prisma avanzado"}]📋 S5·16 · Minilab 2 · después (terminal)
Sección titulada «📋 S5·16 · Minilab 2 · después (terminal)»curl -s --get --data-urlencode "q=' OR '1'='1" \ $API/courses/search# → []curl -s --get --data-urlencode "q=%%" \ $API/courses/search# → []curl -s "$API/courses/search?q=Type"# → [{"id":1,"title":"TypeScript desde cero"}]📋 S5·17 · Minilab 3 · antes: seis logins fallidos (terminal)
Sección titulada «📋 S5·17 · Minilab 3 · antes: seis logins fallidos (terminal)»for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code}\n" \ -H 'Content-Type: application/json' \ -d '{"email":"marta@academia.dev","password":"mala"}' \ $API/auth/logindone# → 401 401 401 401 401 401 (nadie frena)📋 S5·18 · instalar las dependencias de hoy (terminal)
Sección titulada «📋 S5·18 · instalar las dependencias de hoy (terminal)»npm view express-rate-limit# el oficial, no "express-rate-limiter"npm install express-rate-limit helmet corsnpm install -D @types/cors📋 S5·19 · rate-limit.ts · imports y skip (src/middlewares/rate-limit.ts, nuevo)
Sección titulada «📋 S5·19 · rate-limit.ts · imports y skip (src/middlewares/rate-limit.ts, nuevo)»// src/middlewares/rate-limit.ts (fichero nuevo)import { rateLimit } from 'express-rate-limit';import { config } from '../config.js';
// En tests no limitamos: Supertest hace todas las peticiones desde la misma IP// y el contador en memoria se comparte entre tests.const skip = () => config.NODE_ENV === 'test';📋 S5·20 · registerRateLimiter (src/middlewares/rate-limit.ts, debajo)
Sección titulada «📋 S5·20 · registerRateLimiter (src/middlewares/rate-limit.ts, debajo)»// sigue en src/middlewares/rate-limit.ts// Registro: el 409 de "email ya registrado" permite enumerar// usuarios; limitar los intentos hace esa enumeración lenta y cara.export const registerRateLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora limit: 10, standardHeaders: 'draft-8', legacyHeaders: false, message: { error: 'Too many sign-ups from this IP, try again later', }, skip,});📋 S5·21 · generalRateLimiter (src/middlewares/rate-limit.ts, al final)
Sección titulada «📋 S5·21 · generalRateLimiter (src/middlewares/rate-limit.ts, al final)»// sigue en src/middlewares/rate-limit.tsexport const generalRateLimiter = rateLimit({ windowMs: 60 * 1000, limit: 100, standardHeaders: 'draft-8', legacyHeaders: false, skip,});📋 S5·22 · auth.routes.ts completo (src/routes/auth.routes.ts)
Sección titulada «📋 S5·22 · auth.routes.ts completo (src/routes/auth.routes.ts)»// src/routes/auth.routes.ts (completo): limitadores delante de validateBodyimport { Router } from 'express';import { authController } from '../controllers/auth.controller.js';import { loginRateLimiter, registerRateLimiter } from '../middlewares/rate-limit.js';import { validateBody } from '../middlewares/validate.js';import { loginSchema, registerSchema } from '../schemas/auth.schema.js';
export const authRouter = Router();
authRouter.post('/register', registerRateLimiter, validateBody(registerSchema), authController.register);authRouter.post('/login', loginRateLimiter, validateBody(loginSchema), authController.login);📋 S5·23 · app.ts completo tras el Minilab 3 (src/app.ts)
Sección titulada «📋 S5·23 · app.ts completo tras el Minilab 3 (src/app.ts)»// src/app.ts (completo tras el Minilab 3: sustituye el tuyo)import express from 'express';import { config } from './config.js';import { errorHandler } from './middlewares/error-handler.js';import { generalRateLimiter } from './middlewares/rate-limit.js';import { authRouter } from './routes/auth.routes.js';import { coursesRouter } from './routes/courses.routes.js';import { usersRouter } from './routes/users.routes.js';
// La app se exporta sin arrancar: server.ts hace listen() y los tests// (sesión 6) la usan directamente con Supertest.export const app = express();
app.set('trust proxy', config.TRUST_PROXY); // tras express()
app.use('/api/v1', generalRateLimiter); // antes de express.json()app.use(express.json());
app.use('/api/v1/auth', authRouter);app.use('/api/v1/courses', coursesRouter);app.use('/api/v1/users', usersRouter);
app.use(errorHandler); // siempre el último📋 S5·24 · TRUST_PROXY (.env.example)
Sección titulada «📋 S5·24 · TRUST_PROXY (.env.example)»# Nº de proxies delante de la API (0 en local; 1 detrás de un load balancer)TRUST_PROXY=0📋 S5·25 · Minilab 3 · después: el mismo bucle (terminal)
Sección titulada «📋 S5·25 · Minilab 3 · después: el mismo bucle (terminal)»for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code}\n" \ -H 'Content-Type: application/json' \ -d '{"email":"marta@academia.dev","password":"mala"}' \ $API/auth/logindone# → 401 401 401 401 401 429📋 S5·26 · Minilab 3 · un login más, con cabeceras (terminal)
Sección titulada «📋 S5·26 · Minilab 3 · un login más, con cabeceras (terminal)»curl -i -H 'Content-Type: application/json' \ -d '{"email":"marta@academia.dev","password":"mala"}' \ $API/auth/login# → HTTP/1.1 429 Too Many Requests# RateLimit-Policy: "5-in-15min"; q=5; …# RateLimit: "5-in-15min"; r=0; t=900# {"error":"Too many login attempts, try again later"}📋 S5·27 · Minilab 4 · antes (terminal)
Sección titulada «📋 S5·27 · Minilab 4 · antes (terminal)»curl -s -D - -o /dev/null \ -H "Origin: http://localhost:5173" \ $API/courses# → HTTP/1.1 200 OK# X-Powered-By: Express# RateLimit: "100-in-1min"; …# (ni rastro de Access-Control-Allow-Origin)📋 S5·28 · CORS_ORIGINS (src/config.ts, dentro de envSchema)
Sección titulada «📋 S5·28 · CORS_ORIGINS (src/config.ts, dentro de envSchema)»// src/config.ts: dentro de envSchemaCORS_ORIGINS: z.string() .default('http://localhost:5173') .transform((s) => s.split(',').map((o) => o.trim())),📋 S5·29 · CORS_ORIGINS (.env.example)
Sección titulada «📋 S5·29 · CORS_ORIGINS (.env.example)»# .env.example: añade la línea (el .env no se toca: es el default)CORS_ORIGINS="http://localhost:5173"📋 S5·30 · Minilab 4 · después (terminal)
Sección titulada «📋 S5·30 · Minilab 4 · después (terminal)»curl -s -D - -o /dev/null -H "Origin: http://localhost:5173" $API/courses# → HTTP/1.1 200 OK · Access-Control-Allow-Origin: http://localhost:5173 · Vary: Origincurl -s -D - -o /dev/null -H "Origin: https://evil.example" $API/courses# → HTTP/1.1 200 OK · Vary: Origin (sin Access-Control-Allow-Origin)curl -s -H "Origin: https://evil.example" $API/courses# → [{"id":1,"title":"TypeScript desde cero", … }] ← SÍ: los cursos📋 S5·31 · Minilab 5 · antes (terminal)
Sección titulada «📋 S5·31 · Minilab 5 · antes (terminal)»curl -s -D - -o /dev/null $API/courses# → HTTP/1.1 200 OK# X-Powered-By: Express# Vary: Origin# RateLimit: "100-in-1min"; …# (sin X-Content-Type-Options)📋 S5·32 · app.ts completo, el orden final (src/app.ts)
Sección titulada «📋 S5·32 · app.ts completo, el orden final (src/app.ts)»// src/app.ts (completo: sustituye el tuyo)import cors from 'cors';import express from 'express';import helmet from 'helmet';import { config } from './config.js';import { errorHandler } from './middlewares/error-handler.js';import { generalRateLimiter } from './middlewares/rate-limit.js';import { authRouter } from './routes/auth.routes.js';import { coursesRouter } from './routes/courses.routes.js';import { usersRouter } from './routes/users.routes.js';
// La app se exporta sin arrancar: server.ts hace listen() y los tests// (sesión 6) la usan directamente con Supertest.export const app = express();
app.set('trust proxy', config.TRUST_PROXY);
// El orden importa: cabeceras → CORS → límite → parseo del body → rutas → erroresapp.use(helmet());app.use(cors({ origin: config.CORS_ORIGINS }));app.use('/api/v1', generalRateLimiter);app.use(express.json({ limit: '100kb' }));
app.use('/api/v1/auth', authRouter);app.use('/api/v1/courses', coursesRouter);app.use('/api/v1/users', usersRouter);
app.use(errorHandler); // siempre el último📋 S5·33 · Minilab 5 · después (terminal)
Sección titulada «📋 S5·33 · Minilab 5 · después (terminal)»curl -s -D - -o /dev/null $API/courses# → HTTP/1.1 200 OK# Content-Security-Policy: default-src 'self';base-uri 'self';…# Cross-Origin-Opener-Policy: same-origin# Referrer-Policy: no-referrer# Strict-Transport-Security: max-age=31536000; includeSubDomains# X-Content-Type-Options: nosniff# X-Frame-Options: SAMEORIGIN# …# (X-Powered-By: Express ya no sale)📋 S5·34 · receta «Cambiar de rama» al checkpoint (terminal)
Sección titulada «📋 S5·34 · receta «Cambiar de rama» al checkpoint (terminal)»git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s5-cp origin/s5-checkpointnpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed📋 S5·35 · traer y lanzar checks/s5.ts (terminal)
Sección titulada «📋 S5·35 · traer y lanzar checks/s5.ts (terminal)»# en tu rama el script aún no está: tráetelogit checkout origin/s5-checkpoint -- checks/s5.tsnpx tsx checks/s5.ts # lee la misma API que tus curl