Correções de bugs, layout de cards e webhook test funcional
- Pipeline cancel/approve/reject corrigido com busca bidirecional - Secrets injetados no executor via cleanEnv - Versionamento automático ao atualizar agentes - writeJsonAsync com log de erro - Removido asyncHandler.js (código morto) - Restaurado permissionMode padrão bypassPermissions - Ícones dos cards alinhados à direita com wrapper - Botão Editar convertido para ícone nos cards - Webhook test agora dispara execução real do agente/pipeline - Corrigido App.navigateTo no teste de webhook
This commit is contained in:
78
server.js
78
server.js
@@ -6,6 +6,8 @@ import { dirname, join } from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import apiRouter, { setWsBroadcast, setWsBroadcastTo, hookRouter } from './src/routes/api.js';
|
||||
import * as manager from './src/agents/manager.js';
|
||||
import { setGlobalBroadcast } from './src/agents/manager.js';
|
||||
@@ -14,16 +16,17 @@ import { flushAllStores } from './src/store/db.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const AUTH_TOKEN = process.env.AUTH_TOKEN || '';
|
||||
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'http://localhost:3000';
|
||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || '';
|
||||
|
||||
|
||||
function timingSafeCompare(a, b) {
|
||||
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return crypto.timingSafeEqual(bufA, bufB);
|
||||
const hashA = crypto.createHash('sha256').update(a).digest();
|
||||
const hashB = crypto.createHash('sha256').update(b).digest();
|
||||
return crypto.timingSafeEqual(hashA, hashB);
|
||||
}
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
@@ -34,6 +37,14 @@ const apiLimiter = rateLimit({
|
||||
message: { error: 'Limite de requisições excedido. Tente novamente em breve.' },
|
||||
});
|
||||
|
||||
const hookLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 30,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Limite de requisições de webhook excedido.' },
|
||||
});
|
||||
|
||||
function verifyWebhookSignature(req, res, next) {
|
||||
if (!WEBHOOK_SECRET) return next();
|
||||
const sig = req.headers['x-hub-signature-256'];
|
||||
@@ -77,24 +88,29 @@ app.get('/api/health', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false,
|
||||
}));
|
||||
|
||||
app.use(compression());
|
||||
|
||||
app.use('/api', apiLimiter);
|
||||
|
||||
if (AUTH_TOKEN) {
|
||||
app.use('/api', (req, res, next) => {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : req.query.token;
|
||||
if (!timingSafeCompare(token, AUTH_TOKEN)) {
|
||||
return res.status(401).json({ error: 'Token de autenticação inválido' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api', (req, res, next) => {
|
||||
if (!AUTH_TOKEN) return next();
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : req.query.token;
|
||||
if (!timingSafeCompare(token, AUTH_TOKEN)) {
|
||||
return res.status(401).json({ error: 'Token de autenticação inválido' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.json({
|
||||
verify: (req, res, buf) => { req.rawBody = buf; },
|
||||
verify: (req, res, buf) => { req.rawBody = buf || Buffer.alloc(0); },
|
||||
}));
|
||||
app.use('/hook', verifyWebhookSignature, hookRouter);
|
||||
app.use(express.static(join(__dirname, 'public')));
|
||||
app.use('/hook', hookLimiter, verifyWebhookSignature, hookRouter);
|
||||
app.use(express.static(join(__dirname, 'public'), { maxAge: '1h', etag: true }));
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
const connectedClients = new Map();
|
||||
@@ -104,20 +120,30 @@ wss.on('connection', (ws, req) => {
|
||||
|
||||
if (AUTH_TOKEN) {
|
||||
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
|
||||
if (token !== AUTH_TOKEN) {
|
||||
if (!timingSafeCompare(token, AUTH_TOKEN)) {
|
||||
ws.close(4001, 'Token inválido');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ws.clientId = clientId;
|
||||
ws.isAlive = true;
|
||||
connectedClients.set(clientId, ws);
|
||||
|
||||
ws.on('pong', () => { ws.isAlive = true; });
|
||||
ws.on('close', () => connectedClients.delete(clientId));
|
||||
ws.on('error', () => connectedClients.delete(clientId));
|
||||
ws.send(JSON.stringify({ type: 'connected', clientId }));
|
||||
});
|
||||
|
||||
const wsHeartbeat = setInterval(() => {
|
||||
wss.clients.forEach(ws => {
|
||||
if (!ws.isAlive) return ws.terminate();
|
||||
ws.isAlive = false;
|
||||
ws.ping();
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
function broadcast(message) {
|
||||
const payload = JSON.stringify(message);
|
||||
for (const [, client] of connectedClients) {
|
||||
@@ -145,6 +171,8 @@ function gracefulShutdown(signal) {
|
||||
flushAllStores();
|
||||
console.log('Dados persistidos.');
|
||||
|
||||
clearInterval(wsHeartbeat);
|
||||
|
||||
httpServer.close(() => {
|
||||
console.log('Servidor HTTP encerrado.');
|
||||
process.exit(0);
|
||||
@@ -159,10 +187,18 @@ function gracefulShutdown(signal) {
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[FATAL] Exceção não capturada:', err.message);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[WARN] Promise rejeitada não tratada:', reason);
|
||||
});
|
||||
|
||||
manager.restoreSchedules();
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`Painel administrativo disponível em http://localhost:${PORT}`);
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
console.log(`Painel administrativo disponível em http://${HOST}:${PORT}`);
|
||||
console.log(`WebSocket server ativo na mesma porta.`);
|
||||
if (AUTH_TOKEN) console.log('Autenticação por token ativada.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user