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:
8
backend/.env
Normal file
8
backend/.env
Normal file
@@ -0,0 +1,8 @@
|
||||
PORT=3005
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=pointcontrol
|
||||
DB_USER=pointcontrol_user
|
||||
DB_PASSWORD=SuaSenhaSegura123!
|
||||
JWT_SECRET=seu-jwt-secret-super-secreto-aqui-mude-isso
|
||||
NODE_ENV=production
|
||||
111
backend/check_device_status.js
Normal file
111
backend/check_device_status.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const { query } = require('./config/database');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const deviceName = process.argv[2] || 'DESKTOP-BC16GDH';
|
||||
|
||||
console.log(`\n🔍 Verificando status do dispositivo: ${deviceName}\n`);
|
||||
|
||||
// Buscar informações do dispositivo
|
||||
const deviceResult = await query(
|
||||
`SELECT device_id, device_name, is_active, last_seen, created_at,
|
||||
(SELECT name FROM users WHERE id = devices.user_id) as user_name
|
||||
FROM devices
|
||||
WHERE device_name = $1`,
|
||||
[deviceName]
|
||||
);
|
||||
|
||||
if (deviceResult.rows.length === 0) {
|
||||
console.log(`❌ Dispositivo "${deviceName}" não encontrado no banco de dados\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const device = deviceResult.rows[0];
|
||||
|
||||
console.log('📱 Informações do Dispositivo:');
|
||||
console.log(` Nome: ${device.device_name}`);
|
||||
console.log(` Device ID: ${device.device_id}`);
|
||||
console.log(` Status (is_active): ${device.is_active ? '✅ Ativo' : '❌ Inativo'}`);
|
||||
console.log(` Último Heartbeat: ${device.last_seen ? new Date(device.last_seen).toLocaleString('pt-BR') : '❌ Nunca'}`);
|
||||
console.log(` Criado em: ${new Date(device.created_at).toLocaleString('pt-BR')}`);
|
||||
console.log(` Usuário vinculado: ${device.user_name || 'Nenhum'}`);
|
||||
|
||||
// Calcular status real baseado em last_seen
|
||||
let realStatus = false;
|
||||
let statusMessage = '';
|
||||
|
||||
if (device.last_seen) {
|
||||
const lastSeenDate = new Date(device.last_seen);
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor((now - lastSeenDate) / 1000 / 60);
|
||||
|
||||
if (diffMinutes <= 5) {
|
||||
realStatus = true;
|
||||
statusMessage = `✅ ONLINE (último heartbeat há ${diffMinutes} minuto(s))`;
|
||||
} else {
|
||||
realStatus = false;
|
||||
statusMessage = `❌ OFFLINE (último heartbeat há ${diffMinutes} minuto(s) - mais de 5 minutos)`;
|
||||
}
|
||||
} else {
|
||||
realStatus = false;
|
||||
statusMessage = '❌ OFFLINE (nunca recebeu heartbeat)';
|
||||
}
|
||||
|
||||
console.log(`\n📊 Status Real: ${statusMessage}`);
|
||||
|
||||
// Verificar atividades recentes
|
||||
const activitiesResult = await query(
|
||||
`SELECT COUNT(*) as total,
|
||||
MAX(timestamp) as ultima_atividade,
|
||||
MIN(timestamp) as primeira_atividade
|
||||
FROM activities
|
||||
WHERE device_id = (SELECT id FROM devices WHERE device_name = $1)`,
|
||||
[deviceName]
|
||||
);
|
||||
|
||||
if (activitiesResult.rows[0].total > 0) {
|
||||
const activities = activitiesResult.rows[0];
|
||||
console.log(`\n📈 Atividades Registradas:`);
|
||||
console.log(` Total: ${activities.total}`);
|
||||
console.log(` Primeira: ${new Date(activities.primeira_atividade).toLocaleString('pt-BR')}`);
|
||||
console.log(` Última: ${new Date(activities.ultima_atividade).toLocaleString('pt-BR')}`);
|
||||
|
||||
const lastActivityDate = new Date(activities.ultima_atividade);
|
||||
const now = new Date();
|
||||
const diffHours = Math.floor((now - lastActivityDate) / 1000 / 60 / 60);
|
||||
const diffMinutes = Math.floor((now - lastActivityDate) / 1000 / 60) % 60;
|
||||
|
||||
if (diffHours > 0) {
|
||||
console.log(` ⚠️ Última atividade há ${diffHours}h ${diffMinutes}min`);
|
||||
} else {
|
||||
console.log(` ✅ Última atividade há ${diffMinutes}min`);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n⚠️ Nenhuma atividade registrada para este dispositivo`);
|
||||
}
|
||||
|
||||
// Verificar últimas 5 atividades
|
||||
const recentActivities = await query(
|
||||
`SELECT timestamp, application_name, window_title, idle_time_seconds
|
||||
FROM activities
|
||||
WHERE device_id = (SELECT id FROM devices WHERE device_name = $1)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 5`,
|
||||
[deviceName]
|
||||
);
|
||||
|
||||
if (recentActivities.rows.length > 0) {
|
||||
console.log(`\n📋 Últimas 5 Atividades:`);
|
||||
recentActivities.rows.forEach((activity, index) => {
|
||||
console.log(` ${index + 1}. ${new Date(activity.timestamp).toLocaleString('pt-BR')} - ${activity.application_name} - ${activity.window_title.substring(0, 50)}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Erro:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
32
backend/config/database.js
Normal file
32
backend/config/database.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('Erro inesperado no PostgreSQL:', err);
|
||||
});
|
||||
|
||||
const query = async (text, params) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
console.log('Query:', { text: text.substring(0, 80), duration, rows: res.rowCount });
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Erro na query:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { query, pool };
|
||||
59
backend/create_missing_tables.js
Normal file
59
backend/create_missing_tables.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { query } = require('./config/database');
|
||||
|
||||
async function createTables() {
|
||||
try {
|
||||
console.log('🔧 Criando tabela browsing_history...');
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS browsing_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
title VARCHAR(500),
|
||||
browser VARCHAR(100),
|
||||
visited_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
console.log('✅ Tabela browsing_history criada');
|
||||
|
||||
console.log('🔧 Criando índices para browsing_history...');
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_browsing_history_device_id ON browsing_history(device_id)
|
||||
`);
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_browsing_history_visited_at ON browsing_history(visited_at DESC)
|
||||
`);
|
||||
console.log('✅ Índices criados');
|
||||
|
||||
console.log('🔧 Criando tabela session_events...');
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS session_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('logon', 'logoff')),
|
||||
username VARCHAR(255),
|
||||
event_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
console.log('✅ Tabela session_events criada');
|
||||
|
||||
console.log('🔧 Criando índices para session_events...');
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_session_events_device_id ON session_events(device_id)
|
||||
`);
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_session_events_event_time ON session_events(event_time DESC)
|
||||
`);
|
||||
console.log('✅ Índices criados');
|
||||
|
||||
console.log('\n✅ Todas as tabelas foram criadas com sucesso!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao criar tabelas:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createTables();
|
||||
|
||||
27
backend/create_tables.sql
Normal file
27
backend/create_tables.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Criar tabela browsing_history para armazenar histórico de navegação
|
||||
CREATE TABLE IF NOT EXISTS browsing_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
title VARCHAR(500),
|
||||
browser VARCHAR(100),
|
||||
visited_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Criar tabela session_events para armazenar eventos de logon/logoff
|
||||
CREATE TABLE IF NOT EXISTS session_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
device_id VARCHAR(255) NOT NULL,
|
||||
event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('logon', 'logoff')),
|
||||
username VARCHAR(255),
|
||||
event_time TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Criar índices para melhor performance
|
||||
CREATE INDEX IF NOT EXISTS idx_browsing_history_device_id ON browsing_history(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_browsing_history_visited_at ON browsing_history(visited_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_events_device_id ON session_events(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_events_event_time ON session_events(event_time DESC);
|
||||
|
||||
22
backend/fix_database_permissions.sql
Normal file
22
backend/fix_database_permissions.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Script para corrigir permissões das tabelas browsing_history e session_events
|
||||
-- Execute como superusuário do PostgreSQL (postgres)
|
||||
|
||||
-- Usuário do banco: pointcontrol_user (confirmado)
|
||||
|
||||
-- Conceder permissões na tabela browsing_history
|
||||
GRANT ALL PRIVILEGES ON TABLE browsing_history TO pointcontrol_user;
|
||||
GRANT USAGE, SELECT ON SEQUENCE browsing_history_id_seq TO pointcontrol_user;
|
||||
|
||||
-- Conceder permissões na tabela session_events
|
||||
GRANT ALL PRIVILEGES ON TABLE session_events TO pointcontrol_user;
|
||||
GRANT USAGE, SELECT ON SEQUENCE session_events_id_seq TO pointcontrol_user;
|
||||
|
||||
-- Verificar permissões (opcional)
|
||||
SELECT grantee, privilege_type
|
||||
FROM information_schema.role_table_grants
|
||||
WHERE table_name = 'browsing_history';
|
||||
|
||||
SELECT grantee, privilege_type
|
||||
FROM information_schema.role_table_grants
|
||||
WHERE table_name = 'session_events';
|
||||
|
||||
23
backend/middleware/auth.js
Normal file
23
backend/middleware/auth.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
console.log('❌ Token não fornecido para:', req.path);
|
||||
return res.status(401).json({ error: 'Token não fornecido' });
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
console.log('❌ Token inválido para:', req.path, err.message);
|
||||
return res.status(403).json({ error: 'Token inválido' });
|
||||
}
|
||||
console.log('✅ Token válido para:', req.path, 'User:', user.email, 'Company:', user.company_id);
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { authenticateToken };
|
||||
2532
backend/package-lock.json
generated
Normal file
2532
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
backend/package.json
Normal file
17
backend/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "pointcontrol-api",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"cors": "^2.8.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pdfkit": "^0.14.0"
|
||||
}
|
||||
}
|
||||
78
backend/routes/activities.js
Normal file
78
backend/routes/activities.js
Normal 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
413
backend/routes/activity.js
Normal 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
59
backend/routes/auth.js
Normal 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
130
backend/routes/dashboard.js
Normal 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
263
backend/routes/devices.js
Normal 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
54
backend/routes/keys.js
Normal 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
134
backend/routes/teams.js
Normal 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
421
backend/routes/users.js
Normal 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;
|
||||
82
backend/server.js
Normal file
82
backend/server.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware CORS
|
||||
const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
// Permitir requisições sem origin (mobile apps, Postman, etc)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
// Lista de origens permitidas
|
||||
const allowedOrigins = [
|
||||
'https://admin.noidle.tech',
|
||||
'https://admin.pointcontrol.co',
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001'
|
||||
];
|
||||
|
||||
if (allowedOrigins.indexOf(origin) !== -1 || origin.includes('localhost')) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(null, true); // Permitir todas por enquanto, pode restringir depois
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key']
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Log
|
||||
app.use((req, res, next) => {
|
||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Rotas
|
||||
const authRoutes = require('./routes/auth');
|
||||
const dashboardRoutes = require('./routes/dashboard');
|
||||
const devicesRoutes = require('./routes/devices');
|
||||
const activitiesRoutes = require('./routes/activities');
|
||||
const activityRoutes = require('./routes/activity');
|
||||
const keysRoutes = require('./routes/keys');
|
||||
const usersRoutes = require('./routes/users');
|
||||
const teamsRoutes = require("./routes/teams");
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
app.use('/api/devices', devicesRoutes);
|
||||
app.use('/api/activities', activitiesRoutes);
|
||||
app.use("/api/activity", activityRoutes);
|
||||
app.use('/api/keys', keysRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use("/api/teams", teamsRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Rota não encontrada' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Erro:', err);
|
||||
res.status(500).json({ error: 'Erro interno do servidor' });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ PointControl API rodando na porta ${PORT}`);
|
||||
console.log(`📅 ${new Date().toISOString()}`);
|
||||
});
|
||||
Reference in New Issue
Block a user