From 35fe8db1f971a20626b508ebf2206d35d078ad8a Mon Sep 17 00:00:00 2001 From: Roberto Castellanos Date: Sun, 19 Oct 2025 22:12:18 -0600 Subject: [PATCH] Agregango archivos necesarios para auth --- src/config/permissions.js | 501 ++++++++++++++++++++++++++++++ src/middleware/checkPermission.js | 143 +++++++++ 2 files changed, 644 insertions(+) create mode 100644 src/config/permissions.js create mode 100644 src/middleware/checkPermission.js diff --git a/src/config/permissions.js b/src/config/permissions.js new file mode 100644 index 0000000..bdc7470 --- /dev/null +++ b/src/config/permissions.js @@ -0,0 +1,501 @@ +/** + * ======================================== + * CONFIGURACIÓN DE PERMISOS Y ROLES + * ======================================== + * + * Este archivo define: + * 1. Roles disponibles en el sistema + * 2. Permisos por rol + * 3. Rutas y recursos accesibles por rol + * 4. Metadata para construir menús en el frontend + * + * IMPORTANTE: Este archivo puede migrar a BD fácilmente + * manteniendo la misma estructura. + */ + +// ======================================== +// DEFINICIÓN DE ROLES +// ======================================== +const ROLES = { + ADMIN: 'admin', + USER: 'usuario', + GUEST: 'invitado' +}; + +// ======================================== +// DEFINICIÓN DE RECURSOS +// ======================================== +// Cada recurso representa un módulo del sistema +const RESOURCES = { + // Gestión de usuarios + USERS: 'users', + PROFILE: 'profile', + + // Catálogos + CATEGORIES: 'categories', + ACTIVITIES: 'activities', + BADGES: 'badges', + LEVELS: 'levels', + + // Usuario + GOALS: 'goals', + AGENDA: 'agenda', + ACTIVITY_LOG: 'activity_log', + NOTIFICATIONS: 'notifications', + + // Métricas + USER_METRICS: 'user_metrics', + ADMIN_METRICS: 'admin_metrics', + + // Relaciones + USER_BADGES: 'user_badges', + USER_ROLES: 'user_roles' +}; + +// ======================================== +// DEFINICIÓN DE ACCIONES +// ======================================== +const ACTIONS = { + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + LIST: 'list', + MANAGE: 'manage' // Todas las acciones +}; + +// ======================================== +// PERMISOS POR ROL +// ======================================== +// Define qué puede hacer cada rol en cada recurso +const PERMISSIONS = { + [ROLES.ADMIN]: { + // El admin tiene acceso total a TODO + [RESOURCES.USERS]: [ACTIONS.MANAGE], + [RESOURCES.PROFILE]: [ACTIONS.MANAGE], + [RESOURCES.CATEGORIES]: [ACTIONS.MANAGE], + [RESOURCES.ACTIVITIES]: [ACTIONS.MANAGE], + [RESOURCES.BADGES]: [ACTIONS.MANAGE], + [RESOURCES.LEVELS]: [ACTIONS.MANAGE], + [RESOURCES.GOALS]: [ACTIONS.MANAGE], + [RESOURCES.AGENDA]: [ACTIONS.MANAGE], + [RESOURCES.ACTIVITY_LOG]: [ACTIONS.MANAGE], + [RESOURCES.NOTIFICATIONS]: [ACTIONS.MANAGE], + [RESOURCES.USER_METRICS]: [ACTIONS.MANAGE], + [RESOURCES.ADMIN_METRICS]: [ACTIONS.MANAGE], + [RESOURCES.USER_BADGES]: [ACTIONS.MANAGE], + [RESOURCES.USER_ROLES]: [ACTIONS.MANAGE] + }, + + [ROLES.USER]: { + // El usuario normal tiene acceso limitado + [RESOURCES.PROFILE]: [ACTIONS.READ, ACTIONS.UPDATE], + + // Puede VER catálogos pero no modificarlos + [RESOURCES.CATEGORIES]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.ACTIVITIES]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.BADGES]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.LEVELS]: [ACTIONS.READ, ACTIONS.LIST], + + // Puede gestionar SUS PROPIOS recursos + [RESOURCES.GOALS]: [ACTIONS.CREATE, ACTIONS.READ, ACTIONS.UPDATE, ACTIONS.DELETE, ACTIONS.LIST], + [RESOURCES.AGENDA]: [ACTIONS.CREATE, ACTIONS.READ, ACTIONS.UPDATE, ACTIONS.DELETE, ACTIONS.LIST], + [RESOURCES.ACTIVITY_LOG]: [ACTIONS.CREATE, ACTIONS.READ, ACTIONS.UPDATE, ACTIONS.DELETE, ACTIONS.LIST], + [RESOURCES.NOTIFICATIONS]: [ACTIONS.READ, ACTIONS.LIST, ACTIONS.UPDATE], + + // Puede ver SUS PROPIAS métricas + [RESOURCES.USER_METRICS]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.USER_BADGES]: [ACTIONS.READ, ACTIONS.LIST], + + // NO puede ver métricas de admin + [RESOURCES.ADMIN_METRICS]: [], + [RESOURCES.USER_ROLES]: [] + }, + + [ROLES.GUEST]: { + // Invitado solo puede ver catálogos públicos + [RESOURCES.CATEGORIES]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.ACTIVITIES]: [ACTIONS.READ, ACTIONS.LIST], + [RESOURCES.BADGES]: [ACTIONS.READ, ACTIONS.LIST] + } +}; + +// ======================================== +// CONFIGURACIÓN DE RUTAS +// ======================================== +// Define las rutas del sistema con metadata para menús +const ROUTE_CONFIG = [ + // ========== PERFIL ========== + { + resource: RESOURCES.PROFILE, + path: '/api/auth/me', + method: 'GET', + requiresAuth: true, + action: ACTIONS.READ, + metadata: { + label: 'Mi Perfil', + icon: 'user', + showInMenu: true, + group: 'Cuenta', + order: 1 + } + }, + { + resource: RESOURCES.PROFILE, + path: '/api/auth/me/profile', + method: 'PUT', + requiresAuth: true, + action: ACTIONS.UPDATE, + metadata: { + showInMenu: false + } + }, + + // ========== CATÁLOGOS ========== + { + resource: RESOURCES.CATEGORIES, + path: '/api/categorias', + method: 'GET', + requiresAuth: false, + action: ACTIONS.LIST, + metadata: { + label: 'Categorías', + icon: 'folder', + showInMenu: true, + group: 'Catálogos', + order: 10, + roles: [ROLES.ADMIN, ROLES.USER] + } + }, + { + resource: RESOURCES.CATEGORIES, + path: '/api/categorias', + method: 'POST', + requiresAuth: true, + action: ACTIONS.CREATE, + metadata: { + showInMenu: false, + roles: [ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.ACTIVITIES, + path: '/api/actividades', + method: 'GET', + requiresAuth: false, + action: ACTIONS.LIST, + metadata: { + label: 'Actividades', + icon: 'activity', + showInMenu: true, + group: 'Catálogos', + order: 11, + roles: [ROLES.ADMIN, ROLES.USER] + } + }, + { + resource: RESOURCES.ACTIVITIES, + path: '/api/actividades', + method: 'POST', + requiresAuth: true, + action: ACTIONS.CREATE, + metadata: { + showInMenu: false, + roles: [ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.BADGES, + path: '/api/insignias', + method: 'GET', + requiresAuth: false, + action: ACTIONS.LIST, + metadata: { + label: 'Insignias', + icon: 'award', + showInMenu: true, + group: 'Catálogos', + order: 12, + roles: [ROLES.ADMIN, ROLES.USER] + } + }, + + { + resource: RESOURCES.LEVELS, + path: '/api/niveles', + method: 'GET', + requiresAuth: false, + action: ACTIONS.LIST, + metadata: { + label: 'Niveles', + icon: 'trending-up', + showInMenu: true, + group: 'Catálogos', + order: 13, + roles: [ROLES.ADMIN, ROLES.USER] + } + }, + + // ========== MIS RECURSOS ========== + { + resource: RESOURCES.GOALS, + path: '/api/metas', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Mis Metas', + icon: 'target', + showInMenu: true, + group: 'Mi Progreso', + order: 20, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + { + resource: RESOURCES.GOALS, + path: '/api/metas', + method: 'POST', + requiresAuth: true, + action: ACTIONS.CREATE, + metadata: { + showInMenu: false + } + }, + + { + resource: RESOURCES.AGENDA, + path: '/api/agenda', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Mi Agenda', + icon: 'calendar', + showInMenu: true, + group: 'Mi Progreso', + order: 21, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.ACTIVITY_LOG, + path: '/api/registroactividad', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Mis Actividades', + icon: 'list', + showInMenu: true, + group: 'Mi Progreso', + order: 22, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.NOTIFICATIONS, + path: '/api/notificaciones', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Notificaciones', + icon: 'bell', + showInMenu: true, + group: 'Mi Progreso', + order: 23, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + + // ========== MÉTRICAS ========== + { + resource: RESOURCES.USER_METRICS, + path: '/api/metricas', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Mis Estadísticas', + icon: 'bar-chart', + showInMenu: true, + group: 'Estadísticas', + order: 30, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.USER_BADGES, + path: '/api/usuario-insignias', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Mis Insignias', + icon: 'award', + showInMenu: true, + group: 'Estadísticas', + order: 31, + roles: [ROLES.USER, ROLES.ADMIN] + } + }, + + // ========== ADMIN ========== + { + resource: RESOURCES.ADMIN_METRICS, + path: '/api/admin/metrics', + method: 'GET', + requiresAuth: true, + action: ACTIONS.READ, + metadata: { + label: 'Métricas del Sistema', + icon: 'pie-chart', + showInMenu: true, + group: 'Administración', + order: 100, + roles: [ROLES.ADMIN] + } + }, + + { + resource: RESOURCES.USER_ROLES, + path: '/api/usuario-roles', + method: 'GET', + requiresAuth: true, + action: ACTIONS.LIST, + metadata: { + label: 'Gestión de Roles', + icon: 'shield', + showInMenu: true, + group: 'Administración', + order: 101, + roles: [ROLES.ADMIN] + } + } +]; + +// ======================================== +// FUNCIONES DE AYUDA +// ======================================== + +/** + * Verifica si un rol tiene permiso para una acción en un recurso + * @param {string} role - El rol a verificar + * @param {string} resource - El recurso + * @param {string} action - La acción + * @returns {boolean} + */ +function hasPermission(role, resource, action) { + const rolePermissions = PERMISSIONS[role]; + if (!rolePermissions) return false; + + const resourcePermissions = rolePermissions[resource]; + if (!resourcePermissions) return false; + + // Si tiene permiso MANAGE, tiene todos los permisos + if (resourcePermissions.includes(ACTIONS.MANAGE)) return true; + + // Sino, verificar la acción específica + return resourcePermissions.includes(action); +} + +/** + * Obtiene todas las rutas accesibles para un rol + * @param {string} role - El rol + * @param {boolean} onlyMenuItems - Si solo devolver rutas que se muestran en menú + * @returns {Array} + */ +function getRoutesForRole(role, onlyMenuItems = false) { + return ROUTE_CONFIG.filter(route => { + // Verificar si tiene permiso para esta ruta + const hasAccess = hasPermission(role, route.resource, route.action); + if (!hasAccess) return false; + + // Si solo queremos items de menú + if (onlyMenuItems && !route.metadata.showInMenu) return false; + + // Verificar si la ruta especifica roles permitidos + if (route.metadata.roles && !route.metadata.roles.includes(role)) { + return false; + } + + return true; + }); +} + +/** + * Construye el menú para un rol específico + * @param {string} role - El rol + * @returns {Object} - Menú agrupado + */ +function buildMenuForRole(role) { + const routes = getRoutesForRole(role, true); + + // Agrupar por grupo + const grouped = {}; + routes.forEach(route => { + const group = route.metadata.group || 'Otros'; + if (!grouped[group]) { + grouped[group] = []; + } + grouped[group].push({ + label: route.metadata.label, + icon: route.metadata.icon, + path: route.path, + method: route.method, + order: route.metadata.order || 999 + }); + }); + + // Ordenar items dentro de cada grupo + Object.keys(grouped).forEach(group => { + grouped[group].sort((a, b) => a.order - b.order); + }); + + return grouped; +} + +/** + * Obtiene todos los permisos de un rol + * @param {string} role - El rol + * @returns {Object} + */ +function getPermissionsForRole(role) { + return PERMISSIONS[role] || {}; +} + +/** + * Verifica si un usuario (con sus roles) puede acceder a una ruta + * @param {Array} userRoles - Array de roles del usuario + * @param {string} resource - Recurso + * @param {string} action - Acción + * @returns {boolean} + */ +function canAccess(userRoles, resource, action) { + if (!Array.isArray(userRoles) || userRoles.length === 0) return false; + + // Si tiene al menos un rol con permiso, puede acceder + return userRoles.some(role => hasPermission(role, resource, action)); +} + +// ======================================== +// EXPORTACIÓN +// ======================================== +module.exports = { + ROLES, + RESOURCES, + ACTIONS, + PERMISSIONS, + ROUTE_CONFIG, + hasPermission, + getRoutesForRole, + buildMenuForRole, + getPermissionsForRole, + canAccess +}; diff --git a/src/middleware/checkPermission.js b/src/middleware/checkPermission.js new file mode 100644 index 0000000..dcb91c6 --- /dev/null +++ b/src/middleware/checkPermission.js @@ -0,0 +1,143 @@ +/** + * ======================================== + * MIDDLEWARE DE VERIFICACIÓN DE PERMISOS + * ======================================== + * + * Este middleware verifica si el usuario autenticado tiene + * permisos para acceder a un recurso con una acción específica. + * + * FLUJO: + * 1. requireAuth → Verifica token y adjunta req.user + * 2. checkPermission → Verifica permisos del usuario + * 3. Ruta → Se ejecuta solo si tiene permiso + * + * USO: + * router.get('/metas', + * requireAuth, + * checkPermission(RESOURCES.GOALS, ACTIONS.LIST), + * async (req, res) => { ... } + * ); + */ + +const { canAccess } = require('../config/permissions'); + +/** + * ======================================== + * checkPermission + * ======================================== + * Factory function que retorna un middleware de verificación de permisos + * + * @param {string} resource - El recurso a verificar (ej: 'goals') + * @param {string} action - La acción a verificar (ej: 'read', 'create') + * @returns {Function} Middleware de Express + * + * EJEMPLO: + * const { RESOURCES, ACTIONS } = require('../config/permissions'); + * const checkPermission = require('../middleware/checkPermission'); + * + * router.post('/metas', + * requireAuth, + * checkPermission(RESOURCES.GOALS, ACTIONS.CREATE), + * async (req, res) => { + * // Solo llega aquí si tiene permiso + * } + * ); + */ +function checkPermission(resource, action) { + return (req, res, next) => { + // 1. Verificar que el usuario esté autenticado + if (!req.user) { + return res.status(401).json({ + ok: false, + error: 'No autenticado. Este middleware requiere requireAuth antes.' + }); + } + + // 2. Obtener roles del usuario + // Los roles deben venir en req.user.roles (array) + const userRoles = req.user.roles || []; + + // 3. Log para debugging + console.log(`[PERMISOS] Usuario ${req.user.id_usuario} intenta ${action} en ${resource}`); + console.log(`[PERMISOS] Roles del usuario:`, userRoles); + + // 4. Verificar si tiene permiso + const hasAccess = canAccess(userRoles, resource, action); + + if (!hasAccess) { + console.log(`[PERMISOS] ❌ ACCESO DENEGADO`); + return res.status(403).json({ + ok: false, + error: 'No tienes permisos para realizar esta acción', + details: { + resource, + action, + yourRoles: userRoles + } + }); + } + + console.log(`[PERMISOS] ✅ ACCESO PERMITIDO`); + + // 5. Tiene permiso, continuar + next(); + }; +} + +/** + * ======================================== + * requireRole + * ======================================== + * Middleware simplificado que solo verifica si el usuario tiene un rol específico + * + * @param {string|Array} allowedRoles - Rol o roles permitidos + * @returns {Function} Middleware de Express + * + * EJEMPLO: + * router.get('/admin/metrics', + * requireAuth, + * requireRole('admin'), + * async (req, res) => { ... } + * ); + */ +function requireRole(allowedRoles) { + // Normalizar a array + const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles]; + + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + ok: false, + error: 'No autenticado' + }); + } + + const userRoles = req.user.roles || []; + + // Verificar si tiene al menos uno de los roles permitidos + const hasRole = userRoles.some(role => roles.includes(role)); + + if (!hasRole) { + return res.status(403).json({ + ok: false, + error: 'No tienes el rol necesario para acceder', + details: { + requiredRoles: roles, + yourRoles: userRoles + } + }); + } + + next(); + }; +} + +/** + * ======================================== + * EXPORTACIÓN + * ======================================== + */ +module.exports = { + checkPermission, + requireRole +};