From 141911537d33dc00e9c4f38776d5dd3e4a9125f8 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 16 Jul 2026 11:54:33 +0200 Subject: [PATCH] feat: wire up the 13 orphaned Fastify routes via internal API server proxy The 2026-07-14 backend refactor (PR #1) moved 13 features' route logic out of server.js into src/features/*/routes.ts as Fastify plugins, and left "// Migrated to src/features/X/" comments behind -- but never actually built or started anything that served those plugins. Found a complete, unused Fastify app bootstrap already sitting at src/api/server.ts (added in an earlier commit, 5554c1a, alongside the BGP hijack/PDF-export/ASPA-adoption work) that imports and registers all 16 route modules, including 3 (pdf-export, aspa-adoption, hijack-alerts) with proper Postgres-backed implementations, tests, and PDF templates -- more capable than the puppeteer-based inline version kept from the production snapshot. Also confirmed there is no code anywhere that calls initializeDatabase() or startApiServer(): this Fastify server had never actually been run, not even once. What this commit does: - Fixes 6 real TypeScript compile errors blocking `tsc` from building the affected modules cleanly (all pre-existing, none introduced by this branch): - src/routes/hijack-alerts.ts: route generic was missing `Body` in its type param, so it didn't match the handler's own declared type - src/features/aspa-adoption/scheduler.ts: node-cron v4's ScheduledTask dropped `.destroy()` (relevant now that this branch already bumped node-cron 3->4 for the uuid CVE fix) -- `.stop()` alone is sufficient - src/features/{bgp-communities,rpki-history}/routes.ts: `response.json()` typed as `{}` under this @types/node version, cast to `any` to match this file's existing loose-typing style - src/features/pdf-export/cache-manager.ts: `NodeJS.Timer` -> `NodeJS.Timeout` (what setInterval/clearInterval actually use) - src/features/pdf-export/renderer.ts: this uses Playwright, not Puppeteer -- `timeout` isn't a page.pdf() option in Playwright, moved to page.setDefaultTimeout() before the call - Adds src/api/index.ts as the actual entry point (there wasn't one). Calls initializeDatabase() before dynamically importing ./server.js, because src/routes/hijack-alerts.ts calls getDatabase() at module load time -- a static top-level import of ./server would have crashed on startup before initializeDatabase() ever ran. Verified this boots cleanly and serves real responses (changelog-data, ix-matrix, rib/prefix graceful-503, bgp-communities with a live RIPE Stat call) in local testing. - Wires server.js to reverse-proxy the 11 genuinely-orphaned paths (submarine-cables, global-infra, communities, irr-audit, asset-expand, rpki-history, aspath, looking-glass, ix-matrix, changelog-data, rib/*, prefix-changes) to this new internal server via a plain http.request() proxy -- chosen over exposing a second Cloudflare Tunnel ingress rule so this stays a same-origin, internal-only change with no DNS/tunnel config to touch. Verified the proxy mechanism itself against a live instance of the new server. Deliberately did NOT proxy /api/webhooks, /api/hijacks, or /api/hijack-subscribe -- server.js already serves those paths inline (file-backed, live, working), and src/routes/hijack-alerts.ts registers the same path names against an empty Postgres table. Proxying would have silently shadowed working data with nothing. - Default port 3102, not 3100: checked Erik and found 3100 already bound by an unrelated docker-proxy container. 3102 is free and reads as "the second peercortex port" next to the existing 3101. - Adds a `peercortex-api` PM2 app to ecosystem.config.js on Erik (backed up first via rollback-standard.sh) -- config only, NOT started. Nothing is deployed to Erik as part of this commit: no dist/ upload, no `npm install` for fastify/pg on Erik, no `pm2 start`. server.js and public/index.html on Erik are untouched. --- .gitignore | 1 + deploy.sh | 5 ++ server.js | 91 ++++++++++++++++++++---- src/api/index.ts | 49 +++++++++++++ src/api/server.ts | 4 +- src/features/aspa-adoption/scheduler.ts | 1 - src/features/bgp-communities/routes.ts | 2 +- src/features/pdf-export/cache-manager.ts | 2 +- src/features/pdf-export/renderer.ts | 2 +- src/features/rpki-history/routes.ts | 2 +- src/routes/hijack-alerts.ts | 2 +- 11 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 src/api/index.ts diff --git a/.gitignore b/.gitignore index 6915d9d..8bd748a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ audit/__pycache__/ audit/asn_registry.json audit/latest_report.txt backups/ +.rollback/ public/index.html.bak public/lia.html server.js.bak* diff --git a/deploy.sh b/deploy.sh index 34b226a..4b35cd8 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,5 +1,10 @@ #!/bin/bash # PeerCortex safe deploy script — always backup before restart +# NOTE: this only covers server.js + public/index.html (the main site, +# PM2 process "peercortex"). The Fastify API server (PM2 process +# "peercortex-api", dist/api/index.js, port 3102) needs `npm run build` +# run first and `pm2 restart peercortex-api` separately -- see +# ecosystem.config.js on Erik for both app definitions. BACKUP_DIR=/opt/peercortex-app/backups mkdir -p $BACKUP_DIR TS=$(date +%Y%m%d_%H%M%S) diff --git a/server.js b/server.js index 4061b91..e54e8ff 100644 --- a/server.js +++ b/server.js @@ -3103,6 +3103,40 @@ async function fetchWhois(resource) { return result; } +// ============================================================ +// Internal API Server Proxy +// ============================================================ +// Several routes were extracted into src/api/ + src/features/*/routes.ts +// (Fastify, dist/api/index.js, run as a separate PM2 process on +// API_SERVER_PORT) but were never re-wired here after that migration — +// they just silently 404'd. This forwards those specific paths through +// rather than reimplementing them inline a second time. +const API_SERVER_PORT = parseInt(process.env.API_SERVER_PORT || "3102", 10); + +function proxyToApiServer(req, res, targetUrl) { + const proxyReq = http.request( + { + hostname: "127.0.0.1", + port: API_SERVER_PORT, + path: targetUrl, + method: req.method, + headers: Object.assign({}, req.headers, { host: "127.0.0.1:" + API_SERVER_PORT }), + }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + } + ); + proxyReq.on("error", (err) => { + console.error("[API Proxy] Error forwarding " + targetUrl + ":", err.message); + if (!res.headersSent) { + res.writeHead(502, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "API server unavailable", detail: err.message })); + } + }); + req.pipe(proxyReq); +} + // ============================================================ // HTTP Server // ============================================================ @@ -5877,10 +5911,14 @@ const server = http.createServer(async (req, res) => { // Feature 28: Submarine Cable overlay (TeleGeography proxy) // ── Submarine Cables map data ───────────────────────────────── - // Migrated to src/features/submarine-cables/ + if (reqPath === '/api/submarine-cables') { + return proxyToApiServer(req, res, req.url); + } // ── Global datacenter/IXP map (PeeringDB proxy) ─────────────── - // Migrated to src/features/global-infra/ + if (reqPath === '/api/global-infra') { + return proxyToApiServer(req, res, req.url); + } // ── Changelog page ───────────────────────────────────────── @@ -5927,26 +5965,39 @@ ${html} } // ── BGP Community Decoder ──────────────────────────────────── - // Migrated to src/features/bgp-communities/ + if (reqPath === '/api/communities') { + return proxyToApiServer(req, res, req.url); + } // ── IRR Audit ───────────────────────────────────────────────── - // Migrated to src/features/irr-audit/ - + if (reqPath === '/api/irr-audit') { + return proxyToApiServer(req, res, req.url); + } // ── AS-SET Expander ─────────────────────────────────────────── - // Migrated to src/features/asset-expand/ - + if (reqPath === '/api/asset-expand') { + return proxyToApiServer(req, res, req.url); + } + // ── Routing History (prefix table via RIPE Stat routing-history) ── - // Migrated to src/features/rpki-history/ + if (reqPath === '/api/rpki-history') { + return proxyToApiServer(req, res, req.url); + } // ── AS-PATH Visualizer (RIPE Stat looking-glass) ──────────────── - // Migrated to src/features/aspath/ + if (reqPath === '/api/aspath') { + return proxyToApiServer(req, res, req.url); + } // ── Looking Glass (RIPE Stat) ───────────────────────────────── - // Migrated to src/features/looking-glass/ + if (reqPath === '/api/looking-glass') { + return proxyToApiServer(req, res, req.url); + } // ── IXP Peering Matrix ──────────────────────────────────────── - // Migrated to src/features/ix-matrix/ + if (reqPath === '/api/ix-matrix') { + return proxyToApiServer(req, res, req.url); + } // ── CORS preflight for all /api/webhooks + /api/hijacks ────── @@ -6137,16 +6188,26 @@ ${html} // ── Hijack Alerts (legacy read) ─────────────────────────────── - // Migrated to src/routes/hijack-alerts (Fastify feature) + // src/routes/hijack-alerts.ts registers /api/webhooks + /api/hijacks under + // the same paths this file already serves above (file-backed, live). + // Deliberately NOT proxied here — that would shadow working data with an + // empty Postgres-backed implementation. Not a gap, just two feature + // branches that ended up naming their routes the same thing. // ── Changelog JSON API ──────────────────────────────────────── - // Migrated to src/features/changelog/ + if (reqPath === '/changelog-data') { + return proxyToApiServer(req, res, req.url); + } // ── bio-rd RIB routes ───────────────────────────────────────── - // Migrated to src/features/rib/ + if (reqPath.startsWith('/api/rib/')) { + return proxyToApiServer(req, res, req.url); + } // ── Prefix Changes ────────────────────────────────────────────── - // Migrated to src/features/prefix-changes/ + if (reqPath === '/api/prefix-changes') { + return proxyToApiServer(req, res, req.url); + } // ============================================================ // FEATURE 2: PDF Export diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..b9cfa71 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,49 @@ +import fs from 'fs' +import { initializeDatabase, closeDatabaseConnection } from '../lib/db' + +function loadEnv(): void { + const envPath = process.env.ENV_PATH ?? '/opt/peercortex-app/.env' + try { + const envContent = fs.readFileSync(envPath, 'utf8') + for (const line of envContent.split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIdx = trimmed.indexOf('=') + if (eqIdx > 0) { + const key = trimmed.substring(0, eqIdx).trim() + const val = trimmed.substring(eqIdx + 1).trim() + if (!process.env[key]) process.env[key] = val + } + } + console.log('[API] Environment variables loaded from', envPath) + } catch { + console.warn('[API] Could not read .env file at', envPath) + } +} + +async function main(): Promise { + loadEnv() + + // initializeDatabase() must run before anything imports src/routes/hijack-alerts, + // which calls getDatabase() at module load time. Dynamic import defers evaluation + // of ./server (and its own static imports) until after the pool exists. + initializeDatabase() + + const { startApiServer } = await import('./server.js') + const port = parseInt(process.env.API_PORT ?? '3102', 10) + await startApiServer(port) +} + +async function shutdown(signal: string): Promise { + console.log(`[API] ${signal} received, shutting down`) + await closeDatabaseConnection() + process.exit(0) +} + +process.on('SIGTERM', () => void shutdown('SIGTERM')) +process.on('SIGINT', () => void shutdown('SIGINT')) + +main().catch((error) => { + console.error('[API] Fatal startup error:', error) + process.exit(1) +}) diff --git a/src/api/server.ts b/src/api/server.ts index 513807f..0b13c5f 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -17,7 +17,7 @@ import { changelogRoutes } from '../features/changelog/routes' import { ribRoutes } from '../features/rib/routes' import { hijackSubscribeRoutes } from '../features/hijack-subscribe/routes' -export async function createApiServer(port: number = 3100) { +export async function createApiServer(_port: number = 3102) { const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL ?? 'info', @@ -66,7 +66,7 @@ export async function createApiServer(port: number = 3100) { return fastify } -export async function startApiServer(port: number = 3100): Promise { +export async function startApiServer(port: number = 3102): Promise { const server = await createApiServer(port) try { diff --git a/src/features/aspa-adoption/scheduler.ts b/src/features/aspa-adoption/scheduler.ts index dce3677..b914398 100644 --- a/src/features/aspa-adoption/scheduler.ts +++ b/src/features/aspa-adoption/scheduler.ts @@ -41,7 +41,6 @@ export class ASPAAdoptionScheduler { stop(): void { if (this.task) { this.task.stop() - this.task.destroy() this.task = null console.log('[ASPA Scheduler] Job stopped') } diff --git a/src/features/bgp-communities/routes.ts b/src/features/bgp-communities/routes.ts index 50aa5fa..c72ea73 100644 --- a/src/features/bgp-communities/routes.ts +++ b/src/features/bgp-communities/routes.ts @@ -29,7 +29,7 @@ export async function bgpCommunitiesRoutes(fastify: FastifyInstance): Promise = new Map() private readonly TTL_MS = 5 * 60 * 1000 - private cleanupInterval: NodeJS.Timer | null = null + private cleanupInterval: NodeJS.Timeout | null = null constructor() { this.startCleanup() diff --git a/src/features/pdf-export/renderer.ts b/src/features/pdf-export/renderer.ts index 135ad15..fee25ff 100644 --- a/src/features/pdf-export/renderer.ts +++ b/src/features/pdf-export/renderer.ts @@ -24,12 +24,12 @@ export class PDFRenderer { const page = await this.browser.newPage() try { + page.setDefaultTimeout(timeout_ms) await page.setContent(html, { waitUntil: 'networkidle' }) const pdfBuffer = await page.pdf({ format: 'A4', margin: { top: '0', right: '0', bottom: '0', left: '0' }, - timeout: timeout_ms, }) return pdfBuffer diff --git a/src/features/rpki-history/routes.ts b/src/features/rpki-history/routes.ts index b3e34d9..acace73 100644 --- a/src/features/rpki-history/routes.ts +++ b/src/features/rpki-history/routes.ts @@ -29,7 +29,7 @@ export async function rpkiHistoryRoutes(fastify: FastifyInstance): Promise if (!response.ok) { throw new Error(`RIPE Stat returned HTTP ${response.status}`); } - const data = await response.json(); + const data = await response.json() as any; const byOrigin = (data && data.data && data.data.by_origin) || []; const prefixes: any[] = []; diff --git a/src/routes/hijack-alerts.ts b/src/routes/hijack-alerts.ts index 506aa9a..8289002 100644 --- a/src/routes/hijack-alerts.ts +++ b/src/routes/hijack-alerts.ts @@ -25,7 +25,7 @@ interface CreateHijackRequest { export async function hijackAlertsRoutes(fastify: FastifyInstance): Promise { // Register webhook subscription - fastify.post<{ Querystring: { asn: string } }>( + fastify.post<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>( '/webhooks', async (request: FastifyRequest<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>, reply: FastifyReply) => { try {