PeerCortex/server/routes/pdf-export.js
Rene Fichtmueller 570c178c61
Some checks failed
build-verify / build-verify (push) Failing after 10s
build-verify / build-verify (pull_request) Failing after 10s
refactor: extract proxy-stubs/changelog/hijack-alerts/pdf-export/aspa-adoption routes
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.
2026-07-17 08:08:48 +02:00

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