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;