import express from 'express';
import formidable from 'formidable';
import { createUsuarioValidations } from '../validations/createUsuarioValidations.js';
import { usuariosRepository } from '../repositories/usuariosRepository.js';
import { updateUsuarioValidations } from '../validations/updateUsuarioValidations.js';
import { loginUsuarioValidations } from '../validations/loginUsuarioValidations.js';
import { sha512 } from 'js-sha512';
import { validateObjectIdFormat } from '../validations/validateObjectIdFormat.js';
import { createUserToken } from '../utils/createUserToken.js';
import { sessionChecker } from '../security/sessionChecker.js';
import isValidToken from '../utils/isValidToken.js';
const invalidTokens = new Set();
/**
* Controlador para gestionar rutas relacionadas con usuarios.
*
* @module usuariosController
* @requires express
* @requires formidable
* @requires ../validations/createUsuarioValidations
* @requires ../repositories/usuariosRepository
* @requires ../validations/updateUsuarioValidations
* @requires ../validations/loginUsuarioValidations
* @requires js-sha512
* @requires ../validations/validateObjectIdFormat
* @requires ../utils/createUserToken
* @requires ../security/sessionChecker
*/
const usuariosController = express.Router();
/**
* Ruta para crear y listar usuarios.
*
* @name POST /usuarios
* @function
* @memberof module:usuariosController
* @inner
* @param {Object} req.body - Datos del usuario a crear
* @param {string} req.body.nombre - Nombre del usuario
* @param {string} req.body.email - Correo electrónico del usuario
* @param {string} req.body.password - Contraseña del usuario
* @param {string} [req.body.rol='usuario'] - Rol del usuario (administrador o usuario)
* @param {File} [req.files.fotoPerfil] - Foto de perfil del usuario
* @returns {Object} 201 - Usuario creado
* @returns {Object} 400 - Error de validación o usuario existente
* @example
* POST /usuarios
* {
* "nombre": "Juan Pérez",
* "email": "juan@example.com",
* "password": "Contraseña123!",
* "rol": "usuario"
* }
*
* Response: 201 Created
* {
* "_id": "60d5ecb54d6eb31234567890",
* "nombre": "Juan Pérez",
* "email": "juan@example.com",
* "rol": "usuario",
* "createdAt": "2023-06-25T12:00:00.000Z",
* "updatedAt": "2023-06-25T12:00:00.000Z"
* }
*/
usuariosController.route('/usuarios')
.post(async (req, res, next) => {
try {
const form = formidable({ multiples: true });
form.parse(req, async (err, fields, files) => {
req.curatedBody = fields;
if (files && files.fotoPerfil) {
req.curatedBody.fotoPerfil = files.fotoPerfil[0].filepath;
}
// Convertir campos a string para garantizar consistencia
['nombre', 'email', 'password', 'rol'].forEach(field => {
if (fields[field]) req.curatedBody[field] = fields[field].toString();
});
const existingUsuario = await usuariosRepository.getOneByEmailAndPassword(
req.curatedBody.email,
sha512(req.curatedBody.password)
);
if (existingUsuario) {
return res.status(400).json({ message: 'Ya hay un usuario existente con ese nombre' });
}
const validatedData = await createUsuarioValidations.validate(
req.curatedBody,
{ abortEarly: false, stripUnknown: true }
);
const createdItem = await usuariosRepository.create(validatedData);
res.status(201).json(createdItem);
});
} catch (e) {
next(e);
}
})
/**
* Lista todos los usuarios.
*
* @name GET /usuarios
* @function
* @memberof module:usuariosController
* @inner
* @param {Object} req - Objeto de solicitud Express
* @param {Object} res - Objeto de respuesta Express
* @returns {Object[]} 200 - Lista de usuarios
* @example
* GET /usuarios
*
* Response: 200 OK
* [
* {
* "_id": "60d5ecb54d6eb31234567890",
* "nombre": "Juan Pérez",
* "email": "juan@example.com",
* "rol": "usuario",
* "createdAt": "2023-06-25T12:00:00.000Z",
* "updatedAt": "2023-06-25T12:00:00.000Z"
* },
* // ... más usuarios
* ]
*/
.get(sessionChecker(['administrador', 'usuario'], true), async (req, res) => {
const itemList = await usuariosRepository.list();
const preparedData = itemList.map((item) => {
const usuarioItem = item.toJSON();
delete usuarioItem.password;
return usuarioItem;
});
res.json(preparedData);
});
/**
* Ruta para gestionar logins de usuarios.
*
* @name POST /usuarios/logins
* @function
* @memberof module:usuariosController
* @inner
* @param {Object} req.body - Credenciales de inicio de sesión
* @param {string} req.body.email - Correo electrónico del usuario
* @param {string} req.body.password - Contraseña del usuario
* @returns {Object} 201 - Usuario autenticado y token
* @returns {Object} 401 - Credenciales inválidas
* @example
* POST /usuarios/logins
* {
* "email": "juan@example.com",
* "password": "Contraseña123!"
* }
*
* Response: 201 Created
* {
* "user": {
* "_id": "60d5ecb54d6eb31234567890",
* "nombre": "Juan Pérez",
* "email": "juan@example.com",
* "rol": "usuario"
* },
* "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
*/
usuariosController.route('/usuarios/logins')
.post(loginUsuarioValidations, async (req, res) => {
const userEmail = req.curatedBody.email;
const receivedPasswordHash = sha512(req.curatedBody.password);
const user = await usuariosRepository.getOneByEmailAndPassword(userEmail, receivedPasswordHash);
if (!user) {
return res.status(401).json({ message: 'Usuario y/o contraseña incorrectos' });
}
delete user.password;
const responseData = {
user,
token: createUserToken(user)
};
res.status(201).json(responseData);
});
/**
* Ruta para gestionar el cierre de sesión de un usuario.
*
* @name POST /usuarios/logout
* @function
* @memberof module:usuariosController
* @inner
* @param {string} req.headers.authorization - Token de autorización
* @returns {Object} 200 - Mensaje de sesión cerrada
* @returns {Object} 401 - Token inválido
* @example
* POST /usuarios/logout
* Headers: { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
*
* Response: 200 OK
* {
* "message": "Sesión cerrada correctamente"
* }
*/
usuariosController.route('/usuarios/logout')
.post(async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
// Verificar si el token es válido (antes de invalidarlo)
if (!isValidToken(token, invalidTokens)) {
return res.status(401).json({ message: 'Token inválido' });
}
invalidTokens.add(token);
res.status(200).json({ message: 'Sesión cerrada correctamente' });
});
/**
* Ruta para verificar si el token sigue siendo válido.
*
* @name POST /usuarios/verify-token
* @function
* @memberof module:usuariosController
* @inner
* @param {string} req.headers.authorization - Token de autorización
* @returns {Object} 200 - Token válido
* @returns {Object} 401 - Token inválido o expirado
* @example
* POST /usuarios/verify-token
* Headers: { "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
*
* Response: 200 OK
* {
* "message": "Token válido"
* }
*/
usuariosController.route('/usuarios/verify-token')
.post(async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(400).json({ message: 'Token no proporcionado' });
}
// Verificar si el token es válido
if (!isValidToken(token, invalidTokens) || invalidTokens.has(token)) {
return res.status(401).json({ message: 'Token inválido o expirado' });
}
// Si es válido, devolver una respuesta positiva
res.status(200).json({ message: 'Token válido' });
} catch (error) {
res.status(500).json({ message: 'Error al verificar el token', error: error.message });
}
});
/**
* Ruta para gestionar un usuario específico por su ID.
*
* @name GET /usuarios/:id
* @function
* @memberof module:usuariosController
* @inner
* @param {string} req.params.id - ID del usuario (ObjectId de MongoDB)
* @returns {Object} 200 - Datos del usuario
* @returns {Object} 404 - Usuario no encontrado
* @example
* GET /usuarios/60d5ecb54d6eb31234567890
*
* Response: 200 OK
* {
* "_id": "60d5ecb54d6eb31234567890",
* "nombre": "Juan Pérez",
* "email": "juan@example.com",
* "rol": "usuario",
* "createdAt": "2023-06-25T12:00:00.000Z",
* "updatedAt": "2023-06-25T12:00:00.000Z"
* }
*/
usuariosController.route('/usuarios/:id')
.get(sessionChecker(['administrador', 'usuario'], true), validateObjectIdFormat(), async (req, res) => {
const itemId = req.params.id;
const item = await usuariosRepository.getOne(itemId);
if (!item) {
return res.status(404).json({ message: `Usuario con id ${itemId} no encontrado` });
}
const response = item.toJSON();
delete response.password;
res.json(response);
})
/**
* Actualiza un usuario por su ID.
*
* @name PUT /usuarios/:id
* @function
* @memberof module:usuariosController
* @inner
* @param {string} req.params.id - ID del usuario (ObjectId de MongoDB)
* @param {Object} req.body - Datos del usuario a actualizar
* @param {string} [req.body.nombre] - Nuevo nombre del usuario
* @param {string} [req.body.email] - Nuevo correo electrónico del usuario
* @param {string} [req.body.password] - Nueva contraseña del usuario
* @param {string} [req.body.rol] - Nuevo rol del usuario
* @param {File} [req.files.fotoPerfil] - Nueva foto de perfil del usuario
* @returns {Object} 201 - Usuario actualizado
* @returns {Object} 404 - Usuario no encontrado
* @example
* PUT /usuarios/60d5ecb54d6eb31234567890
* {
* "nombre": "Juan Pérez Actualizado",
* "email": "juan.nuevo@example.com"
* }
*
* Response: 201 Created
* {
* "_id": "60d5ecb54d6eb31234567890",
* "nombre": "Juan Pérez Actualizado",
* "email": "juan.nuevo@example.com",
* "rol": "usuario",
* "createdAt": "2023-06-25T12:00:00.000Z",
* "updatedAt": "2023-06-25T13:00:00.000Z"
* }
*/
.put(sessionChecker(['administrador', 'usuario'], true), validateObjectIdFormat(), async (req, res, next) => {
const itemId = req.params.id;
try {
const form = formidable({ multiples: true });
form.parse(req, async (err, fields, files) => {
req.curatedBody = fields;
if (files && files.fotoPerfil) {
req.curatedBody.fotoPerfil = files.fotoPerfil[0].filepath;
}
['nombre', 'email', 'password', 'rol'].forEach(field => {
if (fields[field]) req.curatedBody[field] = fields[field].toString();
});
const validatedData = await updateUsuarioValidations.validate(
req.curatedBody,
{ abortEarly: false, stripUnknown: true }
);
const updatedItem = await usuariosRepository.update(itemId, validatedData);
if (!updatedItem) {
return res.status(404).json({ message: `Usuario con id ${itemId} no encontrado` });
}
res.status(201).json(updatedItem);
});
} catch (e) {
next(e);
}
})
/**
* Elimina un usuario por su ID.
*
* @name DELETE /usuarios/:id
* @function
* @memberof module:usuariosController
* @inner
* @param {string} req.params.id - ID del usuario (ObjectId de MongoDB)
* @returns {Object} 204 - Usuario eliminado exitosamente (sin contenido)
* @returns {Object} 404 - Usuario no encontrado
* @throws {Error} 500 - Error del servidor al intentar eliminar el usuario
* @example
* DELETE /usuarios/60d5ecb54d6eb31234567890
*
* Response: 204 No Content
*
* @example
* DELETE /usuarios/60d5ecb54d6eb31234567890
*
* Response: 404 Not Found
* {
* "message": "Usuario con id 60d5ecb54d6eb31234567890 no encontrado"
* }
*/
.delete(sessionChecker(['administrador'], true), validateObjectIdFormat(), async (req, res) => {
const itemId = req.params.id;
const item = await usuariosRepository.remove(itemId);
if (!item) {
return res.status(404).json({ message: `Usuario con id ${itemId} no encontrado` });
}
res.status(204).json();
});
export { usuariosController };