CampgroundTickets/backend/src/server.ts
Hank b71dce0878 Add /test page with sample QR codes for scanner testing
GET /test (gated by ENABLE_TEST_PAGE) renders scannable QR codes for a set of
personas covering different attributes — solo, family with ice+parking, a real
donor for Banquet mode, ice-only, a pre-exhausted ticket, and an invalid code.
Idempotently seeds them into the current NocoDB table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 04:47:47 +00:00

68 lines
2.3 KiB
TypeScript

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);
});
}