Changeset 834bcc3 in Klonkt for src/server.js
- Timestamp:
- 06/23/2026 06:14:27 PM (3 months ago)
- Branches:
- main
- Children:
- d774679
- Parents:
- bb42dfb
- File:
-
- 1 edited
-
src/server.js (modified) (12 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/server.js
rbb42dfb r834bcc3 2 2 * Klonkt Beta — server bootstrap 3 3 * 4 * Perso onlijkmulti-site platform — Node + SQLite + htmx.4 * Personal multi-site platform — Node + SQLite + htmx. 5 5 * Stack: Express + better-sqlite3 + EJS + htmx + ws. 6 6 */ … … 87 87 "'self'", 88 88 "'unsafe-inline'", 89 // Eigen custom-embeds (embed-player.js) laden de OFFICIELE player-API's90 // van deze hosts. Zonder deze whitelist blokkeert de CSP ze stil (alleen91 // een console-fout) en faalt de embed-speler.89 // Our custom embeds (embed-player.js) load the OFFICIAL player APIs 90 // from these hosts. Without this whitelist the CSP silently blocks them 91 // (only a console error) and the embed player fails. 92 92 "https://www.youtube.com", // YouTube IFrame Player API (+ www-widgetapi.js) 93 "https://s.ytimg.com", // YouTube player -assets93 "https://s.ytimg.com", // YouTube player assets 94 94 "https://w.soundcloud.com", // SoundCloud Widget API (api.js) 95 95 "https://open.spotify.com", // Spotify iFrame API (loader) 96 "https://*.spotifycdn.com", // Spotify iFrame API ( echtebundle: embed-cdn.spotifycdn.com)96 "https://*.spotifycdn.com", // Spotify iFrame API (real bundle: embed-cdn.spotifycdn.com) 97 97 ], 98 // Helmet's default zet script-src-attr op 'none', wat ALLE inline event-99 // handlers (onchange/onclick/onsubmit) blokkeert — daardoor deed o.a. de100 // avatar-upload (<input onchange="this.form.submit()">) en de rol-dropdown101 // niets. We staan inline handlers expliciet toe, consistent met de al102 // t oegestane inline <script> hierboven.98 // Helmet's default sets script-src-attr to 'none', which blocks ALL inline 99 // event handlers (onchange/onclick/onsubmit) — causing e.g. the avatar 100 // upload (<input onchange="this.form.submit()">) and the role dropdown to 101 // silently do nothing. We explicitly allow inline handlers, consistent with 102 // the already-allowed inline <script> above. 103 103 scriptSrcAttr: ["'unsafe-inline'"], 104 104 styleSrc: ["'self'", "'unsafe-inline'"], 105 // blob: nodig voor de afbeeldings-editor (Cropper) — die toont de gekozen106 // foto via een URL.createObjectURL(blob:…). Zonder blob: blokkeert de CSP107 // de <img> stil → leeg bewerk-venster. (media-src heeft blob: al voor audio.)105 // blob: required for the image editor (Cropper) — it displays the chosen 106 // photo via URL.createObjectURL(blob:…). Without blob: the CSP silently 107 // blocks the <img> → empty edit window. (media-src already has blob: for audio.) 108 108 imgSrc: ["'self'", "data:", "https:", "blob:"], 109 109 connectSrc: ["'self'", "wss:", "ws:", "https://*.spotifycdn.com", "https://*.scdn.co"], … … 121 121 "https://embed.music.apple.com", 122 122 "https://www.youtube-nocookie.com", 123 "https://www.youtube.com", // YouTube IFrame API maakt soms een www.youtube.com-iframe123 "https://www.youtube.com", // YouTube IFrame API sometimes creates a www.youtube.com iframe 124 124 "https://player.vimeo.com", 125 125 ], … … 143 143 if (!isDev) app.set('trust proxy', 1); 144 144 145 // Schema aanmaken/bijwerken VÓÓR iets de DB aanraakt: de session-store doet 146 // bij constructie al een query op de `sessions`-tabel, dus bij een verse 147 // install moeten de tabellen eerst bestaan (anders: "no such table: sessions" 148 // → crash-loop op de allereerste boot). 145 // Create/migrate the schema BEFORE anything touches the DB: the session store 146 // queries the `sessions` table on construction, so on a fresh install the tables 147 // must exist first (otherwise: "no such table: sessions" → crash loop on first boot). 149 148 initializeDatabase(); 150 startScheduler(); // release -planning: zet geplande posts live zodra publish_at bereikt is151 152 // Vangnet: garandeer dat er altijd een primaire site is(solo/hub/circle).153 // Idempotent — doe t niets als er al een site is of nog geen beheerder.149 startScheduler(); // release planning: publish scheduled posts when publish_at is reached 150 151 // Safety net: guarantee that there is always a primary site (solo/hub/circle). 152 // Idempotent — does nothing if a site already exists or there is no admin yet. 154 153 ensurePrimarySite(); 155 154 … … 173 172 app.use('/assets', express.static(path.join(__dirname, 'assets'), { maxAge: isDev ? 0 : '1y' })); 174 173 app.use('/media', express.static(process.env.MEDIA_PATH || './storage/media', { 175 // Publi eke media (post-covers, avatars) moet door andere Klonkt-sites in hun176 // CIRKEL cross-origin embedbaar zijn. Helmet zet standaard CORP=same-origin,177 // wat die afbeeldingen in de browser blokkeert (bestand komt wél binnen, maar178 // de browser weigert 'm te tonen). Voor /media dus expliciet cross-origin.174 // Public media (post covers, avatars) must be cross-origin embeddable by other 175 // Klonkt sites in their CIRCLE. Helmet sets CORP=same-origin by default, which 176 // causes the browser to block those images (the file arrives, but the browser 177 // refuses to render it). Set cross-origin explicitly for /media. 179 178 setHeaders: (res) => res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'), 180 179 })); 181 180 182 // ( Verwijderd) TWA / digital-asset-links — alleen nodig voor de APK/TWA-variant.183 // Klonkt is PWA-only; geen assetlinks.json meer.184 185 // Cir kels: periodieke achtergrond-sync van remote instances (no-op tenzijtenancy='circle').181 // (Removed) TWA / digital-asset-links — only needed for the APK/TWA variant. 182 // Klonkt is PWA-only; assetlinks.json is no longer served. 183 184 // Circles: periodic background sync of remote instances (no-op unless tenancy='circle'). 186 185 startCircleSyncLoop(); 187 186 … … 203 202 })(); 204 203 205 // Cir kels-federatie: publieke, site-agnostischeendpoints (/.klonkt/*).206 // Vóór resolveSite/theme — ze hebben geen site-context nodig.204 // Circle federation: public, site-agnostic endpoints (/.klonkt/*). 205 // Before resolveSite/theme — they don't need a site context. 207 206 app.use(federationRoutes); 208 207 … … 211 210 app.use(loadTheme); 212 211 213 // Li chtgewicht CSRF-defense: weiger cross-origin state-wijzigenderequests.214 // Same-origin forms + HTMX s turen een matchende Origin; ontbreekt Origin dan215 // laten we door (non-browser clients). sameSite:'lax' op de sessiecookie is de216 // tweede laag. (Geldt niet voorGET/HEAD/OPTIONS.)212 // Lightweight CSRF defense: reject cross-origin state-mutating requests. 213 // Same-origin forms + HTMX send a matching Origin; missing Origin is allowed 214 // through (non-browser clients). sameSite:'lax' on the session cookie is the 215 // second layer. (Does not apply to GET/HEAD/OPTIONS.) 217 216 app.use((req, res, next) => { 218 217 if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next(); 219 218 const origin = req.get('origin'); 220 if (!origin) return next(); // geen Origin -> geen browser-CSRF-vector219 if (!origin) return next(); // no Origin → no browser CSRF vector 221 220 let originHost; 222 221 try { originHost = new URL(origin).host; } catch { return res.status(403).send('Ongeldige origin'); } … … 225 224 }); 226 225 227 // Kijker-accounts: alles bekijken mag (incl. Beheer), niets wijzigen. Dit is de228 // ENIGE schrijf-blokkade — fail-closed, vóór alle route-handlers. Elke state-229 // wijzigende methode wordt geweigerd (de login-POST zet de sessie pas ná deze230 // guard, dus die valt er niet onder). I.p.v. rauwe 403-tekst tonen we een nette231 // pagina (of, bij HTMX, een ingeswapte melding).226 // Viewer accounts: may view everything (including Admin), change nothing. This is 227 // the ONLY write gate — fail-closed, before all route handlers. Every state-mutating 228 // method is rejected (the login POST sets the session after this guard, so it is 229 // not affected). Instead of raw 403 text we render a clean page (or, for HTMX, 230 // a swapped-in message). 232 231 app.use((req, res, next) => { 233 232 const mutating = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS'; 234 233 if (mutating && isViewer(req.session?.user)) { 235 234 if (req.headers['hx-request'] === 'true') { 236 // htmx swapt niet op 4xx; stuur 200 + retarget zodat de melding in #pcms-main verschijnt.235 // htmx doesn't swap on 4xx; send 200 + retarget so the message appears in #pcms-main. 237 236 res.setHeader('HX-Retarget', '#pcms-main'); 238 237 res.setHeader('HX-Reswap', 'innerHTML'); … … 277 276 // Feed/sitemap routes are mounted at root because they're at well-known paths 278 277 app.use('/', feedRoutes); 279 app.use('/leden', artistsRoutes); // doorzoekbare leden-directory (alleen hub; solo: next())278 app.use('/leden', artistsRoutes); // searchable member directory (hub only; solo: next()) 280 279 app.get('/artiesten', (req, res) => res.redirect(301, req.originalUrl.replace(/^\/artiesten/, '/leden'))); // oude URL -> /leden 281 280 app.use('/', hubRoutes); // hub-overview op '/' (solo: next() -> postsRoutes) … … 306 305 307 306 // A stable identity per site so installs don't collide (Chromium uses `id`). 308 // NB: een id-wissel orphant bestaande PWA-installs (er is geen migratie die een309 // install over een id-verandering heen tilt) — wie een site al als PWA had,310 // moet 'm één keer opnieuw installeren. Data blijft server-side, dus niets kwijt.307 // NB: changing the id orphans existing PWA installs (no migration carries an 308 // install across an id change) — anyone who already installed the site as a 309 // PWA will need to reinstall once. Data stays server-side, so nothing is lost. 311 310 const idBase = site?.slug ? `klonkt-${site.slug}` : 'klonkt'; 312 311 … … 376 375 self.clients.claim(); 377 376 }); 378 // ALLEEN navigaties (HTML-pagina's) onderscheppen, voor een offline-fallback.379 // Afbeeldingen, CSS, JS en /media NIET aanraken — laat de browser die native380 // afhandelen. Anders kon een mislukte netwerk-fetch terugvallen op een lege381 // cache-match (undefined) en zo een afbeelding "kapot" maken bij een gewone382 // refresh (hard reload omzeilt de SW en werkte daarom wél).377 // ONLY intercept navigations (HTML pages) for an offline fallback. 378 // Do NOT touch images, CSS, JS or /media — let the browser handle those natively. 379 // Otherwise a failed network fetch could fall back to an empty cache match 380 // (undefined) and "break" an image on a normal refresh (hard reload bypasses 381 // the SW, which is why that case worked fine). 383 382 self.addEventListener('fetch', e => { 384 383 if (e.request.method !== 'GET') return; 385 if (e.request.mode !== 'navigate') return; // alleen page-loads384 if (e.request.mode !== 'navigate') return; // page loads only 386 385 e.respondWith( 387 386 fetch(e.request).catch(() => caches.match('/').then(r => r || Response.error())) … … 404 403 app.use((req, res) => { 405 404 res.status(404); 406 // Nette, mobielvriendelijke 404 via de shell (viewport + nav + huisstijl).407 // Valt terug op kale HTML als het renderen onverhoopt faalt.405 // Clean, mobile-friendly 404 via the shell (viewport + nav + site theme). 406 // Falls back to bare HTML if rendering unexpectedly fails. 408 407 try { 409 408 return renderPage(req, res, 'pages/404', {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)