d7478d0f21
Al agregar un miembro, el popup ahora permite elegir un usuario existente o escribir un correo nuevo. Si ese correo no pertenece a nadie, se crea una cuenta en estado 'invited' (nombre/clave placeholder) y se le manda un correo con un enlace de un solo uso para que complete su registro (elige su nombre y clave). La fila de project_members se crea de una vez al invitar, asi que en cuanto acepta ya es miembro sin pasos adicionales. A diferencia del autorregistro publico, no pasa por aprobacion de un admin -- quien lo agrego a su proyecto ya lo vetó. Nuevos endpoints publicos GET /api/auth/invite y POST /api/auth/accept-invite. Nueva pagina AcceptInvitePage (/aceptar-invitacion). Migracion 004 agrega el status 'invited' al enum de users. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
144 lines
7.5 KiB
JavaScript
144 lines
7.5 KiB
JavaScript
import * as repo from './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 membership = await repo.getMembership(project.id, user.id);
|
|
if (membership === 'owner' || membership === 'admin') return true;
|
|
if (user.roleCode === ROLES.PROJECT_LEAD && membership !== null) return true;
|
|
return false;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
export async function getProject(id) {
|
|
const project = await repo.findById(id);
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
const members = await repo.listMembers(id);
|
|
return { ...project, members };
|
|
}
|
|
|
|
export async function createProject(payload, currentUser) {
|
|
if (!payload.name || !payload.priorityId || !payload.statusId) {
|
|
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
|
|
}
|
|
if (![ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER, ROLES.PROJECT_LEAD].includes(currentUser.roleCode)) {
|
|
throw new HttpError(403, 'No tienes permisos para crear proyectos');
|
|
}
|
|
const project = await repo.create({
|
|
...payload,
|
|
ownerUserId: payload.ownerUserId || currentUser.id,
|
|
createdBy: currentUser.id,
|
|
});
|
|
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'project', entityId: project.id });
|
|
return project;
|
|
}
|
|
|
|
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 (!payload.name || !payload.priorityId || !payload.statusId) {
|
|
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
|
|
}
|
|
const updated = await repo.update(id, { ...payload, ownerUserId: payload.ownerUserId || project.owner_user_id });
|
|
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'project', entityId: id });
|
|
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.
|
|
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');
|
|
await repo.softDelete(id, currentUser.id);
|
|
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'project', entityId: id });
|
|
}
|
|
|
|
export async function restoreProject(id, currentUser) {
|
|
if (currentUser.roleCode !== ROLES.SUPERADMIN) throw new HttpError(403, 'Solo el superadministrador puede restaurar proyectos');
|
|
const project = await repo.findById(id, { includeDeleted: true });
|
|
if (!project || !project.deleted_at) throw new HttpError(404, 'Proyecto no encontrado en la papelera');
|
|
await repo.restore(id);
|
|
await logAudit({ userId: currentUser.id, action: 'restore', entityType: 'project', entityId: id });
|
|
}
|
|
|
|
// Borrado fisico e irreversible (arrastra actividades/comentarios/adjuntos por ON DELETE CASCADE).
|
|
// Solo el superadmin, y solo sobre un proyecto que ya paso por eliminacion logica primero.
|
|
export async function permanentlyDeleteProject(id, currentUser) {
|
|
if (currentUser.roleCode !== ROLES.SUPERADMIN) throw new HttpError(403, 'Solo el superadministrador puede eliminar proyectos definitivamente');
|
|
const project = await repo.findById(id, { includeDeleted: true });
|
|
if (!project) throw new HttpError(404, 'Proyecto no encontrado');
|
|
if (!project.deleted_at) throw new HttpError(400, 'El proyecto debe eliminarse primero antes de poder borrarlo definitivamente');
|
|
await repo.hardDelete(id);
|
|
await logAudit({ userId: currentUser.id, action: 'permanent_delete', entityType: 'project', entityId: id });
|
|
}
|
|
|
|
// Recalcula el avance del proyecto como promedio del avance de sus actividades.
|
|
export async function recalcProjectProgress(projectId) {
|
|
const avg = await repo.averageActivityProgress(projectId);
|
|
await repo.updateProgress(projectId, Math.round(avg * 100) / 100);
|
|
}
|
|
|
|
// Agrega por id de usuario existente O por correo. Si el correo no pertenece a nadie todavia,
|
|
// 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.
|
|
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');
|
|
|
|
let targetUserId = userId;
|
|
let invited = false;
|
|
if (!targetUserId) {
|
|
if (!email) throw new HttpError(400, 'Indica un usuario existente o un correo');
|
|
const result = await usersService.inviteUser(email, currentUser.id);
|
|
targetUserId = result.userId;
|
|
invited = result.invited;
|
|
}
|
|
|
|
const members = await repo.addMember(projectId, targetUserId, roleInProject);
|
|
await logAudit({
|
|
userId: currentUser.id, action: 'add_member', entityType: 'project', entityId: projectId,
|
|
detail: { userId: targetUserId, invited },
|
|
});
|
|
return { members, invited };
|
|
}
|
|
|
|
export async function setMemberAdmin(projectId, userId, isAdmin, 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);
|
|
await logAudit({
|
|
userId: currentUser.id, action: isAdmin ? 'grant_project_admin' : 'revoke_project_admin',
|
|
entityType: 'project', entityId: projectId, detail: { userId },
|
|
});
|
|
return members;
|
|
}
|
|
|
|
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');
|
|
const members = await repo.removeMember(projectId, userId);
|
|
await logAudit({ userId: currentUser.id, action: 'remove_member', entityType: 'project', entityId: projectId, detail: { userId } });
|
|
return members;
|
|
}
|