source: Klonkt/src/middleware/site.js@ 586f9dd

main
Last change on this file since 586f9dd was 7881080, checked in by roboburr <roboburr@…>, 3 months ago

Hub #3: explicit is_primary flag + shared getPrimarySite() (DRY)

The "oldest site = primary site" assumption was duplicated independently in
resolveSite, hub.js, account.js and admin.js (fragile: if the oldest happened
to be an artist site, the hub home would be wrong). Now:

  • sites.is_primary column + backfill (marks the oldest if none is primary yet; ensurePrimarySite sets it on fresh installs) → existing behaviour exactly preserved.
  • one getPrimarySite() helper (is_primary, fallback oldest) replaces the 4 copies.
  • god can CHOOSE the primary/main site: ★ button + "primary" badge on /admin/sites (exactly one primary via a transaction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 3.8 KB
Line 
1/**
2 * Site middleware — resolve which site this request is for.
3 *
4 * Resolution order:
5 * 1. Path /sites/:slug → that site
6 * 2. (Future) Subdomain bedrijf1.example.com → matching site
7 * 3. Default site (first one in DB)
8 *
9 * Sets res.locals.site for all downstream handlers.
10 */
11
12import db from '../config/database.js';
13import { getTenancy } from '../services/SettingsService.js';
14
15/**
16 * De primaire/hoofd-site — ÉÉN bron van waarheid (vervangt de "oudste site ="
17 * hoofd"-aanname die voorheen los in resolveSite/hub/account/admin stond).
18 * Leest de expliciete is_primary-vlag; valt terug op de oudste als die (nog)
19 * nergens staat, zodat bestaand gedrag exact behouden blijft.
20 */
21export function getPrimarySite() {
22 return db.prepare('SELECT * FROM sites WHERE is_primary = 1 LIMIT 1').get()
23 || db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get()
24 || null;
25}
26
27export function resolveSite(req, res, next) {
28 const tenancy = getTenancy();
29 res.locals.tenancy = tenancy; // ook beschikbaar voor views
30
31 // In HUB-mode mapt /sites/:slug en (later) een subdomein naar een specifieke
32 // site. In SOLO-mode bestaat er maar één site: we slaan die routing over en
33 // pinnen altijd op de primaire site.
34 if (tenancy === 'hub') {
35 // Een Klonkt-site is bereikbaar via /user/:slug (canoniek) én /sites/:slug (legacy).
36 const m = req.path.match(/^\/(sites|user)\/([a-zA-Z0-9_-]+)(\/.*)?$/);
37 if (m) {
38 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(m[2]);
39 if (site) {
40 res.locals.site = site;
41 req.url = (m[3] || '/'); // strip /<prefix>/:slug zodat downstream de rest ziet
42 res.locals.siteUrlBase = `/${m[1]}/${m[2]}`;
43 return next();
44 }
45 }
46 const host = req.get('host')?.toLowerCase().replace(/:\d+$/, '');
47 if (host) {
48 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(host);
49 if (site) {
50 res.locals.site = site;
51 res.locals.siteUrlBase = '';
52 return next();
53 }
54 }
55 }
56
57 // Solo (of hub zonder match): pin op de primaire/hoofd-site.
58 const defaultSite = getPrimarySite();
59 if (defaultSite) {
60 res.locals.site = defaultSite;
61 res.locals.siteUrlBase = '';
62 }
63
64 next();
65}
66
67/**
68 * Audio tracks loader — pulls site-level tracks for the persistent player widget.
69 * Per Robin: player is separate from the footer, gated by site.enable_audio_player.
70 * Returns empty array if no site or audio is disabled — shell.ejs uses the
71 * length to decide whether to mount audio-player.js.
72 */
73export function loadAudioTracks(req, res, next) {
74 const site = res.locals.site;
75 if (!site || site.enable_audio_player === 0) {
76 res.locals.audioTracks = [];
77 return next();
78 }
79
80 try {
81 res.locals.audioTracks = db.prepare(`
82 SELECT t.id, t.title, t.artist, t.duration, t.position,
83 m.url AS media_url
84 FROM audio_tracks t
85 LEFT JOIN media m ON m.id = t.media_id
86 WHERE t.site_id = ?
87 ORDER BY t.position ASC, t.created_at ASC
88 `).all(site.id);
89 } catch (e) {
90 // media table might not be queryable in some test setups — fall back gracefully
91 res.locals.audioTracks = [];
92 }
93
94 next();
95}
96
97/**
98 * Theme loader — applies user/site theme preferences.
99 */
100export function loadTheme(req, res, next) {
101 const PALETTES = ['sage','paper','ocean','forest','stone','midnight','sunset','cream'];
102
103 const user = req.session?.user;
104 const site = res.locals.site;
105
106 // Priority: user setting > site setting > default
107 const palette = (user && PALETTES.includes(user.palette) ? user.palette : null)
108 || (site && PALETTES.includes(site.palette) ? site.palette : null)
109 || 'sage';
110
111 res.locals.palette = palette;
112 res.locals.theme = (user && ['dark','light'].includes(user.theme)) ? user.theme : 'dark';
113
114 next();
115}
Note: See TracBrowser for help on using the repository browser.