|
|
|
@@ -1,35 +1,83 @@
|
|
|
|
|
import * as repo from './repository.js';
|
|
|
|
|
import * as activitiesRepo from '../activities/repository.js';
|
|
|
|
|
import * as usersService from '../users/service.js';
|
|
|
|
|
import { HttpError } from '../../utils/http.js';
|
|
|
|
|
import { logAudit } from '../../utils/audit.js';
|
|
|
|
|
import { ROLES } from '../../middleware/auth.js';
|
|
|
|
|
|
|
|
|
|
// Alcance de escritura: admin/superadmin/it_manager siempre; un proyecto puede tener uno o mas
|
|
|
|
|
// "administradores del proyecto" (role_in_project = 'owner'/'admin' en project_members) con acceso
|
|
|
|
|
// total sin importar su rol global — un collaborator puede ser admin de UN proyecto puntual;
|
|
|
|
|
// ademas, por compatibilidad, cualquier project_lead que sea miembro (con o sin ese tag) tambien
|
|
|
|
|
// puede escribir, igual que antes de introducir el rol de administrador por proyecto.
|
|
|
|
|
export async function canWriteProject(user, project) {
|
|
|
|
|
if ([ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER].includes(user.roleCode)) return true;
|
|
|
|
|
const FULL_ACCESS_ROLES = [ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER];
|
|
|
|
|
|
|
|
|
|
// Nadie ve ni toca el proyecto de otro dueño sin que se lo compartan -- salvo los roles
|
|
|
|
|
// globales de administracion, que siguen viendo todo para poder dar soporte (acordado con el
|
|
|
|
|
// usuario). "Compartido" = tiene una fila en project_members, sin importar el nivel.
|
|
|
|
|
export async function canReadProject(user, project) {
|
|
|
|
|
if (FULL_ACCESS_ROLES.includes(user.roleCode)) return true;
|
|
|
|
|
const membership = await repo.getMembership(project.id, user.id);
|
|
|
|
|
if (membership === 'owner' || membership === 'admin') return true;
|
|
|
|
|
return membership !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Niveles simples, no granulares (acordado): Lectura (role_in_project NULL) o Escritura
|
|
|
|
|
// (role_in_project = 'write') -- crear/editar/eliminar(logico) actividades y tareas, nunca el
|
|
|
|
|
// proyecto en si. 'owner' es quien lo creo. 'admin' es un valor legado (de cuando existia un
|
|
|
|
|
// tercer nivel "administrador de proyecto") que se sigue respetando como escritura en filas
|
|
|
|
|
// viejas, pero ya no se asigna desde la UI.
|
|
|
|
|
export async function canWriteProject(user, project) {
|
|
|
|
|
if (FULL_ACCESS_ROLES.includes(user.roleCode)) return true;
|
|
|
|
|
const membership = await repo.getMembership(project.id, user.id);
|
|
|
|
|
if (['owner', 'admin', 'write'].includes(membership)) return true;
|
|
|
|
|
if (user.roleCode === ROLES.PROJECT_LEAD && membership !== null) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isProjectOwner(user, project) {
|
|
|
|
|
return project.owner_user_id === user.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compartir (agregar/quitar miembros, cambiar su nivel) es exclusivo del dueño original -- ni
|
|
|
|
|
// un admin del proyecto ni un rol global de administracion pueden compartir el proyecto de otra
|
|
|
|
|
// persona sin que el dueño lo autorice (acordado con el usuario).
|
|
|
|
|
export function canManageMembers(user, project) {
|
|
|
|
|
return isProjectOwner(user, project);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Borrar el PROYECTO (no su contenido) es exclusivo del dueño o de un rol global de
|
|
|
|
|
// administracion -- un colaborador con permiso de Escritura puede borrar actividades/tareas
|
|
|
|
|
// dentro del proyecto, pero nunca el proyecto en si (acordado con el usuario).
|
|
|
|
|
export function canDeleteProject(user, project) {
|
|
|
|
|
return FULL_ACCESS_ROLES.includes(user.roleCode) || isProjectOwner(user, project);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Solo el superadmin puede ver la papelera (proyectos eliminados logicamente).
|
|
|
|
|
export async function listProjects(filters, currentUser) {
|
|
|
|
|
if (filters.trash && currentUser?.roleCode !== ROLES.SUPERADMIN) {
|
|
|
|
|
throw new HttpError(403, 'Solo el superadministrador puede ver la papelera');
|
|
|
|
|
}
|
|
|
|
|
return repo.findAll(filters);
|
|
|
|
|
if (filters.trash || FULL_ACCESS_ROLES.includes(currentUser?.roleCode)) {
|
|
|
|
|
return repo.findAll(filters);
|
|
|
|
|
}
|
|
|
|
|
// El resto de roles solo ve proyectos donde es miembro (compartidos con el, o propios).
|
|
|
|
|
return repo.findAll({ ...filters, memberUserId: currentUser.id });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getProject(id) {
|
|
|
|
|
// Usado por comments/attachments, que cuelgan de 'project' o 'activity' (entity_type/entity_id
|
|
|
|
|
// generico, ver BASE_DE_DATOS.md) -- resuelve a que proyecto pertenece la entidad para poder
|
|
|
|
|
// aplicar el mismo chequeo de lectura/escritura por membresia en los dos modulos.
|
|
|
|
|
export async function resolveProjectForEntity(entityType, entityId) {
|
|
|
|
|
if (entityType === 'project') return repo.findById(entityId);
|
|
|
|
|
if (entityType === 'activity') {
|
|
|
|
|
const activity = await activitiesRepo.findById(entityId, { includeDeleted: true });
|
|
|
|
|
if (!activity) return null;
|
|
|
|
|
return repo.findById(activity.project_id);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getProject(id, currentUser) {
|
|
|
|
|
const project = await repo.findById(id);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canReadProject(currentUser, project))) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
const members = await repo.listMembers(id);
|
|
|
|
|
return { ...project, members };
|
|
|
|
|
return { ...project, members, isOwner: isProjectOwner(currentUser, project) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createProject(payload, currentUser) {
|
|
|
|
@@ -48,10 +96,13 @@ export async function createProject(payload, currentUser) {
|
|
|
|
|
return project;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Editar los datos del PROYECTO en si (nombre, fechas, prioridad, etc.) es exclusivo del dueño o
|
|
|
|
|
// de un rol global de administracion -- un colaborador compartido con Escritura solo puede
|
|
|
|
|
// crear/editar actividades y tareas, nunca el proyecto (acordado con el usuario).
|
|
|
|
|
export async function updateProject(id, payload, currentUser) {
|
|
|
|
|
const project = await repo.findById(id);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
if (!canDeleteProject(currentUser, project)) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
if (!payload.name || !payload.priorityId || !payload.statusId) {
|
|
|
|
|
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
|
|
|
|
|
}
|
|
|
|
@@ -60,12 +111,12 @@ export async function updateProject(id, payload, currentUser) {
|
|
|
|
|
return updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Eliminacion logica: cualquiera que pueda editar el proyecto puede "eliminarlo" (deja de verse
|
|
|
|
|
// en el listado, pero sigue en la BD). Solo el superadmin puede restaurarlo o borrarlo definitivo.
|
|
|
|
|
// Eliminacion logica: solo el dueño o un rol global de administracion. Solo el superadmin puede
|
|
|
|
|
// restaurarlo o borrarlo definitivo (sin cambios).
|
|
|
|
|
export async function deleteProject(id, currentUser) {
|
|
|
|
|
const project = await repo.findById(id);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
if (!canDeleteProject(currentUser, project)) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
await repo.softDelete(id, currentUser.id);
|
|
|
|
|
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'project', entityId: id });
|
|
|
|
|
}
|
|
|
|
@@ -99,10 +150,17 @@ export async function recalcProjectProgress(projectId) {
|
|
|
|
|
// crea una cuenta 'invited' y le manda un correo para que complete su propio registro (nombre y
|
|
|
|
|
// clave) — la fila de project_members ya queda creada de una vez, asi que en cuanto acepte la
|
|
|
|
|
// invitacion ya aparece como miembro sin pasos adicionales.
|
|
|
|
|
// role_in_project que llega del cliente: solo 'write' o null/undefined (Lectura) son validos
|
|
|
|
|
// para asignar de ahora en mas -- 'owner'/'admin' son valores que el propio backend controla.
|
|
|
|
|
function normalizeRoleInProject(role) {
|
|
|
|
|
return role === 'write' ? 'write' : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function addMember(projectId, { userId, email }, roleInProject, currentUser) {
|
|
|
|
|
const project = await repo.findById(projectId);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
if (!canManageMembers(currentUser, project)) throw new HttpError(403, 'Solo el dueño del proyecto puede compartirlo');
|
|
|
|
|
roleInProject = normalizeRoleInProject(roleInProject);
|
|
|
|
|
|
|
|
|
|
let targetUserId = userId;
|
|
|
|
|
let invited = false;
|
|
|
|
@@ -121,13 +179,14 @@ export async function addMember(projectId, { userId, email }, roleInProject, cur
|
|
|
|
|
return { members, invited };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function setMemberAdmin(projectId, userId, isAdmin, currentUser) {
|
|
|
|
|
export async function setMemberRole(projectId, userId, roleInProject, currentUser) {
|
|
|
|
|
const project = await repo.findById(projectId);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
const members = await repo.updateMemberRole(projectId, userId, isAdmin ? 'admin' : null);
|
|
|
|
|
if (!canManageMembers(currentUser, project)) throw new HttpError(403, 'Solo el dueño del proyecto puede cambiar permisos');
|
|
|
|
|
roleInProject = normalizeRoleInProject(roleInProject);
|
|
|
|
|
const members = await repo.updateMemberRole(projectId, userId, roleInProject);
|
|
|
|
|
await logAudit({
|
|
|
|
|
userId: currentUser.id, action: isAdmin ? 'grant_project_admin' : 'revoke_project_admin',
|
|
|
|
|
userId: currentUser.id, action: roleInProject === 'write' ? 'grant_project_write' : 'revoke_project_write',
|
|
|
|
|
entityType: 'project', entityId: projectId, detail: { userId },
|
|
|
|
|
});
|
|
|
|
|
return members;
|
|
|
|
@@ -136,7 +195,7 @@ export async function setMemberAdmin(projectId, userId, isAdmin, currentUser) {
|
|
|
|
|
export async function removeMember(projectId, userId, currentUser) {
|
|
|
|
|
const project = await repo.findById(projectId);
|
|
|
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
|
|
|
if (!(await canWriteProject(currentUser, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
|
|
|
|
if (!canManageMembers(currentUser, project)) throw new HttpError(403, 'Solo el dueño del proyecto puede quitar miembros');
|
|
|
|
|
const members = await repo.removeMember(projectId, userId);
|
|
|
|
|
await logAudit({ userId: currentUser.id, action: 'remove_member', entityType: 'project', entityId: projectId, detail: { userId } });
|
|
|
|
|
return members;
|
|
|
|
|