validado seguridad
This commit is contained in:
		
							parent
							
								
									830220b2f0
								
							
						
					
					
						commit
						90c2ad934c
					
				
							
								
								
									
										30
									
								
								server.js
								
								
								
								
							
							
						
						
									
										30
									
								
								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`); | ||||
| }); | ||||
|  | @ -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' }); | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
|  | @ -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); } | ||||
|   } | ||||
|  |  | |||
|  | @ -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' }); | ||||
|   } | ||||
| }); | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' }); | ||||
|   } | ||||
| }); | ||||
|  |  | |||
|  | @ -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' }); | ||||
|   } | ||||
| }); | ||||
|  |  | |||
|  | @ -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'), | ||||
| ]; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue