Final Phase C batch: moves the remaining inline route bodies (12 proxy stubs, /changelog, the webhooks/hijacks group, PDF export, ASPA adoption tracker + IPv6 stats endpoints) into server/routes/*.js and server/services/api-proxy.js, verified byte-for-byte via the smoke-test diff. Also drops the now-duplicate proxyToApiServer()/API_SERVER_PORT that were left behind in server.js after api-proxy.js absorbed them. server.js: 6838 -> 578 lines, under the 800-line project limit.
113 lines
5.3 KiB
JavaScript
113 lines
5.3 KiB
JavaScript
const http = require("http");
|
|
const { buildPdfHtml, buildComparisonPdfHtml } = require("../pdf-export/templates");
|
|
const { renderHtmlToPdf, pdfCacheGet, pdfCacheSet } = require("../pdf-export/renderer");
|
|
|
|
const PORT = process.env.PORT || 3101;
|
|
|
|
// Fetch JSON from an internal endpoint on this same process (loopback) --
|
|
// re-triggers the full HTTP/caching/rate-limit path for /api/validate,
|
|
// /api/aspa, /api/lookup rather than calling their handler logic directly.
|
|
async function fetchInternal(path) {
|
|
return new Promise((resolve, reject) => {
|
|
const opts = { hostname: '127.0.0.1', port: PORT, path, method: 'GET' };
|
|
const req2 = http.request(opts, res2 => {
|
|
let body = '';
|
|
res2.on('data', c => body += c);
|
|
res2.on('end', () => {
|
|
try { resolve(JSON.parse(body)); }
|
|
catch(e) { reject(new Error('JSON parse failed: ' + body.slice(0, 200))); }
|
|
});
|
|
});
|
|
req2.on('error', reject);
|
|
req2.setTimeout(30000, () => { req2.destroy(); reject(new Error('Internal request timeout')); });
|
|
req2.end();
|
|
});
|
|
}
|
|
|
|
// GET /api/export/pdf?asn=X&format=report|executive|technical
|
|
async function handleExportPdf(req, res, url) {
|
|
const rawAsn = (url.searchParams.get('asn') || '').replace(/[^0-9]/g, '');
|
|
const format = ['executive', 'technical', 'report'].includes(url.searchParams.get('format'))
|
|
? url.searchParams.get('format') : 'report';
|
|
if (!rawAsn) {
|
|
res.writeHead(400, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: 'Missing asn parameter' }));
|
|
}
|
|
const cacheKey = `pdf:${rawAsn}:${format}`;
|
|
const cached = pdfCacheGet(cacheKey);
|
|
if (cached) {
|
|
res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length });
|
|
return res.end(cached);
|
|
}
|
|
try {
|
|
const [validateData, aspaData] = await Promise.all([
|
|
fetchInternal(`/api/validate?asn=${rawAsn}`),
|
|
fetchInternal(`/api/aspa?asn=${rawAsn}`).catch(() => null),
|
|
]);
|
|
if (validateData.error) {
|
|
res.writeHead(400, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: validateData.error }));
|
|
}
|
|
const html = buildPdfHtml(validateData, aspaData, format);
|
|
const pdfBuf = await renderHtmlToPdf(html);
|
|
pdfCacheSet(cacheKey, pdfBuf);
|
|
res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'Content-Length': pdfBuf.length });
|
|
return res.end(pdfBuf);
|
|
} catch (err) {
|
|
console.error('[PDF] Error:', err.message);
|
|
res.writeHead(503, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message }));
|
|
}
|
|
}
|
|
|
|
// GET /api/export/pdf/compare?asn1=X&asn2=Y
|
|
async function handleExportPdfCompare(req, res, url) {
|
|
const asn1 = (url.searchParams.get('asn1') || '').replace(/[^0-9]/g, '');
|
|
const asn2 = (url.searchParams.get('asn2') || '').replace(/[^0-9]/g, '');
|
|
if (!asn1 || !asn2) {
|
|
res.writeHead(400, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: 'Missing asn1 or asn2 parameter' }));
|
|
}
|
|
if (asn1 === asn2) {
|
|
res.writeHead(400, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: 'asn1 and asn2 must be different' }));
|
|
}
|
|
const cacheKey = `pdf:compare:${Math.min(+asn1,+asn2)}:${Math.max(+asn1,+asn2)}`;
|
|
const cached = pdfCacheGet(cacheKey);
|
|
if (cached) {
|
|
res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length });
|
|
return res.end(cached);
|
|
}
|
|
try {
|
|
const [v1, a1, l1, v2, a2, l2] = await Promise.all([
|
|
fetchInternal(`/api/validate?asn=${asn1}`),
|
|
fetchInternal(`/api/aspa?asn=${asn1}`).catch(() => null),
|
|
fetchInternal(`/api/lookup?asn=${asn1}`).catch(() => null),
|
|
fetchInternal(`/api/validate?asn=${asn2}`),
|
|
fetchInternal(`/api/aspa?asn=${asn2}`).catch(() => null),
|
|
fetchInternal(`/api/lookup?asn=${asn2}`).catch(() => null),
|
|
]);
|
|
if (v1.error || v2.error) {
|
|
res.writeHead(400, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: v1.error || v2.error }));
|
|
}
|
|
const html = buildComparisonPdfHtml(v1, a1, l1, v2, a2, l2);
|
|
const pdfBuf = await renderHtmlToPdf(html);
|
|
pdfCacheSet(cacheKey, pdfBuf);
|
|
res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'Content-Length': pdfBuf.length });
|
|
return res.end(pdfBuf);
|
|
} catch (err) {
|
|
console.error('[PDF] Error:', err.message);
|
|
res.writeHead(503, {'Content-Type':'application/json'});
|
|
return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message }));
|
|
}
|
|
}
|
|
|
|
// CORS preflight for PDF export (already handled globally above, belt-and-suspenders)
|
|
function handleExportOptions(req, res) {
|
|
res.writeHead(204);
|
|
return res.end();
|
|
}
|
|
|
|
module.exports = { handleExportPdf, handleExportPdfCompare, handleExportOptions };
|