feat: administradores por proyecto y Configuracion solo para superadmin

- Un proyecto puede tener uno o mas administradores (role_in_project=
  'admin' en project_members), independiente del rol global del usuario.
  Nuevo PUT /api/projects/:id/members/:userId/role; canWriteProject lo
  reconoce ademas del comportamiento previo (admin/superadmin/it_manager
  global, o project_lead miembro). UI: casilla al agregar miembro + boton
  de escudo para promover/degradar (nunca toca al Dueño).
- Menu "Configuracion" (apariencia, catalogos, SMTP) visible y accesible
  solo para el superadmin: oculto en el nav y con guarda en la ruta
  /configuracion para quien la teclee directo. Escritura de catalogos
  restringida de [superadmin, admin] a solo superadmin para que el
  backend sea consistente con la UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-09 09:41:06 -05:00
parent e018eba8c1
commit f96391d360
16 changed files with 144 additions and 28 deletions
+21 -4
View File
@@ -3,12 +3,17 @@ import { HttpError } from '../../utils/http.js';
import { logAudit } from '../../utils/audit.js';
import { ROLES } from '../../middleware/auth.js';
// Alcance de escritura: admin/it_manager siempre; project_lead solo si es dueño o miembro.
// 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;
if (user.roleCode !== ROLES.PROJECT_LEAD) return false;
if (project.owner_user_id === user.id) return true;
return repo.isMember(project.id, user.id);
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).
@@ -98,6 +103,18 @@ export async function addMember(projectId, userId, roleInProject, currentUser) {
return members;
}
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');