teach/src/server.js
Ramon f9b2be8187
Some checks failed
dev - build & deploy naar test / build-and-deploy (push) Failing after 4s
v0.3.03-beta: versterk publieke beveiliging
2026-07-14 12:42:16 +02:00

113 lines
3.9 KiB
JavaScript

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