back/src/routes/auth.routes.js

338 lines
10 KiB
JavaScript

/**
* ========================================
* 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;