Files
NoIdle/backend/routes/activity.js
root 6086c13be7 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
2025-11-16 22:56:35 +00:00

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;