From 8641100a1859866b39d522af792aa5b2c4c15ba9 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Feb 2026 17:27:36 -0500 Subject: [PATCH] feat: per-user panel permissions system Replace hardcoded role-based access with granular per-panel permissions. Each user can now be assigned any combination of 6 panels (Corporate, BI Executive, Clientes, Providers, Usuarios, Meu Dashboard) regardless of their role. Existing users are auto-migrated with defaults based on role. - Add src/panels.js with panel registry and default permissions - Add permissions column to SQLite + migration for existing users - Add requirePermission() middleware, replace requireRole on all routes - Dynamic nav in buildHeader based on user permissions - Permissions checkbox UI in admin panel with role presets - Anti-lockout: users cannot remove 'usuarios' from themselves Co-Authored-By: Claude Opus 4.6 --- server.js | 148 ++++++++++++++++------------- src/admin-bi.js | 2 +- src/admin-cliente.js | 2 +- src/admin-dashboard.js | 2 +- src/admin-home.js | 2 +- src/admin-panel.js | 211 ++++++++++++++++++++++++++++++----------- src/admin-providers.js | 2 +- src/auth.js | 22 ++++- src/dashboard.js | 2 +- src/db-local.js | 15 +++ src/panels.js | 22 +++++ src/ui-template.js | 68 ++++++------- 12 files changed, 324 insertions(+), 174 deletions(-) create mode 100644 src/panels.js diff --git a/server.js b/server.js index c30e287..9cb0464 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,8 @@ require('dotenv').config(); const express = require('express'); const session = require('express-session'); const path = require('path'); -const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth'); +const { authenticate, requireAuth, requireRole, requirePermission, createAgente, createUser } = require('./src/auth'); +const { PANELS, PANEL_ROUTE } = require('./src/panels'); const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData, fetchProviderPerformance, fetchFailedTransactions, fetchProviderTrend } = require('./src/queries'); const { buildAdminProvidersHTML } = require('./src/admin-providers'); const pool = require('./src/db-rds'); @@ -47,17 +48,20 @@ app.use('/public', express.static(path.join(__dirname, 'public'))); // --- Unified Login Routes --- -// Helper function to get redirect URL based on role -function getRedirectByRole(role) { - if (role === 'admin') return '/corporate'; // Admin vai direto pro Corporate Dashboard - if (role === 'corporate') return '/corporate'; - return '/dashboard'; +// Helper function to get redirect URL based on user permissions +function getRedirectForUser(user) { + const perms = Array.isArray(user.permissions) ? user.permissions : JSON.parse(user.permissions || '[]'); + const priority = ['corporate', 'bi', 'cliente', 'providers', 'usuarios', 'dashboard']; + for (const key of priority) { + if (perms.includes(key)) return PANEL_ROUTE[key]; + } + return '/login'; } // Root -> login page (or redirect if logged in) app.get('/', (req, res) => { if (req.session?.user) { - return res.redirect(getRedirectByRole(req.session.user.role)); + return res.redirect(getRedirectForUser(req.session.user)); } res.redirect('/login'); }); @@ -65,7 +69,7 @@ app.get('/', (req, res) => { // Login page app.get('/login', (req, res) => { if (req.session?.user) { - return res.redirect(getRedirectByRole(req.session.user.role)); + return res.redirect(getRedirectForUser(req.session.user)); } res.sendFile(path.join(__dirname, 'public', 'login.html')); }); @@ -79,16 +83,18 @@ app.post('/login', async (req, res) => { if (!user) return res.redirect(`/login?error=1&email=${emailParam}`); // Unified session + const permissions = JSON.parse(user.permissions || '[]'); req.session.user = { id: user.id, email: user.email, nome: user.nome, role: user.role || 'agente', - agente_id: user.agente_id + agente_id: user.agente_id, + permissions }; - // Redirect based on role - res.redirect(getRedirectByRole(user.role)); + // Redirect based on permissions + res.redirect(getRedirectForUser(req.session.user)); } catch (err) { console.error('Login error:', err); res.redirect(`/login?error=1&email=${emailParam}`); @@ -112,8 +118,8 @@ app.get('/admin/logout', (req, res) => { // --- Agent Routes --- -// Dashboard (agente only) -app.get('/dashboard', requireRole('agente'), async (req, res) => { +// Dashboard (requires 'dashboard' permission) +app.get('/dashboard', requirePermission('dashboard'), async (req, res) => { try { const user = req.session.user; const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(user.agente_id); @@ -128,8 +134,8 @@ app.get('/dashboard', requireRole('agente'), async (req, res) => { // --- Admin Routes (User Management - admin only) --- -// Admin home - User management panel (admin only) -app.get('/admin', requireRole('admin'), (req, res) => { +// Admin home - User management panel +app.get('/admin', requirePermission('usuarios'), (req, res) => { try { const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all(); const html = buildAdminHTML(agentes, req.session.user); @@ -141,19 +147,19 @@ app.get('/admin', requireRole('admin'), (req, res) => { }); // Alias: /admin/usuarios -> /admin -app.get('/admin/usuarios', requireRole('admin'), (req, res) => { +app.get('/admin/usuarios', requirePermission('usuarios'), (req, res) => { res.redirect('/admin'); }); // Legacy route - redirect to /admin -app.get('/admin/agentes', requireRole('admin'), (req, res) => { +app.get('/admin/agentes', requirePermission('usuarios'), (req, res) => { res.redirect('/admin'); }); // --- Corporate Routes (Dashboard + Emulation - corporate and admin) --- // Corporate Dashboard - Full KPIs, Trends and Ranking -app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate', requirePermission('corporate'), async (req, res) => { try { const user = req.session.user; const html = buildAdminDashboardHTML(user); @@ -165,12 +171,12 @@ app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => { }); // Legacy route - redirect to /corporate -app.get('/corporate/dashboard', requireRole('corporate', 'admin'), (req, res) => { +app.get('/corporate/dashboard', requirePermission('corporate'), (req, res) => { res.redirect('/corporate'); }); // Corporate emulate agent - view dashboard as specific agent -app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/emular/:agente_id', requirePermission('corporate'), async (req, res) => { try { const agenteId = parseInt(req.params.agente_id); const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId); @@ -185,7 +191,8 @@ app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async nome: agente.nome + ' (Emulando)', agente_id: agenteId, email: agente.email, - emulatorRole: req.session.user.role // Pass the emulator's role + emulatorRole: req.session.user.role, + permissions: req.session.user.permissions || [] }, true, null, false, true); // isEmulating = true res.send(html); } catch (err) { @@ -195,7 +202,7 @@ app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async }); // Legacy route - redirect to /corporate/emular -app.get('/admin/emular/:agente_id', requireRole('admin'), (req, res) => { +app.get('/admin/emular/:agente_id', requirePermission('corporate'), (req, res) => { res.redirect(`/corporate/emular/${req.params.agente_id}`); }); @@ -221,7 +228,7 @@ app.get('/api/cotacao', async (req, res) => { // --- Corporate API Routes (dashboard data - corporate and admin) --- // API endpoint for corporate dashboard data -app.get('/corporate/api/data', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/data', requirePermission('corporate'), async (req, res) => { try { const dias = parseInt(req.query.dias) || 90; const { rowsBrlUsd, rowsUsdBrl } = await fetchAllTransacoes(dias); @@ -234,7 +241,7 @@ app.get('/corporate/api/data', requireRole('corporate', 'admin'), async (req, re }); // API: KPIs (hoje vs média 30 dias) - com cache -app.get('/corporate/api/kpis', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/kpis', requirePermission('corporate'), async (req, res) => { try { const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000); res.json({ success: true, data }); @@ -245,7 +252,7 @@ app.get('/corporate/api/kpis', requireRole('corporate', 'admin'), async (req, re }); // API: Tendência 30 dias - com cache -app.get('/corporate/api/trend', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/trend', requirePermission('corporate'), async (req, res) => { try { const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000); res.json({ success: true, data }); @@ -256,7 +263,7 @@ app.get('/corporate/api/trend', requireRole('corporate', 'admin'), async (req, r }); // API: Top 5 agentes - com cache por período -app.get('/corporate/api/top-agentes', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/top-agentes', requirePermission('corporate'), async (req, res) => { try { const dias = parseInt(req.query.dias) || 30; const cacheKey = `top-agentes-${dias}`; @@ -281,7 +288,7 @@ app.get('/corporate/api/top-agentes', requireRole('corporate', 'admin'), async ( }); // API: Corporate Dashboard - KPIs por período -app.get('/corporate/api/kpis-period', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/kpis-period', requirePermission('corporate'), async (req, res) => { try { const { inicio, fim } = req.query; if (!inicio || !fim) { @@ -296,7 +303,7 @@ app.get('/corporate/api/kpis-period', requireRole('corporate', 'admin'), async ( }); // API: Corporate Dashboard - Tendência por período -app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async (req, res) => { +app.get('/corporate/api/trend-period', requirePermission('corporate'), async (req, res) => { try { const { inicio, fim } = req.query; if (!inicio || !fim) { @@ -311,30 +318,30 @@ app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async }); // Legacy API routes - redirect to /corporate/api/* -app.get('/admin/api/data', requireRole('admin'), (req, res) => { +app.get('/admin/api/data', requirePermission('corporate'), (req, res) => { res.redirect(`/corporate/api/data?${new URLSearchParams(req.query)}`); }); -app.get('/admin/api/kpis', requireRole('admin'), (req, res) => { +app.get('/admin/api/kpis', requirePermission('corporate'), (req, res) => { res.redirect('/corporate/api/kpis'); }); -app.get('/admin/api/trend', requireRole('admin'), (req, res) => { +app.get('/admin/api/trend', requirePermission('corporate'), (req, res) => { res.redirect('/corporate/api/trend'); }); -app.get('/admin/api/top-agentes', requireRole('admin'), (req, res) => { +app.get('/admin/api/top-agentes', requirePermission('corporate'), (req, res) => { res.redirect(`/corporate/api/top-agentes?${new URLSearchParams(req.query)}`); }); -app.get('/admin/api/corporate/kpis', requireRole('admin'), (req, res) => { +app.get('/admin/api/corporate/kpis', requirePermission('corporate'), (req, res) => { res.redirect(`/corporate/api/kpis-period?${new URLSearchParams(req.query)}`); }); -app.get('/admin/api/corporate/trend', requireRole('admin'), (req, res) => { +app.get('/admin/api/corporate/trend', requirePermission('corporate'), (req, res) => { res.redirect(`/corporate/api/trend-period?${new URLSearchParams(req.query)}`); }); -app.get('/admin/dashboard', requireRole('admin'), (req, res) => { +app.get('/admin/dashboard', requirePermission('corporate'), (req, res) => { res.redirect('/corporate/dashboard'); }); -// --- Admin BI Dashboard (admin only) --- -app.get('/admin/bi', requireRole('admin'), (req, res) => { +// --- Admin BI Dashboard --- +app.get('/admin/bi', requirePermission('bi'), (req, res) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); res.set('Pragma', 'no-cache'); @@ -346,7 +353,7 @@ app.get('/admin/bi', requireRole('admin'), (req, res) => { } }); -app.get('/admin/api/bi', requireRole('admin'), async (req, res) => { +app.get('/admin/api/bi', requirePermission('bi'), async (req, res) => { try { const start = req.query.start; const end = req.query.end; @@ -365,7 +372,7 @@ app.get('/admin/api/bi', requireRole('admin'), async (req, res) => { } }); -app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => { +app.get('/admin/api/bi/revenue', requirePermission('bi'), async (req, res) => { try { const { start, end, granularity } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -378,7 +385,7 @@ app.get('/admin/api/bi/revenue', requireRole('admin'), async (req, res) => { } }); -app.get('/admin/api/bi/strategic', requireRole('admin'), async (req, res) => { +app.get('/admin/api/bi/strategic', requirePermission('bi'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -390,8 +397,8 @@ app.get('/admin/api/bi/strategic', requireRole('admin'), async (req, res) => { } }); -// --- Admin Cliente Dashboard (admin only) --- -app.get('/admin/cliente', requireRole('admin'), (req, res) => { +// --- Admin Cliente Dashboard --- +app.get('/admin/cliente', requirePermission('cliente'), (req, res) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); res.set('Pragma', 'no-cache'); @@ -403,7 +410,7 @@ app.get('/admin/cliente', requireRole('admin'), (req, res) => { } }); -app.get('/admin/api/clientes/top', requireRole('admin'), async (req, res) => { +app.get('/admin/api/clientes/top', requirePermission('cliente'), async (req, res) => { try { const data = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000); res.json(data); @@ -413,7 +420,7 @@ app.get('/admin/api/clientes/top', requireRole('admin'), async (req, res) => { } }); -app.get('/admin/api/clientes/search', requireRole('admin'), async (req, res) => { +app.get('/admin/api/clientes/search', requirePermission('cliente'), async (req, res) => { try { const q = (req.query.q || '').trim(); if (q.length < 2) return res.json([]); @@ -425,7 +432,7 @@ app.get('/admin/api/clientes/search', requireRole('admin'), async (req, res) => } }); -app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res) => { +app.get('/admin/api/cliente/:id/profile', requirePermission('cliente'), async (req, res) => { try { const clienteId = parseInt(req.params.id); if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' }); @@ -461,7 +468,7 @@ app.get('/admin/api/cliente/:id/profile', requireRole('admin'), async (req, res) } }); -app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) => { +app.get('/admin/api/cliente/:id/data', requirePermission('cliente'), async (req, res) => { try { const clienteId = parseInt(req.params.id); const { start, end } = req.query; @@ -507,21 +514,21 @@ app.get('/admin/api/cliente/:id/data', requireRole('admin'), async (req, res) => // Create user (admin only) app.post('/admin/agentes', requireRole('admin'), async (req, res) => { - const { nome, email, agente_id, senha, role } = req.body; + const { nome, email, agente_id, senha, role, permissions } = req.body; try { if (!nome || !email || !senha) { return res.status(400).json({ error: 'Nome, email e senha sao obrigatorios' }); } const userRole = role || 'agente'; - // Admin and Corporate don't need agente_id - const agenteId = (userRole === 'admin' || userRole === 'corporate') ? 0 : (agente_id || 0); + const agenteId = agente_id || 0; - if (userRole === 'agente' && !agente_id) { - return res.status(400).json({ error: 'Agente ID e obrigatorio para agentes' }); + // dashboard permission requires agente_id + if (Array.isArray(permissions) && permissions.includes('dashboard') && !agenteId) { + return res.status(400).json({ error: 'Agente ID e obrigatorio para acesso ao Meu Dashboard' }); } - const result = await createUser(email, senha, nome, userRole, agenteId); + const result = await createUser(email, senha, nome, userRole, agenteId, Array.isArray(permissions) ? permissions : null); res.json({ success: true, id: result.lastInsertRowid }); } catch (err) { console.error('Create user error:', err); @@ -535,7 +542,7 @@ app.post('/admin/agentes', requireRole('admin'), async (req, res) => { // Update user (admin only) app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => { const { id } = req.params; - const { nome, email, agente_id, ativo, senha, role } = req.body; + const { nome, email, agente_id, ativo, senha, role, permissions } = req.body; try { const agent = db.prepare('SELECT * FROM agentes WHERE id = ?').get(id); if (!agent) { @@ -562,6 +569,13 @@ app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => { if (role !== undefined) { db.prepare('UPDATE agentes SET role = ? WHERE id = ?').run(role, id); } + if (Array.isArray(permissions)) { + // Anti-lockout: prevent user from removing 'usuarios' from their own permissions + if (parseInt(id) === req.session.user.id && !permissions.includes('usuarios')) { + return res.status(400).json({ error: 'Voce nao pode remover a permissao "Usuarios" de si mesmo' }); + } + db.prepare('UPDATE agentes SET permissions = ? WHERE id = ?').run(JSON.stringify(permissions), id); + } res.json({ success: true }); } catch (err) { @@ -590,7 +604,7 @@ app.delete('/admin/agentes/:id', requireRole('admin'), (req, res) => { // --- Excel Export Endpoints --- -app.get('/admin/api/export/bi-excel', requireRole('admin'), async (req, res) => { +app.get('/admin/api/export/bi-excel', requirePermission('bi'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -676,7 +690,7 @@ app.get('/admin/api/export/bi-excel', requireRole('admin'), async (req, res) => } }); -app.get('/admin/api/export/clients-excel', requireRole('admin'), async (req, res) => { +app.get('/admin/api/export/clients-excel', requirePermission('cliente'), async (req, res) => { try { const clients = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000); await exportToExcel(res, clients, [ @@ -692,7 +706,7 @@ app.get('/admin/api/export/clients-excel', requireRole('admin'), async (req, res } }); -app.get('/admin/api/export/providers-excel', requireRole('admin'), async (req, res) => { +app.get('/admin/api/export/providers-excel', requirePermission('providers'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -714,7 +728,7 @@ app.get('/admin/api/export/providers-excel', requireRole('admin'), async (req, r } }); -app.get('/admin/api/export/transactions-excel', requireRole('admin'), async (req, res) => { +app.get('/admin/api/export/transactions-excel', requirePermission('bi'), async (req, res) => { try { const { start, end } = req.query; const dias = start && end ? null : 90; @@ -761,7 +775,7 @@ app.get('/admin/api/export/transactions-excel', requireRole('admin'), async (req }); // --- Forecast API --- -app.get('/admin/api/bi/forecast', requireRole('admin'), async (req, res) => { +app.get('/admin/api/bi/forecast', requirePermission('bi'), async (req, res) => { try { const metric = req.query.metric || 'volume'; const days = parseInt(req.query.days) || 30; @@ -792,7 +806,7 @@ app.get('/admin/api/bi/forecast', requireRole('admin'), async (req, res) => { }); // --- Churn Risk API --- -app.get('/admin/api/cliente/:id/churn', requireRole('admin'), async (req, res) => { +app.get('/admin/api/cliente/:id/churn', requirePermission('cliente'), async (req, res) => { try { const clienteId = parseInt(req.params.id); if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' }); @@ -845,7 +859,7 @@ app.get('/admin/api/cliente/:id/churn', requireRole('admin'), async (req, res) = // --- Alert API Endpoints --- -app.get('/admin/api/alerts', requireRole('admin'), (req, res) => { +app.get('/admin/api/alerts', requirePermission('bi'), (req, res) => { try { const unacked = req.query.unacked === '1'; const alerts = getAlerts(24, unacked); @@ -856,7 +870,7 @@ app.get('/admin/api/alerts', requireRole('admin'), (req, res) => { } }); -app.put('/admin/api/alerts/:id/ack', requireRole('admin'), (req, res) => { +app.put('/admin/api/alerts/:id/ack', requirePermission('bi'), (req, res) => { try { const id = parseInt(req.params.id); acknowledgeAlert(id); @@ -867,7 +881,7 @@ app.put('/admin/api/alerts/:id/ack', requireRole('admin'), (req, res) => { } }); -app.get('/admin/api/alerts/history', requireRole('admin'), (req, res) => { +app.get('/admin/api/alerts/history', requirePermission('bi'), (req, res) => { try { const days = parseInt(req.query.days) || 7; const alerts = getAlertHistory(days); @@ -902,8 +916,8 @@ app.get('/health', async (req, res) => { res.json(health); }); -// --- Provider Dashboard (admin only) --- -app.get('/admin/providers', requireRole('admin'), (req, res) => { +// --- Provider Dashboard --- +app.get('/admin/providers', requirePermission('providers'), (req, res) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate'); const html = buildAdminProvidersHTML(req.session.user); @@ -914,7 +928,7 @@ app.get('/admin/providers', requireRole('admin'), (req, res) => { } }); -app.get('/admin/api/providers', requireRole('admin'), async (req, res) => { +app.get('/admin/api/providers', requirePermission('providers'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -926,7 +940,7 @@ app.get('/admin/api/providers', requireRole('admin'), async (req, res) => { } }); -app.get('/admin/api/providers/failed', requireRole('admin'), async (req, res) => { +app.get('/admin/api/providers/failed', requirePermission('providers'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); @@ -938,7 +952,7 @@ app.get('/admin/api/providers/failed', requireRole('admin'), async (req, res) => } }); -app.get('/admin/api/providers/trend', requireRole('admin'), async (req, res) => { +app.get('/admin/api/providers/trend', requirePermission('providers'), async (req, res) => { try { const { start, end } = req.query; if (!start || !end) return res.status(400).json({ error: 'start and end required' }); diff --git a/src/admin-bi.js b/src/admin-bi.js index 1e36dc5..8e249aa 100644 --- a/src/admin-bi.js +++ b/src/admin-bi.js @@ -824,7 +824,7 @@ ${buildHead('BI Executive', pageCSS, pageScripts)} -${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'bi', permissions: user.permissions || [] })}
diff --git a/src/admin-cliente.js b/src/admin-cliente.js index 3db9664..5bffe97 100644 --- a/src/admin-cliente.js +++ b/src/admin-cliente.js @@ -389,7 +389,7 @@ ${buildHead('Clientes 360', pageCSS, pageScripts)} -${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'cliente', permissions: user.permissions || [] })}
diff --git a/src/admin-dashboard.js b/src/admin-dashboard.js index 2780e16..98ebca4 100644 --- a/src/admin-dashboard.js +++ b/src/admin-dashboard.js @@ -459,7 +459,7 @@ ${buildHead('Dashboard Corporate', pageCSS, pageScripts)} -${buildHeader({ role: role, userName: user.nome, activePage: 'dashboard' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'dashboard', permissions: user.permissions || [] })}
diff --git a/src/admin-home.js b/src/admin-home.js index e8472fb..d66e8c6 100644 --- a/src/admin-home.js +++ b/src/admin-home.js @@ -242,7 +242,7 @@ ${buildHead('Home', pageCSS, pageScripts)} -${buildHeader({ role: role, userName: user.nome, activePage: 'home' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'home', permissions: user.permissions || [] })}
diff --git a/src/admin-panel.js b/src/admin-panel.js index 294c39c..6fc5573 100644 --- a/src/admin-panel.js +++ b/src/admin-panel.js @@ -2,10 +2,18 @@ * Admin Panel - HTML builder for user management */ const { buildHeader, buildFooter, buildHead } = require('./ui-template'); +const { PANELS, DEFAULT_PERMISSIONS } = require('./panels'); function buildAdminHTML(agentes, admin) { const now = new Date().toLocaleString('pt-BR'); + // Precompute permissions JSON for each agent row + const agentesWithPerms = agentes.map(a => { + let perms = []; + try { perms = JSON.parse(a.permissions || '[]'); } catch(e) {} + return { ...a, _perms: perms }; + }); + const pageCSS = ` `; + // Panel label map for badges + const panelLabels = {}; + PANELS.forEach(p => { panelLabels[p.key] = p.label; }); + return ` @@ -203,7 +239,7 @@ ${buildHead('Usuarios', pageCSS)} -${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })} +${buildHeader({ role: admin.role || 'admin', userName: admin.nome, activePage: 'users', permissions: admin.permissions || [] })}
@@ -222,6 +258,7 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })} Nome E-mail Role + Permissoes Agente ID Status Criado em @@ -229,17 +266,18 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })} - ${agentes.map(a => ` - + ${agentesWithPerms.map(a => ` + ${a.id} ${a.nome} ${a.email} ${a.role === 'admin' ? 'Admin' : a.role === 'corporate' ? 'Corporate' : 'Agente'} - ${(a.role === 'admin' || a.role === 'corporate') ? '-' : a.agente_id} + ${a._perms.map(k => '' + (panelLabels[k] || k) + '').join(' ')} + ${a.agente_id || '-'} ${a.ativo ? 'Ativo' : 'Inativo'} ${a.created_at ? new Date(a.created_at).toLocaleDateString('pt-BR') : '-'} - ${a.role === 'agente' ? `Emular` : ''} + ${a.agente_id ? `Emular` : ''} @@ -273,13 +311,24 @@ ${buildFooter()}
- -
+
+ +
+ ${PANELS.map(p => ` + + `).join('')} +
+
@@ -325,27 +374,66 @@ ${buildFooter()}
+<\/script> `; } diff --git a/src/admin-providers.js b/src/admin-providers.js index 2a04bd9..a83bf13 100644 --- a/src/admin-providers.js +++ b/src/admin-providers.js @@ -405,7 +405,7 @@ ${buildHead('Provider Performance', pageCSS, pageScripts)} -${buildHeader({ role: role, userName: user.nome, activePage: 'providers' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'providers', permissions: user.permissions || [] })}
diff --git a/src/auth.js b/src/auth.js index 034d5c4..ab04e6f 100644 --- a/src/auth.js +++ b/src/auth.js @@ -10,11 +10,13 @@ const SALT_ROUNDS = 10; /** * Cria um novo usuario (agente, corporate ou admin) */ -async function createUser(email, senha, nome, role = 'agente', agenteId = 0) { +async function createUser(email, senha, nome, role = 'agente', agenteId = 0, permissions = null) { + const { DEFAULT_PERMISSIONS } = require('./panels'); const hash = await bcrypt.hash(senha, SALT_ROUNDS); + const perms = permissions || DEFAULT_PERMISSIONS[role] || DEFAULT_PERMISSIONS.agente; return db.prepare( - 'INSERT INTO agentes (email, senha_hash, agente_id, nome, role) VALUES (?, ?, ?, ?, ?)' - ).run(email, hash, agenteId, nome, role); + 'INSERT INTO agentes (email, senha_hash, agente_id, nome, role, permissions) VALUES (?, ?, ?, ?, ?, ?)' + ).run(email, hash, agenteId, nome, role, JSON.stringify(perms)); } /** @@ -56,6 +58,19 @@ function requireRole(...roles) { }; } +/** + * Middleware que verifica permissao por painel + * Uso: requirePermission('bi') - usuario deve ter painel 'bi' nas permissions + */ +function requirePermission(panelKey) { + return (req, res, next) => { + if (!req.session?.user) return res.redirect('/login'); + const perms = req.session.user.permissions || []; + if (perms.includes(panelKey)) return next(); + return res.status(403).send('Acesso negado'); + }; +} + /** * Middleware simples - apenas verifica se esta logado * Retrocompatibilidade com requireAuth @@ -81,5 +96,6 @@ module.exports = { authenticate, requireAuth, requireRole, + requirePermission, updatePassword }; diff --git a/src/dashboard.js b/src/dashboard.js index 256c280..475e58a 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -417,7 +417,7 @@ ${isEmulating ? ` Voltar ao BI - CCC
` : ''} -${buildHeader({ role, userName: agente.nome, activePage: 'dashboard', showNav: !isEmulating })} +${buildHeader({ role, userName: agente.nome, activePage: 'dashboard', showNav: !isEmulating, permissions: agente.permissions || [] })}
diff --git a/src/db-local.js b/src/db-local.js index 073260f..00562c4 100644 --- a/src/db-local.js +++ b/src/db-local.js @@ -32,6 +32,21 @@ try { // Column already exists, ignore } +// Add permissions column (migration) +try { + db.exec(`ALTER TABLE agentes ADD COLUMN permissions TEXT DEFAULT '[]'`); +} catch (e) { + // Column already exists, ignore +} + +// Migrate existing users with empty permissions — set defaults based on role +const { DEFAULT_PERMISSIONS } = require('./panels'); +const usersNeedingPerms = db.prepare("SELECT id, role FROM agentes WHERE permissions = '[]' OR permissions IS NULL").all(); +for (const u of usersNeedingPerms) { + const defaults = DEFAULT_PERMISSIONS[u.role] || DEFAULT_PERMISSIONS.agente; + db.prepare('UPDATE agentes SET permissions = ? WHERE id = ?').run(JSON.stringify(defaults), u.id); +} + // Legacy table - keep for reference but no longer used db.exec(` CREATE TABLE IF NOT EXISTS admins ( diff --git a/src/panels.js b/src/panels.js new file mode 100644 index 0000000..e53ec2c --- /dev/null +++ b/src/panels.js @@ -0,0 +1,22 @@ +/** + * Panel Registry — constantes de paineis e permissoes default + */ +const PANELS = [ + { key: 'corporate', label: 'Corporate', route: '/corporate' }, + { key: 'bi', label: 'BI Executive', route: '/admin/bi' }, + { key: 'cliente', label: 'Clientes', route: '/admin/cliente' }, + { key: 'providers', label: 'Providers', route: '/admin/providers' }, + { key: 'usuarios', label: 'Usuarios', route: '/admin' }, + { key: 'dashboard', label: 'Meu Dashboard', route: '/dashboard' }, +]; + +const DEFAULT_PERMISSIONS = { + admin: ['corporate', 'bi', 'cliente', 'providers', 'usuarios', 'dashboard'], + corporate: ['corporate'], + agente: ['dashboard'], +}; + +const PANEL_ROUTE = {}; +PANELS.forEach(p => { PANEL_ROUTE[p.key] = p.route; }); + +module.exports = { PANELS, DEFAULT_PERMISSIONS, PANEL_ROUTE }; diff --git a/src/ui-template.js b/src/ui-template.js index e75b100..0717910 100644 --- a/src/ui-template.js +++ b/src/ui-template.js @@ -383,16 +383,18 @@ const headerCSS = ` * @param {string} options.userName - Nome do usuário * @param {string} options.activePage - Página ativa para nav * @param {boolean} options.showNav - Mostrar navegação + * @param {string[]} options.permissions - Paineis permitidos para o usuario */ function buildHeader(options = {}) { - const { role = 'agente', userName = '', activePage = '', showNav = true } = options; - const isAdmin = role === 'admin'; - const isCorporate = role === 'corporate'; + const { role = 'agente', userName = '', activePage = '', showNav = true, permissions = [] } = options; + const { PANELS } = require('./panels'); - // Determine header class: admin (green), corporate (purple), agent (purple) + // Determine header class based on permissions + const adminPanels = ['bi', 'cliente', 'providers', 'usuarios']; + const hasAdminPanels = adminPanels.some(k => permissions.includes(k)); let headerClass = 'agent'; - if (isAdmin) headerClass = 'admin'; - else if (isCorporate) headerClass = 'corporate'; + if (hasAdminPanels) headerClass = 'admin'; + else if (permissions.includes('corporate')) headerClass = 'corporate'; const initials = userName .split(' ') @@ -401,45 +403,29 @@ function buildHeader(options = {}) { .join('') .toUpperCase(); - // Admin navigation: Corporate Dashboard + BI + Clients + Providers + Users - const adminNav = ` - - `; + // Build nav dynamically from permissions + const activeMap = { 'dashboard': 'dashboard', 'corporate': 'dashboard', 'usuarios': 'users' }; + const navLinks = PANELS + .filter(p => permissions.includes(p.key)) + .map(p => { + const pageKey = activeMap[p.key] || p.key; + const isActive = activePage === pageKey || activePage === p.key; + return `${p.label}`; + }) + .join('\n '); + const nav = ``; - // Corporate navigation: Dashboard only - const corporateNav = ` - - `; + // Home URL: first permitted panel + const firstPanel = PANELS.find(p => permissions.includes(p.key)); + const homeUrl = firstPanel ? firstPanel.route : '/login'; - // Agent navigation: Just their dashboard - const agentNav = ` - - `; - - // Select navigation based on role - let nav = agentNav; - if (isAdmin) nav = adminNav; - else if (isCorporate) nav = corporateNav; - - // Home URL based on role - let homeUrl = '/dashboard'; - if (isAdmin) homeUrl = '/corporate'; // Admin home is Corporate Dashboard - else if (isCorporate) homeUrl = '/corporate'; + // Show alert bell if user has BI permission + const showAlerts = permissions.includes('bi'); // Role label for display let roleLabel = 'Agente'; - if (isAdmin) roleLabel = 'Admin'; - else if (isCorporate) roleLabel = 'Corporate'; + if (role === 'admin') roleLabel = 'Admin'; + else if (role === 'corporate') roleLabel = 'Corporate'; return `
@@ -453,7 +439,7 @@ function buildHeader(options = {}) { ${showNav ? nav : ''}
- ${isAdmin ? ` + ${showAlerts ? `