414 lines
17 KiB
JavaScript
414 lines
17 KiB
JavaScript
|
|
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;
|