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