import { existsSync } from "node:fs"; import { join } from "node:path"; import Fastify from "fastify"; import jwt from "@fastify/jwt"; import rateLimit from "@fastify/rate-limit"; import fastifyStatic from "@fastify/static"; import formbody from "@fastify/formbody"; import { loadConfig } from "./config.js"; import { buildContext } from "./context.js"; import { authRoutes } from "./routes/auth.js"; import { webhookRoutes } from "./routes/webhook.js"; import { ticketRoutes } from "./routes/tickets.js"; import { testRoutes } from "./routes/test.js"; export async function build() { const config = loadConfig(); const app = Fastify({ logger: { level: config.LOG_LEVEL }, trustProxy: true, // behind nginx bodyLimit: 1_000_000, }); app.decorate("ctx", buildContext(config)); await app.register(jwt, { secret: config.TOKEN_SECRET }); await app.register(rateLimit, { global: false }); await app.register(formbody); // accept application/x-www-form-urlencoded webhooks too await app.register(authRoutes); await app.register(webhookRoutes); await app.register(ticketRoutes); await app.register(testRoutes); // Serve the exported Expo web build (if present) with SPA fallback. const webDir = config.WEB_DIR ?? join(process.cwd(), "web"); if (existsSync(webDir)) { await app.register(fastifyStatic, { root: webDir, wildcard: false }); app.setNotFoundHandler((req, reply) => { // Let unknown /api routes 404 as JSON; everything else -> SPA index. if (req.raw.url && req.raw.url.startsWith("/api")) { return reply.code(404).send({ error: "not_found" }); } return reply.sendFile("index.html"); }); app.log.info({ webDir }, "serving static web build"); } else { app.log.warn({ webDir }, "no web build found; serving API only"); } return app; } // Entry point (skipped when imported by tests). const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}`; if (isMain) { const config = loadConfig(); build() .then((app) => app.listen({ port: config.PORT, host: config.HOST })) .then((addr) => { // eslint-disable-next-line no-console console.log(`camptickets backend listening on ${addr}`); }) .catch((err) => { // eslint-disable-next-line no-console console.error(err); process.exit(1); }); }