Ir al contenido

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)»
Ventana de terminal
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s5 origin/s5-start # checkpoint: mi-s5-cp origin/s5-checkpoint
npm install
npx prisma generate
npx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed
Ventana de terminal
export API=http://localhost:3000/api/v1 # export: si no, los checks no la ven
Ventana de terminal
# Luis (STUDENT) hace login y copia su token
curl -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)»
Ventana de 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)»
Ventana de terminal
npm view express-rate-limiter
npm view express-rate-limit

📋 S5·07 · Minilab 1 (1/2) · antes (terminal)

Sección titulada «📋 S5·07 · Minilab 1 (1/2) · antes (terminal)»
Ventana de 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)»
Ventana de 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)»
Ventana de 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)»
Ventana de 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)»
Ventana de 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)»
Ventana de 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)»
Ventana de 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/login
done
# → 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)»
Ventana de terminal
npm view express-rate-limit
# el oficial, no "express-rate-limiter"
npm install express-rate-limit helmet cors
npm 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.ts
export 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 validateBody
import { 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
Ventana de terminal
# 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)»
Ventana de 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/login
done
# → 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)»
Ventana de 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)»
Ventana de 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 envSchema
CORS_ORIGINS: z.string()
.default('http://localhost:5173')
.transform((s) => s.split(',').map((o) => o.trim())),
Ventana de terminal
# .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)»
Ventana de 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: Origin
curl -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)»
Ventana de 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 → errores
app.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)»
Ventana de 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)»
Ventana de terminal
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s5-cp origin/s5-checkpoint
npm install
npx prisma generate
npx 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)»
Ventana de terminal
# en tu rama el script aún no está: tráetelo
git checkout origin/s5-checkpoint -- checks/s5.ts
npx tsx checks/s5.ts # lee la misma API que tus curl