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; }