feat: version 3.0.0 -- Correcciones de auditoria de seguridad, accesibilidad y redundancia

Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico,
revocacion de JWT por version de sesion (028), control optimista de edicion (029),
logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion
de proyectos, accesibilidad de dialogos/controles e i18n.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-23 11:36:02 -05:00
parent 8ed1d63505
commit e65111a7f4
112 changed files with 2285 additions and 651 deletions
+25 -11
View File
@@ -8,7 +8,7 @@ const LIST_SELECT = `
p.requesting_department_id, rd.name AS requesting_department_name,
p.responsible_department_id, sd.name AS responsible_department_name,
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by,
p.risk_level, p.notes, p.edit_version, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by,
(SELECT COUNT(*) FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id <> p.owner_user_id) AS shared_count,
(SELECT COUNT(*) FROM project_members pm2 WHERE pm2.project_id = p.id) AS members_count,
(SELECT COUNT(*) FROM activities act WHERE act.project_id = p.id AND act.deleted_at IS NULL) AS activities_total,
@@ -38,12 +38,16 @@ function castCounts(row) {
};
}
export async function findAll({ statusId, priorityId, ownerUserId, memberUserId, trash } = {}) {
export async function findAll({ statusId, priorityId, ownerUserId, memberUserId, trash, page = 1, pageSize = 20, dueSoon, progress, q } = {}) {
const clauses = [trash ? 'p.deleted_at IS NOT NULL' : 'p.deleted_at IS NULL'];
const params = [];
if (statusId) { clauses.push('p.status_id = ?'); params.push(statusId); }
if (priorityId) { clauses.push('p.priority_id = ?'); params.push(priorityId); }
if (ownerUserId) { clauses.push('p.owner_user_id = ?'); params.push(ownerUserId); }
if (dueSoon) clauses.push('ps.is_closed = 0 AND p.estimated_end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY)');
if (progress === 'hide') clauses.push('p.progress_pct < 100');
if (progress === 'only') clauses.push('p.progress_pct >= 100');
if (q) { clauses.push('(p.code LIKE ? OR p.name LIKE ? OR u.name LIKE ?)'); params.push(...Array(3).fill(`%${q}%`)); }
// Restringe a proyectos donde el usuario es miembro (compartidos con el, o el propio dueño ya
// que quien crea un proyecto queda como miembro 'owner' automaticamente -- ver create()).
if (memberUserId) {
@@ -51,8 +55,18 @@ export async function findAll({ statusId, priorityId, ownerUserId, memberUserId,
params.push(memberUserId);
}
const where = `WHERE ${clauses.join(' AND ')}`;
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.created_at DESC`, params);
return rows.map(castCounts);
const [[{ total }]] = await pool.query(`SELECT COUNT(*) AS total FROM projects p JOIN project_statuses ps ON ps.id = p.status_id JOIN users u ON u.id = p.owner_user_id ${where}`, params);
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.updated_at DESC, p.id DESC LIMIT ? OFFSET ?`, [...params, pageSize, (page - 1) * pageSize]);
return { items: rows.map(castCounts), total: Number(total), page, pageSize };
}
export async function findOptions(memberUserId) {
const [rows] = await pool.query(
`SELECT p.id, p.code, p.name FROM projects p WHERE p.deleted_at IS NULL
${memberUserId ? 'AND EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id AND pm.user_id = ?)' : ''}
ORDER BY p.name`, memberUserId ? [memberUserId] : []
);
return rows;
}
export async function findById(id, { includeDeleted = false } = {}) {
@@ -62,11 +76,11 @@ export async function findById(id, { includeDeleted = false } = {}) {
}
export async function softDelete(id, userId) {
await pool.query('UPDATE projects SET deleted_at = NOW(), deleted_by = ? WHERE id = ? AND deleted_at IS NULL', [userId, id]);
await pool.query('UPDATE projects SET deleted_at = NOW(), deleted_by = ?, edit_version = edit_version + 1 WHERE id = ? AND deleted_at IS NULL', [userId, id]);
}
export async function restore(id) {
await pool.query('UPDATE projects SET deleted_at = NULL, deleted_by = NULL WHERE id = ?', [id]);
await pool.query('UPDATE projects SET deleted_at = NULL, deleted_by = NULL, edit_version = edit_version + 1 WHERE id = ?', [id]);
}
export async function hardDelete(id) {
@@ -106,21 +120,21 @@ export async function getDefaultStatusId() {
}
export async function update(id, data) {
await pool.query(
const [result] = await pool.query(
`UPDATE projects SET
name = ?, description = ?, objective = ?, project_type_id = ?, priority_id = ?, status_id = ?,
owner_user_id = ?, requesting_department_id = ?, responsible_department_id = ?,
start_date = ?, estimated_end_date = ?, actual_end_date = ?, risk_level = ?, notes = ?
WHERE id = ?`,
start_date = ?, estimated_end_date = ?, actual_end_date = ?, risk_level = ?, notes = ?, edit_version = edit_version + 1
WHERE id = ? AND edit_version = ? AND deleted_at IS NULL`,
[
data.name, data.description || null, data.objective || null, data.projectTypeId || null,
data.priorityId, data.statusId, data.ownerUserId,
data.requestingDepartmentId || null, data.responsibleDepartmentId || null,
data.startDate || null, data.estimatedEndDate || null, data.actualEndDate || null,
data.riskLevel || 'low', data.notes || null, id,
data.riskLevel || 'low', data.notes || null, id, data.editVersion,
]
);
return findById(id);
return result.affectedRows ? findById(id) : null;
}
export async function updateProgress(id, progressPct) {