diff --git a/server.js b/server.js index a3e9605..f57455f 100644 --- a/server.js +++ b/server.js @@ -168,7 +168,34 @@ app.get('/api/admin/_ping_token', requireAuth, (req, res) => { }); // ======================================== -// 8. INICIO DEL SERVIDOR +// 8. MANEJADOR DE ERRORES GLOBAL +// ======================================== +// IMPORTANTE: Debe ir AL FINAL, después de todas las rutas +// Este middleware captura todos los errores que se pasan con next(err) + +app.use((err, req, res, next) => { + // Log del error en consola para debugging + console.error('❌ ERROR GLOBAL:', err); + + // En producción, no exponer detalles del error + if (process.env.NODE_ENV === 'production') { + return res.status(err.status || 500).json({ + ok: false, + error: 'Error interno del servidor' + }); + } + + // En desarrollo, mostrar detalles completos + res.status(err.status || 500).json({ + ok: false, + error: err.message || 'Error interno del servidor', + stack: err.stack, + details: err + }); +}); + +// ======================================== +// 9. INICIO DEL SERVIDOR // ======================================== // Lee el puerto del .env o usa 3000 por defecto const PORT = process.env.PORT || 3000; @@ -177,5 +204,6 @@ const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`\n========================================`); console.log(`🚀 API escuchando en http://localhost:${PORT}`); + console.log(`📝 Modo: ${process.env.NODE_ENV || 'development'}`); console.log(`========================================\n`); }); \ No newline at end of file diff --git a/src/middleware/auth.js b/src/middleware/auth.js deleted file mode 100644 index f4e87d1..0000000 --- a/src/middleware/auth.js +++ /dev/null @@ -1,25 +0,0 @@ -// src/middleware/auth.js -const { verifyJWT } = require('../utils/jwt'); - -// auth(required = true) -// - required = true -> la ruta exige token (401 si no viene o es inválido) -// - required = false -> la ruta es pública pero, si viene token válido, adjunta req.user -module.exports = function auth(required = true) { - return (req, res, next) => { - const hdr = req.headers.authorization || ''; - const token = hdr.startsWith('Bearer ') ? hdr.slice(7) : null; - - if (!token) { - if (!required) return next(); // rutas públicas opcionales - return res.status(401).json({ error: 'Token requerido' }); - } - - try { - const payload = verifyJWT(token); // lanza si es inválido o expiró - req.user = payload; // { id, email, role, ... } - next(); - } catch (e) { - return res.status(401).json({ error: 'Token inválido o expirado' }); - } - }; -}; diff --git a/src/routes/actividades.routes.js b/src/routes/actividades.routes.js index 6a1e952..f82df60 100644 --- a/src/routes/actividades.routes.js +++ b/src/routes/actividades.routes.js @@ -87,6 +87,12 @@ router.put('/:id_actividad', async (req, res, next) => { try { const id = +req.params.id_actividad; + + // ✅ Validar ID + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de actividad inválido' }); + } + const fields = ['id_categoria','nombre','descripcion','duracion_minutos','dificultad','puntos_base','activo']; const sets = []; const values = []; @@ -99,13 +105,19 @@ router.put('/:id_actividad', } if (!sets.length) return res.status(400).json({ error: 'Nada para actualizar' }); + // Validar foreign key si se está actualizando if (req.body.id_categoria !== undefined) { const [ck] = await pool.query(`SELECT 1 FROM categorias WHERE id_categoria=?`, [req.body.id_categoria]); if (!ck.length) return res.status(400).json({ error: 'id_categoria no existe' }); } values.push(id); - await pool.query(`UPDATE actividades SET ${sets.join(', ')} WHERE id_actividad=?`, values); + const [updateResult] = await pool.query(`UPDATE actividades SET ${sets.join(', ')} WHERE id_actividad=?`, values); + + // ✅ Verificar que se actualizó algo + if (updateResult.affectedRows === 0) { + return res.status(404).json({ error: 'Actividad no encontrada' }); + } const [row] = await pool.query( `SELECT id_actividad, id_categoria, nombre, descripcion, duracion_minutos, dificultad, puntos_base, activo @@ -122,7 +134,19 @@ router.delete('/:id_actividad', async (req, res, next) => { try { const id = +req.params.id_actividad; - await pool.query(`DELETE FROM actividades WHERE id_actividad=?`, [id]); + + // ✅ Verificar que el ID sea válido + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de actividad inválido' }); + } + + // ✅ Verificar que se eliminó algo + const [result] = await pool.query(`DELETE FROM actividades WHERE id_actividad=?`, [id]); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Actividad no encontrada' }); + } + res.status(204).end(); } catch (err) { next(err); } } diff --git a/src/routes/agenda.routes.js b/src/routes/agenda.routes.js index b475d4e..809c9cf 100644 --- a/src/routes/agenda.routes.js +++ b/src/routes/agenda.routes.js @@ -1,9 +1,21 @@ -// src/routes/agenda.routes.js +/** + * ======================================== + * RUTAS DE AGENDA + * ======================================== + * + * Maneja la agenda/calendario de actividades de los usuarios. + * TODAS las rutas requieren autenticación. + * Los usuarios solo pueden ver/modificar su propia agenda. + */ + const express = require('express'); const router = express.Router(); const pool = require('../config/db'); +const { requireAuth } = require('../utils/jwt'); const handleValidation = require('../middleware/handleValidation'); +const { checkPermission } = require('../middleware/checkPermission'); +const { RESOURCES, ACTIONS } = require('../config/permissions'); const { createAgendaValidator, @@ -18,126 +30,255 @@ const { // 👇 nombre exacto de tu tabla de registros const REG_TABLE = 'registro_actividad'; -// LISTAR agendas -router.get('/', listAgendaValidator, handleValidation, async (req, res) => { - const { user_id, estado, desde, hasta } = req.query; - - let sql = ` - SELECT id_agenda, id_usuario, id_actividad, fecha_programada, estado - FROM agenda_actividades - WHERE 1=1`; - const params = []; - - if (user_id) { sql += ' AND id_usuario = ?'; params.push(Number(user_id)); } - if (estado) { sql += ' AND estado = ?'; params.push(estado); } - if (desde) { sql += ' AND fecha_programada >= ?'; params.push(desde); } - if (hasta) { sql += ' AND fecha_programada <= ?'; params.push(hasta); } - - sql += ' ORDER BY fecha_programada ASC'; - +/** + * ======================================== + * GET /api/agenda + * ======================================== + * Lista las agendas del usuario autenticado con filtros opcionales + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de LIST en recurso AGENDA + * - ✅ Solo muestra agendas del usuario autenticado + * - ✅ Validación de estados permitidos + */ +router.get('/', requireAuth, checkPermission(RESOURCES.AGENDA, ACTIONS.LIST), listAgendaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; // ✅ Del token + const { estado, desde, hasta } = req.query; + + // ✅ Validar estado si se proporciona + const validEstados = ['pendiente', 'realizada', 'omitida', 'reprogramada']; + if (estado && !validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + + let sql = ` + SELECT id_agenda, id_usuario, id_actividad, fecha_programada, estado + FROM agenda_actividades + WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado + const params = [id_usuario]; + + if (estado) { sql += ' AND estado = ?'; params.push(estado); } + if (desde) { sql += ' AND fecha_programada >= ?'; params.push(desde); } + if (hasta) { sql += ' AND fecha_programada <= ?'; params.push(hasta); } + + sql += ' ORDER BY fecha_programada ASC'; + const [rows] = await pool.query(sql, params); - res.json(rows); + res.json({ ok: true, data: rows }); } catch (e) { - console.error(e); + console.error('GET /agenda error:', e); res.status(500).json({ error: 'Error al listar agenda' }); } }); -// DETALLE -router.get('/:id', agendaIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * GET /api/agenda/:id + * ======================================== + * Obtiene una agenda específica del usuario autenticado + * + * SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de READ en recurso AGENDA + */ +router.get('/:id', requireAuth, checkPermission(RESOURCES.AGENDA, ACTIONS.READ), agendaIdValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Solo permite ver agendas propias const [rows] = await pool.query( - 'SELECT * FROM agenda_actividades WHERE id_agenda = ?', - [req.params.id] + 'SELECT * FROM agenda_actividades WHERE id_agenda = ? AND id_usuario = ?', + [id, id_usuario] ); - if (!rows.length) return res.status(404).json({ error: 'Agenda no encontrada' }); - res.json(rows[0]); + + if (!rows.length) { + return res.status(404).json({ error: 'Agenda no encontrada' }); + } + + res.json({ ok: true, data: rows[0] }); } catch (e) { - console.error(e); + console.error('GET /agenda/:id error:', e); res.status(500).json({ error: 'Error al obtener agenda' }); } }); -// CREAR (si no envías "estado", la BD usa DEFAULT 'pendiente') -router.post('/', createAgendaValidator, handleValidation, async (req, res) => { - const { id_usuario, id_actividad, fecha_programada, estado } = req.body; - +/** + * ======================================== + * POST /api/agenda + * ======================================== + * Crea una nueva agenda para el usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de CREATE en recurso AGENDA + * - ✅ Usa id_usuario del token (NO del body) + */ +router.post('/', requireAuth, checkPermission(RESOURCES.AGENDA, ACTIONS.CREATE), createAgendaValidator, handleValidation, async (req, res) => { try { - let sql, params; - if (estado) { - sql = `INSERT INTO agenda_actividades (id_usuario, id_actividad, fecha_programada, estado) - VALUES (?, ?, ?, ?)`; - params = [id_usuario, id_actividad, fecha_programada, estado]; // 'pendiente' | 'realizada' | 'omitida' | 'reprogramada' - } else { - sql = `INSERT INTO agenda_actividades (id_usuario, id_actividad, fecha_programada) - VALUES (?, ?, ?)`; - params = [id_usuario, id_actividad, fecha_programada]; + const { id_usuario } = req.user; // ✅ Del token, no del body + const { id_actividad, fecha_programada, estado = 'pendiente' } = req.body; + + // ✅ Validar estado + const validEstados = ['pendiente', 'realizada', 'omitida', 'reprogramada']; + if (!validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido' }); } - const [insert] = await pool.query(sql, params); - res.status(201).json({ message: 'Agenda creada', id: insert.insertId }); + // ✅ Validar que la actividad exista + const [actExists] = await pool.query( + 'SELECT 1 FROM actividades WHERE id_actividad = ?', + [id_actividad] + ); + if (!actExists.length) { + return res.status(400).json({ error: 'Actividad no existe' }); + } + + const [insert] = await pool.query( + `INSERT INTO agenda_actividades (id_usuario, id_actividad, fecha_programada, estado) + VALUES (?, ?, ?, ?)`, + [id_usuario, id_actividad, fecha_programada, estado] + ); + + res.status(201).json({ ok: true, message: 'Agenda creada', id: insert.insertId }); } catch (e) { - console.error(e); + console.error('POST /agenda error:', e); res.status(500).json({ error: 'Error al crear agenda' }); } }); -// ACTUALIZAR -router.put('/:id', updateAgendaValidator, handleValidation, async (req, res) => { - const { id } = req.params; - const { - id_usuario = null, id_actividad = null, - fecha_programada = null, estado = null - } = req.body; - +/** + * ======================================== + * PUT /api/agenda/:id + * ======================================== + * Actualiza una agenda del usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de UPDATE en recurso AGENDA + * - ✅ Solo permite actualizar agendas propias + * - ✅ NO permite cambiar id_usuario + */ +router.put('/:id', requireAuth, checkPermission(RESOURCES.AGENDA, ACTIONS.UPDATE), updateAgendaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + const { id_actividad, fecha_programada, estado } = req.body; + + // ✅ Construir update dinámico (SIN id_usuario) + const updates = {}; + if (id_actividad !== undefined) updates.id_actividad = id_actividad; + if (fecha_programada !== undefined) updates.fecha_programada = fecha_programada; + if (estado !== undefined) { + const validEstados = ['pendiente', 'realizada', 'omitida', 'reprogramada']; + if (!validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + updates.estado = estado; + } + + if (Object.keys(updates).length === 0) { + return res.status(400).json({ error: 'Nada para actualizar' }); + } + + const sets = Object.keys(updates).map(k => `${k} = ?`).join(', '); + const values = [...Object.values(updates), id, id_usuario]; + + // ✅ Solo actualiza si pertenece al usuario const [result] = await pool.query( - `UPDATE agenda_actividades - SET id_usuario = COALESCE(?, id_usuario), - id_actividad = COALESCE(?, id_actividad), - fecha_programada = COALESCE(?, fecha_programada), - estado = COALESCE(?, estado) - WHERE id_agenda = ?`, - [id_usuario, id_actividad, fecha_programada, estado, id] + `UPDATE agenda_actividades SET ${sets} WHERE id_agenda = ? AND id_usuario = ?`, + values ); - if (result.affectedRows === 0) return res.status(404).json({ error: 'Agenda no encontrada' }); - res.json({ message: 'Agenda actualizada' }); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Agenda actualizada' }); } catch (e) { - console.error(e); + console.error('PUT /agenda/:id error:', e); res.status(500).json({ error: 'Error al actualizar agenda' }); } }); -// ELIMINAR -router.delete('/:id', agendaIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * DELETE /api/agenda/:id + * ======================================== + * Elimina una agenda del usuario autenticado + * + * SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de DELETE en recurso AGENDA + */ +router.delete('/:id', requireAuth, checkPermission(RESOURCES.AGENDA, ACTIONS.DELETE), agendaIdValidator, handleValidation, async (req, res) => { try { - const [result] = await pool.query('DELETE FROM agenda_actividades WHERE id_agenda = ?', [req.params.id]); - if (result.affectedRows === 0) return res.status(404).json({ error: 'Agenda no encontrada' }); - res.json({ message: 'Agenda eliminada' }); + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Solo elimina si pertenece al usuario + const [result] = await pool.query( + 'DELETE FROM agenda_actividades WHERE id_agenda = ? AND id_usuario = ?', + [id, id_usuario] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Agenda eliminada' }); } catch (e) { - console.error(e); + console.error('DELETE /agenda/:id error:', e); res.status(500).json({ error: 'Error al eliminar agenda' }); } }); /** - * INICIAR desde agenda: - * - Crea un registro en registro_actividad (fecha_inicio = NOW(), completada=0) - * - NO cambiamos estado (tu ENUM no tiene "iniciada"). Se mantiene 'pendiente' - * - Devuelve registro_id para luego llamar /api/agenda/:id/finish + * ======================================== + * PATCH /api/agenda/:id/start + * ======================================== + * Inicia una actividad desde la agenda + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Solo permite iniciar agendas propias */ -router.patch('/:id/start', startAgendaValidator, handleValidation, async (req, res) => { - const idAgenda = Number(req.params.id); - +router.patch('/:id/start', requireAuth, startAgendaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const idAgenda = Number(req.params.id); + + if (isNaN(idAgenda) || idAgenda <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Solo permite iniciar agendas propias const [agRows] = await pool.query( - 'SELECT id_agenda, id_usuario, id_actividad, estado FROM agenda_actividades WHERE id_agenda = ?', - [idAgenda] + 'SELECT id_agenda, id_usuario, id_actividad, estado FROM agenda_actividades WHERE id_agenda = ? AND id_usuario = ?', + [idAgenda, id_usuario] ); - if (!agRows.length) return res.status(404).json({ error: 'Agenda no encontrada' }); + + if (!agRows.length) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } const agenda = agRows[0]; + // Bloquea si ya se marcó como realizada u omitida if (agenda.estado === 'omitida' || agenda.estado === 'realizada') { return res.status(400).json({ error: `La agenda está en estado ${agenda.estado}` }); @@ -147,30 +288,51 @@ router.patch('/:id/start', startAgendaValidator, handleValidation, async (req, r const [insReg] = await pool.query( `INSERT INTO ${REG_TABLE} (id_usuario, id_actividad, fecha_inicio, completada) VALUES (?, ?, NOW(), 0)`, - [agenda.id_usuario, agenda.id_actividad] + [id_usuario, agenda.id_actividad] ); res.json({ + ok: true, message: 'Actividad iniciada desde agenda', registro_id: insReg.insertId, }); } catch (e) { - console.error(e); + console.error('PATCH /agenda/:id/start error:', e); res.status(500).json({ error: 'Error al iniciar desde agenda' }); } }); /** - * FINALIZAR (vía agenda): - * - Cierra el registro (fecha_fin = NOW()) - * - Guarda emoción, puntos y comentario si vienen - * - Marca la agenda como 'realizada' + * ======================================== + * PATCH /api/agenda/:id/finish + * ======================================== + * Finaliza una actividad desde la agenda + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Valida ownership de la agenda */ -router.patch('/:id/finish', finishAgendaValidator, handleValidation, async (req, res) => { - const idAgenda = Number(req.params.id); - const { registro_id, emocion = null, puntos_obtenidos = null, comentario = null } = req.body; - +router.patch('/:id/finish', requireAuth, finishAgendaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const idAgenda = Number(req.params.id); + const { registro_id, emocion = null, puntos_obtenidos = null, comentario = null } = req.body; + + if (isNaN(idAgenda) || idAgenda <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Verificar que la agenda pertenece al usuario + const [agRows] = await pool.query( + 'SELECT 1 FROM agenda_actividades WHERE id_agenda = ? AND id_usuario = ?', + [idAgenda, id_usuario] + ); + + if (!agRows.length) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } + + // Actualizar registro await pool.query( `UPDATE ${REG_TABLE} SET fecha_fin = COALESCE(fecha_fin, NOW()), @@ -178,57 +340,90 @@ router.patch('/:id/finish', finishAgendaValidator, handleValidation, async (req, puntos_obtenidos = COALESCE(?, puntos_obtenidos), comentario = COALESCE(?, comentario), completada = 1 - WHERE id_registro = ?`, - [emocion, puntos_obtenidos, comentario, registro_id] + WHERE id_registro = ? AND id_usuario = ?`, + [emocion, puntos_obtenidos, comentario, registro_id, id_usuario] ); + // Marcar agenda como realizada await pool.query( 'UPDATE agenda_actividades SET estado = ? WHERE id_agenda = ?', ['realizada', idAgenda] ); - res.json({ message: 'Actividad finalizada; agenda marcada como realizada' }); + res.json({ ok: true, message: 'Actividad finalizada; agenda marcada como realizada' }); } catch (e) { - console.error(e); + console.error('PATCH /agenda/:id/finish error:', e); res.status(500).json({ error: 'Error al finalizar desde agenda' }); } }); /** - * REPROGRAMAR: cambia la fecha y marca 'reprogramada' + * ======================================== + * PATCH /api/agenda/:id/reprogramar + * ======================================== + * Reprograma una agenda (cambia fecha y marca como 'reprogramada') */ -router.patch('/:id/reprogramar', agendaIdValidator, handleValidation, async (req, res) => { - const { fecha_programada } = req.body; - if (!fecha_programada) return res.status(400).json({ error: 'fecha_programada requerida' }); - +router.patch('/:id/reprogramar', requireAuth, agendaIdValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + const { fecha_programada } = req.body; + + if (!fecha_programada) { + return res.status(400).json({ error: 'fecha_programada requerida' }); + } + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Solo reprograma si pertenece al usuario const [r] = await pool.query( `UPDATE agenda_actividades SET fecha_programada = ?, estado = 'reprogramada' - WHERE id_agenda = ?`, - [fecha_programada, req.params.id] + WHERE id_agenda = ? AND id_usuario = ?`, + [fecha_programada, id, id_usuario] ); - if (r.affectedRows === 0) return res.status(404).json({ error: 'Agenda no encontrada' }); - res.json({ message: 'Agenda reprogramada' }); + + if (r.affectedRows === 0) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Agenda reprogramada' }); } catch (e) { - console.error(e); + console.error('PATCH /agenda/:id/reprogramar error:', e); res.status(500).json({ error: 'Error al reprogramar agenda' }); } }); /** - * OMITIR: marca la agenda como 'omitida' + * ======================================== + * PATCH /api/agenda/:id/omitir + * ======================================== + * Marca una agenda como 'omitida' */ -router.patch('/:id/omitir', agendaIdValidator, handleValidation, async (req, res) => { +router.patch('/:id/omitir', requireAuth, agendaIdValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de agenda inválido' }); + } + + // ✅ Solo omite si pertenece al usuario const [r] = await pool.query( - 'UPDATE agenda_actividades SET estado = ? WHERE id_agenda = ?', - ['omitida', req.params.id] + 'UPDATE agenda_actividades SET estado = ? WHERE id_agenda = ? AND id_usuario = ?', + ['omitida', id, id_usuario] ); - if (r.affectedRows === 0) return res.status(404).json({ error: 'Agenda no encontrada' }); - res.json({ message: 'Agenda marcada como omitida' }); + + if (r.affectedRows === 0) { + return res.status(404).json({ error: 'Agenda no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Agenda marcada como omitida' }); } catch (e) { - console.error(e); + console.error('PATCH /agenda/:id/omitir error:', e); res.status(500).json({ error: 'Error al omitir agenda' }); } }); diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 3493482..8fdf2b1 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -96,19 +96,42 @@ router.post('/register', registerValidator, handleValidation, async (req, res) = [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 }); + // 4. Asignar rol por defecto (usuario normal) + // Primero obtener el id del rol 'usuario' + const [roleRows] = await pool.query( + 'SELECT id_rol FROM roles WHERE nombre = ?', + ['usuario'] + ); - // 5. Prepara el objeto de usuario (sin el password_hash) + let userRoleId = null; + if (roleRows.length > 0) { + userRoleId = roleRows[0].id_rol; + + // Asignar el rol al usuario + await pool.query( + 'INSERT INTO usuarios_roles (id_usuario, id_rol) VALUES (?, ?)', + [result.insertId, userRoleId] + ); + } + + // 5. Genera el token JWT con el ID del usuario y sus roles + const token = signToken({ + id_usuario: result.insertId, + email, + roles: ['usuario'] // Rol por defecto para usuarios nuevos + }); + + // 6. Prepara el objeto de usuario (sin el password_hash) const user = { id_usuario: result.insertId, nombre, apellido, email, - genero + genero, + roles: ['usuario'] }; - // 6. Devuelve respuesta exitosa con el token y datos del usuario + // 7. Devuelve respuesta exitosa con el token y datos del usuario return res.status(201).json({ msg: 'Usuario registrado con éxito', token, @@ -184,19 +207,40 @@ router.post('/login', loginValidator, handleValidation, async (req, res) => { 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 }); + // 5. Obtener roles del usuario + const [roleRows] = await pool.query( + `SELECT r.nombre + FROM usuarios_roles ur + JOIN roles r ON r.id_rol = ur.id_rol + WHERE ur.id_usuario = ?`, + [u.id_usuario] + ); - // 6. Prepara el objeto de usuario (sin password_hash por seguridad) + const roles = roleRows.map(row => row.nombre); + + // Si no tiene roles asignados, asignar 'usuario' por defecto + if (roles.length === 0) { + roles.push('usuario'); + } + + // 6. Genera token JWT con los datos del usuario Y SUS ROLES + const token = signToken({ + id_usuario: u.id_usuario, + email: u.email, + roles // ✅ Incluir roles en el token + }); + + // 7. 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 + genero: u.genero, + roles // ✅ Incluir roles en la respuesta }; - // 7. Devuelve respuesta exitosa + // 8. Devuelve respuesta exitosa return res.json({ msg: 'Login exitoso', token, user }); } catch (err) { @@ -275,14 +319,23 @@ router.get('/me', requireAuth, async (req, res) => { // Roles const [rrows] = await pool.query( - `SELECT r.id_rol, r.nombre + `SELECT r.id_rol, r.nombre, r.descripcion 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 || [] }; + // ✅ Preparar roles en formato simple y completo + const rolesSimple = rrows.map(r => r.nombre); + const rolesCompleto = rrows; + + const user = { + ...urows[0], + roles: rolesSimple, // Array de strings ['usuario', 'admin'] + rolesDetalle: rolesCompleto // Array de objetos con id y descripción + }; + return res.json({ ok: true, user }); } catch (e) { console.error('GET /api/auth/me', e); @@ -290,6 +343,68 @@ router.get('/me', requireAuth, async (req, res) => { } }); +/** + * ======================================== + * GET /api/auth/menu + * ======================================== + * Devuelve el menú de navegación personalizado según los roles del usuario + * + * REQUIERE: Token JWT + * FLUJO: requireAuth → obtiene roles → construye menú → devuelve estructura agrupada + * + * RESPONSE: + * { + * "ok": true, + * "menu": { + * "Cuenta": [ + * { "label": "Mi Perfil", "icon": "user", "path": "/api/auth/me", "method": "GET", "order": 1 } + * ], + * "Catálogos": [ + * { "label": "Categorías", "icon": "folder", "path": "/api/categorias", "method": "GET", "order": 10 }, + * { "label": "Actividades", "icon": "activity", "path": "/api/actividades", "method": "GET", "order": 11 } + * ], + * "Mi Progreso": [...], + * "Administración": [...] // Solo para admin + * } + * } + */ +router.get('/menu', requireAuth, async (req, res) => { + try { + const { buildMenuForRole } = require('../config/permissions'); + const userRoles = req.user.roles || ['usuario']; + + // Construir menú para cada rol del usuario y combinarlos + const menusByRole = userRoles.map(role => buildMenuForRole(role)); + + // Combinar menús de todos los roles (un usuario puede tener múltiples roles) + const combinedMenu = {}; + menusByRole.forEach(menu => { + Object.keys(menu).forEach(group => { + if (!combinedMenu[group]) { + combinedMenu[group] = []; + } + // Evitar duplicados comparando por path + menu[group].forEach(item => { + const exists = combinedMenu[group].some(existing => existing.path === item.path); + if (!exists) { + combinedMenu[group].push(item); + } + }); + }); + }); + + // Ordenar items dentro de cada grupo + Object.keys(combinedMenu).forEach(group => { + combinedMenu[group].sort((a, b) => a.order - b.order); + }); + + return res.json({ ok: true, menu: combinedMenu }); + } catch (e) { + console.error('GET /api/auth/menu', e); + return res.status(500).json({ error: 'Error al obtener menú' }); + } +}); + /** * ======================================== * PUT /api/auth/me/password diff --git a/src/routes/metas.routes.js b/src/routes/metas.routes.js index 780ffbc..daef953 100644 --- a/src/routes/metas.routes.js +++ b/src/routes/metas.routes.js @@ -1,9 +1,21 @@ -// src/routes/metas.routes.js +/** + * ======================================== + * RUTAS DE METAS + * ======================================== + * + * Maneja las metas/objetivos de los usuarios. + * TODAS las rutas requieren autenticación. + * Los usuarios solo pueden ver/modificar sus propias metas. + */ + const express = require('express'); const router = express.Router(); const pool = require('../config/db'); +const { requireAuth } = require('../utils/jwt'); const handleValidation = require('../middleware/handleValidation'); +const { checkPermission } = require('../middleware/checkPermission'); +const { RESOURCES, ACTIONS } = require('../config/permissions'); const { createMetaValidator, @@ -12,59 +24,145 @@ const { listMetasValidator, } = require('../validators/metas.validators'); -// LISTAR metas (con filtros opcionales) -router.get('/', listMetasValidator, handleValidation, async (req, res) => { - const { user_id, estado, tipo, categoria, desde, hasta } = req.query; - - let sql = ` - SELECT id_meta, id_usuario, tipo, valor_objetivo, id_categoria, - fecha_inicio, fecha_fin, estado, progreso_actual, creada_en - FROM metas - WHERE 1=1`; - const params = []; - - if (user_id) { sql += ' AND id_usuario = ?'; params.push(Number(user_id)); } - if (estado) { sql += ' AND estado = ?'; params.push(estado); } - if (tipo) { sql += ' AND tipo = ?'; params.push(tipo); } - if (categoria){ sql += ' AND id_categoria = ?'; params.push(Number(categoria)); } - if (desde) { sql += ' AND fecha_inicio >= ?'; params.push(desde); } - if (hasta) { sql += ' AND fecha_inicio <= ?'; params.push(hasta); } - - sql += ' ORDER BY id_meta DESC'; - +/** + * ======================================== + * GET /api/metas + * ======================================== + * Lista las metas del usuario autenticado con filtros opcionales + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de LIST en recurso GOALS + * - ✅ Solo muestra metas del usuario autenticado (ignora user_id del query) + * - ✅ Validación de estados y tipos permitidos + */ +router.get('/', requireAuth, checkPermission(RESOURCES.GOALS, ACTIONS.LIST), listMetasValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; // ✅ Del token, no del query + const { estado, tipo, categoria, desde, hasta } = req.query; + + // ✅ Validar estado si se proporciona + const validEstados = ['activa', 'completada', 'cancelada']; + if (estado && !validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + + // ✅ Validar tipo si se proporciona + const validTipos = ['diaria', 'semanal', 'mensual']; + if (tipo && !validTipos.includes(tipo)) { + return res.status(400).json({ error: 'Tipo inválido' }); + } + + let sql = ` + SELECT id_meta, id_usuario, tipo, valor_objetivo, id_categoria, + fecha_inicio, fecha_fin, estado, progreso_actual, creada_en + FROM metas + WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado + const params = [id_usuario]; + + if (estado) { sql += ' AND estado = ?'; params.push(estado); } + if (tipo) { sql += ' AND tipo = ?'; params.push(tipo); } + if (categoria) { + const cat = Number(categoria); + if (isNaN(cat)) return res.status(400).json({ error: 'categoria debe ser número' }); + sql += ' AND id_categoria = ?'; + params.push(cat); + } + if (desde) { sql += ' AND fecha_inicio >= ?'; params.push(desde); } + if (hasta) { sql += ' AND fecha_inicio <= ?'; params.push(hasta); } + + sql += ' ORDER BY id_meta DESC'; + const [rows] = await pool.query(sql, params); - res.json(rows); + res.json({ ok: true, data: rows }); } catch (e) { - console.error(e); + console.error('GET /metas error:', e); res.status(500).json({ error: 'Error al listar metas' }); } }); -// DETALLE por id -router.get('/:id', metaIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * GET /api/metas/:id + * ======================================== + * Obtiene una meta específica del usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de READ en recurso GOALS + * - ✅ Solo permite ver metas propias (validación de ownership) + */ +router.get('/:id', requireAuth, checkPermission(RESOURCES.GOALS, ACTIONS.READ), metaIdValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + // ✅ Validar ID + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de meta inválido' }); + } + + // ✅ Solo permitir ver metas propias const [rows] = await pool.query( - 'SELECT * FROM metas WHERE id_meta = ?', - [req.params.id] + 'SELECT * FROM metas WHERE id_meta = ? AND id_usuario = ?', + [id, id_usuario] ); - if (!rows.length) return res.status(404).json({ error: 'Meta no encontrada' }); - res.json(rows[0]); + + if (!rows.length) { + return res.status(404).json({ error: 'Meta no encontrada' }); + } + + res.json({ ok: true, data: rows[0] }); } catch (e) { - console.error(e); + console.error('GET /metas/:id error:', e); res.status(500).json({ error: 'Error al obtener meta' }); } }); -// CREAR meta -router.post('/', createMetaValidator, handleValidation, async (req, res) => { - const { - id_usuario, tipo, valor_objetivo, id_categoria = null, - fecha_inicio, fecha_fin = null, estado = 'activa', - progreso_actual = 0, - } = req.body; - +/** + * ======================================== + * POST /api/metas + * ======================================== + * Crea una nueva meta para el usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de CREATE en recurso GOALS + * - ✅ Usa id_usuario del token (NO del body) + * - ✅ Validación de tipos y estados permitidos + */ +router.post('/', requireAuth, checkPermission(RESOURCES.GOALS, ACTIONS.CREATE), createMetaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; // ✅ Del token, no del body + const { + tipo, valor_objetivo, id_categoria = null, + fecha_inicio, fecha_fin = null, estado = 'activa', + progreso_actual = 0, + } = req.body; + + // ✅ Validar tipo + const validTipos = ['diaria', 'semanal', 'mensual']; + if (!validTipos.includes(tipo)) { + return res.status(400).json({ error: 'Tipo inválido. Debe ser: diaria, semanal o mensual' }); + } + + // ✅ Validar estado + const validEstados = ['activa', 'completada', 'cancelada']; + if (!validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido. Debe ser: activa, completada o cancelada' }); + } + + // ✅ Validar id_categoria si se proporciona + if (id_categoria !== null) { + const [catExists] = await pool.query( + 'SELECT 1 FROM categorias WHERE id_categoria = ?', + [id_categoria] + ); + if (!catExists.length) { + return res.status(400).json({ error: 'Categoría no existe' }); + } + } + const [result] = await pool.query( `INSERT INTO metas (id_usuario, tipo, valor_objetivo, id_categoria, fecha_inicio, fecha_fin, @@ -72,51 +170,133 @@ router.post('/', createMetaValidator, handleValidation, async (req, res) => { VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`, [id_usuario, tipo, valor_objetivo, id_categoria, fecha_inicio, fecha_fin, estado, progreso_actual] ); - res.status(201).json({ message: 'Meta creada', id: result.insertId }); + + res.status(201).json({ ok: true, message: 'Meta creada', id: result.insertId }); } catch (e) { - console.error(e); + console.error('POST /metas error:', e); res.status(500).json({ error: 'Error al crear meta' }); } }); -// ACTUALIZAR meta (parcial) -router.put('/:id', updateMetaValidator, handleValidation, async (req, res) => { - const { id } = req.params; - const { - id_usuario = null, tipo = null, valor_objetivo = null, id_categoria = null, - fecha_inicio = null, fecha_fin = null, estado = null, progreso_actual = null, - } = req.body; - +/** + * ======================================== + * PUT /api/metas/:id + * ======================================== + * Actualiza una meta del usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de UPDATE en recurso GOALS + * - ✅ Solo permite actualizar metas propias (ownership) + * - ✅ NO permite cambiar id_usuario (eliminado COALESCE peligroso) + * - ✅ Validación de estados y tipos + */ +router.put('/:id', requireAuth, checkPermission(RESOURCES.GOALS, ACTIONS.UPDATE), updateMetaValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + // ✅ Validar ID + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de meta inválido' }); + } + + const { + tipo, valor_objetivo, id_categoria, + fecha_inicio, fecha_fin, estado, progreso_actual, + } = req.body; + + // ✅ Construir update dinámico (SIN id_usuario) + const updates = {}; + if (tipo !== undefined) { + const validTipos = ['diaria', 'semanal', 'mensual']; + if (!validTipos.includes(tipo)) { + return res.status(400).json({ error: 'Tipo inválido' }); + } + updates.tipo = tipo; + } + if (estado !== undefined) { + const validEstados = ['activa', 'completada', 'cancelada']; + if (!validEstados.includes(estado)) { + return res.status(400).json({ error: 'Estado inválido' }); + } + updates.estado = estado; + } + if (valor_objetivo !== undefined) updates.valor_objetivo = valor_objetivo; + if (id_categoria !== undefined) updates.id_categoria = id_categoria; + if (fecha_inicio !== undefined) updates.fecha_inicio = fecha_inicio; + if (fecha_fin !== undefined) updates.fecha_fin = fecha_fin; + if (progreso_actual !== undefined) updates.progreso_actual = progreso_actual; + + if (Object.keys(updates).length === 0) { + return res.status(400).json({ error: 'Nada para actualizar' }); + } + + // ✅ Validar id_categoria si se está actualizando + if (id_categoria !== undefined && id_categoria !== null) { + const [catExists] = await pool.query( + 'SELECT 1 FROM categorias WHERE id_categoria = ?', + [id_categoria] + ); + if (!catExists.length) { + return res.status(400).json({ error: 'Categoría no existe' }); + } + } + + const sets = Object.keys(updates).map(k => `${k} = ?`).join(', '); + const values = [...Object.values(updates), id, id_usuario]; + + // ✅ Solo actualiza si pertenece al usuario const [result] = await pool.query( - `UPDATE metas - SET id_usuario = COALESCE(?, id_usuario), - tipo = COALESCE(?, tipo), - valor_objetivo = COALESCE(?, valor_objetivo), - id_categoria = COALESCE(?, id_categoria), - fecha_inicio = COALESCE(?, fecha_inicio), - fecha_fin = COALESCE(?, fecha_fin), - estado = COALESCE(?, estado), - progreso_actual = COALESCE(?, progreso_actual) - WHERE id_meta = ?`, - [id_usuario, tipo, valor_objetivo, id_categoria, fecha_inicio, fecha_fin, estado, progreso_actual, id] + `UPDATE metas SET ${sets} WHERE id_meta = ? AND id_usuario = ?`, + values ); - if (result.affectedRows === 0) return res.status(404).json({ error: 'Meta no encontrada' }); - res.json({ message: 'Meta actualizada' }); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Meta no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Meta actualizada' }); } catch (e) { - console.error(e); + console.error('PUT /metas/:id error:', e); res.status(500).json({ error: 'Error al actualizar meta' }); } }); -// ELIMINAR meta -router.delete('/:id', metaIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * DELETE /api/metas/:id + * ======================================== + * Elimina una meta del usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de DELETE en recurso GOALS + * - ✅ Solo permite eliminar metas propias (ownership) + */ +router.delete('/:id', requireAuth, checkPermission(RESOURCES.GOALS, ACTIONS.DELETE), metaIdValidator, handleValidation, async (req, res) => { try { - const [result] = await pool.query('DELETE FROM metas WHERE id_meta = ?', [req.params.id]); - if (result.affectedRows === 0) return res.status(404).json({ error: 'Meta no encontrada' }); - res.json({ message: 'Meta eliminada' }); + const { id_usuario } = req.user; + const id = Number(req.params.id); + + // ✅ Validar ID + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de meta inválido' }); + } + + // ✅ Solo elimina si pertenece al usuario + const [result] = await pool.query( + 'DELETE FROM metas WHERE id_meta = ? AND id_usuario = ?', + [id, id_usuario] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: 'Meta no encontrada o no autorizado' }); + } + + res.json({ ok: true, message: 'Meta eliminada' }); } catch (e) { - console.error(e); + console.error('DELETE /metas/:id error:', e); res.status(500).json({ error: 'Error al eliminar meta' }); } }); diff --git a/src/routes/registroActividad.routes.js b/src/routes/registroActividad.routes.js index ab11495..49739ab 100644 --- a/src/routes/registroActividad.routes.js +++ b/src/routes/registroActividad.routes.js @@ -1,9 +1,20 @@ -// src/routes/registroActividad.routes.js +/** + * ======================================== + * RUTAS DE REGISTRO DE ACTIVIDAD + * ======================================== + * + * Maneja el registro de actividades completadas por los usuarios. + * TODAS las rutas requieren autenticación. + * Los usuarios solo pueden ver/modificar sus propios registros. + */ + const express = require('express'); const router = express.Router(); const pool = require('../config/db'); const handleValidation = require('../middleware/handleValidation'); -const { requireAuth } = require('../utils/jwt'); // ✅ Importa el middleware +const { requireAuth } = require('../utils/jwt'); +const { checkPermission } = require('../middleware/checkPermission'); +const { RESOURCES, ACTIONS } = require('../config/permissions'); const { createRegistroValidator, @@ -13,14 +24,20 @@ const { finishRegistroValidator, } = require('../validators/registroActividad.validators'); -/* ========================================== - POST /api/registroactividad/start - Body: { id_actividad:number } - Crea un registro con fecha_inicio = NOW() - y completada = 0 para el usuario autenticado - ➜ Bloquea si ya está 'realizada' para ese usuario - ========================================== */ -router.post('/start', requireAuth, async (req, res) => { +/** + * ======================================== + * POST /api/registroactividad/start + * ======================================== + * Inicia una nueva actividad para el usuario autenticado + * + * Body: { id_actividad: number } + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de CREATE en recurso ACTIVITY_LOG + * - ✅ Usa id_usuario del token + */ +router.post('/start', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.CREATE), async (req, res) => { try { const { id_usuario } = req.user; const { id_actividad } = req.body; @@ -55,16 +72,23 @@ router.post('/start', requireAuth, async (req, res) => { } }); -/* =========================================================== - PATCH /api/registroactividad/finish/:id - Body opcional: { emocion?, puntos_obtenidos?, comentario? } - Marca fecha_fin = NOW() (si estaba null) y completada = 1. - Valida que el registro pertenezca al usuario del token. - ➜ Además, hace UPSERT en usuario_actividad_estado a 'realizada' - =========================================================== */ +/** + * ======================================== + * PATCH /api/registroactividad/finish/:id + * ======================================== + * Finaliza una actividad en progreso + * + * Body opcional: { emocion?, puntos_obtenidos?, comentario? } + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de UPDATE en recurso ACTIVITY_LOG + * - ✅ Solo permite finalizar registros propios + */ router.patch( '/finish/:id', requireAuth, + checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.UPDATE), finishRegistroValidator, handleValidation, async (req, res) => { @@ -122,65 +146,116 @@ router.patch( } ); -/* ===================================================== - GET /api/registroactividad - Con filtros (?user_id, actividad_id, completada, desde, hasta) - ===================================================== */ -router.get('/', listRegistrosValidator, handleValidation, async (req, res) => { - const { user_id, actividad_id, completada, desde, hasta } = req.query; - - let sql = ` - SELECT id_registro, id_usuario, id_actividad, fecha_inicio, fecha_fin, - emocion, completada, puntos_obtenidos, comentario - FROM registro_actividad - WHERE 1=1`; - const params = []; - - if (user_id) { sql += ' AND id_usuario = ?'; params.push(Number(user_id)); } - if (actividad_id) { sql += ' AND id_actividad = ?'; params.push(Number(actividad_id)); } - if (completada !== undefined) { sql += ' AND completada = ?'; params.push(Number(completada)); } - if (desde) { sql += ' AND fecha_inicio >= ?'; params.push(desde); } - if (hasta) { sql += ' AND fecha_inicio <= ?'; params.push(hasta); } - - sql += ' ORDER BY id_registro DESC'; - +/** + * ======================================== + * GET /api/registroactividad + * ======================================== + * Lista los registros del usuario autenticado con filtros opcionales + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de LIST en recurso ACTIVITY_LOG + * - ✅ Solo muestra registros del usuario autenticado (ignora user_id del query) + */ +router.get('/', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.LIST), listRegistrosValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; // ✅ Del token + const { actividad_id, completada, desde, hasta } = req.query; + + let sql = ` + SELECT id_registro, id_usuario, id_actividad, fecha_inicio, fecha_fin, + emocion, completada, puntos_obtenidos, comentario + FROM registro_actividad + WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado + const params = [id_usuario]; + + if (actividad_id) { + const actId = Number(actividad_id); + if (isNaN(actId)) return res.status(400).json({ error: 'actividad_id debe ser número' }); + sql += ' AND id_actividad = ?'; + params.push(actId); + } + if (completada !== undefined) { + sql += ' AND completada = ?'; + params.push(Number(completada)); + } + if (desde) { sql += ' AND fecha_inicio >= ?'; params.push(desde); } + if (hasta) { sql += ' AND fecha_inicio <= ?'; params.push(hasta); } + + sql += ' ORDER BY id_registro DESC'; + const [rows] = await pool.query(sql, params); - res.json(rows); + res.json({ ok: true, data: rows }); } catch (e) { - console.error(e); + console.error('GET /registroactividad error:', e); res.status(500).json({ error: 'Error al listar registros' }); } }); -/* =============================== - GET /api/registroactividad/:id - =============================== */ -router.get('/:id', registroIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * GET /api/registroactividad/:id + * ======================================== + * Obtiene un registro específico del usuario autenticado + * + * SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de READ en recurso ACTIVITY_LOG + */ +router.get('/:id', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.READ), registroIdValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de registro inválido' }); + } + + // ✅ Solo permite ver registros propios const [rows] = await pool.query( - 'SELECT * FROM registro_actividad WHERE id_registro = ?', - [req.params.id] + 'SELECT * FROM registro_actividad WHERE id_registro = ? AND id_usuario = ?', + [id, id_usuario] ); - if (!rows.length) return res.status(404).json({ error: 'Registro no encontrado' }); - res.json(rows[0]); + + if (!rows.length) { + return res.status(404).json({ error: 'Registro no encontrado' }); + } + + res.json({ ok: true, data: rows[0] }); } catch (e) { - console.error(e); + console.error('GET /registroactividad/:id error:', e); res.status(500).json({ error: 'Error al obtener registro' }); } }); -/* ========================================================== - POST /api/registroactividad (genérico) - Si no envías fecha_inicio, se inserta sin ese campo. - ========================================================== */ -router.post('/', createRegistroValidator, handleValidation, async (req, res) => { - const { - id_usuario, id_actividad, fecha_inicio = null, - emocion = null, completada = 0, puntos_obtenidos = 0, comentario = null - } = req.body; - +/** + * ======================================== + * POST /api/registroactividad + * ======================================== + * Crea un nuevo registro de actividad para el usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de CREATE en recurso ACTIVITY_LOG + * - ✅ Usa id_usuario del token (NO del body) + */ +router.post('/', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.CREATE), createRegistroValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; // ✅ Del token, no del body + const { + id_actividad, fecha_inicio = null, + emocion = null, completada = 0, puntos_obtenidos = 0, comentario = null + } = req.body; + + // ✅ Validar que la actividad exista + const [actExists] = await pool.query( + 'SELECT 1 FROM actividades WHERE id_actividad = ?', + [id_actividad] + ); + if (!actExists.length) { + return res.status(400).json({ error: 'Actividad no existe' }); + } + let sql, params; if (fecha_inicio) { sql = ` @@ -197,57 +272,105 @@ router.post('/', createRegistroValidator, handleValidation, async (req, res) => } const [ins] = await pool.query(sql, params); - res.status(201).json({ message: 'Registro creado', id: ins.insertId }); + res.status(201).json({ ok: true, message: 'Registro creado', id: ins.insertId }); } catch (e) { - console.error(e); + console.error('POST /registroactividad error:', e); res.status(500).json({ error: 'Error al crear registro' }); } }); -/* =================================== - PUT /api/registroactividad/:id - Actualización parcial - =================================== */ -router.put('/:id', updateRegistroValidator, handleValidation, async (req, res) => { - const { id } = req.params; - const { - id_usuario = null, id_actividad = null, - fecha_inicio = null, fecha_fin = null, - emocion = null, completada = null, puntos_obtenidos = null, comentario = null, - } = req.body; - +/** + * ======================================== + * PUT /api/registroactividad/:id + * ======================================== + * Actualiza un registro del usuario autenticado + * + * CAMBIOS DE SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de UPDATE en recurso ACTIVITY_LOG + * - ✅ Solo permite actualizar registros propios + * - ✅ NO permite cambiar id_usuario + */ +router.put('/:id', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.UPDATE), updateRegistroValidator, handleValidation, async (req, res) => { try { + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de registro inválido' }); + } + + const { + id_actividad, fecha_inicio, fecha_fin, + emocion, completada, puntos_obtenidos, comentario, + } = req.body; + + // ✅ Construir update dinámico (SIN id_usuario) + const updates = {}; + if (id_actividad !== undefined) updates.id_actividad = id_actividad; + if (fecha_inicio !== undefined) updates.fecha_inicio = fecha_inicio; + if (fecha_fin !== undefined) updates.fecha_fin = fecha_fin; + if (emocion !== undefined) updates.emocion = emocion; + if (completada !== undefined) updates.completada = completada; + if (puntos_obtenidos !== undefined) updates.puntos_obtenidos = puntos_obtenidos; + if (comentario !== undefined) updates.comentario = comentario; + + if (Object.keys(updates).length === 0) { + return res.status(400).json({ error: 'Nada para actualizar' }); + } + + const sets = Object.keys(updates).map(k => `${k} = ?`).join(', '); + const values = [...Object.values(updates), id, id_usuario]; + + // ✅ Solo actualiza si pertenece al usuario const [r] = await pool.query( - `UPDATE registro_actividad - SET id_usuario = COALESCE(?, id_usuario), - id_actividad = COALESCE(?, id_actividad), - fecha_inicio = COALESCE(?, fecha_inicio), - fecha_fin = COALESCE(?, fecha_fin), - emocion = COALESCE(?, emocion), - completada = COALESCE(?, completada), - puntos_obtenidos = COALESCE(?, puntos_obtenidos), - comentario = COALESCE(?, comentario) - WHERE id_registro = ?`, - [id_usuario, id_actividad, fecha_inicio, fecha_fin, emocion, completada, puntos_obtenidos, comentario, id] + `UPDATE registro_actividad SET ${sets} WHERE id_registro = ? AND id_usuario = ?`, + values ); - if (r.affectedRows === 0) return res.status(404).json({ error: 'Registro no encontrado' }); - res.json({ message: 'Registro actualizado' }); + + if (r.affectedRows === 0) { + return res.status(404).json({ error: 'Registro no encontrado o no autorizado' }); + } + + res.json({ ok: true, message: 'Registro actualizado' }); } catch (e) { - console.error(e); + console.error('PUT /registroactividad/:id error:', e); res.status(500).json({ error: 'Error al actualizar registro' }); } }); -/* ================================ - DELETE /api/registroactividad/:id - ================================ */ -router.delete('/:id', registroIdValidator, handleValidation, async (req, res) => { +/** + * ======================================== + * DELETE /api/registroactividad/:id + * ======================================== + * Elimina un registro del usuario autenticado + * + * SEGURIDAD: + * - ✅ Requiere autenticación + * - ✅ Requiere permiso de DELETE en recurso ACTIVITY_LOG + */ +router.delete('/:id', requireAuth, checkPermission(RESOURCES.ACTIVITY_LOG, ACTIONS.DELETE), registroIdValidator, handleValidation, async (req, res) => { try { - const [r] = await pool.query('DELETE FROM registro_actividad WHERE id_registro = ?', [req.params.id]); - if (r.affectedRows === 0) return res.status(404).json({ error: 'Registro no encontrado' }); - res.json({ message: 'Registro eliminado' }); + const { id_usuario } = req.user; + const id = Number(req.params.id); + + if (isNaN(id) || id <= 0) { + return res.status(400).json({ error: 'ID de registro inválido' }); + } + + // ✅ Solo elimina si pertenece al usuario + const [r] = await pool.query( + 'DELETE FROM registro_actividad WHERE id_registro = ? AND id_usuario = ?', + [id, id_usuario] + ); + + if (r.affectedRows === 0) { + return res.status(404).json({ error: 'Registro no encontrado o no autorizado' }); + } + + res.json({ ok: true, message: 'Registro eliminado' }); } catch (e) { - console.error(e); + console.error('DELETE /registroactividad/:id error:', e); res.status(500).json({ error: 'Error al eliminar registro' }); } }); diff --git a/src/validators/auth.validators.js b/src/validators/auth.validators.js index 6d8ed17..43cb83c 100644 --- a/src/validators/auth.validators.js +++ b/src/validators/auth.validators.js @@ -42,13 +42,23 @@ const registerValidator = [ .notEmpty() // No puede estar vacío .withMessage('Nombre requerido'), + body('apellido') + .trim() + .notEmpty() + .withMessage('Apellido requerido'), + body('email') .isEmail() // Verifica formato email - .withMessage('Email inválido'), + .withMessage('Email inválido') + .normalizeEmail(), // Normaliza el email + + body('genero') + .isIn(['M', 'F', 'Otro']) + .withMessage('Género debe ser M, F u Otro'), body('password') .isLength({ min: 6 }) // Mínimo 6 caracteres - .withMessage('Mínimo 6 caracteres'), + .withMessage('Password debe tener mínimo 6 caracteres'), ]; /**