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 <noreply@anthropic.com>
This commit is contained in:
148
server.js
148
server.js
@@ -10,7 +10,8 @@ require('dotenv').config();
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const path = require('path');
|
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 { 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 { buildAdminProvidersHTML } = require('./src/admin-providers');
|
||||||
const pool = require('./src/db-rds');
|
const pool = require('./src/db-rds');
|
||||||
@@ -47,17 +48,20 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
|
|||||||
|
|
||||||
// --- Unified Login Routes ---
|
// --- Unified Login Routes ---
|
||||||
|
|
||||||
// Helper function to get redirect URL based on role
|
// Helper function to get redirect URL based on user permissions
|
||||||
function getRedirectByRole(role) {
|
function getRedirectForUser(user) {
|
||||||
if (role === 'admin') return '/corporate'; // Admin vai direto pro Corporate Dashboard
|
const perms = Array.isArray(user.permissions) ? user.permissions : JSON.parse(user.permissions || '[]');
|
||||||
if (role === 'corporate') return '/corporate';
|
const priority = ['corporate', 'bi', 'cliente', 'providers', 'usuarios', 'dashboard'];
|
||||||
return '/dashboard';
|
for (const key of priority) {
|
||||||
|
if (perms.includes(key)) return PANEL_ROUTE[key];
|
||||||
|
}
|
||||||
|
return '/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root -> login page (or redirect if logged in)
|
// Root -> login page (or redirect if logged in)
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
if (req.session?.user) {
|
if (req.session?.user) {
|
||||||
return res.redirect(getRedirectByRole(req.session.user.role));
|
return res.redirect(getRedirectForUser(req.session.user));
|
||||||
}
|
}
|
||||||
res.redirect('/login');
|
res.redirect('/login');
|
||||||
});
|
});
|
||||||
@@ -65,7 +69,7 @@ app.get('/', (req, res) => {
|
|||||||
// Login page
|
// Login page
|
||||||
app.get('/login', (req, res) => {
|
app.get('/login', (req, res) => {
|
||||||
if (req.session?.user) {
|
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'));
|
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}`);
|
if (!user) return res.redirect(`/login?error=1&email=${emailParam}`);
|
||||||
|
|
||||||
// Unified session
|
// Unified session
|
||||||
|
const permissions = JSON.parse(user.permissions || '[]');
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
nome: user.nome,
|
nome: user.nome,
|
||||||
role: user.role || 'agente',
|
role: user.role || 'agente',
|
||||||
agente_id: user.agente_id
|
agente_id: user.agente_id,
|
||||||
|
permissions
|
||||||
};
|
};
|
||||||
|
|
||||||
// Redirect based on role
|
// Redirect based on permissions
|
||||||
res.redirect(getRedirectByRole(user.role));
|
res.redirect(getRedirectForUser(req.session.user));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login error:', err);
|
console.error('Login error:', err);
|
||||||
res.redirect(`/login?error=1&email=${emailParam}`);
|
res.redirect(`/login?error=1&email=${emailParam}`);
|
||||||
@@ -112,8 +118,8 @@ app.get('/admin/logout', (req, res) => {
|
|||||||
|
|
||||||
// --- Agent Routes ---
|
// --- Agent Routes ---
|
||||||
|
|
||||||
// Dashboard (agente only)
|
// Dashboard (requires 'dashboard' permission)
|
||||||
app.get('/dashboard', requireRole('agente'), async (req, res) => {
|
app.get('/dashboard', requirePermission('dashboard'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(user.agente_id);
|
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 Routes (User Management - admin only) ---
|
||||||
|
|
||||||
// Admin home - User management panel (admin only)
|
// Admin home - User management panel
|
||||||
app.get('/admin', requireRole('admin'), (req, res) => {
|
app.get('/admin', requirePermission('usuarios'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all();
|
const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all();
|
||||||
const html = buildAdminHTML(agentes, req.session.user);
|
const html = buildAdminHTML(agentes, req.session.user);
|
||||||
@@ -141,19 +147,19 @@ app.get('/admin', requireRole('admin'), (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Alias: /admin/usuarios -> /admin
|
// Alias: /admin/usuarios -> /admin
|
||||||
app.get('/admin/usuarios', requireRole('admin'), (req, res) => {
|
app.get('/admin/usuarios', requirePermission('usuarios'), (req, res) => {
|
||||||
res.redirect('/admin');
|
res.redirect('/admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legacy route - redirect to /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');
|
res.redirect('/admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Corporate Routes (Dashboard + Emulation - corporate and admin) ---
|
// --- Corporate Routes (Dashboard + Emulation - corporate and admin) ---
|
||||||
|
|
||||||
// Corporate Dashboard - Full KPIs, Trends and Ranking
|
// 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 {
|
try {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const html = buildAdminDashboardHTML(user);
|
const html = buildAdminDashboardHTML(user);
|
||||||
@@ -165,12 +171,12 @@ app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Legacy route - redirect to /corporate
|
// 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');
|
res.redirect('/corporate');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Corporate emulate agent - view dashboard as specific agent
|
// 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 {
|
try {
|
||||||
const agenteId = parseInt(req.params.agente_id);
|
const agenteId = parseInt(req.params.agente_id);
|
||||||
const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId);
|
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)',
|
nome: agente.nome + ' (Emulando)',
|
||||||
agente_id: agenteId,
|
agente_id: agenteId,
|
||||||
email: agente.email,
|
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
|
}, true, null, false, true); // isEmulating = true
|
||||||
res.send(html);
|
res.send(html);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -195,7 +202,7 @@ app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Legacy route - redirect to /corporate/emular
|
// 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}`);
|
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) ---
|
// --- Corporate API Routes (dashboard data - corporate and admin) ---
|
||||||
|
|
||||||
// API endpoint for corporate dashboard data
|
// 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 {
|
try {
|
||||||
const dias = parseInt(req.query.dias) || 90;
|
const dias = parseInt(req.query.dias) || 90;
|
||||||
const { rowsBrlUsd, rowsUsdBrl } = await fetchAllTransacoes(dias);
|
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
|
// 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 {
|
try {
|
||||||
const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000);
|
const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000);
|
||||||
res.json({ success: true, data });
|
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
|
// 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 {
|
try {
|
||||||
const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000);
|
const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000);
|
||||||
res.json({ success: true, data });
|
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
|
// 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 {
|
try {
|
||||||
const dias = parseInt(req.query.dias) || 30;
|
const dias = parseInt(req.query.dias) || 30;
|
||||||
const cacheKey = `top-agentes-${dias}`;
|
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
|
// 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 {
|
try {
|
||||||
const { inicio, fim } = req.query;
|
const { inicio, fim } = req.query;
|
||||||
if (!inicio || !fim) {
|
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
|
// 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 {
|
try {
|
||||||
const { inicio, fim } = req.query;
|
const { inicio, fim } = req.query;
|
||||||
if (!inicio || !fim) {
|
if (!inicio || !fim) {
|
||||||
@@ -311,30 +318,30 @@ app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Legacy API routes - redirect to /corporate/api/*
|
// 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)}`);
|
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');
|
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');
|
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)}`);
|
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)}`);
|
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)}`);
|
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');
|
res.redirect('/corporate/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Admin BI Dashboard (admin only) ---
|
// --- Admin BI Dashboard ---
|
||||||
app.get('/admin/bi', requireRole('admin'), (req, res) => {
|
app.get('/admin/bi', requirePermission('bi'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
res.set('Pragma', 'no-cache');
|
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 {
|
try {
|
||||||
const start = req.query.start;
|
const start = req.query.start;
|
||||||
const end = req.query.end;
|
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 {
|
try {
|
||||||
const { start, end, granularity } = req.query;
|
const { start, end, granularity } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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) ---
|
// --- Admin Cliente Dashboard ---
|
||||||
app.get('/admin/cliente', requireRole('admin'), (req, res) => {
|
app.get('/admin/cliente', requirePermission('cliente'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
res.set('Pragma', 'no-cache');
|
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 {
|
try {
|
||||||
const data = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000);
|
const data = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000);
|
||||||
res.json(data);
|
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 {
|
try {
|
||||||
const q = (req.query.q || '').trim();
|
const q = (req.query.q || '').trim();
|
||||||
if (q.length < 2) return res.json([]);
|
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 {
|
try {
|
||||||
const clienteId = parseInt(req.params.id);
|
const clienteId = parseInt(req.params.id);
|
||||||
if (!clienteId) return res.status(400).json({ error: 'Invalid client 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 {
|
try {
|
||||||
const clienteId = parseInt(req.params.id);
|
const clienteId = parseInt(req.params.id);
|
||||||
const { start, end } = req.query;
|
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)
|
// Create user (admin only)
|
||||||
app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
|
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 {
|
try {
|
||||||
if (!nome || !email || !senha) {
|
if (!nome || !email || !senha) {
|
||||||
return res.status(400).json({ error: 'Nome, email e senha sao obrigatorios' });
|
return res.status(400).json({ error: 'Nome, email e senha sao obrigatorios' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRole = role || 'agente';
|
const userRole = role || 'agente';
|
||||||
// Admin and Corporate don't need agente_id
|
const agenteId = agente_id || 0;
|
||||||
const agenteId = (userRole === 'admin' || userRole === 'corporate') ? 0 : (agente_id || 0);
|
|
||||||
|
|
||||||
if (userRole === 'agente' && !agente_id) {
|
// dashboard permission requires agente_id
|
||||||
return res.status(400).json({ error: 'Agente ID e obrigatorio para agentes' });
|
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 });
|
res.json({ success: true, id: result.lastInsertRowid });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Create user error:', err);
|
console.error('Create user error:', err);
|
||||||
@@ -535,7 +542,7 @@ app.post('/admin/agentes', requireRole('admin'), async (req, res) => {
|
|||||||
// Update user (admin only)
|
// Update user (admin only)
|
||||||
app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => {
|
app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => {
|
||||||
const { id } = req.params;
|
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 {
|
try {
|
||||||
const agent = db.prepare('SELECT * FROM agentes WHERE id = ?').get(id);
|
const agent = db.prepare('SELECT * FROM agentes WHERE id = ?').get(id);
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
@@ -562,6 +569,13 @@ app.put('/admin/agentes/:id', requireRole('admin'), async (req, res) => {
|
|||||||
if (role !== undefined) {
|
if (role !== undefined) {
|
||||||
db.prepare('UPDATE agentes SET role = ? WHERE id = ?').run(role, id);
|
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 });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -590,7 +604,7 @@ app.delete('/admin/agentes/:id', requireRole('admin'), (req, res) => {
|
|||||||
|
|
||||||
// --- Excel Export Endpoints ---
|
// --- 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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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 {
|
try {
|
||||||
const clients = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000);
|
const clients = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000);
|
||||||
await exportToExcel(res, clients, [
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
const dias = start && end ? null : 90;
|
const dias = start && end ? null : 90;
|
||||||
@@ -761,7 +775,7 @@ app.get('/admin/api/export/transactions-excel', requireRole('admin'), async (req
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Forecast API ---
|
// --- 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 {
|
try {
|
||||||
const metric = req.query.metric || 'volume';
|
const metric = req.query.metric || 'volume';
|
||||||
const days = parseInt(req.query.days) || 30;
|
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 ---
|
// --- 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 {
|
try {
|
||||||
const clienteId = parseInt(req.params.id);
|
const clienteId = parseInt(req.params.id);
|
||||||
if (!clienteId) return res.status(400).json({ error: 'Invalid client 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 ---
|
// --- Alert API Endpoints ---
|
||||||
|
|
||||||
app.get('/admin/api/alerts', requireRole('admin'), (req, res) => {
|
app.get('/admin/api/alerts', requirePermission('bi'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
const unacked = req.query.unacked === '1';
|
const unacked = req.query.unacked === '1';
|
||||||
const alerts = getAlerts(24, unacked);
|
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 {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.id);
|
||||||
acknowledgeAlert(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 {
|
try {
|
||||||
const days = parseInt(req.query.days) || 7;
|
const days = parseInt(req.query.days) || 7;
|
||||||
const alerts = getAlertHistory(days);
|
const alerts = getAlertHistory(days);
|
||||||
@@ -902,8 +916,8 @@ app.get('/health', async (req, res) => {
|
|||||||
res.json(health);
|
res.json(health);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Provider Dashboard (admin only) ---
|
// --- Provider Dashboard ---
|
||||||
app.get('/admin/providers', requireRole('admin'), (req, res) => {
|
app.get('/admin/providers', requirePermission('providers'), (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
const html = buildAdminProvidersHTML(req.session.user);
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
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 {
|
try {
|
||||||
const { start, end } = req.query;
|
const { start, end } = req.query;
|
||||||
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
if (!start || !end) return res.status(400).json({ error: 'start and end required' });
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ ${buildHead('BI Executive', pageCSS, pageScripts)}
|
|||||||
</head>
|
</head>
|
||||||
<body class="trading-console">
|
<body class="trading-console">
|
||||||
|
|
||||||
${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
|
${buildHeader({ role: role, userName: user.nome, activePage: 'bi', permissions: user.permissions || [] })}
|
||||||
|
|
||||||
<div class="trading-terminal">
|
<div class="trading-terminal">
|
||||||
<div class="live-rate-bar">
|
<div class="live-rate-bar">
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ ${buildHead('Clientes 360', pageCSS, pageScripts)}
|
|||||||
</head>
|
</head>
|
||||||
<body class="trading-console">
|
<body class="trading-console">
|
||||||
|
|
||||||
${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
|
${buildHeader({ role: role, userName: user.nome, activePage: 'cliente', permissions: user.permissions || [] })}
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
|
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ ${buildHead('Dashboard Corporate', pageCSS, pageScripts)}
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
${buildHeader({ role: role, userName: user.nome, activePage: 'dashboard' })}
|
${buildHeader({ role: role, userName: user.nome, activePage: 'dashboard', permissions: user.permissions || [] })}
|
||||||
|
|
||||||
<div class="trading-terminal">
|
<div class="trading-terminal">
|
||||||
<div class="live-rate-bar">
|
<div class="live-rate-bar">
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ ${buildHead('Home', pageCSS, pageScripts)}
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
${buildHeader({ role: role, userName: user.nome, activePage: 'home' })}
|
${buildHeader({ role: role, userName: user.nome, activePage: 'home', permissions: user.permissions || [] })}
|
||||||
|
|
||||||
<div class="trading-terminal">
|
<div class="trading-terminal">
|
||||||
<div class="live-rate-bar">
|
<div class="live-rate-bar">
|
||||||
|
|||||||
@@ -2,10 +2,18 @@
|
|||||||
* Admin Panel - HTML builder for user management
|
* Admin Panel - HTML builder for user management
|
||||||
*/
|
*/
|
||||||
const { buildHeader, buildFooter, buildHead } = require('./ui-template');
|
const { buildHeader, buildFooter, buildHead } = require('./ui-template');
|
||||||
|
const { PANELS, DEFAULT_PERMISSIONS } = require('./panels');
|
||||||
|
|
||||||
function buildAdminHTML(agentes, admin) {
|
function buildAdminHTML(agentes, admin) {
|
||||||
const now = new Date().toLocaleString('pt-BR');
|
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 = `
|
const pageCSS = `
|
||||||
<style>
|
<style>
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -51,6 +59,12 @@ function buildAdminHTML(agentes, admin) {
|
|||||||
.status-badge.corporate { background: var(--corporate-bg); color: var(--corporate-accent); }
|
.status-badge.corporate { background: var(--corporate-bg); color: var(--corporate-accent); }
|
||||||
.status-badge.agent { background: var(--blue-bg); color: var(--blue); }
|
.status-badge.agent { background: var(--blue-bg); color: var(--blue); }
|
||||||
|
|
||||||
|
.perm-badge {
|
||||||
|
display: inline-block; padding: 2px 7px; border-radius: 8px;
|
||||||
|
font-size: 10px; font-weight: 600; margin: 1px 2px;
|
||||||
|
background: var(--blue-bg); color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
.actions { display: flex; gap: 6px; }
|
.actions { display: flex; gap: 6px; }
|
||||||
.btn-action {
|
.btn-action {
|
||||||
padding: 6px 12px; border-radius: 6px; font-size: 11px; font-weight: 600;
|
padding: 6px 12px; border-radius: 6px; font-size: 11px; font-weight: 600;
|
||||||
@@ -73,7 +87,7 @@ function buildAdminHTML(agentes, admin) {
|
|||||||
}
|
}
|
||||||
.modal-overlay.active { display: flex; }
|
.modal-overlay.active { display: flex; }
|
||||||
.modal {
|
.modal {
|
||||||
background: var(--card); border-radius: 16px; width: 100%; max-width: 480px;
|
background: var(--card); border-radius: 16px; width: 100%; max-width: 520px;
|
||||||
max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -122,6 +136,20 @@ function buildAdminHTML(agentes, admin) {
|
|||||||
}
|
}
|
||||||
.btn-submit:hover { background: #25732a; }
|
.btn-submit:hover { background: #25732a; }
|
||||||
|
|
||||||
|
/* Permissions checkboxes */
|
||||||
|
.perm-grid {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.perm-check {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 10px; border-radius: 8px; border: 1.5px solid var(--border);
|
||||||
|
cursor: pointer; transition: all 0.15s; font-size: 13px;
|
||||||
|
}
|
||||||
|
.perm-check:hover { border-color: var(--admin-accent); background: var(--green-bg); }
|
||||||
|
.perm-check input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--admin-accent); }
|
||||||
|
.perm-check.checked { border-color: var(--admin-accent); background: var(--green-bg); }
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
padding: 12px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
|
padding: 12px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
|
||||||
margin-bottom: 20px; display: none;
|
margin-bottom: 20px; display: none;
|
||||||
@@ -166,6 +194,7 @@ function buildAdminHTML(agentes, admin) {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.modal-footer button { width: 100%; }
|
.modal-footer button { width: 100%; }
|
||||||
|
.perm-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -193,9 +222,16 @@ function buildAdminHTML(agentes, admin) {
|
|||||||
[data-theme="dark"] .btn-edit:hover { background: rgba(88,166,255,0.15); }
|
[data-theme="dark"] .btn-edit:hover { background: rgba(88,166,255,0.15); }
|
||||||
[data-theme="dark"] .btn-toggle:hover { background: rgba(240,136,62,0.15); }
|
[data-theme="dark"] .btn-toggle:hover { background: rgba(240,136,62,0.15); }
|
||||||
[data-theme="dark"] .btn-password:hover { background: rgba(188,140,255,0.15); }
|
[data-theme="dark"] .btn-password:hover { background: rgba(188,140,255,0.15); }
|
||||||
|
[data-theme="dark"] .perm-check { border-color: var(--border); }
|
||||||
|
[data-theme="dark"] .perm-check:hover { background: rgba(63,185,80,0.08); }
|
||||||
|
[data-theme="dark"] .perm-check.checked { background: rgba(63,185,80,0.08); border-color: var(--green); }
|
||||||
</style>
|
</style>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Panel label map for badges
|
||||||
|
const panelLabels = {};
|
||||||
|
PANELS.forEach(p => { panelLabels[p.key] = p.label; });
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="pt-BR">
|
<html lang="pt-BR">
|
||||||
<head>
|
<head>
|
||||||
@@ -203,7 +239,7 @@ ${buildHead('Usuarios', pageCSS)}
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })}
|
${buildHeader({ role: admin.role || 'admin', userName: admin.nome, activePage: 'users', permissions: admin.permissions || [] })}
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div id="alertBox" class="alert"></div>
|
<div id="alertBox" class="alert"></div>
|
||||||
@@ -222,6 +258,7 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })}
|
|||||||
<th>Nome</th>
|
<th>Nome</th>
|
||||||
<th>E-mail</th>
|
<th>E-mail</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
|
<th>Permissoes</th>
|
||||||
<th>Agente ID</th>
|
<th>Agente ID</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Criado em</th>
|
<th>Criado em</th>
|
||||||
@@ -229,17 +266,18 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })}
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="agentesTable">
|
<tbody id="agentesTable">
|
||||||
${agentes.map(a => `
|
${agentesWithPerms.map(a => `
|
||||||
<tr data-id="${a.id}">
|
<tr data-id="${a.id}" data-permissions='${JSON.stringify(a._perms)}'>
|
||||||
<td>${a.id}</td>
|
<td>${a.id}</td>
|
||||||
<td>${a.nome}</td>
|
<td>${a.nome}</td>
|
||||||
<td>${a.email}</td>
|
<td>${a.email}</td>
|
||||||
<td><span class="status-badge ${a.role === 'admin' ? 'admin' : a.role === 'corporate' ? 'corporate' : 'agent'}">${a.role === 'admin' ? 'Admin' : a.role === 'corporate' ? 'Corporate' : 'Agente'}</span></td>
|
<td><span class="status-badge ${a.role === 'admin' ? 'admin' : a.role === 'corporate' ? 'corporate' : 'agent'}">${a.role === 'admin' ? 'Admin' : a.role === 'corporate' ? 'Corporate' : 'Agente'}</span></td>
|
||||||
<td>${(a.role === 'admin' || a.role === 'corporate') ? '-' : a.agente_id}</td>
|
<td>${a._perms.map(k => '<span class="perm-badge">' + (panelLabels[k] || k) + '</span>').join(' ')}</td>
|
||||||
|
<td>${a.agente_id || '-'}</td>
|
||||||
<td><span class="status-badge ${a.ativo ? 'active' : 'inactive'}">${a.ativo ? 'Ativo' : 'Inativo'}</span></td>
|
<td><span class="status-badge ${a.ativo ? 'active' : 'inactive'}">${a.ativo ? 'Ativo' : 'Inativo'}</span></td>
|
||||||
<td>${a.created_at ? new Date(a.created_at).toLocaleDateString('pt-BR') : '-'}</td>
|
<td>${a.created_at ? new Date(a.created_at).toLocaleDateString('pt-BR') : '-'}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
${a.role === 'agente' ? `<a href="/corporate/emular/${a.agente_id}" class="btn-action btn-emular" title="Ver como este agente">Emular</a>` : ''}
|
${a.agente_id ? `<a href="/corporate/emular/${a.agente_id}" class="btn-action btn-emular" title="Ver como este agente">Emular</a>` : ''}
|
||||||
<button class="btn-action btn-edit" onclick="openEditModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}', '${a.email.replace(/'/g, "\\'")}', ${a.agente_id}, '${a.role || 'agente'}', event)">Editar</button>
|
<button class="btn-action btn-edit" onclick="openEditModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}', '${a.email.replace(/'/g, "\\'")}', ${a.agente_id}, '${a.role || 'agente'}', event)">Editar</button>
|
||||||
<button class="btn-action btn-toggle" onclick="toggleAgente(${a.id}, ${a.ativo})">${a.ativo ? 'Desativar' : 'Ativar'}</button>
|
<button class="btn-action btn-toggle" onclick="toggleAgente(${a.id}, ${a.ativo})">${a.ativo ? 'Desativar' : 'Ativar'}</button>
|
||||||
<button class="btn-action btn-password" onclick="openPasswordModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}')">Senha</button>
|
<button class="btn-action btn-password" onclick="openPasswordModal(${a.id}, '${a.nome.replace(/'/g, "\\'")}')">Senha</button>
|
||||||
@@ -273,13 +311,24 @@ ${buildFooter()}
|
|||||||
<input type="email" id="agentEmail" name="email" required placeholder="usuario@email.com">
|
<input type="email" id="agentEmail" name="email" required placeholder="usuario@email.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Tipo de Usuario</label>
|
<label>Tipo de Usuario (preset)</label>
|
||||||
<select id="agentRole" name="role" onchange="toggleAgenteIdField()">
|
<select id="agentRole" name="role" onchange="onRoleChange()">
|
||||||
<option value="agente">Agente</option>
|
<option value="agente">Agente</option>
|
||||||
<option value="corporate">Corporate</option>
|
<option value="corporate">Corporate</option>
|
||||||
<option value="admin">Administrador</option>
|
<option value="admin">Administrador</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permissoes de Acesso</label>
|
||||||
|
<div class="perm-grid" id="permGrid">
|
||||||
|
${PANELS.map(p => `
|
||||||
|
<label class="perm-check" id="permLabel_${p.key}">
|
||||||
|
<input type="checkbox" name="perm_${p.key}" value="${p.key}" onchange="onPermChange(this)">
|
||||||
|
${p.label}
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group" id="agenteIdGroup">
|
<div class="form-group" id="agenteIdGroup">
|
||||||
<label>Agente ID (Sistema)</label>
|
<label>Agente ID (Sistema)</label>
|
||||||
<input type="number" id="agentAgenteId" name="agente_id" placeholder="ID numerico do agente">
|
<input type="number" id="agentAgenteId" name="agente_id" placeholder="ID numerico do agente">
|
||||||
@@ -325,27 +374,66 @@ ${buildFooter()}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let isEditing = false;
|
var isEditing = false;
|
||||||
|
|
||||||
|
var DEFAULT_PERMS = ${JSON.stringify(DEFAULT_PERMISSIONS)};
|
||||||
|
|
||||||
function showAlert(message, type) {
|
function showAlert(message, type) {
|
||||||
const alert = document.getElementById('alertBox');
|
var alert = document.getElementById('alertBox');
|
||||||
alert.textContent = message;
|
alert.textContent = message;
|
||||||
alert.className = 'alert ' + type + ' show';
|
alert.className = 'alert ' + type + ' show';
|
||||||
setTimeout(() => { alert.className = 'alert'; }, 4000);
|
setTimeout(function() { alert.className = 'alert'; }, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPermCheckboxes() {
|
||||||
|
return document.querySelectorAll('#permGrid input[type="checkbox"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPermissions(keys) {
|
||||||
|
getPermCheckboxes().forEach(function(cb) {
|
||||||
|
cb.checked = keys.includes(cb.value);
|
||||||
|
updatePermLabel(cb);
|
||||||
|
});
|
||||||
|
toggleAgenteIdField();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPermChange(cb) {
|
||||||
|
updatePermLabel(cb);
|
||||||
|
toggleAgenteIdField();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePermLabel(cb) {
|
||||||
|
var label = document.getElementById('permLabel_' + cb.value);
|
||||||
|
if (label) {
|
||||||
|
if (cb.checked) label.classList.add('checked');
|
||||||
|
else label.classList.remove('checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedPermissions() {
|
||||||
|
var perms = [];
|
||||||
|
getPermCheckboxes().forEach(function(cb) {
|
||||||
|
if (cb.checked) perms.push(cb.value);
|
||||||
|
});
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRoleChange() {
|
||||||
|
var role = document.getElementById('agentRole').value;
|
||||||
|
var defaults = DEFAULT_PERMS[role] || [];
|
||||||
|
setPermissions(defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAgenteIdField() {
|
function toggleAgenteIdField() {
|
||||||
const role = document.getElementById('agentRole').value;
|
var perms = getSelectedPermissions();
|
||||||
const agenteIdGroup = document.getElementById('agenteIdGroup');
|
var agenteIdGroup = document.getElementById('agenteIdGroup');
|
||||||
const agenteIdInput = document.getElementById('agentAgenteId');
|
var agenteIdInput = document.getElementById('agentAgenteId');
|
||||||
// Admin and Corporate don't need agente_id
|
if (perms.includes('dashboard')) {
|
||||||
if (role === 'admin' || role === 'corporate') {
|
|
||||||
agenteIdGroup.style.display = 'none';
|
|
||||||
agenteIdInput.required = false;
|
|
||||||
agenteIdInput.value = '';
|
|
||||||
} else {
|
|
||||||
agenteIdGroup.style.display = 'block';
|
agenteIdGroup.style.display = 'block';
|
||||||
agenteIdInput.required = !isEditing;
|
agenteIdInput.required = !isEditing;
|
||||||
|
} else {
|
||||||
|
agenteIdGroup.style.display = 'none';
|
||||||
|
agenteIdInput.required = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +447,8 @@ function openCreateModal(event) {
|
|||||||
document.getElementById('agentRole').value = 'agente';
|
document.getElementById('agentRole').value = 'agente';
|
||||||
document.getElementById('senhaGroup').style.display = 'block';
|
document.getElementById('senhaGroup').style.display = 'block';
|
||||||
document.getElementById('agentSenha').required = true;
|
document.getElementById('agentSenha').required = true;
|
||||||
toggleAgenteIdField();
|
onRoleChange();
|
||||||
setTimeout(() => {
|
setTimeout(function() {
|
||||||
document.getElementById('agentModal').classList.add('active');
|
document.getElementById('agentModal').classList.add('active');
|
||||||
document.getElementById('agentNome').focus();
|
document.getElementById('agentNome').focus();
|
||||||
}, 10);
|
}, 10);
|
||||||
@@ -378,8 +466,16 @@ function openEditModal(id, nome, email, agenteId, role, event) {
|
|||||||
document.getElementById('agentRole').value = role || 'agente';
|
document.getElementById('agentRole').value = role || 'agente';
|
||||||
document.getElementById('senhaGroup').style.display = 'none';
|
document.getElementById('senhaGroup').style.display = 'none';
|
||||||
document.getElementById('agentSenha').required = false;
|
document.getElementById('agentSenha').required = false;
|
||||||
toggleAgenteIdField();
|
|
||||||
setTimeout(() => {
|
// Load permissions from data attribute
|
||||||
|
var row = document.querySelector('tr[data-id="' + id + '"]');
|
||||||
|
var perms = [];
|
||||||
|
if (row) {
|
||||||
|
try { perms = JSON.parse(row.getAttribute('data-permissions') || '[]'); } catch(e) {}
|
||||||
|
}
|
||||||
|
setPermissions(perms);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
document.getElementById('agentModal').classList.add('active');
|
document.getElementById('agentModal').classList.add('active');
|
||||||
document.getElementById('agentNome').focus();
|
document.getElementById('agentNome').focus();
|
||||||
}, 10);
|
}, 10);
|
||||||
@@ -399,25 +495,26 @@ function closeModal(id) {
|
|||||||
|
|
||||||
async function submitAgentForm(e) {
|
async function submitAgentForm(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = document.getElementById('agentId').value;
|
var id = document.getElementById('agentId').value;
|
||||||
const role = document.getElementById('agentRole').value;
|
var role = document.getElementById('agentRole').value;
|
||||||
const data = {
|
var permissions = getSelectedPermissions();
|
||||||
|
var data = {
|
||||||
nome: document.getElementById('agentNome').value,
|
nome: document.getElementById('agentNome').value,
|
||||||
email: document.getElementById('agentEmail').value,
|
email: document.getElementById('agentEmail').value,
|
||||||
role: role,
|
role: role,
|
||||||
|
permissions: permissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only agents need agente_id
|
// Agente ID
|
||||||
if (role === 'agente') {
|
var agenteIdVal = document.getElementById('agentAgenteId').value;
|
||||||
const agenteId = document.getElementById('agentAgenteId').value;
|
if (permissions.includes('dashboard')) {
|
||||||
if (!isEditing && !agenteId) {
|
if (!isEditing && !agenteIdVal) {
|
||||||
showAlert('Agente ID e obrigatorio para agentes', 'error');
|
showAlert('Agente ID e obrigatorio para acesso ao Meu Dashboard', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.agente_id = parseInt(agenteId) || 0;
|
data.agente_id = parseInt(agenteIdVal) || 0;
|
||||||
} else {
|
} else {
|
||||||
// Admin and Corporate don't have agente_id
|
data.agente_id = parseInt(agenteIdVal) || 0;
|
||||||
data.agente_id = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
@@ -425,18 +522,18 @@ async function submitAgentForm(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = isEditing ? '/admin/agentes/' + id : '/admin/agentes';
|
var url = isEditing ? '/admin/agentes/' + id : '/admin/agentes';
|
||||||
const method = isEditing ? 'PUT' : 'POST';
|
var method = isEditing ? 'PUT' : 'POST';
|
||||||
const res = await fetch(url, {
|
var res = await fetch(url, {
|
||||||
method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
var result = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showAlert(isEditing ? 'Usuario atualizado com sucesso!' : 'Usuario criado com sucesso!', 'success');
|
showAlert(isEditing ? 'Usuario atualizado com sucesso!' : 'Usuario criado com sucesso!', 'success');
|
||||||
closeModal('agentModal');
|
closeModal('agentModal');
|
||||||
setTimeout(() => location.reload(), 1000);
|
setTimeout(function() { location.reload(); }, 1000);
|
||||||
} else {
|
} else {
|
||||||
showAlert(result.error || 'Erro ao salvar usuario', 'error');
|
showAlert(result.error || 'Erro ao salvar usuario', 'error');
|
||||||
}
|
}
|
||||||
@@ -447,9 +544,9 @@ async function submitAgentForm(e) {
|
|||||||
|
|
||||||
async function submitPasswordForm(e) {
|
async function submitPasswordForm(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = document.getElementById('passwordAgentId').value;
|
var id = document.getElementById('passwordAgentId').value;
|
||||||
const newPassword = document.getElementById('newPassword').value;
|
var newPassword = document.getElementById('newPassword').value;
|
||||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
var confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
showAlert('As senhas nao coincidem', 'error');
|
showAlert('As senhas nao coincidem', 'error');
|
||||||
@@ -457,12 +554,12 @@ async function submitPasswordForm(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/admin/agentes/' + id, {
|
var res = await fetch('/admin/agentes/' + id, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ senha: newPassword })
|
body: JSON.stringify({ senha: newPassword })
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
var result = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showAlert('Senha redefinida com sucesso!', 'success');
|
showAlert('Senha redefinida com sucesso!', 'success');
|
||||||
closeModal('passwordModal');
|
closeModal('passwordModal');
|
||||||
@@ -475,19 +572,19 @@ async function submitPasswordForm(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAgente(id, currentStatus) {
|
async function toggleAgente(id, currentStatus) {
|
||||||
const action = currentStatus ? 'desativar' : 'ativar';
|
var action = currentStatus ? 'desativar' : 'ativar';
|
||||||
if (!confirm('Deseja ' + action + ' este usuario?')) return;
|
if (!confirm('Deseja ' + action + ' este usuario?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/admin/agentes/' + id, {
|
var res = await fetch('/admin/agentes/' + id, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ativo: currentStatus ? 0 : 1 })
|
body: JSON.stringify({ ativo: currentStatus ? 0 : 1 })
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
var result = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showAlert('Usuario ' + (currentStatus ? 'desativado' : 'ativado') + ' com sucesso!', 'success');
|
showAlert('Usuario ' + (currentStatus ? 'desativado' : 'ativado') + ' com sucesso!', 'success');
|
||||||
setTimeout(() => location.reload(), 1000);
|
setTimeout(function() { location.reload(); }, 1000);
|
||||||
} else {
|
} else {
|
||||||
showAlert(result.error || 'Erro ao alterar status', 'error');
|
showAlert(result.error || 'Erro ao alterar status', 'error');
|
||||||
}
|
}
|
||||||
@@ -497,25 +594,25 @@ async function toggleAgente(id, currentStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close modal on overlay click
|
// Close modal on overlay click
|
||||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
|
||||||
overlay.addEventListener('click', (e) => {
|
overlay.addEventListener('click', function(e) {
|
||||||
if (e.target === overlay) overlay.classList.remove('active');
|
if (e.target === overlay) overlay.classList.remove('active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent clicks inside modal from closing it
|
// Prevent clicks inside modal from closing it
|
||||||
document.querySelectorAll('.modal').forEach(modal => {
|
document.querySelectorAll('.modal').forEach(function(modal) {
|
||||||
modal.addEventListener('click', (e) => e.stopPropagation());
|
modal.addEventListener('click', function(e) { e.stopPropagation(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal on Escape key
|
// Close modal on Escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
document.querySelectorAll('.modal-overlay.active').forEach(m => m.classList.remove('active'));
|
document.querySelectorAll('.modal-overlay.active').forEach(function(m) { m.classList.remove('active'); });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
<\/script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ ${buildHead('Provider Performance', pageCSS, pageScripts)}
|
|||||||
</head>
|
</head>
|
||||||
<body class="trading-console">
|
<body class="trading-console">
|
||||||
|
|
||||||
${buildHeader({ role: role, userName: user.nome, activePage: 'providers' })}
|
${buildHeader({ role: role, userName: user.nome, activePage: 'providers', permissions: user.permissions || [] })}
|
||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
|
|
||||||
|
|||||||
22
src/auth.js
22
src/auth.js
@@ -10,11 +10,13 @@ const SALT_ROUNDS = 10;
|
|||||||
/**
|
/**
|
||||||
* Cria um novo usuario (agente, corporate ou admin)
|
* 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 hash = await bcrypt.hash(senha, SALT_ROUNDS);
|
||||||
|
const perms = permissions || DEFAULT_PERMISSIONS[role] || DEFAULT_PERMISSIONS.agente;
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
'INSERT INTO agentes (email, senha_hash, agente_id, nome, role) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO agentes (email, senha_hash, agente_id, nome, role, permissions) VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
).run(email, hash, agenteId, nome, role);
|
).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
|
* Middleware simples - apenas verifica se esta logado
|
||||||
* Retrocompatibilidade com requireAuth
|
* Retrocompatibilidade com requireAuth
|
||||||
@@ -81,5 +96,6 @@ module.exports = {
|
|||||||
authenticate,
|
authenticate,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireRole,
|
requireRole,
|
||||||
|
requirePermission,
|
||||||
updatePassword
|
updatePassword
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ ${isEmulating ? `
|
|||||||
<a href="${backUrl}">Voltar ao BI - CCC</a>
|
<a href="${backUrl}">Voltar ao BI - CCC</a>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${buildHeader({ role, userName: agente.nome, activePage: 'dashboard', showNav: !isEmulating })}
|
${buildHeader({ role, userName: agente.nome, activePage: 'dashboard', showNav: !isEmulating, permissions: agente.permissions || [] })}
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div class="filters-inner">
|
<div class="filters-inner">
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ try {
|
|||||||
// Column already exists, ignore
|
// 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
|
// Legacy table - keep for reference but no longer used
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS admins (
|
CREATE TABLE IF NOT EXISTS admins (
|
||||||
|
|||||||
22
src/panels.js
Normal file
22
src/panels.js
Normal file
@@ -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 };
|
||||||
@@ -383,16 +383,18 @@ const headerCSS = `
|
|||||||
* @param {string} options.userName - Nome do usuário
|
* @param {string} options.userName - Nome do usuário
|
||||||
* @param {string} options.activePage - Página ativa para nav
|
* @param {string} options.activePage - Página ativa para nav
|
||||||
* @param {boolean} options.showNav - Mostrar navegação
|
* @param {boolean} options.showNav - Mostrar navegação
|
||||||
|
* @param {string[]} options.permissions - Paineis permitidos para o usuario
|
||||||
*/
|
*/
|
||||||
function buildHeader(options = {}) {
|
function buildHeader(options = {}) {
|
||||||
const { role = 'agente', userName = '', activePage = '', showNav = true } = options;
|
const { role = 'agente', userName = '', activePage = '', showNav = true, permissions = [] } = options;
|
||||||
const isAdmin = role === 'admin';
|
const { PANELS } = require('./panels');
|
||||||
const isCorporate = role === 'corporate';
|
|
||||||
|
|
||||||
// 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';
|
let headerClass = 'agent';
|
||||||
if (isAdmin) headerClass = 'admin';
|
if (hasAdminPanels) headerClass = 'admin';
|
||||||
else if (isCorporate) headerClass = 'corporate';
|
else if (permissions.includes('corporate')) headerClass = 'corporate';
|
||||||
|
|
||||||
const initials = userName
|
const initials = userName
|
||||||
.split(' ')
|
.split(' ')
|
||||||
@@ -401,45 +403,29 @@ function buildHeader(options = {}) {
|
|||||||
.join('')
|
.join('')
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
// Admin navigation: Corporate Dashboard + BI + Clients + Providers + Users
|
// Build nav dynamically from permissions
|
||||||
const adminNav = `
|
const activeMap = { 'dashboard': 'dashboard', 'corporate': 'dashboard', 'usuarios': 'users' };
|
||||||
<nav class="header-nav">
|
const navLinks = PANELS
|
||||||
<a href="/corporate" class="${activePage === 'dashboard' ? 'active' : ''}">Corporate</a>
|
.filter(p => permissions.includes(p.key))
|
||||||
<a href="/admin/bi" class="${activePage === 'bi' ? 'active' : ''}">BI Executive</a>
|
.map(p => {
|
||||||
<a href="/admin/cliente" class="${activePage === 'cliente' ? 'active' : ''}">Clientes</a>
|
const pageKey = activeMap[p.key] || p.key;
|
||||||
<a href="/admin/providers" class="${activePage === 'providers' ? 'active' : ''}">Providers</a>
|
const isActive = activePage === pageKey || activePage === p.key;
|
||||||
<a href="/admin" class="${activePage === 'users' ? 'active' : ''}">Usuarios</a>
|
return `<a href="${p.route}" class="${isActive ? 'active' : ''}">${p.label}</a>`;
|
||||||
</nav>
|
})
|
||||||
`;
|
.join('\n ');
|
||||||
|
const nav = `<nav class="header-nav">${navLinks}</nav>`;
|
||||||
|
|
||||||
// Corporate navigation: Dashboard only
|
// Home URL: first permitted panel
|
||||||
const corporateNav = `
|
const firstPanel = PANELS.find(p => permissions.includes(p.key));
|
||||||
<nav class="header-nav">
|
const homeUrl = firstPanel ? firstPanel.route : '/login';
|
||||||
<a href="/corporate" class="${activePage === 'dashboard' ? 'active' : ''}">Dashboard</a>
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Agent navigation: Just their dashboard
|
// Show alert bell if user has BI permission
|
||||||
const agentNav = `
|
const showAlerts = permissions.includes('bi');
|
||||||
<nav class="header-nav">
|
|
||||||
<a href="/dashboard" class="active">Meu Dashboard</a>
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// Role label for display
|
// Role label for display
|
||||||
let roleLabel = 'Agente';
|
let roleLabel = 'Agente';
|
||||||
if (isAdmin) roleLabel = 'Admin';
|
if (role === 'admin') roleLabel = 'Admin';
|
||||||
else if (isCorporate) roleLabel = 'Corporate';
|
else if (role === 'corporate') roleLabel = 'Corporate';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<header class="app-header ${headerClass}">
|
<header class="app-header ${headerClass}">
|
||||||
@@ -453,7 +439,7 @@ function buildHeader(options = {}) {
|
|||||||
</a>
|
</a>
|
||||||
${showNav ? nav : ''}
|
${showNav ? nav : ''}
|
||||||
<div class="header-user">
|
<div class="header-user">
|
||||||
${isAdmin ? `
|
${showAlerts ? `
|
||||||
<div class="alert-bell" id="alertBell" onclick="toggleAlertDropdown()" title="Alerts">
|
<div class="alert-bell" id="alertBell" onclick="toggleAlertDropdown()" title="Alerts">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
||||||
<span class="alert-badge" id="alertBadge" style="display:none">0</span>
|
<span class="alert-badge" id="alertBadge" style="display:none">0</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user