source: Klonkt/src/middleware/site.js@ 34e63f0

main
Last change on this file since 34e63f0 was 34e63f0, checked in by roboburr <roboburr@…>, 5 weeks ago

De antwoordpoort ook in Klonkts eigen web-UI (shaer-r4c, vervolg)

De poort weigert nu in deliverReply, dus het antwoord komt er niet doorheen --
maar de webinterface toonde het veld gewoon nog. Precies de knop-die-niet-werkt
waar ik in de app-code tegen waarschuwde: een kind duwt tegen een deur die op
slot zit en krijgt geen uitleg.

res.locals.mayReply staat er nu een keer, naast res.locals.site, zodat geen route
hem apart hoeft door te geven.

NIET IN DE PARTIAL ZELF, en dat scheelde een fout. reply-editor.ejs leek het
knooppunt -- vier includes, een plek -- maar hij wordt ook voor BEWERKEN gebruikt
(msg-edit, /fediverse/:id/edit). Een guard daarbinnen had het bewerken van een
eigen bericht meegesloopt.

Dus per include, en in Berichten precies de tak die de server ook kiest:
/messages/reply gaat naar deliverReply als er een interaction+post is (een
antwoord, valt onder de poort) en anders naar deliverDirectNote (een bericht,
andere poort). Alleen de eerste verdwijnt.

De vier gewijzigde templates compileren; de suite (686) blijft groen.

  • Property mode set to 100644
File size: 4.3 KB
Line 
1/**
2 * Site middleware — resolve which site this request is for.
3 *
4 * Resolution order (hub-modus):
5 * 1. Pad /user/:slug → die site (legacy /sites/:slug → 301 naar /user/)
6 * 2. Anders (solo, of hub-landing): de primaire/hoofd-site
7 *
8 * Sets res.locals.site for all downstream handlers.
9 */
10
11import db from '../config/database.js';
12import { audioUrl } from '../services/AudioStreamService.js';
13import { audioEnabled } from '../config/features.js';
14import * as Guardianship from '../services/guardianship/index.js';
15
16/**
17 * The primary/main site — ONE source of truth (replaces the "oldest site ="
18 * main" assumption that was previously scattered across resolveSite/hub/account/admin).
19 * Reads the explicit is_primary flag; falls back to the oldest if it isn't set
20 * anywhere yet, so existing behaviour is preserved exactly.
21 */
22export function getPrimarySite() {
23 return db.prepare('SELECT * FROM sites WHERE is_primary = 1 LIMIT 1').get()
24 || db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get()
25 || null;
26}
27
28export function resolveSite(req, res, next) {
29 // One instance is one owner (Robins besluit, 31-7): there is one site tree,
30 // pinned to the primary site. The /user/:slug routing that hub mode needed
31 // is gone with it.
32 const defaultSite = getPrimarySite();
33 if (defaultSite) {
34 res.locals.site = defaultSite;
35 res.locals.siteUrlBase = '';
36 // Mag deze account antwoorden (shaer-r4c)? Eén keer hier, zodat de
37 // antwoordvelden in de views hem kunnen lezen zonder dat elke route hem
38 // apart doorgeeft. De server weigert het antwoord toch al in deliverReply;
39 // dit voorkomt alleen dat een kind tegen een deur duwt die op slot zit.
40 try {
41 const isWard = Guardianship.listGuardians(defaultSite.slug).length > 0;
42 res.locals.mayReply = Guardianship.wardGateAllowed(defaultSite.gate_replies, isWard);
43 } catch { res.locals.mayReply = true; }
44 }
45
46 next();
47}
48
49/**
50 * Audio tracks loader — pulls site-level tracks for the persistent player widget.
51 * Per Robin: player is separate from the footer, gated by site.enable_audio_player.
52 * Returns empty array if no site or audio is disabled — shell.ejs uses the
53 * length to decide whether to mount audio-player.js.
54 */
55export function loadAudioTracks(req, res, next) {
56 if (!audioEnabled()) { res.locals.audioTracks = []; return next(); } // lite-modus
57 const site = res.locals.site;
58 if (!site || site.enable_audio_player === 0) {
59 res.locals.audioTracks = [];
60 return next();
61 }
62
63 try {
64 // m.filename = the bare filename; the playable URL is the gated stream route
65 // (audioUrl). The media table has NO url column — the old query selected
66 // m.url and always failed silently (empty player). Now we build the URL from filename.
67 const rows = db.prepare(`
68 SELECT t.id, t.title, t.artist, t.duration, t.position, m.filename
69 FROM audio_tracks t
70 LEFT JOIN media m ON m.id = t.media_id
71 WHERE t.site_id = ?
72 ORDER BY t.position ASC, t.created_at ASC
73 `).all(site.id);
74 res.locals.audioTracks = rows.map((r) => ({
75 id: r.id, title: r.title, artist: r.artist, duration: r.duration, position: r.position,
76 media_url: r.filename ? audioUrl(r.filename) : null,
77 }));
78 } catch (e) {
79 // media table might not be queryable in some test setups — fall back gracefully
80 res.locals.audioTracks = [];
81 }
82
83 next();
84}
85
86/**
87 * Theme loader — applies user/site theme preferences.
88 */
89export function loadTheme(req, res, next) {
90 const PALETTES = ['klonkt','forest','ocean','teal','lilac','sunset','candy','amber'];
91
92 const user = req.session?.user;
93 const site = res.locals.site;
94
95 // A site always renders in ITS OWN palette, regardless of who is viewing. There is
96 // no per-user palette UI (user.palette is vestigial/stale data from old migrations),
97 // and the htmx pcmsNav path (render.js) already uses the site palette only — so reading
98 // user.palette here made a full page load (owner logged in) flip to the viewer's stale
99 // palette while htmx-nav kept the site's, i.e. "palette changes on hard refresh".
100 const palette = (site && PALETTES.includes(site.palette) ? site.palette : null)
101 || 'klonkt';
102
103 res.locals.palette = palette;
104 res.locals.theme = (user && ['dark','light'].includes(user.theme)) ? user.theme : 'dark';
105
106 next();
107}
Note: See TracBrowser for help on using the repository browser.