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.
129 lines
4.0 KiB
JavaScript
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;
|
|
}
|