import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import Fastify from 'fastify'; import fastifyStatic from '@fastify/static'; import fastifyCookie from '@fastify/cookie'; import fastifyHelmet from '@fastify/helmet'; import fastifyRateLimit from '@fastify/rate-limit'; import pg from 'pg'; import api, { bootstrapSuper } from './api.js'; import { runMigrations } from './migrate.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PORT = Number(process.env.PORT ?? 3000); const HOST = process.env.HOST ?? '0.0.0.0'; const TRUST_PROXY_HOPS = Math.max(0, Number(process.env.TRUST_PROXY_HOPS ?? 2)); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const app = Fastify({ logger: true, // Pangolin/Traefik en nginx vormen normaal twee vertrouwde proxy-hops. // Vertrouw nooit willekeurige X-Forwarded-* headers van directe clients. trustProxy: TRUST_PROXY_HOPS, // Alleen de borddata-route krijgt expliciet een ruimere limiet. bodyLimit: 1024 * 1024, }); // --- Database pool ----------------------------------------------------------- // Verbinding via DATABASE_URL, bv: postgres://teach:pw@db:5432/teach const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: Number(process.env.PG_POOL_MAX ?? 10), }); // Maak de pool bereikbaar in routes via app.pg app.decorate('pg', pool); // --- Health checks (gebruikt door Docker + load balancer) -------------------- app.get('/healthz', async () => ({ status: 'ok' })); app.get('/readyz', async (req, reply) => { try { await pool.query('SELECT 1'); return { status: 'ready' }; } catch (err) { req.log.error({ err }, 'database not ready'); reply.code(503); return { status: 'db_unavailable' }; } }); // --- HTTP-beveiliging -------------------------------------------------------- await app.register(fastifyCookie); await app.register(fastifyRateLimit, { global: false }); await app.register(fastifyHelmet, { global: true, hsts: IS_PRODUCTION ? { maxAge: 31536000, includeSubDomains: true } : false, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'blob:', 'https:'], mediaSrc: ["'self'", 'blob:', 'https:'], connectSrc: ["'self'", 'https://api.arasaac.org'], frameSrc: ['https://www.youtube.com', 'https://player.vimeo.com'], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'none'"], upgradeInsecureRequests: IS_PRODUCTION ? [] : null, }, }, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, }); app.addHook('onSend', async (_req, reply) => { reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self)'); }); // --- API --------------------------------------------------------------------- // Inloggen, rollen, gebruikersbeheer en per-gebruiker data. Zie src/api.js. app.register(api, { prefix: '/api' }); // --- Static frontend --------------------------------------------------------- // Serveert public/index.html (de digibord-app) op / app.register(fastifyStatic, { root: join(__dirname, '..', 'public'), index: ['index.html'], }); // --- Start ------------------------------------------------------------------- const start = async () => { try { await runMigrations(pool, app.log, join(__dirname, '..', 'db')); await bootstrapSuper(pool, app.log); await app.listen({ port: PORT, host: HOST }); } catch (err) { app.log.error(err); process.exit(1); } }; // Nette shutdown zodat Docker de container snel kan stoppen for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, async () => { app.log.info(`${signal} ontvangen, afsluiten...`); await app.close(); await pool.end(); process.exit(0); }); } start();