import Fastify from 'fastify'; import fastifyStatic from '@fastify/static'; import fastifyCookie from '@fastify/cookie'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync } from 'node:fs'; import { openDb } from './db.js'; import { registerPublicRoutes } from './routes/public.js'; import { registerAdminRoutes } from './routes/admin.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); function parsePortEnv(name: string, fallback: number): number { const raw = process.env[name]; if (raw === undefined) return fallback; const n = Number(raw); if (!Number.isInteger(n) || n < 1 || n > 65535) { // A bare Number(...) turned an unparseable PORT into NaN, and // Fastify listens on a random free port for a NaN — silently wrong, // not an error anyone would see. console.warn(`${name}=${JSON.stringify(raw)} is not a valid port — using default ${fallback}`); return fallback; } return n; } const PORT = parsePortEnv('PORT', 3000); const HOST = process.env.HOST ?? '0.0.0.0'; const DB_PATH = process.env.DB_PATH ?? join(process.cwd(), 'wutz.db'); if (!process.env.ADMIN_PASSWORD) { // Not fatal — public routes work fine without it — but previously this // only surfaced as a 500 at the first login attempt, which in practice // is the middle of the event. Warn at boot instead. console.warn('ADMIN_PASSWORD is not set — the admin backoffice login will fail until it is.'); } const app = Fastify({ logger: true, // Off by default: only trust X-Forwarded-* when the deployer has // actually put a reverse proxy in front (the README recommends one for // TLS termination). Without this, req.ip behind such a proxy is always // the proxy's own address, making the stored client_ip column and its // CSV export uniformly useless. trustProxy: process.env.WUTZ_TRUST_PROXY === '1', }); await app.register(fastifyCookie); const db = openDb(DB_PATH); registerPublicRoutes(app, db); registerAdminRoutes(app, db); app.get('/healthz', async () => ({ ok: true })); // Static client (built by Vite into client/dist) const clientDist = join(__dirname, '..', '..', 'client', 'dist'); if (existsSync(clientDist)) { await app.register(fastifyStatic, { root: clientDist, prefix: '/', wildcard: false, }); app.setNotFoundHandler((req, reply) => { if (req.url.startsWith('/api') || req.url.startsWith('/admin/api')) { reply.code(404).send({ error: 'not found' }); return; } if (req.url.startsWith('/admin')) { reply.sendFile('admin.html'); return; } reply.sendFile('index.html'); }); } else { app.log.warn(`client dist not found at ${clientDist} — run \`pnpm --filter client build\``); } try { await app.listen({ port: PORT, host: HOST }); app.log.info(`listening on http://${HOST}:${PORT}`); } catch (err) { app.log.error(err); process.exit(1); }