diff --git a/Configuracion/clienteRedis.js b/Configuracion/clienteRedis.js index 5f5f3a7..f6ec29e 100644 --- a/Configuracion/clienteRedis.js +++ b/Configuracion/clienteRedis.js @@ -7,4 +7,4 @@ const redis = createClient({ redis.on('error', (err) => console.error('Error Redis', err)); redis.connect(); -module.exports = redis; \ No newline at end of file +module.exports = redis; \ No newline at end of file diff --git a/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js b/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js index 184fc06..c497540 100644 --- a/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js +++ b/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js @@ -3,7 +3,7 @@ * @description * Contiene funciones para consultar y validar grupos de empleados asociados a un cliente. * Incluye lógica para obtener todos los grupos de un cliente y verificar duplicados por nombre. - * + * * RF22 - Consulta Lista de Grupo Empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF22 */ @@ -29,6 +29,7 @@ exports.obtenerGrupoDeEmpleados = async (idCliente) => { try { const gruposDeEmpleados = await correrQuery(query, [idCliente]); + return gruposDeEmpleados; } catch { return []; @@ -55,7 +56,7 @@ exports.existeGrupoConNombre = (nombreGrupo, idCliente) => { (err, resultados) => { if (err) return reject(err); resolve(resultados.length > 0); - }, + } ); }); -}; \ No newline at end of file +}; diff --git a/Productos/Controladores/crearProducto.controller.js b/Productos/Controladores/crearProducto.controller.js index 8852ef2..f78d11a 100644 --- a/Productos/Controladores/crearProducto.controller.js +++ b/Productos/Controladores/crearProducto.controller.js @@ -103,11 +103,11 @@ exports.crearProducto = [ // Upload image processing remains unchanged const urlImagenProductoPromise = imagenProducto ? enviarS3({ - Bucket: process.env.AWS_BUCKET_NAME, - Key: `productos/${imagenProducto.originalname}`, - Body: imagenProducto.buffer, - ContentType: imagenProducto.mimetype, - }) + Bucket: process.env.AWS_BUCKET_NAME, + Key: `productos/${imagenProducto.originalname}`, + Body: imagenProducto.buffer, + ContentType: imagenProducto.mimetype, + }) : Promise.resolve(null); // prettier-ignore @@ -176,4 +176,4 @@ exports.crearProducto = [ if (conexion) conexion.release(); } }, -]; \ No newline at end of file +]; diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js new file mode 100644 index 0000000..e653874 --- /dev/null +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -0,0 +1,45 @@ +const repositorio = require('@altertex/usu/repos/repositorioActualizarUsuario'); +const bcrypt = require('bcryptjs'); + +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] + +/** + * Controlador para actualizar la información de un usuario. + * + * Este endpoint recibe un objeto con los cambios que se aplicarán + * sobre un usuario y usa su repositorio para hacer el cambio en la + * base de datos. + * + * @function actualizarUsuario + * @async + * @param {Express.Request} req - Objeto de solicitud HTTP de Express. + * @param {object} req.body - Cuerpo de la solicitud. + * @param {object|Array} req.body.cambios - Información del usuario a actualizar. + * @param {Express.Response} res - Objecto de respuesta HTTP de Express. + * @returns {Promise} Retorna una respuesta JSON indicando éxito o un error. + */ +exports.actualizarUsuario = async (req, res) => { + const cambios = req.body.cambios || req.body; + + if (!cambios) { + return res.status(400).json({ mensaje: 'No se enviaron los datos del usuario' }); + } + + const datos = Array.isArray(cambios) ? cambios : [cambios]; + + if (!datos[0].idUsuario) { + return res.status(400).json({ mensaje: 'ID del usuario no proporcionado' }); + } + + if (datos[0].contrasenia) { + const contraseniaEncriptada = await bcrypt.hash(datos[0].contrasenia, 10); + datos[0].contrasenia = contraseniaEncriptada; + } + + try { + await repositorio.actualizarUsuario(datos); + return res.status(200).json({ mensaje: 'Usuario actualizado correctamente' }); + } catch (error) { + return res.status(400).json({ mensaje: error.message }); + } +}; diff --git a/Usuarios/Controladores/consultarListaUsuarios.controller.js b/Usuarios/Controladores/consultarListaUsuarios.controller.js index 1a52225..70e675d 100644 --- a/Usuarios/Controladores/consultarListaUsuarios.controller.js +++ b/Usuarios/Controladores/consultarListaUsuarios.controller.js @@ -26,9 +26,20 @@ exports.consultarListaUsuarios = async (req, res) => { .json({ mensaje: MENSAJES_USUARIOS.USUARIOS_NO_ENCONTRADOS.mensaje }); } + const generoMap = { + masculino: 'Hombre', + femenino: 'Mujer', + otro: 'Otro', + }; + + const resultadosMapeados = resultados.map((usuario) => ({ + ...usuario, + genero: generoMap[usuario.genero] || usuario.genero, + })); + return res.status(MENSAJES_USUARIOS.LISTA_USUARIOS_OBTENIDA.codigo).json({ mensaje: MENSAJES_USUARIOS.LISTA_USUARIOS_OBTENIDA.mensaje, - listaUsuarios: resultados, + listaUsuarios: resultadosMapeados, }); } catch { return res diff --git a/Usuarios/Controladores/eliminarUsuario.controller.js b/Usuarios/Controladores/eliminarUsuario.controller.js index 7078f34..7bf238c 100644 --- a/Usuarios/Controladores/eliminarUsuario.controller.js +++ b/Usuarios/Controladores/eliminarUsuario.controller.js @@ -42,6 +42,12 @@ exports.eliminarUsuario = async (req, res) => { } const idsNumericos = idsUsuarios.map(Number); + + if (idsNumericos.includes(idSolicitante)) { + return res.status(403).json({ + mensaje: 'No puedes eliminar tu propio usuario.', + }); + } const [usuariosObjetivo] = await db.query(` SELECT u.idUsuario, r.nombre AS rol diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js new file mode 100644 index 0000000..8100363 --- /dev/null +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -0,0 +1,104 @@ +const correrQuery = require('@altertex/util/ser/correrQuery'); +const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); +const CONSULTAS_USUARIOS = require('@altertex/util/const/consultasUsuarios'); + +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4 + +/** + * Repositorio para actualizar los datos de un usuario en la BD. + * + * Recorre un arreglo de objetos que contienen la información de cada usuario para + * actualizar su información. + * + * Utiliza un array de objetos con la información del usuario, y hace la + * consulta correspondiente a la base de datos. + * + * @function actualizarUsuario + * @async + * @param {Array<{ idUsuario: number, nombreCompleto: string, correoElectronico: string, telefono: string }>} datos - Lista de + * información del usuario a actualizar. + * @throws {Error} Si el arreglo está vacío o si ocurre un error en la base de datos. + * @returns {Promise} Promesa que se resuelve cuando todas las actualizaciones han sido ejecutadas. + */ +exports.actualizarUsuario = async (datos) => { + if (!Array.isArray(datos) || datos.length === 0) { + throw new Error(MENSAJES.ERROR_OBTENER_USUARIO.mensaje); + } + + try { + for (const usuario of datos) { + const { + idUsuario, + correoElectronico, + nombreCompleto, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + idRol, + genero, + estatus, + cliente, + } = usuario; + + const resultadoCorreo = await correrQuery( + CONSULTAS_USUARIOS.VALIDAR_CORREO_DUPLICADO_ACTUALIZACION, + [correoElectronico, idUsuario] + ); + + if (resultadoCorreo.length > 0) { + throw new Error(MENSAJES.USUARIO_YA_EXISTE.mensaje); + } + + const conContrasena = contrasenia && contrasenia.trim() !== ''; + + if (conContrasena) { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ + nombreCompleto, + correoElectronico, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } else { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ + nombreCompleto, + correoElectronico, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } + + // Asociar cliente(s) + if (cliente) { + const clientes = Array.isArray(cliente) ? cliente : [cliente]; + + // Eliminar asociaciones anteriores + await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]); + + for (const idCliente of clientes) { + if (idCliente) { + await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ + idUsuario, + idCliente, + ]); + } + } + } + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_ROL_USUARIO, [idRol, idUsuario]); + } + } catch (error) { + if (error.code === 'ER_TRUNCATED_WRONG_VALUE') { + throw new Error(MENSAJES.ERROR_FECHA_NO_VALIDA.mensaje); + } + throw new Error(error.message || MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje); + } +}; diff --git a/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js b/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js new file mode 100644 index 0000000..d3239e9 --- /dev/null +++ b/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js @@ -0,0 +1,227 @@ +const express = require('express'); +const ruteador = express.Router(); +const PERMISOS = require('@altertex/util/const/permisos'); +const RUTAS = require('@altertex/util/const/rutas'); +const controlador = require('@altertex/usu/ctrl/actualizarUsuario.controller'); +const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); +const autorizarToken = require('@altertex/util/inter/autorizarToken'); +const revisarPermisos = require('@altertex/util/inter/verificarPermisos'); +const validarYSanitizar = require('@altertex/util/inter/validarYSanitizar'); +const limitePeticionesDiarias = require('@altertex/util/inter/limitePeticiones'); + +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] + +/** + * @swagger + * /api/usuarios/actualizar: + * put: + * summary: Actualiza la información de un usuario. + * tags: + * - Usuarios + * security: + * - ApiKeyAuth: [] + * - BearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - type: object + * description: Información del usuario a actualizar directamente + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * enum: [Masculino, Femenino, Otro] + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * - type: object + * properties: + * usuarios: + * oneOf: + * - type: object + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * - type: array + * items: + * type: object + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * responses: + * 200: + * description: Información del usuario actualizada correctamente. + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: "Usuario actualizado con éxito." + * datos: + * type: array + * items: + * type: object + * 400: + * description: Error en los datos enviados o en la actualización. + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: "Error al actualizar el usuario." + * x-codeSamples: + * - lang: JavaScript + * label: cURL + * source: | + * curl -X PUT "https://tu-api.com/api/usuarios/actualizar" \ + * -H "x-api-key: TU_API_KEY" \ + * -H "Authorization: Bearer TU_TOKEN" \ + * -H "Content-Type: application/json" \ + * -d '{ + * "idUsuario": 30, + * "nombreCompleto": "Luis Hernández", + * "correoElectronico": "lhernandez@gmail.com", + * "contrasenia": "NuevaContraseniaSegura123", + * "numeroTelefono": "5551234567", + * "direccion": "Av. Revolución 123, CDMX", + * "fechaNacimiento": "1995-06-15", + * "genero": "Masculino", + * "estatus": true, + * "idRol": 2 + * }' + */ + +ruteador.put( + RUTAS.USUARIOS.ACTUALIZAR, + revisarApiKey(), + validarYSanitizar, + autorizarToken, + limitePeticionesDiarias, + revisarPermisos(PERMISOS.ACTUALIZAR_USUARIO), + controlador.actualizarUsuario +); + +module.exports = ruteador; diff --git a/Usuarios/Rutas/indexUsuarios.routes.js b/Usuarios/Rutas/indexUsuarios.routes.js index 5dc5ed1..f6fa16d 100644 --- a/Usuarios/Rutas/indexUsuarios.routes.js +++ b/Usuarios/Rutas/indexUsuarios.routes.js @@ -4,6 +4,7 @@ const rutasCrearUsuario = require('@altertex/usu/rutasInd/crearUsuario.routes'); const rutasLeerUsuario = require('@altertex/usu/rutasInd/leerUsuario.routes'); const rutasConsultarListaUsuarios = require('@altertex/usu/rutasInd/consultarListaUsuarios.routes'); const rutasEliminarUsuario = require('@altertex/usu/rutasInd/eliminarUsuario.routes'); +const rutasActualizarUsuario = require('@altertex/usu/rutasInd/actualizarUsuario.routes'); const RUTAS = require('@altertex/util/const/rutas'); @@ -11,5 +12,6 @@ ruteador.use(RUTAS.USUARIOS.BASE, rutasConsultarListaUsuarios); ruteador.use(RUTAS.USUARIOS.BASE, rutasCrearUsuario); ruteador.use(RUTAS.USUARIOS.BASE, rutasLeerUsuario); ruteador.use(RUTAS.USUARIOS.BASE, rutasEliminarUsuario); +ruteador.use(RUTAS.USUARIOS.BASE, rutasActualizarUsuario); module.exports = ruteador; diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index 0f679e9..e206201 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -40,6 +40,11 @@ module.exports = { VALUES (?, ?); `, + BORRAR_ASOCIACIONES_CLIENTE_USUARIO: ` + DELETE FROM usuario_cliente + WHERE idUsuario = ?; + `, + ASOCIAR_USUARIO_A_CLIENTE: ` INSERT INTO usuario_cliente (idUsuario, idCliente) VALUES (?, ?); @@ -54,8 +59,9 @@ module.exports = { u.fechaNacimiento, u.genero, u.estatus, - r.nombre AS rol, - uc.idCliente, + r.idRol AS rol, + r.nombre AS nombreRol, + uc.idCliente AS idCliente, c.nombreComercial AS nombreCliente FROM usuario u LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario @@ -66,29 +72,60 @@ module.exports = { `, OBTENER_LISTA: ` - SELECT u.idUsuario, - u.nombreCompleto AS nombre, - r.nombre AS rol, - c.nombreComercial AS cliente, - u.estatus, - u.correoElectronico AS correo, - u.numeroTelefono AS telefono - FROM usuario u - LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario - LEFT JOIN rol r ON ur.idRol = r.idRol - LEFT JOIN usuario_cliente uc ON u.idUsuario = uc.idUsuario - LEFT JOIN cliente c ON uc.idCliente = c.idCliente - WHERE u.idUsuario NOT IN (SELECT ur2.idUsuario - FROM usuario_rol ur2 - WHERE ur2.idRol = 3); - - `, + SELECT u.idUsuario, + u.nombreCompleto AS nombre, + r.idRol AS rol, + c.nombreComercial AS cliente, + u.estatus, + u.correoElectronico AS correo, + u.numeroTelefono AS telefono + FROM usuario u + LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario + LEFT JOIN rol r ON ur.idRol = r.idRol + LEFT JOIN usuario_cliente uc ON u.idUsuario = uc.idUsuario + LEFT JOIN cliente c ON uc.idCliente = c.idCliente + WHERE u.idUsuario NOT IN (SELECT ur2.idUsuario + FROM usuario_rol ur2 + WHERE ur2.idRol = 3); +`, ELIMINAR_USUARIOS: ` DELETE FROM usuario WHERE idUsuario = (?); `, + + ACTUALIZAR_DATOS_USUARIO: ` + UPDATE usuario SET + nombreCompleto = ?, + correoElectronico = ?, + contrasenia = ?, + numeroTelefono = ?, + direccion = ?, + fechaNacimiento = ?, + genero = ?, + estatus = ? + WHERE idUsuario = ?; + `, + + ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA: ` + UPDATE usuario SET + nombreCompleto = ?, + correoElectronico = ?, + numeroTelefono = ?, + direccion = ?, + fechaNacimiento = ?, + genero = ?, + estatus = ? + WHERE idUsuario = ?; + `, + + ACTUALIZAR_ROL_USUARIO: ` + UPDATE usuario_rol SET + idRol = ? + WHERE idUsuario = ?; + `, + VALIDAR_CORREO: ` SELECT idUsuario FROM usuario @@ -183,6 +220,9 @@ module.exports = { WHERE idUsuario IN (?) AND puedeActivar2FA = true; `, - - + VALIDAR_CORREO_DUPLICADO_ACTUALIZACION: ` + SELECT idUsuario + FROM usuario + WHERE correoElectronico = ? AND idUsuario <> ?; + `, }; diff --git a/Utilidades/Constantes/mensajesUsuarios.js b/Utilidades/Constantes/mensajesUsuarios.js index 898f47a..9e01f97 100644 --- a/Utilidades/Constantes/mensajesUsuarios.js +++ b/Utilidades/Constantes/mensajesUsuarios.js @@ -14,6 +14,10 @@ module.exports = { codigo: 200, mensaje: 'Lista de usuarios obtenida exitosamente.', }, + USUARIO_ACTUALIZADO: { + codigo: 200, + mensaje: 'Usuario actualizado correctamente.', + }, // 204 - Sin contenido USUARIOS_NO_ENCONTRADOS: { @@ -87,4 +91,12 @@ module.exports = { codigo: 500, mensaje: 'Ocurrió un error al intentar eliminar el usuario.', }, + ERROR_ACTUALIZAR_USUARIO: { + codigo: 500, + mensaje: 'Ocurrió un error al intentar actualizar el usuario.', + }, + ERROR_FECHA_NO_VALIDA: { + codigo: 500, + mensaje: 'La fecha de nacimiento proporcionada no es válida.', + }, }; diff --git a/Utilidades/Constantes/rutas.js b/Utilidades/Constantes/rutas.js index c9ba52b..66bf6d9 100644 --- a/Utilidades/Constantes/rutas.js +++ b/Utilidades/Constantes/rutas.js @@ -16,6 +16,7 @@ module.exports = { CREAR: '/crear', ELIMINAR_USUARIOS: '/eliminar-usuarios', LEER: '/consultar-usuario', + ACTUALIZAR: '/actualizar-usuario', }, CATEGORIAS: { BASE: '/categorias', diff --git a/package-lock.json b/package-lock.json index 8474434..ce03fdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6133,6 +6133,7 @@ "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", "dependencies": { "cookie": "0.4.0", "cookie-signature": "1.0.6", @@ -11441,6 +11442,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/redis/-/redis-5.1.0.tgz", "integrity": "sha512-5G5k9sYo5H5L0kd7UETiJZFTkIClH31fSmaEk2eU8E7mrmF0J1t6RqmFXOCJmSRlNd3QyvDUK/AWL9psbKaN0Q==", + "license": "MIT", "dependencies": { "@redis/bloom": "5.1.0", "@redis/client": "5.1.0",