Rene Fichtmueller 3399392f1f refactor: extract static/search/feedback route handlers
server/routes/static.js (/, /index.html, /index-editorial.html,
/favicon.ico, /lia), server/routes/search.js (/api/search, /api/visitors),
server/routes/feedback.js (/api/feedback OPTIONS/POST/GET).

Verified via node --check, eslint no-undef (only the 3 known pre-existing
bugs remain, no new ones), and smoke-test harness (28/28 match).
server.js: 3714 -> 3556 lines.
2026-07-17 07:31:19 +02:00

84 lines
3.4 KiB
JavaScript

const { fetchJSONWithRetry } = require("../services/http-helpers");
const { PEERINGDB_API_KEY } = require("../services/peeringdb");
const { trackVisitor } = require("../visitors");
// Name Search (RIPE Stat + PeeringDB combined)
async function handleSearch(req, res) {
const params = new URL(req.url, 'http://localhost').searchParams;
const q = (params.get('q') || '').trim();
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=120');
if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); }
try {
const results = [];
const seen = new Set();
// Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names)
try {
const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q);
const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 });
const cats = ripeData && ripeData.data && ripeData.data.categories || [];
for (var ci = 0; ci < cats.length; ci++) {
var suggs = cats[ci].suggestions || [];
for (var si = 0; si < suggs.length; si++) {
var s = suggs[si];
var val = (s.value || '').toString();
// Only ASN results
if (/^AS\d+$/i.test(val) && !seen.has(val)) {
seen.add(val);
// Use description (e.g. "FLEXOPTIX, DE") as the display label
var ripeName = s.description || s.label || val;
results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' });
}
}
}
} catch(e) { /* RIPE Stat failed, continue */ }
// Source 2: PeeringDB name search (best for network operator names)
try {
var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10';
if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY;
const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 });
var nets = pdbData && pdbData.data || [];
for (var ni = 0; ni < nets.length; ni++) {
var net = nets[ni];
var asnKey = 'AS' + net.asn;
if (net.asn && !seen.has(asnKey)) {
seen.add(asnKey);
var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · ');
results.push({
asn: String(net.asn),
label: net.name || asnKey,
description: pdbDesc,
source: 'PeeringDB'
});
}
}
} catch(e) { /* PeeringDB failed, continue */ }
// Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB
results.sort((a, b) => {
if (a.source === b.source) return 0;
return a.source === 'RIPE Stat' ? -1 : 1;
});
res.writeHead(200);
return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) }));
} catch(e) {
res.writeHead(500); return res.end(JSON.stringify({error: e.message}));
}
}
// GET /api/visitors — unique visitor count
function handleVisitors(req, res) {
res.setHeader("Content-Type", "application/json");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", "no-store");
const count = trackVisitor(req);
res.writeHead(200);
return res.end(JSON.stringify({ visitors: count }));
}
module.exports = { handleSearch, handleVisitors };