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.
This commit is contained in:
parent
04bc8e3c4c
commit
141911537d
1
.gitignore
vendored
1
.gitignore
vendored
@ -49,6 +49,7 @@ audit/__pycache__/
|
|||||||
audit/asn_registry.json
|
audit/asn_registry.json
|
||||||
audit/latest_report.txt
|
audit/latest_report.txt
|
||||||
backups/
|
backups/
|
||||||
|
.rollback/
|
||||||
public/index.html.bak
|
public/index.html.bak
|
||||||
public/lia.html
|
public/lia.html
|
||||||
server.js.bak*
|
server.js.bak*
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# PeerCortex safe deploy script — always backup before restart
|
# 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
|
BACKUP_DIR=/opt/peercortex-app/backups
|
||||||
mkdir -p $BACKUP_DIR
|
mkdir -p $BACKUP_DIR
|
||||||
TS=$(date +%Y%m%d_%H%M%S)
|
TS=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|||||||
91
server.js
91
server.js
@ -3103,6 +3103,40 @@ async function fetchWhois(resource) {
|
|||||||
return result;
|
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
|
// HTTP Server
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@ -5877,10 +5911,14 @@ const server = http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
// Feature 28: Submarine Cable overlay (TeleGeography proxy)
|
// Feature 28: Submarine Cable overlay (TeleGeography proxy)
|
||||||
// ── Submarine Cables map data ─────────────────────────────────
|
// ── 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) ───────────────
|
// ── 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 ─────────────────────────────────────────
|
// ── Changelog page ─────────────────────────────────────────
|
||||||
@ -5927,26 +5965,39 @@ ${html}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── BGP Community Decoder ────────────────────────────────────
|
// ── BGP Community Decoder ────────────────────────────────────
|
||||||
// Migrated to src/features/bgp-communities/
|
if (reqPath === '/api/communities') {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ── IRR Audit ─────────────────────────────────────────────────
|
// ── IRR Audit ─────────────────────────────────────────────────
|
||||||
// Migrated to src/features/irr-audit/
|
if (reqPath === '/api/irr-audit') {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ── AS-SET Expander ───────────────────────────────────────────
|
// ── 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) ──
|
// ── 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) ────────────────
|
// ── 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) ─────────────────────────────────
|
// ── Looking Glass (RIPE Stat) ─────────────────────────────────
|
||||||
// Migrated to src/features/looking-glass/
|
if (reqPath === '/api/looking-glass') {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ── IXP Peering Matrix ────────────────────────────────────────
|
// ── 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 ──────
|
// ── CORS preflight for all /api/webhooks + /api/hijacks ──────
|
||||||
@ -6137,16 +6188,26 @@ ${html}
|
|||||||
|
|
||||||
|
|
||||||
// ── Hijack Alerts (legacy read) ───────────────────────────────
|
// ── 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 ────────────────────────────────────────
|
// ── Changelog JSON API ────────────────────────────────────────
|
||||||
// Migrated to src/features/changelog/
|
if (reqPath === '/changelog-data') {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ── bio-rd RIB routes ─────────────────────────────────────────
|
// ── bio-rd RIB routes ─────────────────────────────────────────
|
||||||
// Migrated to src/features/rib/
|
if (reqPath.startsWith('/api/rib/')) {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Prefix Changes ──────────────────────────────────────────────
|
// ── Prefix Changes ──────────────────────────────────────────────
|
||||||
// Migrated to src/features/prefix-changes/
|
if (reqPath === '/api/prefix-changes') {
|
||||||
|
return proxyToApiServer(req, res, req.url);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FEATURE 2: PDF Export
|
// FEATURE 2: PDF Export
|
||||||
|
|||||||
49
src/api/index.ts
Normal file
49
src/api/index.ts
Normal file
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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)
|
||||||
|
})
|
||||||
@ -17,7 +17,7 @@ import { changelogRoutes } from '../features/changelog/routes'
|
|||||||
import { ribRoutes } from '../features/rib/routes'
|
import { ribRoutes } from '../features/rib/routes'
|
||||||
import { hijackSubscribeRoutes } from '../features/hijack-subscribe/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({
|
const fastify = Fastify({
|
||||||
logger: {
|
logger: {
|
||||||
level: process.env.LOG_LEVEL ?? 'info',
|
level: process.env.LOG_LEVEL ?? 'info',
|
||||||
@ -66,7 +66,7 @@ export async function createApiServer(port: number = 3100) {
|
|||||||
return fastify
|
return fastify
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startApiServer(port: number = 3100): Promise<void> {
|
export async function startApiServer(port: number = 3102): Promise<void> {
|
||||||
const server = await createApiServer(port)
|
const server = await createApiServer(port)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export class ASPAAdoptionScheduler {
|
|||||||
stop(): void {
|
stop(): void {
|
||||||
if (this.task) {
|
if (this.task) {
|
||||||
this.task.stop()
|
this.task.stop()
|
||||||
this.task.destroy()
|
|
||||||
this.task = null
|
this.task = null
|
||||||
console.log('[ASPA Scheduler] Job stopped')
|
console.log('[ASPA Scheduler] Job stopped')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export async function bgpCommunitiesRoutes(fastify: FastifyInstance): Promise<vo
|
|||||||
throw new Error(`RIPE Stat API returned ${response.status}`);
|
throw new Error(`RIPE Stat API returned ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json() as any;
|
||||||
|
|
||||||
const rawComms: string[] = [];
|
const rawComms: string[] = [];
|
||||||
if (data && data.data && data.data.bgp_state) {
|
if (data && data.data && data.data.bgp_state) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ interface CacheEntry {
|
|||||||
export class PDFCacheManager {
|
export class PDFCacheManager {
|
||||||
private cache: Map<string, CacheEntry> = new Map()
|
private cache: Map<string, CacheEntry> = new Map()
|
||||||
private readonly TTL_MS = 5 * 60 * 1000
|
private readonly TTL_MS = 5 * 60 * 1000
|
||||||
private cleanupInterval: NodeJS.Timer | null = null
|
private cleanupInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startCleanup()
|
this.startCleanup()
|
||||||
|
|||||||
@ -24,12 +24,12 @@ export class PDFRenderer {
|
|||||||
const page = await this.browser.newPage()
|
const page = await this.browser.newPage()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
page.setDefaultTimeout(timeout_ms)
|
||||||
await page.setContent(html, { waitUntil: 'networkidle' })
|
await page.setContent(html, { waitUntil: 'networkidle' })
|
||||||
|
|
||||||
const pdfBuffer = await page.pdf({
|
const pdfBuffer = await page.pdf({
|
||||||
format: 'A4',
|
format: 'A4',
|
||||||
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
||||||
timeout: timeout_ms,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return pdfBuffer
|
return pdfBuffer
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export async function rpkiHistoryRoutes(fastify: FastifyInstance): Promise<void>
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`RIPE Stat returned HTTP ${response.status}`);
|
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 byOrigin = (data && data.data && data.data.by_origin) || [];
|
||||||
const prefixes: any[] = [];
|
const prefixes: any[] = [];
|
||||||
|
|||||||
@ -25,7 +25,7 @@ interface CreateHijackRequest {
|
|||||||
|
|
||||||
export async function hijackAlertsRoutes(fastify: FastifyInstance): Promise<void> {
|
export async function hijackAlertsRoutes(fastify: FastifyInstance): Promise<void> {
|
||||||
// Register webhook subscription
|
// Register webhook subscription
|
||||||
fastify.post<{ Querystring: { asn: string } }>(
|
fastify.post<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>(
|
||||||
'/webhooks',
|
'/webhooks',
|
||||||
async (request: FastifyRequest<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>, reply: FastifyReply) => {
|
async (request: FastifyRequest<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>, reply: FastifyReply) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user