- Node.js v18+
- MongoDB (local o Atlas)
git clone https://github.com/AlessiaPrecedo/Backend2.git
cd Backend2
npm installCopiá el archivo de ejemplo y completá los valores:
cp .env.example .envPORT=8080
MONGO_URL=mongodb://localhost:27017/backend2
JWT_SECRET=unaclavesecretamuylargarandom123
JWT_EXPIRES_IN=1h
NODE_ENV=development# Desarrollo (recarga automática)
npm run dev
# Producción
npm startnpm testCorre con el test runner nativo de Node (node:test, sin dependencias extra) y cubre: hash de contraseñas, generación/verificación de JWT (incluye token manipulado y expirado), el middleware validateLoginFields, y el middleware auth (que internamente usa la estrategia current de Passport). No incluye tests de integración contra una base de datos real.
| Capa | Responsabilidad |
|---|---|
| Route | Define el endpoint y aplica middlewares |
| Controller | Recibe el request HTTP, dispara la estrategia de Passport correspondiente, y genera el JWT / setea la cookie cuando corresponde |
| Passport strategy | Contiene la lógica de autenticación (validaciones, hash, reglas de negocio de login/registro) |
| Repository | Único punto de contacto con la base de datos |
| Utils | Helpers reutilizables (bcrypt, JWT, errores) |
- DAO: archivos en
src/dao/*son los únicos que importan los modelos de Mongoose y exponen métodos CRUD/consulta (findById, findOne, create, update, aggregate, etc.). - Repository: usa los DAOs; no importa modelos directamente; expone métodos orientados al dominio (ej:
findByEmail,countOccupiedSpots,findPublishedEvents). - Service: consume repositories, contiene la lógica de negocio (validaciones, control de cupos, permisos, envío de emails).
- Controller: coordina request/response; no contiene lógica de negocio.
- DTOs: en
src/dto/*transforman documentos a objetos de respuesta filtrando campos sensibles (ej:password).
- Flujo completo: registro → login → crear evento → inscribirse → consultar mis tickets → cancelar (la respuesta de
/api/sessions/currentno incluyepassword). - Respuesta de ticket con
populateno incluyepassworddel usuario. - Endpoint con error de negocio devuelve código HTTP correcto (400/401/403/404/409 según corresponda, no 500).
- Endpoint protegido sin sesión → 401; con sesión sin permisos → 403.
Toda la autenticación pasa por estrategias de Passport, centralizadas en src/config/passport.js. app.js solo llama a initializePassport() y a passport.initialize(); no define ninguna estrategia ahí.
| Estrategia | Tipo | Qué hace |
|---|---|---|
register |
passport-local (con passReqToCallback) |
Valida los campos, normaliza el email, verifica que no exista otro usuario con ese email, hashea la contraseña con bcrypt y crea el usuario con role: "user" por defecto |
login |
passport-local |
Busca el usuario por email y compara la contraseña con bcrypt. Si algo no coincide, devuelve done(null, false) sin indicar cuál fue el problema |
current |
passport-jwt (con extractor custom desde cookie) |
Lee el JWT de la cookie currentUser, lo verifica, y deja el payload decodificado en req.user |
Puntos importantes:
- El JWT se genera en el controller (
controllers/session.controller.js), nunca dentro de una estrategia. La estrategialoginsolo confirma la identidad del usuario. - La cookie
currentUserse setea conhttpOnly: true,sameSite: "lax",maxAge: 3600000ysecuresolo siNODE_ENV=production. POST /api/sessions/logoutno pasa por Passport: solo borra la cookie.
passport.js está pensado para crecer sin tocar app.js ni las rutas: agregar un login social es sumar un nuevo passport.use("google", new GoogleStrategy(...)) dentro de initializePassport(), y una ruta nueva que apunte a esa estrategia. El resto de la app (cookie, JWT, middleware auth) no cambia.
| Método | Ruta | Acceso | Descripción |
|---|---|---|---|
| GET | /api/events |
Público | Listar eventos con filtros, paginación y ordenamiento |
| GET | /api/events/:id |
Público | Obtener un evento por ID |
| POST | /api/events |
organizer, admin |
Crear evento. organizer se asigna desde req.user |
| PUT | /api/events/:id |
Dueño del evento o admin |
Actualizar evento. Eventos cancelados requieren justificación para editar |
| PATCH | /api/events/:id/status |
Dueño del evento o admin |
Cambiar el status de un evento |
Filtros admitidos para GET /api/events:
status—draft,published,cancelled,finishedcategorylocationdateFrom,dateTopage,limitsort(ej:sort=date,sort=-date)
| Método | Ruta | Descripción |
|---|---|---|
| POST | /api/sessions/register |
Registro de usuario |
| POST | /api/sessions/login |
Login de usuario, setea cookie currentUser con el JWT |
| GET | /api/sessions/current |
Devuelve el usuario autenticado a partir de la cookie |
| POST | /api/sessions/logout |
Elimina la cookie currentUser |
| Método | Ruta | Descripción |
|---|---|---|
| GET | /users |
Obtener todos los usuarios |
| GET | /users/:id |
Obtener usuario por ID |
| POST | /users |
Crear usuario |
| PUT | /users/:id |
Actualizar usuario |
| DELETE | /users/:id |
Eliminar usuario |
| Método | Ruta | Acceso | Descripción |
|---|---|---|---|
| POST | /api/events/:eid/tickets |
Autenticado | Crear inscripción a un evento con control de cupos y confirmación |
| GET | /api/tickets/my-tickets |
Autenticado | Listar los tickets del usuario autenticado con datos del evento |
| GET | /api/events/:eid/tickets |
organizer o admin |
Ver las inscripciones de un evento propio |
| PATCH | /api/tickets/:tid/cancel |
Dueño o admin |
Cancelar un ticket y liberar cupo automáticamente |
confirmed: inscripción activa confirmada.pending: inscripción pendiente (si se usa en futuras extensiones).cancelled: ticket cancelado, no ocupa cupo.
- El usuario autenticado envía
quantityy eleventIdpor la ruta/api/events/:eid/tickets. - El servicio valida que el evento exista, esté
published, tenga cupos disponibles y que el usuario no tenga ya un ticket activo para ese evento. - Si todo está bien, se crea el ticket y se envía un email de confirmación mediante Nodemailer.
- Al cancelar, el ticket pasa a
cancelledy se registracancelledAt; el cupo queda disponible automáticamente porque los tickets cancelados no cuentan en el cálculo.
El proyecto usa las siguientes variables de entorno para Nodemailer:
MAIL_HOSTMAIL_PORTMAIL_USERMAIL_PASSMAIL_FROM
POST /api/sessions/register
Delega en la estrategia register de Passport.
{
"first_name": "Kenny",
"last_name": "Test",
"email": "kenny@test.com",
"password": "abc12345"
}{
"status": "success",
"payload": {
"_id": "664f...",
"first_name": "Kenny",
"last_name": "Test",
"email": "kenny@test.com",
"role": "user"
}
}La contraseña no se devuelve en la respuesta, ni en texto plano ni hasheada.
| Caso | Body | Respuesta esperada |
|---|---|---|
| Registro exitoso | Todos los campos válidos | 201 con datos del usuario |
| Campo faltante | Sin first_name, por ejemplo |
400 All fields are required |
| Email inválido | email: "kenny" |
400 Invalid email format |
| Contraseña corta | password: "abc" |
400 Password must be at least 8 characters long |
| Email ya registrado | Mismo email dos veces | 400 User already exists |
POST /api/sessions/login
Valida presencia de campos con validateLoginFields, y delega en la estrategia login de Passport. El JWT lo genera el controller una vez que Passport confirma la identidad.
{
"email": "kenny@test.com",
"password": "abc12345"
}{
"status": "success",
"message": "Login correcto"
}Además de la respuesta, se setea una cookie currentUser (httpOnly, sameSite: lax, maxAge: 1h, secure solo en producción) con el JWT firmado, cuyo payload es { id, email, role }.
{ "status": "error", "message": "Email y password son requeridos" }{ "status": "error", "message": "Invalid credentials" }GET /api/sessions/current
Protegida por el middleware auth, que usa la estrategia current de Passport para leer y validar el JWT desde la cookie currentUser (se envía automáticamente por el navegador tras el login).
{
"status": "success",
"payload": {
"id": "664f...",
"email": "kenny@test.com",
"role": "user"
}
}{ "status": "error", "message": "No autenticado" }{ "status": "error", "message": "Token inválido o expirado" }POST /api/sessions/logout
Elimina la cookie currentUser. No pasa por Passport.
{ "status": "success", "message": "Logout exitoso" }| Caso | Request | Respuesta esperada |
|---|---|---|
Registro → login → /current → logout → /current |
Flujo completo | 201 → 200 + cookie → 200 payload → 200 → 401 |
| Registro con email duplicado | Mismo email dos veces | 400 "User already exists" |
| Login con email inexistente | Email que no existe | 401 "Invalid credentials" |
| Login con password incorrecta | Email correcto, password mal | 401 "Invalid credentials" |
/current sin cookie |
Sin header Cookie |
401 "No autenticado" |
/current con token manipulado/expirado |
Cookie alterada o vencida | 401 "Token inválido o expirado" |
role vive en el modelo User, con 3 valores posibles: user (default), organizer, admin. El registro público (POST /api/sessions/register) siempre asigna role: "user", sin importar qué venga en el body — así nadie puede autoasignarse admin u organizer.
| Acción | user | organizer | admin |
|---|---|---|---|
| Consultar eventos publicados | ✅ | ✅ | ✅ |
| Crear eventos | ❌ | ✅ | ✅ |
| Modificar/cancelar eventos propios | ❌ | ✅ | ✅ |
| Modificar cualquier evento | ❌ | ❌ | ✅ |
| Ver todos los usuarios | ❌ | ❌ | ✅ |
- 401 (No autenticado): no hay cookie, o el JWT es inválido/expiró. No sabemos quién sos. Lo maneja el middleware
auth. - 403 (Prohibido): sabemos quién sos (JWT válido), pero tu
roleno tiene permiso para esa acción. Lo maneja el middlewareauthorize.
| Middleware | Archivo | Qué hace |
|---|---|---|
auth |
middlewares/auth.middleware.js |
Valida el JWT de la cookie currentUser (vía la estrategia current de Passport). Responde 401 si no hay sesión válida y deja el usuario en req.user |
authorize(...roles) |
middlewares/authorize.middleware.js |
Recibe los roles permitidos como parámetro y los compara contra req.user.role. Responde 403 si no coincide |
Siempre se usan en ese orden: auth primero (saber quién sos), authorize después (ver si te corresponde).
| Método | Ruta | Protección |
|---|---|---|
| GET | /api/sessions/current |
auth (cualquier usuario autenticado) |
| POST | /events |
auth + authorize("organizer","admin") |
| PUT | /events/:id |
auth + authorize("organizer","admin") + dueño del evento (o admin) |
| DELETE | /events/:id |
auth + authorize("organizer","admin") + dueño del evento (o admin) |
| GET | /users |
auth + authorize("admin") |
Al crear un evento, el organizer se toma de req.user.id (nunca del body). Al modificar o borrar un evento, si req.user.role !== "admin", se valida que event.organizer coincida con req.user.id; si no, 403.
| Campo | Tipo | Notas |
|---|---|---|
event |
ObjectId (ref Event) |
referencia, no objeto embebido |
user |
ObjectId (ref User) |
referencia, no objeto embebido |
quantity |
Number | debe ser un entero > 0 |
status |
String | pending, confirmed, cancelled |
reservationCode |
String | código único generado al confirmar |
createdAt |
Date | automático (timestamps) |
cancelledAt |
Date | se completa recién al cancelar |
| Método | Ruta | Acceso |
|---|---|---|
| POST | /api/events/:eid/tickets |
autenticado |
| GET | /api/tickets/my-tickets |
autenticado (solo sus propios tickets) |
| GET | /api/events/:eid/tickets |
dueño del evento (organizer) o admin |
| PATCH | /api/tickets/:tid/cancel |
dueño del ticket o admin |
Al hacer POST /api/events/:eid/tickets con { "quantity": N }, en este orden:
quantitytiene que ser un entero mayor a 0- El evento tiene que existir (404 si no)
- El evento tiene que estar
published(esto ya descartadraft,cancelledyfinisheden un solo chequeo) - El usuario no puede tener ya un ticket activo (no cancelado) para ese mismo evento
- Tiene que haber cupo:
capacity - (suma de quantity de todos los tickets NO cancelados)tiene que ser ≥ a laquantitypedida - Si todo pasa: se crea el ticket con
status: "confirmed"y unreservationCodeúnico, y se dispara el email de confirmación (si el mail falla, no revierte la inscripción — solo se loguea el error)
PATCH /api/tickets/:tid/cancel: valida que el ticket exista, que sea del usuario que lo pide (o que sea admin), y que no esté ya cancelado. Cambia status a cancelled y completa cancelledAt — nunca borra el documento. Como el cálculo de cupo solo suma tickets no cancelados, el cupo queda libre automáticamente apenas se cancela.
utils/mailer.js usa Nodemailer con las credenciales de MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, MAIL_FROM (siempre desde variables de entorno, nunca hardcodeadas). Se dispara un mail de confirmación al crear un ticket.
- La contraseña se hashea con
bcryptantes de guardarse en MongoDB - El
roleno puede manipularse desde el body; siempre se asignauserpor defecto - El email se normaliza (trim + lowercase) antes de guardarse
- El JWT nunca incluye la contraseña en su payload
- La cookie del JWT es
httpOnly, por lo que no es accesible desde JavaScript del navegador