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

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

tenancy: runtime Solo/Hub mode + mode-aware /admin (Phase 1)

Admin can switch live between modes in Admin -> Settings:

  • solo: exactly one site (the primary/owner site), no /sites routing, leaner /admin.
  • hub: main site + /sites/:slug routing (company; /gebruikers + assignment flow = Phase 2).

Switching is non-destructive (hides only, deletes nothing).

  • app_settings (key/value singleton) + SettingsService (cached getTenancy/setTenancy).
  • resolveSite: solo pins to the primary site + disables /sites/:slug + host mapping.
  • /admin/settings (god-only) toggle; /admin dashboard rewritten, mode-aware.

Tested on the demo: solo/hub dashboard tiles, /sites routing per mode, toggle back/forth.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 3.3 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
15export function resolveSite(req, res, next) {
16 const tenancy = getTenancy();
17 res.locals.tenancy = tenancy; // ook beschikbaar voor views
18
19 // In HUB-mode mapt /sites/:slug en (later) een subdomein naar een specifieke
20 // site. In SOLO-mode bestaat er maar één site: we slaan die routing over en
21 // pinnen altijd op de primaire site.
22 if (tenancy === 'hub') {
23 const m = req.path.match(/^\/sites\/([a-zA-Z0-9_-]+)(\/.*)?$/);
24 if (m) {
25 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(m[1]);
26 if (site) {
27 res.locals.site = site;
28 req.url = (m[2] || '/'); // strip /sites/:slug zodat downstream de rest ziet
29 res.locals.siteUrlBase = `/sites/${m[1]}`;
30 return next();
31 }
32 }
33 const host = req.get('host')?.toLowerCase().replace(/:\d+$/, '');
34 if (host) {
35 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(host);
36 if (site) {
37 res.locals.site = site;
38 res.locals.siteUrlBase = '';
39 return next();
40 }
41 }
42 }
43
44 // Solo (of hub zonder match): de primaire/owner-site (eerste aangemaakte).
45 const defaultSite = db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
46 if (defaultSite) {
47 res.locals.site = defaultSite;
48 res.locals.siteUrlBase = '';
49 }
50
51 next();
52}
53
54/**
55 * Audio tracks loader — pulls site-level tracks for the persistent player widget.
56 * Per Robin: player is separate from the footer, gated by site.enable_audio_player.
57 * Returns empty array if no site or audio is disabled — shell.ejs uses the
58 * length to decide whether to mount audio-player.js.
59 */
60export function loadAudioTracks(req, res, next) {
61 const site = res.locals.site;
62 if (!site || site.enable_audio_player === 0) {
63 res.locals.audioTracks = [];
64 return next();
65 }
66
67 try {
68 res.locals.audioTracks = db.prepare(`
69 SELECT t.id, t.title, t.artist, t.duration, t.position,
70 m.url AS media_url
71 FROM audio_tracks t
72 LEFT JOIN media m ON m.id = t.media_id
73 WHERE t.site_id = ?
74 ORDER BY t.position ASC, t.created_at ASC
75 `).all(site.id);
76 } catch (e) {
77 // media table might not be queryable in some test setups — fall back gracefully
78 res.locals.audioTracks = [];
79 }
80
81 next();
82}
83
84/**
85 * Theme loader — applies user/site theme preferences.
86 */
87export function loadTheme(req, res, next) {
88 const PALETTES = ['sage','paper','ocean','forest','stone','midnight','sunset','cream'];
89
90 const user = req.session?.user;
91 const site = res.locals.site;
92
93 // Priority: user setting > site setting > default
94 const palette = (user && PALETTES.includes(user.palette) ? user.palette : null)
95 || (site && PALETTES.includes(site.palette) ? site.palette : null)
96 || 'sage';
97
98 res.locals.palette = palette;
99 res.locals.theme = (user && ['dark','light'].includes(user.theme)) ? user.theme : 'dark';
100
101 next();
102}
Note: See TracBrowser for help on using the repository browser.