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>
68 lines
2.3 KiB
TypeScript
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);
|
|
});
|
|
}
|