fix: version 2.5.0 -- compartir proyectos con permisos reales (Lectura/Escritura)

Bug de seguridad real corregido: cualquier usuario autenticado veia
TODOS los proyectos (listado, detalle, actividades, tareas,
comentarios, adjuntos, KPIs del panel), sin importar si era miembro.
Ahora nadie ve un proyecto ajeno sin que su dueno se lo comparta --
los roles de administracion (superadmin/admin/it_manager) mantienen
visibilidad total para soporte.

Nuevo modelo de permisos por proyecto, simple y no granular:
- Solo el dueno original puede compartir un proyecto (agregar/quitar
  miembros, cambiar su nivel) -- ni un admin del proyecto ni un rol
  global pueden hacerlo por el.
- Dos niveles: Lectura (solo ver) y Escritura (crear/editar/eliminar
  actividades y tareas -- nunca el proyecto en si: no lo borra, no
  edita sus datos, no crea proyectos nuevos).

Actividades y tareas pasan a borrado logico (antes se borraban
fisico sin poder recuperar) -- nueva papelera por proyecto con
restaurar.

Incluye tambien el filtro "Usuario" en Proyectos (probado en dev en
un paso anterior), que va de la mano con este cambio de visibilidad.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-25 18:19:41 -05:00
parent 9b20f85ae0
commit fda83e9dff
30 changed files with 469 additions and 140 deletions
+24 -2
View File
@@ -2,6 +2,7 @@ import * as repo from './repository.js';
import * as activitiesRepo from '../activities/repository.js';
import * as projectsRepo from '../projects/repository.js';
import { canWriteActivity, recalcFromTasks } from '../activities/service.js';
import { canReadProject } from '../projects/service.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
@@ -15,7 +16,18 @@ async function requireActivityAccess(activityId, currentUser) {
return activity;
}
export const listByActivity = (activityId) => repo.findByActivity(activityId);
export async function listByActivity(activityId, currentUser) {
const activity = await activitiesRepo.findById(activityId);
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
const project = await projectsRepo.findById(activity.project_id);
if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Actividad no encontrada');
return repo.findByActivity(activityId);
}
export async function listTrash(activityId, currentUser) {
await requireActivityAccess(activityId, currentUser);
return repo.findTrashByActivity(activityId);
}
export async function createTask(activityId, payload, currentUser) {
await requireActivityAccess(activityId, currentUser);
@@ -60,8 +72,18 @@ export async function removeTask(id, currentUser) {
const task = await repo.findById(id);
if (!task) throw new HttpError(404, 'Tarea no encontrada');
await requireActivityAccess(task.activity_id, currentUser);
await repo.remove(id);
await repo.softDelete(id, currentUser.id);
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'task', entityId: id });
const counts = await repo.countsByActivity(task.activity_id);
await recalcFromTasks(task.activity_id, counts.done, counts.total);
}
export async function restoreTask(id, currentUser) {
const task = await repo.findById(id, { includeDeleted: true });
if (!task || !task.deleted_at) throw new HttpError(404, 'Tarea no encontrada en la papelera');
await requireActivityAccess(task.activity_id, currentUser);
await repo.restore(id);
await logAudit({ userId: currentUser.id, action: 'restore', entityType: 'task', entityId: id });
const counts = await repo.countsByActivity(task.activity_id);
await recalcFromTasks(task.activity_id, counts.done, counts.total);
}