source: Klonkt/src/middleware/site.js@ fbfd7a1

main
Last change on this file since fbfd7a1 was 34a0053, checked in by Robin <roboburr@…>, 4 weeks ago

Het slot afmaken: posten tegenhouden en grijzen wat dicht is

Robin keek naar zijn verhuisde instance en zei: alles is nog functioneel. Klopt, en
er zaten drie fouten in wat ik gisteren bouwde.

EEN: movedLock stond niet in de default-export, terwijl de routes hem via die weg
aanroepen. ActivityPubService.movedLock was dus geen functie, en elke route die hem
raakte gooide een 500. Daarom zag Robin ook geen banner op Connect: die pagina
strandde voordat hij hem kon tonen. Twee bestaande tests vielen hierdoor om zodra
ik de poort in de aanmaakroute zette, en dat was maar goed ook.

TWEE: de poort zat op de BEZORGING en niet op het MAKEN. deliverCreate weigerde,
maar de post werd daarvoor al opgeslagen. Je kon dus gewoon schrijven en publiceren;
het federeerde alleen niet. Dan lijkt het gelukt, staat het er, en sterft het met
het domein: precies de halve toestand die dit slot moest voorkomen, en die ik
vandaag drie keer bij anderen heb aangewezen.

DRIE: de UI wist van niets. Alleen connect.ejs en de homepage kenden movedTo; de
posteditor, de like- en boostknoppen en het reactieveld stonden er gewoon. Voor een
gebruiker ziet dat eruit als een werkende site.

Changed files:
src/services/ActivityPubService.js

  • movedLock in de default-export (de oorzaak van de 500's)

src/routes/posts.js

  • /posts/create weigert met 409 op een verhuisd account, VOOR de post bestaat
  • /posts/:slug/save weigert alleen het alsnog PUBLICEREN van een concept; een bestaande post bewerken mag, daar wil je "ik ben verhuisd naar" in kunnen zetten en die URI bestaat al

src/middleware/site.js

  • res.locals.movedTo, zodat elke view kan grijzen wat toch geweigerd wordt. Zelfde plek en zelfde reden als mayReply erboven

src/views/partials/tl-item.ejs

  • like, boost en reageren krijgen disabled; blokkeren blijft klikbaar, dat is een veiligheidsklep net als rapporteren

src/views/pages/news.ejs

  • banner bovenaan plus de grijze staat. Een uitleg boven de lijst en niet een tooltip per knop, want een tooltip op een disabled knop is onzichtbaar

test/moved-lockdown.test.js

  • 12 en 13: de aanmaakroute strandt op 409, en een gewoon account niet

remarks: 951 groen in UTC en Europe/Amsterdam. De Krant in beide standen gerenderd:
banner alleen als er verhuisd is, geen onvertaalde sleutels. Onderweg viel mijn
eigen testopzet twee keer om op ontbrekende locals (timeline, thumb); dat was de
test.

Les voor mezelf: ik had dit gisteren "af" genoemd terwijl de enige controle een
grep op de uitgerolde bestanden was. Dat de code ergens staat is niet hetzelfde als
dat hij draait.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 4.8 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 // Verhuisd (FEP-7628)? Dan staat de uitgaande kant op slot. Om dezelfde reden
45 // hier en niet per route: elke view moet kunnen grijzen wat toch geweigerd
46 // wordt. Een knop die niets doet is erger dan geen knop, want je gaat zoeken
47 // naar een storing die er niet is. De poort zelf zit in de service; dit is
48 // alleen de deurbel die zegt dat er niet opengedaan wordt.
49 res.locals.movedTo = defaultSite.moved_to && /^https?:\/\//i.test(String(defaultSite.moved_to))
50 ? String(defaultSite.moved_to) : null;
51 }
52
53 next();
54}
55
56/**
57 * Audio tracks loader — pulls site-level tracks for the persistent player widget.
58 * Per Robin: player is separate from the footer, gated by site.enable_audio_player.
59 * Returns empty array if no site or audio is disabled — shell.ejs uses the
60 * length to decide whether to mount audio-player.js.
61 */
62export function loadAudioTracks(req, res, next) {
63 if (!audioEnabled()) { res.locals.audioTracks = []; return next(); } // lite-modus
64 const site = res.locals.site;
65 if (!site || site.enable_audio_player === 0) {
66 res.locals.audioTracks = [];
67 return next();
68 }
69
70 try {
71 // m.filename = the bare filename; the playable URL is the gated stream route
72 // (audioUrl). The media table has NO url column — the old query selected
73 // m.url and always failed silently (empty player). Now we build the URL from filename.
74 const rows = db.prepare(`
75 SELECT t.id, t.title, t.artist, t.duration, t.position, m.filename
76 FROM audio_tracks t
77 LEFT JOIN media m ON m.id = t.media_id
78 WHERE t.site_id = ?
79 ORDER BY t.position ASC, t.created_at ASC
80 `).all(site.id);
81 res.locals.audioTracks = rows.map((r) => ({
82 id: r.id, title: r.title, artist: r.artist, duration: r.duration, position: r.position,
83 media_url: r.filename ? audioUrl(r.filename) : null,
84 }));
85 } catch (e) {
86 // media table might not be queryable in some test setups — fall back gracefully
87 res.locals.audioTracks = [];
88 }
89
90 next();
91}
92
93/**
94 * Theme loader — applies user/site theme preferences.
95 */
96export function loadTheme(req, res, next) {
97 const PALETTES = ['klonkt','forest','ocean','teal','lilac','sunset','candy','amber'];
98
99 const user = req.session?.user;
100 const site = res.locals.site;
101
102 // A site always renders in ITS OWN palette, regardless of who is viewing. There is
103 // no per-user palette UI (user.palette is vestigial/stale data from old migrations),
104 // and the htmx pcmsNav path (render.js) already uses the site palette only — so reading
105 // user.palette here made a full page load (owner logged in) flip to the viewer's stale
106 // palette while htmx-nav kept the site's, i.e. "palette changes on hard refresh".
107 const palette = (site && PALETTES.includes(site.palette) ? site.palette : null)
108 || 'klonkt';
109
110 res.locals.palette = palette;
111 res.locals.theme = (user && ['dark','light'].includes(user.theme)) ? user.theme : 'dark';
112
113 next();
114}
Note: See TracBrowser for help on using the repository browser.