/* ═══════════════════════════════════════════════════════════════ NGU API Binds to localhost only. nginx is the only thing that talks to it, which is what lets the routes trust X-Forwarded-For and skip CORS entirely — in production the API and the site share an origin, and in development Vite proxies /api so they share one there too. ═══════════════════════════════════════════════════════════════ */ import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; import { openDatabase, migrate } from "./db.js"; import { rateLimit } from "./rateLimit.js"; import content from "./routes/content.js"; import people from "./routes/people.js"; import history from "./routes/history.js"; import feedback from "./routes/feedback.js"; import auth from "./routes/auth.js"; import admin from "./routes/admin.js"; import panel from "./routes/panel.js"; import adminEntities from "./routes/admin-entities.js"; import { startSessionSweeper } from "./auth.js"; import { syncDescriptorsWithSchema } from "./admin-schema-sync.js"; import { ENTITIES } from "./admin-schema.js"; const HOST = process.env.HOST ?? "127.0.0.1"; const PORT = Number(process.env.PORT ?? 3001); const DB_PATH = process.env.DB_PATH ?? "./ngu.db"; /* ── Boot ────────────────────────────────────────────────────── */ const db = await openDatabase(DB_PATH); const version = migrate(db); syncDescriptorsWithSchema(db, ENTITIES);syncDescriptorsWithSchema(db, ENTITIES); console.log(`db ${DB_PATH} (${db.driverName}, schema v${version})`); /* ── App ─────────────────────────────────────────────────────── */ const app = new Hono(); app.use("*", logger()); app.use("*", async (c, next) => { c.set("db", db); await next(); }); app.get("/api/health", (c) => c.json({ ok: true, schema: version, driver: db.driverName }), ); app.route("/api", content); app.route("/api", people); app.route("/api", history); // Tighter limit on the write path than anything else gets. app.use("/api/feedback", rateLimit({ windowMs: 60_000, max: 5 })); app.route("/api/feedback", feedback); app.use("/api/auth/login", rateLimit({ windowMs: 15 * 60_000, max: 10 })); app.route("/api/admin/panel", panel); app.route("/api/auth", auth); app.route("/api/admin", admin); app.route("/api/admin", adminEntities); startSessionSweeper(db); app.notFound((c) => c.json({ error: "Not found" }, 404)); app.onError((err, c) => { console.error(err); // Never leak internals to the browser. return c.json({ error: "Something went wrong." }, 500); }); /* ── Serve ───────────────────────────────────────────────────── */ const server = serve({ fetch: app.fetch, hostname: HOST, port: PORT }, (info) => console.log(`listening http://${info.address}:${info.port}`), ); /* ── Shutdown ────────────────────────────────────────────────── systemd sends SIGTERM on stop and restart. Closing the handle flushes the WAL cleanly, which saves a recovery pass on the next boot. ───────────────────────────────────────────────────────────── */ for (const signal of ["SIGTERM", "SIGINT"]) { process.on(signal, () => { console.log(`${signal} — shutting down`); server.close(() => { try { db.close(); } catch { /* nothing useful to do here */ } process.exit(0); }); }); }