import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { cors } from "hono/cors"; import { bodyLimit } from "hono/body-limit"; import { MIN_CHARS, THREAD_PAGE, GENDER_LABEL, RELATIONSHIP_LABEL, PASSWORD_MIN, PASSWORD_MAX, NAME_MAX, BIO_MAX, LOCATION_DECIMALS, PHOTO_MAX_BYTES, REPORT_REASONS, REPORT_DETAILS_MAX, COMPLAINTS_CONTACT, SEARCH_WINDOW_MIN, SEARCH_PER_WINDOW, SIGNUP_PRODUCT_ID, FAREWELL_MAX, AGE_MIN, AGE_MAX } from "../src/constants.js"; import { hashPassword, verifyPassword, newToken, hashToken } from "./auth.js"; import { rateLimiter, clientIp, tooMany, inWords } from "./ratelimit.js"; import { letterDenial } from "./quota.js"; import { verifyPurchase, grantedPurchase } from "./billing.js"; import { privacyHtml, landingHtml, childSafetyHtml } from "./privacy.js"; import { weetjesHtml } from "./weetjes.js"; import { collectReport } from "./report.js"; import { renderPublicHtml, MIN_LEDEN } from "./report-html.js"; import { fontPath } from "./huisstijl.js"; import { readFile } from "node:fs/promises"; import { checkPhoto } from "./media.js"; import { noteSearch } from "./search-log.js"; import promotie, { PROMOTIE_PATH } from "./promotie.js"; import wachtwoord, { stuurHerstelMail, linkBase, RESET_TTL_MS } from "./wachtwoord.js"; import * as db from "./queries.js"; const PORT = Number(process.env.PORT ?? 3000); /* * Toegestane origins. Vite-dev gaat via de proxy (zelfde origin), maar een * Capacitor-app praat rechtstreeks: Android = https://localhost, iOS = capacitor://localhost. */ const CORS_ORIGINS = (process.env.CORS_ORIGINS ?? "http://localhost:5173,https://localhost,capacitor://localhost,http://localhost") .split(",").map((s) => s.trim()).filter(Boolean); const GENDERS = Object.keys(GENDER_LABEL); const RELATIONSHIPS = Object.keys(RELATIONSHIP_LABEL); const REASONS = Object.keys(REPORT_REASONS); /* * Een brief met foto is een JSON-verzoek met base64 erin (~1,35x de bytes); daarboven houdt het op. * Nginx weigert grotere verzoeken al eerder (client_max_body_size 2M), dit is de eigen grens. */ const MAX_BODY_BYTES = Math.ceil(PHOTO_MAX_BYTES * 1.4) + 64 * 1024; /* * En voor al het andere veel minder. Een brief met foto is het enige zware verzoek dat deze API * kent; een profiel, een melding of een inlogpoging is hooguit een paar kilobyte. Hier stond geen * enkele grens omheen — alleen `/letters` keek naar de opgegeven Content-Length, en die kan een * client weglaten of erover liegen. Wat nginx doorliet (2 MB) werd dus zonder meer ingelezen. */ const JSON_MAX_BYTES = 64 * 1024; /* * Drie lagen: `root` is de app op het domein (publieke pagina's: `/`, `/privacy`), daarin gemount * `app` = de JSON-API onder `/api` en `media` = de foto's onder `/media`. Nginx proxyt het hele * domein hierheen. De Capacitor-app praat alleen met `/api` en `/media`; er draait bewust géén * web-versie van de app hier. */ /* * De toegangslog: alleen het pad, nadrukkelijk zónder querystring. Hono's eigen logger() logt * `url.slice(url.indexOf("/", 8))` en neemt de querystring dus wél mee — daarmee stond elke * zoekopdracht (`GET /api/search?q=...`) letterlijk in de log, en met de bewaartermijn van 30 dagen * erbij is dat een zoekgeschiedenis in een dating-app. Wat we van zoeken willen weten telt * search-log.js, per woord en zonder wie. Deze log is voor storingen en beveiliging en hoort dus * niet meer te weten dan welk pad wanneer werd opgevraagd en of het lukte. * * Nginx logt standaard de héle requestregel, mét querystring en mét IP. Zie de README bij * "Productie": daar hoort `access_log` de zoekopdrachten ook niet vast te leggen. */ const pathLogger = async (c, next) => { const start = Date.now(); const { method } = c.req; const path = new URL(c.req.url).pathname; await next(); console.log(`--> ${method} ${path} ${c.res.status} ${Date.now() - start}ms`); }; const root = new Hono(); root.use(pathLogger); const app = new Hono(); const media = new Hono(); const crossOrigin = cors({ origin: CORS_ORIGINS, allowHeaders: ["Content-Type", "Authorization"], allowMethods: ["GET", "PUT", "POST", "DELETE", "OPTIONS"], }); app.use(crossOrigin); media.use(crossOrigin); /* * Het dak op wat er binnenkomt, vóór er ook maar iets ingelezen wordt. Twee maten, want een brief * met foto is nu eenmaal duizend keer zo groot als een inlogpoging, en de ruime maat hoort niet te * gelden op een pad waar hij nergens voor nodig is. */ const grens = (max, melding) => bodyLimit({ maxSize: max, onError: (c) => c.json({ error: melding }, 413) }); const brievenGrens = grens(MAX_BODY_BYTES, "De foto is te groot"); const jsonGrens = grens(JSON_MAX_BYTES, "Dit verzoek is te groot"); app.use((c, next) => (c.req.path.endsWith("/letters") ? brievenGrens : jsonGrens)(c, next)); /* * Wat elke pagina van deze server meekrijgt. Er stond niets — geen CSP, geen frame-ancestors, geen * Referrer-Policy — terwijl hier een wachtwoordformulier bij zit (/wachtwoord) en een beheerpagina * (/promotieteam). Op géén van deze pagina's staat één regel JavaScript, dus `script-src` kan * gewoon dicht; alleen de stijl staat inline in een