feat: version 3.1.0 -- Acceso por modulo y por usuario
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
|
||||
import { env } from '../config/env.js';
|
||||
import { pool } from '../config/db.js';
|
||||
import { fail } from '../utils/http.js';
|
||||
import { loadModules } from './modules.js';
|
||||
|
||||
export async function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
@@ -31,6 +32,7 @@ export async function requireAuth(req, res, next) {
|
||||
email: user.email,
|
||||
roleCode: user.role_code,
|
||||
departmentId: user.department_id,
|
||||
modules: await loadModules(user.id, user.role_code),
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { pool } from '../config/db.js';
|
||||
import { fail } from '../utils/http.js';
|
||||
|
||||
// Modulos que el superadmin puede habilitar/deshabilitar por usuario. Panel y Configuracion no
|
||||
// estan aqui: el Panel lo ve todo usuario y Configuracion sigue siendo solo de superadmin.
|
||||
export const MODULES = ['projects', 'recurring_tasks', 'notes', 'vault', 'devices', 'organization'];
|
||||
|
||||
// Sin fila en user_modules = habilitado. El superadmin siempre tiene todo.
|
||||
export async function loadModules(userId, roleCode) {
|
||||
if (roleCode === 'superadmin') return [...MODULES];
|
||||
const [rows] = await pool.query('SELECT module FROM user_modules WHERE user_id = ? AND enabled = 0', [userId]);
|
||||
const disabled = new Set(rows.map((r) => r.module));
|
||||
return MODULES.filter((m) => !disabled.has(m));
|
||||
}
|
||||
|
||||
export function hasModule(user, module) {
|
||||
return !!user?.modules?.includes(module);
|
||||
}
|
||||
|
||||
// Va despues de requireAuth (que carga currentUser.modules).
|
||||
export function requireModule(module) {
|
||||
return (req, res, next) => {
|
||||
if (!req.currentUser) return fail(res, 401, 'No autenticado');
|
||||
if (!hasModule(req.currentUser, module)) return fail(res, 403, 'No tienes acceso a este modulo');
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { getActivity, putActivity, deleteActivity, postActivityRestore, putMoveActivity, postConvertToProject, postDependency, deleteDependency } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('projects'));
|
||||
router.get('/:id', getActivity);
|
||||
router.put('/:id', putActivity);
|
||||
router.delete('/:id', deleteActivity);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pool } from '../../config/db.js';
|
||||
import { env } from '../../config/env.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
import { loadModules } from '../../middleware/modules.js';
|
||||
import { sendMail } from '../../utils/mailer.js';
|
||||
import { createNotification } from '../notifications/service.js';
|
||||
import { verificationEmail, recoveryEmail } from '../../utils/emailHtml.js';
|
||||
@@ -49,6 +50,7 @@ export async function login(email, password) {
|
||||
roleCode: user.role_code,
|
||||
roleName: user.role_name,
|
||||
departmentId: user.department_id,
|
||||
modules: await loadModules(user.id, user.role_code),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -88,7 +90,7 @@ export async function register({ name, email, password }) {
|
||||
if (bootstrap) {
|
||||
await logAudit({ userId: result.insertId, action: 'register_bootstrap_superadmin', entityType: 'user', entityId: result.insertId });
|
||||
const token = jwt.sign({ sub: result.insertId, sv: 1 }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
return { bootstrap: true, token, user: { id: result.insertId, name, email, roleCode: 'superadmin', roleName: 'Super Administrador', departmentId: null } };
|
||||
return { bootstrap: true, token, user: { id: result.insertId, name, email, roleCode: 'superadmin', roleName: 'Super Administrador', departmentId: null, modules: await loadModules(result.insertId, 'superadmin') } };
|
||||
}
|
||||
await logAudit({ userId: result.insertId, action: 'register', entityType: 'user', entityId: result.insertId });
|
||||
|
||||
@@ -182,6 +184,7 @@ export async function acceptInvite(token, name, password) {
|
||||
user: {
|
||||
id: full[0].id, name: full[0].name, email: full[0].email,
|
||||
roleCode: full[0].role_code, roleName: full[0].role_name, departmentId: full[0].department_id,
|
||||
modules: await loadModules(full[0].id, full[0].role_code),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, CAN_MANAGE_USERS } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { getDepartments, postDepartment, putDepartment, deleteDepartment } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.get('/', getDepartments);
|
||||
router.post('/', requireRole(...CAN_MANAGE_USERS), postDepartment);
|
||||
router.put('/:id', requireRole(...CAN_MANAGE_USERS), putDepartment);
|
||||
router.delete('/:id', requireRole(...CAN_MANAGE_USERS), deleteDepartment);
|
||||
router.post('/', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postDepartment);
|
||||
router.put('/:id', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), putDepartment);
|
||||
router.delete('/:id', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), deleteDepartment);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, CAN_EDIT_DEVICES, CAN_MANAGE_DEVICES } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { fail } from '../../utils/http.js';
|
||||
import { getAccess as getAccessFor } from './service.js';
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ async function canView(req, res, next) {
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('devices'));
|
||||
router.get('/access', getAccess); // cualquier autenticado: el menu lo usa para decidir si se muestra
|
||||
router.get('/dashboard', canView, getDashboard);
|
||||
router.get('/software', canView, getSoftware);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import {
|
||||
getNotes, postNote, putNote, deleteNote, putReorderNotes, getNoteShares, postNoteShare, deleteNoteShare,
|
||||
} from './controller.js';
|
||||
@@ -9,6 +10,7 @@ const router = Router();
|
||||
// Sin restriccion de rol: cualquier usuario autenticado tiene sus propias notas (y ve las que
|
||||
// otros compartieron con el). Los permisos de compartir se resuelven en el service (dueño-only).
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('notes'));
|
||||
router.get('/', getNotes);
|
||||
router.post('/', postNote);
|
||||
// /reorder ANTES de /:id -- si no, Express lo confunde con un id (":id" matcheria "reorder").
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import {
|
||||
getProjects, getProjectOptions, getProject, postProject, putProject, deleteProject, postProjectRestore,
|
||||
deleteProjectPermanent, postMember, putMemberRole, deleteMember,
|
||||
@@ -10,6 +11,7 @@ import { getActivitiesByProject, getActivitiesTrash, postActivity, putReorderAct
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('projects'));
|
||||
router.get('/', getProjects);
|
||||
router.get('/options', getProjectOptions);
|
||||
router.post('/', postProject);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import {
|
||||
getRecurringTasks, postRecurringTask, putRecurringTask, deleteRecurringTask,
|
||||
postCompleteCycle, postPromoteFromTask,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('recurring_tasks'));
|
||||
router.get('/', getRecurringTasks);
|
||||
router.post('/', postRecurringTask);
|
||||
router.post('/from-task/:taskId', postPromoteFromTask);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as repo from './repository.js';
|
||||
import { getProject } from '../projects/service.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { hasModule } from '../../middleware/modules.js';
|
||||
|
||||
const MIN_LENGTH = 2;
|
||||
|
||||
@@ -10,7 +11,8 @@ export async function search(term, projectId, currentUser) {
|
||||
if (!Number.isSafeInteger(Number(projectId)) || Number(projectId) < 1) throw new HttpError(400, 'projectId invalido');
|
||||
await getProject(Number(projectId), currentUser);
|
||||
}
|
||||
if (q.length < MIN_LENGTH) {
|
||||
// Todo lo que busca es del modulo Proyectos: sin acceso, resultado vacio (no 403 en la barra global).
|
||||
if (q.length < MIN_LENGTH || !hasModule(currentUser, 'projects')) {
|
||||
return projectId ? { activities: [], tasks: [] } : { projects: [], activities: [], tasks: [] };
|
||||
}
|
||||
return projectId ? repo.searchInProject(Number(projectId), q) : repo.searchGlobal(q, currentUser);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { getTasks, getTasksTrash, postTask, putTask, putReorderTasks, patchTaskDone, deleteTask, postTaskRestore } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(['/tasks', '/activities'], requireModule('projects'));
|
||||
router.get('/activities/:activityId/tasks', getTasks);
|
||||
router.get('/activities/:activityId/tasks/trash', getTasksTrash);
|
||||
router.post('/activities/:activityId/tasks', postTask);
|
||||
|
||||
@@ -32,3 +32,9 @@ export const postRejectUser = asyncHandler(async (req, res) => {
|
||||
export const postResendUserEmail = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.resendUserEmail(Number(req.params.id), req.currentUser.id));
|
||||
});
|
||||
|
||||
export const getUserModules = asyncHandler(async (req, res) => ok(res, await service.getUserModules(Number(req.params.id))));
|
||||
|
||||
export const putUserModules = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.setUserModules(Number(req.params.id), req.body?.modules, req.currentUser));
|
||||
});
|
||||
|
||||
@@ -90,3 +90,13 @@ export async function listRoles() {
|
||||
const [rows] = await pool.query('SELECT id, code, name FROM roles ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
|
||||
// pairs: [[module, enabled], ...]. Se guardan todas las filas para que quede explicito.
|
||||
export async function saveModules(userId, pairs, actorId) {
|
||||
const values = pairs.map(([module, enabled]) => [userId, module, enabled ? 1 : 0, actorId]);
|
||||
await pool.query(
|
||||
`INSERT INTO user_modules (user_id, module, enabled, updated_by) VALUES ?
|
||||
ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), updated_by = VALUES(updated_by)`,
|
||||
[values]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth, requireRole, CAN_MANAGE_USERS } from '../../middleware/auth.js';
|
||||
import { requireAuth, requireRole, CAN_MANAGE_USERS, ROLES } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import {
|
||||
getUsers, getRoles, postUser, putUser, patchUserActive, postApproveUser, postRejectUser, postResendUserEmail,
|
||||
getUserModules, putUserModules,
|
||||
} from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -9,11 +11,13 @@ const router = Router();
|
||||
router.use(requireAuth);
|
||||
router.get('/', getUsers);
|
||||
router.get('/roles', getRoles);
|
||||
router.post('/', requireRole(...CAN_MANAGE_USERS), postUser);
|
||||
router.put('/:id', requireRole(...CAN_MANAGE_USERS), putUser);
|
||||
router.patch('/:id/active', requireRole(...CAN_MANAGE_USERS), patchUserActive);
|
||||
router.post('/:id/approve', requireRole(...CAN_MANAGE_USERS), postApproveUser);
|
||||
router.post('/:id/reject', requireRole(...CAN_MANAGE_USERS), postRejectUser);
|
||||
router.post('/:id/resend', requireRole(...CAN_MANAGE_USERS), postResendUserEmail);
|
||||
router.post('/', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postUser);
|
||||
router.put('/:id', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), putUser);
|
||||
router.patch('/:id/active', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), patchUserActive);
|
||||
router.post('/:id/approve', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postApproveUser);
|
||||
router.post('/:id/reject', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postRejectUser);
|
||||
router.get('/:id/modules', requireRole(ROLES.SUPERADMIN), getUserModules);
|
||||
router.put('/:id/modules', requireRole(ROLES.SUPERADMIN), putUserModules);
|
||||
router.post('/:id/resend', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postResendUserEmail);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as repo from './repository.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { requireEditVersion, requireUpdated } from '../../utils/editVersion.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
import { MODULES, loadModules } from '../../middleware/modules.js';
|
||||
import { sendMail } from '../../utils/mailer.js';
|
||||
import { env } from '../../config/env.js';
|
||||
import { invitationEmail, verificationEmail } from '../../utils/emailHtml.js';
|
||||
@@ -148,3 +149,21 @@ export async function rejectUser(id, actorId) {
|
||||
await logAudit({ userId: actorId, action: 'reject', entityType: 'user', entityId: id, detail: { email: existing.email, status: existing.status } });
|
||||
await repo.remove(id);
|
||||
}
|
||||
|
||||
// Acceso por modulo (solo superadmin). El superadmin siempre tiene todo y no se edita.
|
||||
export async function getUserModules(id) {
|
||||
const user = await repo.findById(id);
|
||||
if (!user) throw new HttpError(404, 'Usuario no encontrado');
|
||||
return { modules: await loadModules(id, user.role_code), all: MODULES, locked: user.role_code === 'superadmin' };
|
||||
}
|
||||
|
||||
export async function setUserModules(id, modules, actor) {
|
||||
const user = await repo.findById(id);
|
||||
if (!user) throw new HttpError(404, 'Usuario no encontrado');
|
||||
if (user.role_code === 'superadmin') throw new HttpError(400, 'El superadministrador siempre tiene todos los modulos');
|
||||
if (!Array.isArray(modules) || modules.some((m) => !MODULES.includes(m))) throw new HttpError(400, 'Modulos invalidos');
|
||||
const enabled = new Set(modules);
|
||||
await repo.saveModules(id, MODULES.map((m) => [m, enabled.has(m)]), actor.id);
|
||||
await logAudit({ userId: actor.id, action: 'update_modules', entityType: 'user', entityId: id, detail: { modules } });
|
||||
return getUserModules(id);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { requireVaultUnlock } from './middleware.js';
|
||||
import {
|
||||
getStatus, postPin, postUnlock, postChangePin, postForgotPin, postResetPin,
|
||||
@@ -18,6 +19,7 @@ const unlockLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 8, standardHe
|
||||
router.post('/reset-pin', unlockLimiter, postResetPin);
|
||||
|
||||
router.use(requireAuth);
|
||||
router.use(requireModule('vault'));
|
||||
router.get('/status', getStatus);
|
||||
router.post('/pin', postPin);
|
||||
router.post('/unlock', unlockLimiter, postUnlock);
|
||||
|
||||
Reference in New Issue
Block a user