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
+34 -15
View File
@@ -2,9 +2,8 @@ import * as repo from './repository.js';
import * as projectsRepo from '../projects/repository.js';
import * as commentsRepo from '../comments/repository.js';
import * as attachmentsRepo from '../attachments/repository.js';
import * as attachmentsService from '../attachments/service.js';
import * as tasksRepo from '../tasks/repository.js';
import { recalcProjectProgress, canWriteProject } from '../projects/service.js';
import { recalcProjectProgress, canReadProject, canWriteProject } from '../projects/service.js';
import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
import { ROLES } from '../../middleware/auth.js';
@@ -16,20 +15,31 @@ async function getProjectOr404(projectId) {
return project;
}
// admin/it_manager siempre; project_lead si tiene acceso al proyecto; collaborator solo si es
// el responsable de la actividad (no puede crear actividades nuevas ni reasignarlas a otros).
export async function canWriteActivity(user, project, activity) {
if ([ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER].includes(user.roleCode)) return true;
if (user.roleCode === ROLES.PROJECT_LEAD) return canWriteProject(user, project);
if (user.roleCode === ROLES.COLLABORATOR && activity) return activity.responsible_user_id === user.id;
return false;
// Se delega entero a canWriteProject: con el modelo de compartir por proyecto (Lectura/Escritura,
// ver projects/service.js), quien tiene Escritura puede crear/editar/eliminar cualquier
// actividad o tarea del proyecto -- ya no importa si es el "responsable" asignado o no (esa
// regla vieja, previa a tener niveles de permiso reales por proyecto, quedo obsoleta).
export async function canWriteActivity(user, project) {
return canWriteProject(user, project);
}
export const listByProject = (projectId) => repo.findByProject(projectId);
export async function listByProject(projectId, currentUser) {
const project = await getProjectOr404(projectId);
if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Proyecto no encontrado');
return repo.findByProject(projectId);
}
export async function getActivity(id) {
export async function listTrash(projectId, currentUser) {
const project = await getProjectOr404(projectId);
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
return repo.findTrashByProject(projectId);
}
export async function getActivity(id, currentUser) {
const activity = await repo.findById(id);
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
const project = await getProjectOr404(activity.project_id);
if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Actividad no encontrada');
const dependencies = await repo.listDependencies(id);
return { ...activity, dependencies };
}
@@ -101,7 +111,8 @@ export async function updateActivity(id, payload, currentUser) {
// Bloquea el borrado si hay tareas pendientes (a proposito: evita perder de vista trabajo real
// hecho/por hacer solo porque alguien borro la actividad de un clic). El usuario debe vaciar el
// checklist primero. Comentarios/adjuntos si se limpian solos, no llevan informacion de progreso.
// checklist primero. Es eliminacion logica (recuperable, ver restoreActivity) -- comentarios y
// adjuntos NO se tocan, quedan intactos por si se restaura despues.
export async function removeActivity(id, currentUser) {
const activity = await repo.findById(id);
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
@@ -113,13 +124,21 @@ export async function removeActivity(id, currentUser) {
if (taskCount > 0) {
throw new HttpError(400, `Esta actividad tiene ${taskCount} tarea(s). Elimina primero las tareas antes de borrar la actividad.`);
}
await commentsRepo.removeByEntity('activity', id);
await attachmentsService.removeAllForEntity('activity', id);
await repo.remove(id);
await repo.softDelete(id, currentUser.id);
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'activity', entityId: id });
await recalcProjectProgress(activity.project_id);
}
export async function restoreActivity(id, currentUser) {
const activity = await repo.findById(id, { includeDeleted: true });
if (!activity || !activity.deleted_at) throw new HttpError(404, 'Actividad no encontrada en la papelera');
const project = await getProjectOr404(activity.project_id);
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
await repo.restore(id);
await logAudit({ userId: currentUser.id, action: 'restore', entityType: 'activity', entityId: id });
await recalcProjectProgress(activity.project_id);
}
// Mueve la actividad (y sus tareas/comentarios/adjuntos, que van solos por referenciar activity_id
// en vez de project_id) a otro proyecto. Exige permiso de escritura en AMBOS proyectos.
export async function moveActivity(id, targetProjectId, currentUser) {