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:
@@ -54,7 +54,7 @@ export const ROLES = {
|
||||
VIEWER: 'viewer',
|
||||
};
|
||||
|
||||
export const CAN_MANAGE_CATALOGS = [ROLES.SUPERADMIN, ROLES.ADMIN];
|
||||
export const CAN_MANAGE_CATALOGS = [ROLES.SUPERADMIN];
|
||||
export const CAN_MANAGE_USERS = [ROLES.SUPERADMIN, ROLES.ADMIN];
|
||||
export const CAN_MANAGE_SMTP = [ROLES.SUPERADMIN];
|
||||
export const CAN_WRITE = [ROLES.SUPERADMIN, ROLES.ADMIN, ROLES.IT_MANAGER, ROLES.PROJECT_LEAD, ROLES.COLLABORATOR];
|
||||
|
||||
@@ -38,6 +38,11 @@ export const postMember = asyncHandler(async (req, res) => {
|
||||
ok(res, members, 201);
|
||||
});
|
||||
|
||||
export const putMemberRole = asyncHandler(async (req, res) => {
|
||||
const members = await service.setMemberAdmin(Number(req.params.id), Number(req.params.userId), !!req.body.admin, req.currentUser);
|
||||
ok(res, members);
|
||||
});
|
||||
|
||||
export const deleteMember = asyncHandler(async (req, res) => {
|
||||
const members = await service.removeMember(Number(req.params.id), Number(req.params.userId), req.currentUser);
|
||||
ok(res, members);
|
||||
|
||||
@@ -111,6 +111,15 @@ export async function isMember(projectId, userId) {
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
// null = no es miembro; 'member' = miembro sin rol especifico; 'admin'/'owner' = con ese rol.
|
||||
export async function getMembership(projectId, userId) {
|
||||
const [rows] = await pool.query('SELECT role_in_project FROM project_members WHERE project_id = ? AND user_id = ?', [
|
||||
projectId, userId,
|
||||
]);
|
||||
if (!rows.length) return null;
|
||||
return rows[0].role_in_project || 'member';
|
||||
}
|
||||
|
||||
export async function addMember(projectId, userId, roleInProject) {
|
||||
await pool.query(
|
||||
'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (?, ?, ?)',
|
||||
@@ -119,6 +128,16 @@ export async function addMember(projectId, userId, roleInProject) {
|
||||
return listMembers(projectId);
|
||||
}
|
||||
|
||||
// Nunca toca una fila con role_in_project = 'owner' (protege al dueño de perder ese rol por aqui).
|
||||
export async function updateMemberRole(projectId, userId, roleInProject) {
|
||||
await pool.query(
|
||||
`UPDATE project_members SET role_in_project = ?
|
||||
WHERE project_id = ? AND user_id = ? AND (role_in_project IS NULL OR role_in_project <> 'owner')`,
|
||||
[roleInProject, projectId, userId]
|
||||
);
|
||||
return listMembers(projectId);
|
||||
}
|
||||
|
||||
export async function removeMember(projectId, userId) {
|
||||
await pool.query('DELETE FROM project_members WHERE project_id = ? AND user_id = ?', [projectId, userId]);
|
||||
return listMembers(projectId);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import {
|
||||
getProjects, getProject, postProject, putProject, deleteProject, postProjectRestore,
|
||||
deleteProjectPermanent, postMember, deleteMember,
|
||||
deleteProjectPermanent, postMember, putMemberRole, deleteMember,
|
||||
} from './controller.js';
|
||||
import { getGantt } from '../dashboard/controller.js';
|
||||
import { getActivitiesByProject, postActivity } from '../activities/controller.js';
|
||||
@@ -19,6 +19,7 @@ router.post('/:id/restore', postProjectRestore);
|
||||
router.delete('/:id/permanent', deleteProjectPermanent);
|
||||
router.get('/:id/gantt', getGantt);
|
||||
router.post('/:id/members', postMember);
|
||||
router.put('/:id/members/:userId/role', putMemberRole);
|
||||
router.delete('/:id/members/:userId', deleteMember);
|
||||
router.get('/:projectId/activities', getActivitiesByProject);
|
||||
router.post('/:projectId/activities', postActivity);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user