/** * ======================================== * RUTAS DE AUTENTICACIÓN * ======================================== * * Este módulo maneja todas las operaciones relacionadas con autenticación * y gestión de perfil de usuarios: * - Registro de nuevos usuarios * - Login (inicio de sesión) * - Obtener perfil del usuario autenticado * - Actualizar perfil * - Cambiar contraseña * * ORIGEN: Se registra en server.js como app.use('/api/auth', authRoutes) * DESTINO: Todas las rutas aquí comienzan con /api/auth/* * * TABLA DE RUTAS: * POST /api/auth/register - Crear nuevo usuario * POST /api/auth/login - Iniciar sesión * GET /api/auth/me - Obtener perfil (requiere auth) * PUT /api/auth/me/profile - Actualizar perfil (requiere auth) * PUT /api/auth/me/password - Cambiar contraseña (requiere auth) */ const express = require('express'); const bcrypt = require('bcryptjs'); // Para hashear/verificar contraseñas const pool = require('../config/db'); // Pool de conexiones a MySQL const { signToken, requireAuth } = require('../utils/jwt'); // JWT: generar y verificar tokens const { registerValidator, loginValidator } = require('../validators/auth.validators'); const handleValidation = require('../middleware/handleValidation'); const router = express.Router(); /** * ======================================== * POST /api/auth/register * ======================================== * Registra un nuevo usuario en el sistema * * FLUJO DE EJECUCIÓN: * 1. registerValidator valida los datos del body * 2. handleValidation verifica que no haya errores de validación * 3. Verifica que el email no esté registrado * 4. Hashea la contraseña con bcrypt * 5. Inserta el usuario en la base de datos * 6. Genera un token JWT * 7. Devuelve el token y los datos del usuario * * REQUEST BODY: * { * "nombre": "Juan", * "apellido": "Pérez", * "email": "juan@mail.com", * "genero": "M", * "password": "miPassword123" * } * * RESPONSE (201): * { * "msg": "Usuario registrado con éxito", * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", * "user": { * "id_usuario": 1, * "nombre": "Juan", * "apellido": "Pérez", * "email": "juan@mail.com", * "genero": "M" * } * } * * DESTINO: Frontend guarda el token y lo usa en peticiones futuras */ router.post('/register', registerValidator, handleValidation, async (req, res) => { const { nombre, apellido, email, genero, password } = req.body; try { // 1. Verifica si el email ya está registrado const [exists] = await pool.query( 'SELECT id_usuario FROM usuarios WHERE email = ?', [email] ); if (exists.length) { // Email duplicado - retorna error 409 Conflict return res.status(409).json({ error: 'Email ya registrado' }); } // 2. Hashea la contraseña (bcrypt con 10 rondas de salt) // NUNCA guardar contraseñas en texto plano const hash = await bcrypt.hash(password, 10); // 3. Inserta el nuevo usuario en la base de datos const [result] = await pool.query( `INSERT INTO usuarios (nombre, apellido, email, genero, password_hash, fecha_registro, activo) VALUES (?, ?, ?, ?, ?, NOW(), 1)`, [nombre, apellido, email, genero, hash] ); // 4. Genera el token JWT con el ID del usuario recién creado const token = signToken({ id_usuario: result.insertId, email }); // 5. Prepara el objeto de usuario (sin el password_hash) const user = { id_usuario: result.insertId, nombre, apellido, email, genero }; // 6. Devuelve respuesta exitosa con el token y datos del usuario return res.status(201).json({ msg: 'Usuario registrado con éxito', token, user }); } catch (err) { console.error('REGISTER ERROR:', err); return res.status(500).json({ error: 'Error en el servidor' }); } }); /** * ======================================== * POST /api/auth/login * ======================================== * Autentica a un usuario existente * * FLUJO DE EJECUCIÓN: * 1. loginValidator valida email y password * 2. handleValidation verifica errores * 3. Busca el usuario por email en la BD * 4. Verifica que el usuario esté activo * 5. Compara la contraseña con bcrypt.compare() * 6. Genera token JWT * 7. Devuelve token y datos del usuario * * REQUEST BODY: * { * "email": "juan@mail.com", * "password": "miPassword123" * } * * RESPONSE (200): * { * "msg": "Login exitoso", * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", * "user": { "id_usuario": 1, "nombre": "Juan", ... } * } * * ERRORES: * - 401: Email no existe o contraseña incorrecta * - 401: Usuario inactivo */ router.post('/login', loginValidator, handleValidation, async (req, res) => { const { email, password } = req.body; try { // 1. Busca el usuario por email (incluye password_hash para comparar) const [users] = await pool.query( `SELECT id_usuario, nombre, apellido, email, genero, password_hash, activo FROM usuarios WHERE email = ?`, [email] ); // 2. Verifica que el usuario exista if (!users.length) { // Mensaje genérico para no revelar si el email existe return res.status(401).json({ error: 'Credenciales inválidas' }); } const u = users[0]; // 3. Verifica que el usuario esté activo if (Number(u.activo) === 0) { return res.status(401).json({ error: 'Usuario inactivo' }); } // 4. Compara la contraseña ingresada con el hash almacenado const ok = await bcrypt.compare(password, u.password_hash || ''); if (!ok) { return res.status(401).json({ error: 'Credenciales inválidas' }); } // 5. Genera token JWT con los datos del usuario const token = signToken({ id_usuario: u.id_usuario, email: u.email }); // 6. Prepara el objeto de usuario (sin password_hash por seguridad) const user = { id_usuario: u.id_usuario, nombre: u.nombre, apellido: u.apellido, email: u.email, genero: u.genero }; // 7. Devuelve respuesta exitosa return res.json({ msg: 'Login exitoso', token, user }); } catch (err) { console.error('LOGIN ERROR:', err); return res.status(500).json({ error: 'Error en el servidor' }); } }); /** * ======================================== * PUT /api/auth/me/profile * ======================================== * Actualiza nombre y/o apellido del usuario autenticado * * REQUIERE: Token JWT en header Authorization * FLUJO: requireAuth valida token → actualiza en BD → devuelve usuario actualizado */ router.put('/me/profile', requireAuth, async (req, res) => { try { const { id_usuario } = req.user; const { nombre = null, apellido = null } = req.body; if (nombre === null && apellido === null) { return res.status(400).json({ error: 'Sin cambios' }); } const [result] = await pool.query( `UPDATE usuarios SET nombre = COALESCE(?, nombre), apellido = COALESCE(?, apellido) WHERE id_usuario = ?`, [nombre, apellido, id_usuario] ); if (result.affectedRows === 0) { return res.status(404).json({ error: 'Usuario no encontrado' }); } const [rows] = await pool.query( `SELECT id_usuario, nombre, apellido, email, genero FROM usuarios WHERE id_usuario = ?`, [id_usuario] ); return res.json({ ok: true, user: rows[0] }); } catch (e) { console.error('PUT /me/profile', e); return res.status(500).json({ error: 'Error al actualizar perfil' }); } }); /** * ======================================== * GET /api/auth/me * ======================================== * Devuelve el perfil completo del usuario autenticado + sus roles * * REQUIERE: Token JWT * FLUJO: requireAuth → consulta usuario → consulta roles → devuelve ambos */ router.get('/me', requireAuth, async (req, res) => { try { const { id_usuario } = req.user; // Usuario básico const [urows] = await pool.query( `SELECT id_usuario, nombre, apellido, email, genero FROM usuarios WHERE id_usuario = ?`, [id_usuario] ); if (!urows.length) { return res.status(404).json({ error: 'Usuario no encontrado' }); } // Roles const [rrows] = await pool.query( `SELECT r.id_rol, r.nombre FROM usuarios_roles ur JOIN roles r ON r.id_rol = ur.id_rol WHERE ur.id_usuario = ?`, [id_usuario] ); const user = { ...urows[0], roles: rrows || [] }; return res.json({ ok: true, user }); } catch (e) { console.error('GET /api/auth/me', e); return res.status(500).json({ error: 'Error al obtener perfil' }); } }); /** * ======================================== * PUT /api/auth/me/password * ======================================== * Cambia la contraseña del usuario autenticado * * REQUIERE: Token JWT + contraseña actual correcta * FLUJO: requireAuth → verifica contraseña actual → hashea nueva → actualiza BD */ router.put('/me/password', requireAuth, async (req, res) => { try { const { id_usuario } = req.user; const { actual, nueva } = req.body; if (!actual || !nueva) { return res.status(400).json({ error: 'Faltan campos' }); } // Obtiene hash actual const [rows] = await pool.query( 'SELECT password_hash FROM usuarios WHERE id_usuario = ?', [id_usuario] ); if (!rows.length) { return res.status(404).json({ error: 'Usuario no encontrado' }); } const match = await bcrypt.compare(actual, rows[0].password_hash); if (!match) { return res.status(400).json({ error: 'La contraseña actual no coincide' }); } const newHash = await bcrypt.hash(nueva, 10); await pool.query( 'UPDATE usuarios SET password_hash = ? WHERE id_usuario = ?', [newHash, id_usuario] ); return res.json({ ok: true, message: 'Contraseña actualizada' }); } catch (e) { console.error('PUT /me/password', e); return res.status(500).json({ error: 'Error al actualizar contraseña' }); } }); module.exports = router;