Changeset 6351545 in Klonkt


Ignore:
Timestamp:
06/14/2026 08:18:50 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
07775b7
Parents:
9e27d64
Message:

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@…>

Location:
src
Files:
3 added
5 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r9e27d64 r6351545  
    9393      ON playlist_tracks(playlist_id, position);
    9494  `);
     95
     96  // Globale app-instellingen (key/value singleton). O.a. de tenancy-modus
     97  // (solo = één site, hub = bedrijfssite + /gebruikers). Default = solo.
     98  db.exec(`
     99    CREATE TABLE IF NOT EXISTS app_settings (
     100      key TEXT PRIMARY KEY,
     101      value TEXT,
     102      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
     103    );
     104  `);
     105  db.prepare("INSERT OR IGNORE INTO app_settings (key, value) VALUES ('tenancy', 'solo')").run();
    95106}
    96107
  • src/middleware/site.js

    r9e27d64 r6351545  
    1111
    1212import db from '../config/database.js';
     13import { getTenancy } from '../services/SettingsService.js';
    1314
    1415export function resolveSite(req, res, next) {
    15   // Try /sites/:slug pattern
    16   const m = req.path.match(/^\/sites\/([a-zA-Z0-9_-]+)(\/.*)?$/);
    17   if (m) {
    18     const slug = m[1];
    19     const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
    20     if (site) {
    21       res.locals.site = site;
    22       // Strip /sites/:slug from req.url so downstream routes see the rest
    23       req.url = (m[2] || '/');
    24       // Also rewrite originalUrl for redirect targets to keep the prefix
    25       res.locals.siteUrlBase = `/sites/${slug}`;
    26       return 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      }
    2741    }
    2842  }
    2943
    30   // Future: subdomain mapping
    31   const host = req.get('host')?.toLowerCase().replace(/:\d+$/, '');
    32   if (host) {
    33     const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(host);
    34     if (site) {
    35       res.locals.site = site;
    36       res.locals.siteUrlBase = '';
    37       return next();
    38     }
    39   }
    40 
    41   // Default: pick first site
     44  // Solo (of hub zonder match): de primaire/owner-site (eerste aangemaakte).
    4245  const defaultSite = db.prepare('SELECT * FROM sites ORDER BY created_at ASC LIMIT 1').get();
    4346  if (defaultSite) {
  • src/routes/admin.js

    r9e27d64 r6351545  
    99import { renderPage } from '../middleware/render.js';
    1010import { requireGod } from '../middleware/auth.js';
     11import { getTenancy } from '../services/SettingsService.js';
    1112
    1213const router = express.Router();
    1314
    1415router.get('/', requireGod, (req, res) => {
     16  const tenancy = getTenancy();
     17
     18  // De primaire/owner-site — in solo dé site, in hub de hoofdsite. Geeft de
     19  // "Uiterlijk"-tegel z'n edit-link.
     20  const primarySite = db.prepare(
     21    'SELECT slug, title FROM sites ORDER BY created_at ASC LIMIT 1'
     22  ).get() || null;
     23
    1524  const stats = {
    1625    users: db.prepare('SELECT COUNT(*) AS c FROM users').get().c,
     
    2231  };
    2332
    24   const sites = db.prepare(`
     33  // Sites/users-tabellen zijn alleen in hub relevant; in solo besparen we de query.
     34  const sites = tenancy === 'hub' ? db.prepare(`
    2535    SELECT s.slug, s.title, s.created_at, u.username AS owner_username
    2636    FROM sites s
     
    2838    ORDER BY s.created_at DESC
    2939    LIMIT 50
    30   `).all();
     40  `).all() : [];
    3141
    32   const users = db.prepare(`
     42  const users = tenancy === 'hub' ? db.prepare(`
    3343    SELECT username, email, role, created_at
    3444    FROM users
    3545    ORDER BY created_at DESC
    3646    LIMIT 50
    37   `).all();
     47  `).all() : [];
    3848
    3949  renderPage(req, res, 'pages/admin', {
    40     pageTitle: 'Admin',
     50    pageTitle: 'Beheer',
    4151    bodyClass: 'on-admin',
     52    tenancy,
     53    primarySite,
    4254    stats,
    4355    sites,
  • src/server.js

    r9e27d64 r6351545  
    2929import adminUsersRoutes from './routes/admin-users.js';
    3030import adminCommentsRoutes from './routes/admin-comments.js';
     31import adminSettingsRoutes from './routes/admin-settings.js';
    3132import prutterRoutes from './routes/prutter.js';
    3233import audioRoutes from './routes/audio.js';
     
    169170app.use('/admin/users', adminUsersRoutes);
    170171app.use('/admin/comments', adminCommentsRoutes);
     172app.use('/admin/settings', adminSettingsRoutes);
    171173app.use('/admin', adminRoutes);
    172174app.use('/prutter', prutterRoutes);
  • src/views/pages/admin.ejs

    r9e27d64 r6351545  
    11<div class="container admin-page">
    2   <h1>Admin</h1>
    3   <p class="admin-tagline">God-level overview. <em>(Stub — full dashboard later.)</em></p>
     2  <h1>Beheer</h1>
     3  <% if (tenancy === 'hub') { %>
     4    <p class="admin-tagline">Hub-modus — bedrijfssite met gebruikers, elk hun eigen PrutFolio.</p>
     5  <% } else { %>
     6    <p class="admin-tagline">Solo-modus — jouw site.</p>
     7  <% } %>
    48
    59  <nav class="admin-quick-links" aria-label="Beheer-snelkoppelingen">
    6     <a href="/admin/sites" class="btn">🌐 Sites</a>
    7     <a href="/admin/users" class="btn">👥 Users</a>
    8     <a href="/admin/audio" class="btn">🎵 Audio tracks</a>
    9     <a href="/admin/playlists" class="btn">📃 Playlists</a>
    10     <a href="/admin/comments" class="btn">💬 Comments</a>
     10    <% if (tenancy === 'hub') { %>
     11      <a href="/admin/sites" class="btn">🌐 Sites</a>
     12      <a href="/admin/users" class="btn">👥 Gebruikers</a>
     13      <a href="/admin/audio" class="btn">🎵 Audio</a>
     14      <a href="/admin/playlists" class="btn">📃 Playlists</a>
     15      <a href="/admin/comments" class="btn">💬 Reacties</a>
     16      <a href="/admin/settings" class="btn">⚙️ Instellingen</a>
     17    <% } else { %>
     18      <a href="/posts/new" class="btn">✍️ Nieuwe post</a>
     19      <a href="/admin/audio" class="btn">🎵 Audio</a>
     20      <a href="/admin/comments" class="btn">💬 Reacties</a>
     21      <% if (primarySite) { %>
     22        <a href="/admin/sites/<%= primarySite.slug %>/edit" class="btn">🎨 Uiterlijk</a>
     23      <% } %>
     24      <a href="/account" class="btn">👤 Account</a>
     25      <a href="/admin/settings" class="btn">⚙️ Instellingen</a>
     26    <% } %>
    1127  </nav>
    1228
    1329  <div class="admin-stats">
    14     <div class="stat-card">
    15       <div class="stat-num"><%= stats.users %></div>
    16       <div class="stat-label">Users</div>
    17     </div>
    18     <div class="stat-card">
    19       <div class="stat-num"><%= stats.sites %></div>
    20       <div class="stat-label">Sites</div>
    21     </div>
    22     <div class="stat-card">
    23       <div class="stat-num"><%= stats.posts %></div>
    24       <div class="stat-label">Posts</div>
    25     </div>
    26     <div class="stat-card">
    27       <div class="stat-num"><%= stats.published %></div>
    28       <div class="stat-label">Published</div>
    29     </div>
     30    <% if (tenancy === 'hub') { %>
     31      <div class="stat-card"><div class="stat-num"><%= stats.users %></div><div class="stat-label">Gebruikers</div></div>
     32      <div class="stat-card"><div class="stat-num"><%= stats.sites %></div><div class="stat-label">Sites</div></div>
     33    <% } %>
     34    <div class="stat-card"><div class="stat-num"><%= stats.posts %></div><div class="stat-label">Posts</div></div>
     35    <div class="stat-card"><div class="stat-num"><%= stats.published %></div><div class="stat-label">Gepubliceerd</div></div>
    3036  </div>
    3137
     
    6874  <% } %>
    6975
    70   <p class="admin-todo">
    71     <strong>Coming later:</strong> create/delete sites, manage users, theme picker, audio toggles, Prutter settings.
    72   </p>
    7376</div>
    7477
Note: See TracChangeset for help on using the changeset viewer.