CampgroundTickets/backend/src/server.ts
Hank 4b78c82b35
Some checks failed
Build Android APK / build-apk (push) Failing after 1h11m4s
Add /crush33 comp-ticket portal + ticket-type badge on scan
Portal (/crush33): password-gated page (PORTAL_PASSWORD) for admins to create
entry-only tickets from just name + email, with a category
(Guest/Worker/Performer/Volunteer/Speaker). Creates a 1-admission ticket, emails
the QR, and shows the QR on-screen. New Ticket Type column.

Scanner: shows a prominent TYPE badge (🎭 PERFORMER, 🛠️ WORKER, …) on the
confirm + success screens and in admin, so staff can see it's a special ticket.
Added Worker + Performer personas to /test.

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

76 lines
2.7 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";
import { installRoutes } from "./routes/install.js";
import { webhookDocRoutes } from "./routes/webhookDoc.js";
import { publicLookupRoutes } from "./routes/publicLookup.js";
import { portalRoutes } from "./routes/portal.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);
await app.register(installRoutes);
await app.register(webhookDocRoutes);
await app.register(publicLookupRoutes);
await app.register(portalRoutes);
// 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);
});
}