Files
COMERCIAL-GTI/src/server/index.mjs
T
Gustavo eaaf7af659 Add WABA outreach automation (Z-PRO integration)
Per-product message templates, official/não-oficial channel selection,
manual per-number sending, and a 20-minute internal scheduler for the
existing automatic outreach batch — all pre-existing local work being
committed as-is.
2026-08-03 13:14:09 -03:00

129 lines
4.0 KiB
JavaScript

import http from 'node:http';
import { paths } from '../env.mjs';
import { createRouter } from './router.mjs';
import { serveStatic } from './static.mjs';
import { openSse } from './sse.mjs';
import { registerMetaRoutes } from './routes/meta.mjs';
import { registerCaptureRoutes } from './routes/capture.mjs';
import { registerExtractionRoutes } from './routes/extractions.mjs';
import { registerLeadRoutes } from './routes/leads.mjs';
import { registerExportRoutes } from './routes/exports.mjs';
import { registerAuthRoutes } from './routes/auth.mjs';
import { registerOutreachRoutes } from './routes/outreach.mjs';
import { getCookie, getSession, SESSION_COOKIE } from './session.mjs';
const PUBLIC_API_PATHS = new Set(['/api/auth/login', '/api/health']);
const BODY_LIMIT = 1024 * 1024; // 1MB
function readJsonBody(req) {
return new Promise((resolve, reject) => {
let size = 0;
const chunks = [];
req.on('data', (chunk) => {
size += chunk.length;
if (size > BODY_LIMIT) {
reject(new Error('corpo da requisicao muito grande'));
req.destroy();
return;
}
chunks.push(chunk);
});
req.on('end', () => {
if (!chunks.length) {
resolve(null);
return;
}
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch {
reject(new Error('JSON invalido no corpo da requisicao'));
}
});
req.on('error', reject);
});
}
export function createAppServer({ db, jobs, log = () => {} }) {
const router = createRouter();
const deps = { db, jobs };
registerAuthRoutes(router, deps);
registerMetaRoutes(router, deps);
registerCaptureRoutes(router, deps);
registerExtractionRoutes(router, deps);
registerLeadRoutes(router, deps);
registerExportRoutes(router, deps);
registerOutreachRoutes(router, deps);
// SSE de progresso dos jobs (rota especial: gerencia a resposta manualmente)
router.add('GET', '/api/jobs/:jobId/events', async (req, res, { params }) => {
const sse = openSse(res);
const unsubscribe = jobs.subscribe(params.jobId, (event) => {
const name = event.t === 'done' ? 'done' : event.t === 'error' ? 'error' : event.t;
sse.send(name, event);
});
if (unsubscribe === null) {
sse.send('error', { t: 'error', message: 'job nao encontrado (o servidor pode ter sido reiniciado)' });
sse.close();
return;
}
req.on('close', () => {
unsubscribe();
sse.close();
});
});
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://127.0.0.1');
const pathname = url.pathname;
const json = (status, data) => {
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(data));
};
const session = getSession(getCookie(req, SESSION_COOKIE));
const user = session?.user || null;
try {
if (pathname.startsWith('/api/') && !PUBLIC_API_PATHS.has(pathname) && !user) {
json(401, { erro: 'não autenticado' });
return;
}
const matched = router.match(req.method, pathname);
if (matched) {
let body = null;
if (req.method === 'POST' || req.method === 'PUT') {
body = await readJsonBody(req);
}
await matched.handler(req, res, { params: matched.params, query: url.searchParams, body, json, user });
return;
}
if (req.method === 'GET' && !pathname.startsWith('/api/')) {
if ((pathname === '/' || pathname === '/index.html') && !user) {
res.writeHead(302, { location: '/login.html' });
res.end();
return;
}
if (serveStatic(paths.public, pathname, res)) return;
}
json(404, { erro: `rota nao encontrada: ${req.method} ${pathname}` });
} catch (error) {
log(`erro em ${req.method} ${pathname}: ${error.stack || error.message}`);
if (!res.headersSent) {
json(500, { erro: String(error.message || error) });
} else {
res.end();
}
}
});
return server;
}