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 || [] })}