feat: Implementação completa do NoIdle - Cliente, Backend e Scripts

- Cliente Windows com modo silencioso e auto-start robusto
- Backend Node.js + API REST
- Frontend Next.js + Dashboard
- Scripts PowerShell de configuração e diagnóstico
- Documentação completa
- Build scripts para Windows e Linux
- Solução de auto-start após reinicialização

Resolução do problema: Cliente não voltava ativo após reboot
Solução: Registro do Windows + Task Scheduler + Modo silencioso
This commit is contained in:
root
2025-11-16 22:56:35 +00:00
commit 6086c13be7
58 changed files with 10693 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
// REGISTRAR ATIVIDADE (sem auth - usado pelo client)
router.post('/', async (req, res) => {
try {
const { device_id, window_title, application_name, idle_time_seconds, urls } = req.body;
if (!device_id) {
return res.status(400).json({ error: 'device_id é obrigatório' });
}
const deviceCheck = await query(
'SELECT id, company_id FROM devices WHERE device_id = $1',
[device_id]
);
if (deviceCheck.rows.length === 0) {
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
const device = deviceCheck.rows[0];
const result = await query(
`INSERT INTO activities (device_id, company_id, window_title, application_name, idle_time_seconds, timestamp)
VALUES ($1, $2, $3, $4, $5, NOW()) RETURNING *`,
[device.id, device.company_id, window_title, application_name, idle_time_seconds || 0]
);
// Se tem URLs, salvar também
if (urls && Array.isArray(urls) && urls.length > 0) {
for (const urlData of urls) {
await query(
`INSERT INTO browsing_history (device_id, url, title, browser, visited_at)
VALUES ($1, $2, $3, $4, NOW())`,
[device_id, urlData.url, urlData.title, urlData.browser]
);
}
console.log(`📊 ${urls.length} URLs registradas para ${device_id}`);
}
res.status(201).json({ success: true, activity: result.rows[0] });
} catch (error) {
console.error('Erro ao registrar atividade:', error);
res.status(500).json({ error: 'Erro ao registrar atividade' });
}
});
// LISTAR ATIVIDADES
router.get('/', authenticateToken, async (req, res) => {
try {
const company_id = req.user.company_id;
const { device_id, limit = 100 } = req.query;
let queryText = `SELECT a.*, d.device_name FROM activities a
JOIN devices d ON a.device_id = d.id
WHERE a.company_id = $1`;
const params = [company_id];
if (device_id) {
params.push(device_id);
queryText += ` AND a.device_id = $${params.length}`;
}
queryText += ` ORDER BY a.timestamp DESC LIMIT $${params.length + 1}`;
params.push(limit);
const result = await query(queryText, params);
res.json({ success: true, activities: result.rows });
} catch (error) {
console.error('Erro ao listar atividades:', error);
res.status(500).json({ error: 'Erro ao listar atividades' });
}
});
module.exports = router;

413
backend/routes/activity.js Normal file
View File

@@ -0,0 +1,413 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
const PDFDocument = require('pdfkit');
// LOG DE ATIVIDADE (com URLs opcionais)
router.post('/log', async (req, res) => {
try {
const { device_id, window_title, application_name, idle_time_seconds, urls } = req.body;
console.log(`📥 POST /api/activity/log - Device: ${device_id}`);
console.log(`📥 Body:`, { device_id, window_title, application_name, idle_time_seconds, urls_count: urls?.length || 0 });
if (!device_id) {
console.log('❌ device_id não fornecido');
return res.status(400).json({ error: 'device_id é obrigatório' });
}
// Buscar device
const deviceResult = await query(
'SELECT id, company_id, device_name FROM devices WHERE device_id = $1',
[device_id]
);
if (deviceResult.rows.length === 0) {
console.log(`❌ Dispositivo ${device_id} não encontrado`);
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
console.log(`✅ Dispositivo encontrado: ${deviceResult.rows[0].device_name} (ID: ${deviceResult.rows[0].id})`);
const device = deviceResult.rows[0];
// Inserir atividade
await query(
`INSERT INTO activities (device_id, company_id, window_title, application_name, idle_time_seconds, timestamp)
VALUES ($1, $2, $3, $4, $5, NOW())`,
[device.id, device.company_id, window_title, application_name, idle_time_seconds || 0]
);
// Atualizar last_seen e is_active quando receber atividade (como heartbeat alternativo)
await query(
'UPDATE devices SET last_seen = NOW(), is_active = true WHERE device_id = $1',
[device_id]
);
// Validar se está recebendo dados reais (não apenas "System Idle")
if (window_title === 'System Idle' || window_title === '[IDLE]' || !window_title || window_title.trim() === '') {
console.log(`⚠️ ATENÇÃO: Recebendo atividade com window_title inválido: "${window_title}"`);
console.log(`⚠️ O cliente deve capturar o título real da janela ativa!`);
}
if (application_name === '[IDLE]' || !application_name || application_name.trim() === '') {
console.log(`⚠️ ATENÇÃO: Recebendo atividade com application_name inválido: "${application_name}"`);
console.log(`⚠️ O cliente deve capturar o executável real do processo ativo!`);
}
console.log(`✅ Atividade registrada: ${application_name} - ${window_title}`);
// Se tem URLs, salvar também
if (urls && Array.isArray(urls) && urls.length > 0) {
let urlsSaved = 0;
for (const urlData of urls) {
try {
if (urlData.url && urlData.url.trim() !== '') {
await query(
`INSERT INTO browsing_history (device_id, url, title, browser, visited_at)
VALUES ($1, $2, $3, $4, NOW())`,
[device_id, urlData.url, urlData.title || '', urlData.browser || 'Chrome']
);
urlsSaved++;
}
} catch (urlError) {
console.error(`❌ Erro ao salvar URL ${urlData.url}:`, urlError.message);
// Continua tentando salvar outras URLs mesmo se uma falhar
}
}
if (urlsSaved > 0) {
console.log(`📊 ${urlsSaved} URLs registradas para ${device_id}`);
}
}
res.json({ success: true, message: 'Atividade registrada' });
} catch (error) {
console.error('Erro ao registrar atividade:', error);
res.status(500).json({ error: 'Erro ao registrar atividade' });
}
});
// LOG DE SESSÃO (logon/logoff)
router.post('/session', async (req, res) => {
try {
const { device_id, event_type, username } = req.body;
if (!device_id || !event_type) {
return res.status(400).json({ error: 'device_id e event_type são obrigatórios' });
}
if (!['logon', 'logoff'].includes(event_type)) {
return res.status(400).json({ error: 'event_type deve ser logon ou logoff' });
}
// Verificar se device existe
const deviceCheck = await query(
'SELECT device_id FROM devices WHERE device_id = $1',
[device_id]
);
if (deviceCheck.rows.length === 0) {
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
// Registrar evento
try {
await query(
`INSERT INTO session_events (device_id, event_type, username, event_time)
VALUES ($1, $2, $3, NOW())`,
[device_id, event_type, username]
);
console.log(`🔐 Evento de sessão: ${event_type} - ${device_id} (${username || 'N/A'})`);
res.json({ success: true, message: `Evento ${event_type} registrado` });
} catch (dbError) {
console.error(`❌ Erro ao registrar evento de sessão:`, dbError.message);
if (dbError.code === '42501') {
console.error(`⚠️ ERRO DE PERMISSÃO: A tabela session_events existe mas o usuário do banco não tem permissão.`);
console.error(`⚠️ Execute no PostgreSQL: GRANT ALL ON session_events TO [usuario_do_banco];`);
}
// Retorna sucesso mesmo com erro de banco para não quebrar o cliente
res.json({ success: true, message: `Evento ${event_type} recebido (erro ao salvar no banco)` });
}
} catch (error) {
console.error('Erro ao registrar sessão:', error);
res.status(500).json({ error: 'Erro ao registrar sessão' });
}
});
// LISTAR ATIVIDADES
router.get('/', authenticateToken, async (req, res) => {
try {
const result = await query(
`SELECT a.*, d.device_name, d.hostname
FROM activities a
JOIN devices d ON a.device_id = d.id
WHERE a.company_id = $1
ORDER BY a.timestamp DESC
LIMIT 100`,
[req.user.company_id]
);
res.json({ success: true, activities: result.rows });
} catch (error) {
console.error('Erro ao listar atividades:', error);
res.status(500).json({ error: 'Erro ao listar atividades' });
}
});
// LISTAR HISTÓRICO DE NAVEGAÇÃO
router.get('/browsing', authenticateToken, async (req, res) => {
try {
const result = await query(
`SELECT bh.*, d.device_name, d.hostname
FROM browsing_history bh
JOIN devices d ON bh.device_id = d.device_id
WHERE d.company_id = $1
ORDER BY bh.visited_at DESC
LIMIT 200`,
[req.user.company_id]
);
res.json({ success: true, history: result.rows });
} catch (error) {
console.error('Erro ao listar histórico:', error);
res.status(500).json({ error: 'Erro ao listar histórico' });
}
});
// LISTAR EVENTOS DE SESSÃO
router.get('/sessions', authenticateToken, async (req, res) => {
try {
const result = await query(
`SELECT se.*, d.device_name, d.hostname
FROM session_events se
JOIN devices d ON se.device_id = d.device_id
WHERE d.company_id = $1
ORDER BY se.event_time DESC
LIMIT 100`,
[req.user.company_id]
);
res.json({ success: true, sessions: result.rows });
} catch (error) {
console.error('Erro ao listar sessões:', error);
res.status(500).json({ error: 'Erro ao listar sessões' });
}
});
// GERAR RELATÓRIO PDF POR PERÍODO
router.get('/report/pdf', authenticateToken, async (req, res) => {
try {
const { startDate, endDate, deviceId } = req.query;
const company_id = req.user.company_id;
console.log(`📄 Requisição de relatório PDF - Company: ${company_id}, Start: ${startDate}, End: ${endDate}, Device: ${deviceId || 'Todos'}`);
if (!startDate || !endDate) {
console.log('❌ Datas não fornecidas');
return res.status(400).json({ error: 'Data inicial e data final são obrigatórias' });
}
// Construir query com filtros
let queryText = `
SELECT a.*, d.id as device_table_id, d.device_id as device_string_id, d.device_name, d.hostname, u.name as user_name, u.email as user_email
FROM activities a
JOIN devices d ON a.device_id = d.id
LEFT JOIN users u ON d.user_id = u.id
WHERE a.company_id = $1
AND a.timestamp >= $2::timestamp
AND a.timestamp <= $3::timestamp
`;
const queryParams = [company_id, startDate, endDate];
if (deviceId) {
queryText += ` AND d.id = $4`;
queryParams.push(parseInt(deviceId));
}
queryText += ` ORDER BY a.timestamp DESC`;
console.log(`📊 Buscando atividades...`);
const result = await query(queryText, queryParams);
const activities = result.rows;
console.log(`${activities.length} atividades encontradas`);
// Buscar histórico de navegação no período (com tratamento de erro)
let browsingHistory = [];
try {
let browsingQuery = `
SELECT bh.*, d.device_name, d.hostname
FROM browsing_history bh
JOIN devices d ON bh.device_id = d.device_id
WHERE d.company_id = $1
AND bh.visited_at >= $2::timestamp
AND bh.visited_at <= $3::timestamp
`;
const browsingParams = [company_id, startDate, endDate];
if (deviceId) {
// Se deviceId foi fornecido, precisamos buscar o device_id string correspondente
const deviceCheck = await query('SELECT device_id FROM devices WHERE id = $1', [parseInt(deviceId)]);
if (deviceCheck.rows.length > 0) {
browsingQuery += ` AND bh.device_id = $4`;
browsingParams.push(deviceCheck.rows[0].device_id);
}
}
browsingQuery += ` ORDER BY bh.visited_at DESC LIMIT 500`;
console.log(`📊 Buscando histórico de navegação...`);
const browsingResult = await query(browsingQuery, browsingParams);
browsingHistory = browsingResult.rows;
console.log(`${browsingHistory.length} URLs encontradas`);
} catch (browsingError) {
console.error(`⚠️ Erro ao buscar histórico de navegação (continuando sem histórico):`, browsingError.message);
// Continua sem histórico de navegação se houver erro
browsingHistory = [];
}
// Criar PDF
console.log(`📄 Criando documento PDF...`);
const doc = new PDFDocument({ margin: 50 });
// Configurar headers para download ANTES de fazer pipe
const filename = `relatorio_atividades_${startDate}_${endDate}.pdf`;
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
// Pipe para response
doc.pipe(res);
// Tratamento de erros no stream do PDF
doc.on('error', (err) => {
console.error('❌ Erro no stream do PDF:', err);
if (!res.headersSent) {
res.status(500).json({ error: 'Erro ao gerar PDF' });
}
});
// Cabeçalho
doc.fontSize(20).text('Relatório de Atividades', { align: 'center' });
doc.moveDown();
doc.fontSize(12).text(`Período: ${new Date(startDate).toLocaleDateString('pt-BR')} até ${new Date(endDate).toLocaleDateString('pt-BR')}`, { align: 'center' });
doc.moveDown(2);
// Estatísticas
const totalActivities = activities.length;
const totalBrowsing = browsingHistory.length;
const uniqueDevices = [...new Set(activities.map(a => a.device_name))].length;
doc.fontSize(14).text('Estatísticas', { underline: true });
doc.fontSize(10);
doc.text(`Total de Atividades: ${totalActivities}`);
doc.text(`Total de URLs Visitadas: ${totalBrowsing}`);
doc.text(`Dispositivos Monitorados: ${uniqueDevices}`);
doc.moveDown(2);
// Atividades
if (activities.length > 0) {
doc.fontSize(14).text('Atividades Registradas', { underline: true });
doc.moveDown(0.5);
let yPosition = doc.y;
const pageHeight = doc.page.height;
const margin = 50;
const rowHeight = 15;
let isFirstPage = true;
activities.forEach((activity, index) => {
// Verificar se precisa de nova página
if (yPosition + rowHeight * 3 > pageHeight - margin) {
doc.addPage();
yPosition = margin;
isFirstPage = false;
}
const date = new Date(activity.timestamp).toLocaleString('pt-BR');
const device = activity.device_name || activity.hostname || 'N/A';
const app = activity.application_name || 'N/A';
const window = activity.window_title || 'N/A';
const idle = `${activity.idle_time_seconds || 0}s`;
const user = activity.user_name ? `${activity.user_name} (${activity.user_email})` : 'N/A';
doc.fontSize(9);
doc.text(`${date}`, 50, yPosition);
doc.text(`Dispositivo: ${device}`, 200, yPosition);
doc.text(`Usuário: ${user}`, 350, yPosition);
yPosition += 12;
doc.text(`Aplicativo: ${app}`, 50, yPosition);
doc.text(`Janela: ${window}`, 200, yPosition);
doc.text(`Ociosidade: ${idle}`, 450, yPosition);
yPosition += 15;
// Linha separadora
doc.moveTo(50, yPosition).lineTo(550, yPosition).stroke();
yPosition += 5;
});
} else {
doc.fontSize(12).text('Nenhuma atividade registrada no período selecionado.');
}
// Histórico de Navegação
if (browsingHistory.length > 0) {
doc.addPage();
doc.fontSize(14).text('Histórico de Navegação', { underline: true });
doc.moveDown(0.5);
let yPos = doc.y;
browsingHistory.slice(0, 200).forEach((item) => {
if (yPos + 40 > doc.page.height - margin) {
doc.addPage();
yPos = margin;
}
const date = new Date(item.visited_at).toLocaleString('pt-BR');
const device = item.device_name || item.device_id || 'N/A';
const browser = item.browser || 'N/A';
const url = item.url || 'N/A';
const title = item.title || 'N/A';
doc.fontSize(9);
doc.text(`${date} - ${device} - ${browser}`, 50, yPos);
yPos += 12;
doc.text(`URL: ${url}`, 50, yPos, { width: 500 });
yPos += 12;
doc.text(`Título: ${title}`, 50, yPos, { width: 500 });
yPos += 15;
doc.moveTo(50, yPos).lineTo(550, yPos).stroke();
yPos += 5;
});
}
// Rodapé será adicionado após finalizar o documento
// (pdfkit não permite adicionar rodapé dinamicamente durante a criação)
// Finalizar PDF
console.log(`📄 Finalizando PDF...`);
doc.end();
console.log(`✅ Relatório PDF gerado com sucesso: ${totalActivities} atividades, ${totalBrowsing} URLs`);
} catch (error) {
console.error('❌ Erro ao gerar relatório PDF:', error);
console.error('❌ Stack:', error.stack);
// Se os headers já foram enviados, não podemos enviar JSON
if (!res.headersSent) {
res.status(500).json({
error: 'Erro ao gerar relatório PDF',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
});
} else {
// Se já começou a enviar o PDF, apenas logar o erro
console.error('⚠️ Headers já enviados, não é possível retornar erro JSON');
}
}
});
module.exports = router;

59
backend/routes/auth.js Normal file
View File

@@ -0,0 +1,59 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { query } = require('../config/database');
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email e senha são obrigatórios' });
}
const result = await query(
'SELECT id, email, name, password, role, company_id, is_active FROM admin_users WHERE email = $1',
[email.toLowerCase()]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Credenciais inválidas' });
}
const user = result.rows[0];
if (!user.is_active) {
return res.status(403).json({ error: 'Usuário inativo' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Credenciais inválidas' });
}
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role, company_id: user.company_id },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
token,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
company_id: user.company_id
}
});
} catch (error) {
console.error('Erro no login:', error);
res.status(500).json({ error: 'Erro ao fazer login' });
}
});
module.exports = router;

130
backend/routes/dashboard.js Normal file
View File

@@ -0,0 +1,130 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
// GET /api/dashboard - Estatísticas do dashboard
router.get('/', authenticateToken, async (req, res) => {
try {
const company_id = req.user.company_id;
console.log('GET /api/dashboard - Company ID:', company_id);
// Estatísticas de dispositivos
const devicesStats = await query(
`SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE is_active = true) as active,
COUNT(*) FILTER (WHERE is_active = false) as inactive,
COUNT(*) FILTER (WHERE user_id IS NOT NULL) as assigned
FROM devices
WHERE company_id = $1`,
[company_id]
);
// Estatísticas de usuários
const usersStats = await query(
`SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE is_active = true) as active,
COUNT(*) FILTER (WHERE is_active = false) as inactive
FROM users
WHERE company_id = $1`,
[company_id]
);
// Estatísticas de atividades (últimas 24 horas)
const activitiesStats = await query(
`SELECT
COUNT(*) as total_24h,
COUNT(*) FILTER (WHERE activity_type = 'idle') as idle_count,
COUNT(*) FILTER (WHERE activity_type = 'active') as active_count
FROM activities
WHERE company_id = $1
AND created_at >= NOW() - INTERVAL '24 hours'`,
[company_id]
);
// Estatísticas de chaves de ativação
const keysStats = await query(
`SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE is_used = true) as used,
COUNT(*) FILTER (WHERE is_used = false) as available
FROM activation_keys
WHERE company_id = $1`,
[company_id]
);
// Dispositivos recentes (últimos 5)
const recentDevices = await query(
`SELECT d.*, u.name as user_name, u.email as user_email
FROM devices d
LEFT JOIN users u ON d.user_id = u.id
WHERE d.company_id = $1
ORDER BY d.created_at DESC
LIMIT 5`,
[company_id]
);
// Atividades recentes (últimas 10)
const recentActivities = await query(
`SELECT a.*, d.device_name, u.name as user_name
FROM activities a
LEFT JOIN devices d ON a.device_id = d.device_id
LEFT JOIN users u ON d.user_id = u.id
WHERE a.company_id = $1
ORDER BY a.created_at DESC
LIMIT 10`,
[company_id]
);
// Converter valores string para números
const devicesData = devicesStats.rows[0];
const usersData = usersStats.rows[0];
const activitiesData = activitiesStats.rows[0];
const keysData = keysStats.rows[0];
const response = {
success: true,
stats: {
devices: {
total: parseInt(devicesData.total) || 0,
active: parseInt(devicesData.active) || 0,
inactive: parseInt(devicesData.inactive) || 0,
assigned: parseInt(devicesData.assigned) || 0
},
users: {
total: parseInt(usersData.total) || 0,
active: parseInt(usersData.active) || 0,
inactive: parseInt(usersData.inactive) || 0
},
activities: {
total_24h: parseInt(activitiesData.total_24h) || 0,
idle_count: parseInt(activitiesData.idle_count) || 0,
active_count: parseInt(activitiesData.active_count) || 0
},
keys: {
total: parseInt(keysData.total) || 0,
used: parseInt(keysData.used) || 0,
available: parseInt(keysData.available) || 0
}
},
recent: {
devices: recentDevices.rows,
activities: recentActivities.rows
}
};
console.log('GET /api/dashboard - Resposta:', JSON.stringify(response, null, 2));
res.json(response);
} catch (error) {
console.error('Erro ao obter dados do dashboard:', error);
console.error('Erro detalhado:', error.message);
console.error('Stack:', error.stack);
res.status(500).json({ error: 'Erro ao obter dados do dashboard', details: error.message });
}
});
module.exports = router;

263
backend/routes/devices.js Normal file
View File

@@ -0,0 +1,263 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
// LISTAR DISPOSITIVOS
router.get('/', authenticateToken, async (req, res) => {
try {
const company_id = req.user.company_id;
// Primeiro, marcar dispositivos como inativos se não receberam heartbeat há mais de 5 minutos
await query(
`UPDATE devices
SET is_active = false
WHERE company_id = $1
AND (last_seen IS NULL OR last_seen < NOW() - INTERVAL '5 minutes')
AND is_active = true`,
[company_id]
);
// Agora buscar dispositivos com status atualizado
const result = await query(
`SELECT d.*,
u.name as user_name,
u.email as user_email,
CASE
WHEN d.last_seen IS NULL THEN false
WHEN d.last_seen < NOW() - INTERVAL '5 minutes' THEN false
ELSE d.is_active
END as real_status
FROM devices d
LEFT JOIN users u ON d.user_id = u.id
WHERE d.company_id = $1
ORDER BY d.created_at DESC`,
[company_id]
);
// Atualizar is_active com o real_status calculado
const devices = result.rows.map(device => ({
...device,
is_active: device.real_status,
real_status: undefined // Remover campo auxiliar
}));
res.json({ success: true, devices });
} catch (error) {
console.error('Erro ao listar dispositivos:', error);
res.status(500).json({ error: 'Erro ao listar dispositivos' });
}
});
// ATIVAR DISPOSITIVO (sem auth - usado pelo client)
router.post('/activate', async (req, res) => {
try {
const { activation_key, device_info } = req.body;
if (!activation_key) {
return res.status(400).json({ error: 'Chave de ativação é obrigatória' });
}
// Buscar chave (SEM verificar is_used ou expires_at)
const keyResult = await query(
'SELECT * FROM activation_keys WHERE key = $1',
[activation_key]
);
if (keyResult.rows.length === 0) {
return res.status(400).json({ error: 'Chave de ativação inválida' });
}
const key = keyResult.rows[0];
const device_id = `DEV-${Date.now()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
const device_name = device_info?.device_name || device_info?.hostname || 'Dispositivo';
const hostname = device_info?.hostname || device_name;
const username = device_info?.username || '';
// Criar device
const deviceResult = await query(
`INSERT INTO devices (device_id, device_name, hostname, username, company_id, is_active, created_at)
VALUES ($1, $2, $3, $4, $5, true, NOW()) RETURNING *`,
[device_id, device_name, hostname, username, key.company_id]
);
const device = deviceResult.rows[0];
// Incrementar contador de devices da chave
await query(
'UPDATE activation_keys SET devices_count = devices_count + 1 WHERE id = $1',
[key.id]
);
console.log(`✅ Dispositivo ativado: ${device.device_name} (${device_id})`);
res.status(201).json({
success: true,
device_id: device.device_id,
device_name: device.device_name,
company_id: device.company_id,
message: 'Dispositivo ativado com sucesso'
});
} catch (error) {
console.error('Erro ao ativar dispositivo:', error);
res.status(500).json({ error: 'Erro ao ativar dispositivo', details: error.message });
}
});
// HEARTBEAT (sem auth - usado pelo client)
router.post('/heartbeat', async (req, res) => {
try {
const { device_id } = req.body;
if (!device_id) {
return res.status(400).json({ error: 'device_id é obrigatório' });
}
const result = await query(
'UPDATE devices SET last_seen = NOW(), is_active = true WHERE device_id = $1 RETURNING device_name',
[device_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
console.log(`💓 Heartbeat: ${result.rows[0].device_name}`);
res.json({ success: true, message: 'Heartbeat registrado' });
} catch (error) {
console.error('Erro heartbeat:', error);
res.status(500).json({ error: 'Erro ao registrar heartbeat' });
}
});
// ATUALIZAR DISPOSITIVO
router.put('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { device_name, user_id, team_id, is_active } = req.body;
const company_id = req.user.company_id;
console.log(`🔵 PUT /api/devices/${id} - Requisição recebida`);
console.log(`🔵 Body recebido:`, JSON.stringify({ device_name, user_id, team_id, is_active }, null, 2));
console.log(`🔵 Company ID:`, company_id);
console.log(`🔵 ID recebido (tipo):`, typeof id, id);
// Verificar se o dispositivo existe e pertence à company
// Aceitar tanto id numérico quanto device_id string
let deviceCheck;
if (isNaN(id)) {
// Se não é número, pode ser device_id
deviceCheck = await query(
'SELECT id FROM devices WHERE device_id = $1 AND company_id = $2',
[id, company_id]
);
} else {
// Se é número, é o id numérico
deviceCheck = await query(
'SELECT id FROM devices WHERE id = $1 AND company_id = $2',
[parseInt(id), company_id]
);
}
if (deviceCheck.rows.length === 0) {
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
const deviceId = deviceCheck.rows[0].id; // Usar o ID numérico real
// Se user_id foi fornecido, verificar se o usuário existe e pertence à mesma company
if (user_id !== undefined && user_id !== null) {
const userCheck = await query(
'SELECT id FROM users WHERE id = $1 AND company_id = $2',
[user_id, company_id]
);
if (userCheck.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado ou não pertence à sua empresa' });
}
}
// Construir query dinamicamente baseado nos campos fornecidos
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (device_name !== undefined) {
updateFields.push(`device_name = $${paramCount++}`);
updateValues.push(device_name);
}
if (user_id !== undefined) {
// Converter string vazia para null, e garantir que seja número ou null
let finalUserId = null;
if (user_id !== null && user_id !== '' && user_id !== undefined) {
finalUserId = parseInt(user_id);
if (isNaN(finalUserId)) {
return res.status(400).json({ error: 'user_id deve ser um número válido' });
}
}
updateFields.push(`user_id = $${paramCount++}`);
updateValues.push(finalUserId);
console.log(`🔵 user_id processado: ${user_id} -> ${finalUserId}`);
}
if (team_id !== undefined) {
updateFields.push(`team_id = $${paramCount++}`);
updateValues.push(team_id === null || team_id === '' ? null : team_id);
}
if (is_active !== undefined) {
updateFields.push(`is_active = $${paramCount++}`);
updateValues.push(is_active);
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'Nenhum campo para atualizar' });
}
updateValues.push(deviceId, company_id);
const queryText = `UPDATE devices
SET ${updateFields.join(', ')}
WHERE id = $${paramCount++} AND company_id = $${paramCount}
RETURNING *,
(SELECT name FROM users WHERE id = devices.user_id) as user_name,
(SELECT email FROM users WHERE id = devices.user_id) as user_email`;
const result = await query(queryText, updateValues);
console.log(`✅ Dispositivo ${deviceId} (${id}) atualizado com sucesso`);
res.json({ success: true, device: result.rows[0] });
} catch (error) {
console.error('Erro ao atualizar dispositivo:', error);
console.error('Erro detalhado:', error.message);
res.status(500).json({ error: 'Erro ao atualizar dispositivo', details: error.message });
}
});
// DELETAR DISPOSITIVO
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const company_id = req.user.company_id;
const result = await query(
'DELETE FROM devices WHERE id = $1 AND company_id = $2 RETURNING *',
[id, company_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Dispositivo não encontrado' });
}
res.json({ success: true, message: 'Dispositivo deletado' });
} catch (error) {
console.error('Erro ao deletar dispositivo:', error);
res.status(500).json({ error: 'Erro ao deletar dispositivo' });
}
});
module.exports = router;

54
backend/routes/keys.js Normal file
View File

@@ -0,0 +1,54 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
router.get('/', authenticateToken, async (req, res) => {
try {
const result = await query(
'SELECT * FROM activation_keys WHERE company_id = $1 ORDER BY created_at DESC',
[req.user.company_id]
);
res.json({ success: true, keys: result.rows });
} catch (error) {
console.error('Erro ao listar chaves:', error);
res.status(500).json({ error: 'Erro ao listar chaves' });
}
});
router.post('/', authenticateToken, async (req, res) => {
try {
const { description } = req.body;
const key = `PC-${Date.now()}-${Math.random().toString(36).substring(2, 10).toUpperCase()}`;
const result = await query(
'INSERT INTO activation_keys (key, company_id, description, devices_count) VALUES ($1, $2, $3, 0) RETURNING *',
[key, req.user.company_id, description]
);
res.status(201).json({ success: true, key: result.rows[0] });
} catch (error) {
console.error('Erro ao criar chave:', error);
res.status(500).json({ error: 'Erro ao criar chave' });
}
});
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const result = await query(
'DELETE FROM activation_keys WHERE id = $1 AND company_id = $2 RETURNING *',
[req.params.id, req.user.company_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Chave não encontrada' });
}
res.json({ success: true, message: 'Chave deletada' });
} catch (error) {
console.error('Erro ao deletar chave:', error);
res.status(500).json({ error: 'Erro ao deletar chave' });
}
});
module.exports = router;

134
backend/routes/teams.js Normal file
View File

@@ -0,0 +1,134 @@
const express = require('express');
const router = express.Router();
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
// LISTAR EQUIPES
router.get('/', authenticateToken, async (req, res) => {
try {
const company_id = req.user.company_id;
// Verificar se a tabela teams existe
const tableCheck = await query(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'teams'
)`
);
if (!tableCheck.rows[0].exists) {
// Tabela não existe, retornar array vazio
return res.json({ success: true, teams: [] });
}
const result = await query(
`SELECT t.*,
u.name as manager_name,
(SELECT COUNT(*) FROM users WHERE team_id = t.id) as members_count,
(SELECT COUNT(*) FROM devices WHERE team_id = t.id) as devices_count
FROM teams t
LEFT JOIN users u ON t.manager_id = u.id
WHERE t.company_id = $1
ORDER BY t.created_at DESC`,
[company_id]
);
res.json({ success: true, teams: result.rows });
} catch (error) {
console.error('Erro ao listar equipes:', error);
// Se for erro de permissão, retornar array vazio em vez de erro 500
if (error.message && error.message.includes('permission denied')) {
console.log('⚠️ Tabela teams sem permissão, retornando array vazio');
return res.json({ success: true, teams: [] });
}
res.status(500).json({ error: 'Erro ao listar equipes', details: error.message });
}
});
// CRIAR EQUIPE
router.post('/', authenticateToken, async (req, res) => {
try {
const { name, description, manager_id } = req.body;
const company_id = req.user.company_id;
if (!name) {
return res.status(400).json({ error: 'Nome é obrigatório' });
}
const result = await query(
`INSERT INTO teams (name, description, manager_id, company_id, is_active)
VALUES ($1, $2, $3, $4, true)
RETURNING *`,
[name, description, manager_id, company_id]
);
res.status(201).json({ success: true, team: result.rows[0] });
} catch (error) {
console.error('Erro ao criar equipe:', error);
res.status(500).json({ error: 'Erro ao criar equipe' });
}
});
// ATUALIZAR EQUIPE
router.put('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { name, description, manager_id, is_active } = req.body;
const company_id = req.user.company_id;
const result = await query(
`UPDATE teams
SET name = COALESCE($1, name),
description = COALESCE($2, description),
manager_id = COALESCE($3, manager_id),
is_active = COALESCE($4, is_active)
WHERE id = $5 AND company_id = $6
RETURNING *`,
[name, description, manager_id, is_active, id, company_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Equipe não encontrada' });
}
res.json({ success: true, team: result.rows[0] });
} catch (error) {
console.error('Erro ao atualizar equipe:', error);
res.status(500).json({ error: 'Erro ao atualizar equipe' });
}
});
// DELETAR EQUIPE
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const company_id = req.user.company_id;
// Verificar se tem usuários ou devices vinculados
const check = await query(
'SELECT (SELECT COUNT(*) FROM users WHERE team_id = $1) + (SELECT COUNT(*) FROM devices WHERE team_id = $1) as count',
[id]
);
if (check.rows[0].count > 0) {
return res.status(400).json({ error: 'Não é possível deletar equipe com membros ou dispositivos vinculados' });
}
const result = await query(
'DELETE FROM teams WHERE id = $1 AND company_id = $2 RETURNING *',
[id, company_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Equipe não encontrada' });
}
res.json({ success: true, message: 'Equipe deletada' });
} catch (error) {
console.error('Erro ao deletar equipe:', error);
res.status(500).json({ error: 'Erro ao deletar equipe' });
}
});
module.exports = router;

421
backend/routes/users.js Normal file
View File

@@ -0,0 +1,421 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const { query } = require('../config/database');
const { authenticateToken } = require('../middleware/auth');
router.get('/', authenticateToken, async (req, res) => {
try {
console.log('GET /api/users - User:', req.user);
console.log('GET /api/users - Company ID:', req.user.company_id);
// Buscar usuários
const usersResult = await query(
`SELECT
id,
email,
name,
role,
is_active,
created_at
FROM users
WHERE company_id = $1
ORDER BY created_at DESC`,
[req.user.company_id]
);
// Buscar contagem de dispositivos por usuário
const devicesCountResult = await query(
`SELECT
user_id,
COUNT(*) as devices_count
FROM devices
WHERE company_id = $1 AND user_id IS NOT NULL
GROUP BY user_id`,
[req.user.company_id]
);
// Criar mapa de contagem de dispositivos
const devicesCountMap = {};
devicesCountResult.rows.forEach(row => {
devicesCountMap[row.user_id] = parseInt(row.devices_count) || 0;
});
// Adicionar contagem de dispositivos aos usuários
const users = usersResult.rows.map(user => ({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
is_active: user.is_active,
created_at: user.created_at,
devices_count: devicesCountMap[user.id] || 0
}));
console.log('GET /api/users - Resultados encontrados:', users.length);
res.json({ success: true, users: users });
} catch (error) {
console.error('Erro ao listar usuários:', error);
console.error('Erro detalhado:', error.message);
console.error('Stack:', error.stack);
res.status(500).json({ error: 'Erro ao listar usuários', details: error.message });
}
});
router.post('/', authenticateToken, async (req, res) => {
try {
const { email, name, password } = req.body;
if (!email || !name || !password) {
return res.status(400).json({ error: 'Email, nome e senha são obrigatórios' });
}
// Verificar se o email já existe
const existingUser = await query(
'SELECT id FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'Este email já está cadastrado' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const result = await query(
'INSERT INTO users (email, name, password, company_id, is_active) VALUES ($1, $2, $3, $4, true) RETURNING id, email, name, role, is_active',
[email.toLowerCase(), name, hashedPassword, req.user.company_id]
);
res.status(201).json({ success: true, user: result.rows[0] });
} catch (error) {
console.error('Erro ao criar usuário:', error);
// Tratamento específico para erros conhecidos
if (error.code === '23505') { // Violação de constraint única
return res.status(409).json({ error: 'Este email já está cadastrado' });
}
res.status(500).json({ error: 'Erro ao criar usuário', details: error.message });
}
});
// Rota para obter o perfil do usuário logado (admin)
router.get('/me', authenticateToken, async (req, res) => {
try {
const result = await query(
'SELECT id, email, name, role, company_id, is_active, created_at FROM admin_users WHERE id = $1',
[req.user.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
res.json({ success: true, user: result.rows[0] });
} catch (error) {
console.error('Erro ao obter perfil:', error);
res.status(500).json({ error: 'Erro ao obter perfil' });
}
});
// Rota para alterar a senha do próprio usuário (admin)
router.put('/me/password', authenticateToken, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Senha atual e nova senha são obrigatórias' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: 'A nova senha deve ter pelo menos 6 caracteres' });
}
// Buscar o usuário atual
const userResult = await query(
'SELECT id, password FROM admin_users WHERE id = $1',
[req.user.id]
);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
const user = userResult.rows[0];
// Verificar senha atual
const validPassword = await bcrypt.compare(currentPassword, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Senha atual incorreta' });
}
// Hash da nova senha
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Atualizar senha
await query(
'UPDATE admin_users SET password = $1 WHERE id = $2',
[hashedPassword, req.user.id]
);
res.json({ success: true, message: 'Senha alterada com sucesso' });
} catch (error) {
console.error('Erro ao alterar senha:', error);
res.status(500).json({ error: 'Erro ao alterar senha' });
}
});
// Rota para atualizar perfil do próprio usuário (admin)
router.put('/me', authenticateToken, async (req, res) => {
try {
const { name, email } = req.body;
if (!name) {
return res.status(400).json({ error: 'Nome é obrigatório' });
}
// Se email foi fornecido, verificar se não está em uso por outro usuário
if (email) {
const existingUser = await query(
'SELECT id FROM admin_users WHERE email = $1 AND id != $2',
[email.toLowerCase(), req.user.id]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'Este email já está em uso' });
}
}
// Atualizar perfil
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (name) {
updateFields.push(`name = $${paramCount++}`);
updateValues.push(name);
}
if (email) {
updateFields.push(`email = $${paramCount++}`);
updateValues.push(email.toLowerCase());
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'Nenhum campo para atualizar' });
}
updateValues.push(req.user.id);
const queryText = `UPDATE admin_users SET ${updateFields.join(', ')} WHERE id = $${paramCount} RETURNING id, email, name, role, company_id, is_active`;
const result = await query(queryText, updateValues);
res.json({ success: true, user: result.rows[0] });
} catch (error) {
console.error('Erro ao atualizar perfil:', error);
res.status(500).json({ error: 'Erro ao atualizar perfil' });
}
});
// TROCAR SENHA DE UM USUÁRIO (admin pode trocar senha de qualquer usuário)
// IMPORTANTE: Esta rota deve vir ANTES de /:id para não ser capturada pela rota genérica
router.put('/:id/password', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { newPassword } = req.body;
const company_id = req.user.company_id;
const loggedUserId = req.user.id; // ID do usuário logado (da tabela admin_users)
console.log(`🔵 PUT /api/users/${id}/password - Requisição recebida`);
console.log(`🔵 Body recebido:`, JSON.stringify({ newPassword: newPassword ? '***' : null }, null, 2));
console.log(`🔵 Company ID:`, company_id);
console.log(`🔵 ID do usuário logado:`, loggedUserId);
console.log(`🔵 ID do usuário a alterar:`, id);
if (!newPassword) {
return res.status(400).json({ error: 'Nova senha é obrigatória' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: 'A senha deve ter pelo menos 6 caracteres' });
}
// IMPORTANTE: Verificar se o usuário a alterar é o próprio usuário logado
// Se for, alterar na tabela admin_users. Caso contrário, alterar na tabela users
const idNum = parseInt(id);
const loggedIdNum = parseInt(loggedUserId);
// Buscar email do usuário logado
const loggedUserResult = await query(
'SELECT email FROM admin_users WHERE id = $1',
[loggedIdNum]
);
if (loggedUserResult.rows.length === 0) {
return res.status(404).json({ error: 'Usuário logado não encontrado' });
}
const loggedUserEmail = loggedUserResult.rows[0].email;
// Buscar email do usuário a ser alterado
const userToChangeResult = await query(
'SELECT id, email FROM users WHERE id = $1 AND company_id = $2',
[idNum, company_id]
);
if (userToChangeResult.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
const userToChangeEmail = userToChangeResult.rows[0].email;
// Se o email corresponde ao usuário logado, alterar na tabela admin_users
if (userToChangeEmail.toLowerCase() === loggedUserEmail.toLowerCase()) {
console.log(`🔵 Email corresponde ao usuário logado - alterando na tabela admin_users`);
const adminCheck = await query(
'SELECT id FROM admin_users WHERE email = $1',
[loggedUserEmail]
);
if (adminCheck.rows.length === 0) {
return res.status(404).json({ error: 'Usuário admin não encontrado' });
}
const adminId = adminCheck.rows[0].id;
// Hash da nova senha
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Atualizar senha na tabela admin_users
await query(
'UPDATE admin_users SET password = $1 WHERE id = $2',
[hashedPassword, adminId]
);
console.log(`✅ Senha do admin_user ${adminId} (${loggedUserEmail}) alterada com sucesso na tabela admin_users`);
} else {
// Alterar senha de outro usuário (users)
console.log(`🔵 Alterando senha de outro usuário na tabela users`);
// Hash da nova senha
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Atualizar senha na tabela users
await query(
'UPDATE users SET password = $1 WHERE id = $2 AND company_id = $3',
[hashedPassword, idNum, company_id]
);
console.log(`✅ Senha do usuário ${idNum} (${userToChangeEmail}) alterada com sucesso na tabela users`);
}
res.json({ success: true, message: 'Senha alterada com sucesso' });
} catch (error) {
console.error('Erro ao alterar senha do usuário:', error);
res.status(500).json({ error: 'Erro ao alterar senha', details: error.message });
}
});
// EDITAR USUÁRIO
router.put('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { name, email, role, is_active } = req.body;
const company_id = req.user.company_id;
console.log(`🔵 PUT /api/users/${id} - Requisição recebida`);
console.log(`🔵 Body recebido:`, JSON.stringify({ name, email, role, is_active }, null, 2));
console.log(`🔵 Company ID:`, company_id);
console.log(`🔵 User ID do token:`, req.user.id);
// Verificar se o usuário existe e pertence à company
const userCheck = await query(
'SELECT id FROM users WHERE id = $1 AND company_id = $2',
[id, company_id]
);
if (userCheck.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
// Se email foi fornecido, verificar se não está em uso por outro usuário
if (email) {
const existingUser = await query(
'SELECT id FROM users WHERE email = $1 AND id != $2 AND company_id = $3',
[email.toLowerCase(), id, company_id]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'Este email já está em uso' });
}
}
// Construir query dinamicamente
const updateFields = [];
const updateValues = [];
let paramCount = 1;
if (name !== undefined) {
updateFields.push(`name = $${paramCount++}`);
updateValues.push(name);
}
if (email !== undefined) {
updateFields.push(`email = $${paramCount++}`);
updateValues.push(email.toLowerCase());
}
if (role !== undefined) {
updateFields.push(`role = $${paramCount++}`);
updateValues.push(role);
}
if (is_active !== undefined) {
updateFields.push(`is_active = $${paramCount++}`);
updateValues.push(is_active);
}
if (updateFields.length === 0) {
return res.status(400).json({ error: 'Nenhum campo para atualizar' });
}
updateValues.push(id, company_id);
const queryText = `UPDATE users
SET ${updateFields.join(', ')}
WHERE id = $${paramCount++} AND company_id = $${paramCount}
RETURNING id, email, name, role, is_active, created_at`;
const result = await query(queryText, updateValues);
console.log(`✅ Usuário ${id} atualizado com sucesso`);
res.json({ success: true, user: result.rows[0] });
} catch (error) {
console.error('Erro ao atualizar usuário:', error);
console.error('Erro detalhado:', error.message);
res.status(500).json({ error: 'Erro ao atualizar usuário', details: error.message });
}
});
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const result = await query(
'DELETE FROM users WHERE id = $1 AND company_id = $2 RETURNING *',
[req.params.id, req.user.company_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
res.json({ success: true, message: 'Usuário deletado' });
} catch (error) {
console.error('Erro ao deletar usuário:', error);
res.status(500).json({ error: 'Erro ao deletar usuário' });
}
});
module.exports = router;