validado seguridad

This commit is contained in:
Roberto Castellanos 2025-10-19 22:10:45 -06:00
parent 830220b2f0
commit 90c2ad934c
8 changed files with 964 additions and 314 deletions

View File

@ -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`);
});

View File

@ -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' });
}
};
};

View File

@ -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); }
}

View File

@ -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;
/**
* ========================================
* 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 1=1`;
const params = [];
WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado
const params = [id_usuario];
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';
try {
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) => {
router.patch('/:id/start', requireAuth, startAgendaValidator, handleValidation, async (req, res) => {
try {
const { id_usuario } = req.user;
const idAgenda = Number(req.params.id);
try {
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) => {
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;
try {
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' });
}
});

View File

@ -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

View File

@ -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;
/**
* ========================================
* 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 1=1`;
const params = [];
WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado
const params = [id_usuario];
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 (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';
try {
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) => {
/**
* ========================================
* 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 {
id_usuario, tipo, valor_objetivo, id_categoria = null,
tipo, valor_objetivo, id_categoria = null,
fecha_inicio, fecha_fin = null, estado = 'activa',
progreso_actual = 0,
} = req.body;
try {
// ✅ 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;
/**
* ========================================
* 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 {
id_usuario = null, tipo = null, valor_objetivo = null, id_categoria = null,
fecha_inicio = null, fecha_fin = null, estado = null, progreso_actual = null,
tipo, valor_objetivo, id_categoria,
fecha_inicio, fecha_fin, estado, progreso_actual,
} = req.body;
try {
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]
// ✅ 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 (result.affectedRows === 0) return res.status(404).json({ error: 'Meta no encontrada' });
res.json({ message: 'Meta actualizada' });
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 ${sets} WHERE id_meta = ? AND id_usuario = ?`,
values
);
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' });
}
});

View File

@ -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;
/**
* ========================================
* 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 1=1`;
const params = [];
WHERE id_usuario = ?`; // ✅ Siempre filtrar por usuario autenticado
const params = [id_usuario];
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 (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';
try {
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) => {
/**
* ========================================
* 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_usuario, id_actividad, fecha_inicio = null,
id_actividad, fecha_inicio = null,
emocion = null, completada = 0, puntos_obtenidos = 0, comentario = null
} = req.body;
try {
// ✅ 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;
/**
* ========================================
* 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_usuario = null, id_actividad = null,
fecha_inicio = null, fecha_fin = null,
emocion = null, completada = null, puntos_obtenidos = null, comentario = null,
id_actividad, fecha_inicio, fecha_fin,
emocion, completada, puntos_obtenidos, comentario,
} = req.body;
try {
// ✅ 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' });
}
});

View File

@ -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'),
];
/**