Changeset 72ec6a4 in Klonkt for src/routes


Ignore:
Timestamp:
07/31/2026 06:55:59 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
17546af
Parents:
bfa6fa1
Message:

Hub-modus en guardian-lite eruit

Robins besluit (31-7): een instance is een eigenaar. Twee dingen weg.

HUB-MODUS was al dood: getTenancy() gaf sinds 24-6 hardcoded 'solo'
terug, dus elke tenancy === 'hub'-tak was onbereikbaar. Nu ook echt
verwijderd: getTenancy/setTenancy zelf, de /user/:slug-routing in
resolveSite, de hub-takken in admin, zoeken, audio, posts (neighbours
en related over alle sites), download, en de push-prefix. In de views
verdwijnen de hub-tagline, de hub-navigatie, de sites- en
users-tabellen (die kwamen alleen in hub-modus gevuld en verwezen nu
naar locals die niemand meer meegeeft), de eigenaar-toewijzing bij een
site, de /user/-slugprefix, het hub-brandblok en de hub-thuisknop.

GUARDIAN-LITE was de laatste multi-user-rest: /guardian/invite gaf een
link waarmee iemand via /guardian/join een echte user plus een site met
guardian_only=1 aanmaakte. Dat zette andermans wachtwoordhash, sessie
en PRIVATE actor-sleutel in jouw database, waardoor een verhuizing of
export nooit netjes kon (shaer-qw6q). Routes, formulier, kolom en
uitnodigingstabel zijn weg. Het guardian-DASHBOARD blijft: dat is
FEP-633c en werkt voor guardians met een eigen Klonkt. Bestaande
installaties houden kolom en tabel ongebruikt; nieuwe krijgen ze niet.

Changed files:
src/services/SettingsService.js

  • getTenancy/setTenancy verwijderd; kop herschreven

src/middleware/site.js, src/middleware/render.js

  • /user/:slug-routing weg; tenancy en hubTitle uit de locals

src/routes/admin.js, admin-settings.js, audio.js, download.js,
src/routes/posts.js, search.js

  • hub-takken en hub-queries weg; postNeighbors zonder isHub

src/services/ActivityPubService.js

  • pushPrefix is nu gewoon ; getTenancy-import weg

src/routes/guardian.js

  • /invite en /join verwijderd (dashboard blijft), imports opgeschoond

src/config/database.js

  • guardian_only-kolom en ap_guardian_invites-tabel niet meer aangemaakt

src/views/pages/admin.ejs, admin-users.ejs, admin-site-edit.ejs,
src/views/pages/guardian.ejs, partials/topnav.ejs, chrome.ejs, bottom-tab.ejs

  • alle hub-takken en de uitnodigingsknop weg

remarks: 320 regels weg, 63 erbij. Suite 372 groen; alle 85 templates
compileren; en met een wegwerp-kopie van de database daadwerkelijk
gedraaid en ingelogd: /, /admin, /admin/users, /admin/sites,
/admin/settings, /admin/sites/demo/edit, /admin/media en /guardian
geven alle 200 zonder fouten in het log, en /guardian/invite en
/guardian/join geven nu 404.

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

Location:
src/routes
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • src/routes/admin-settings.js

    rbfa6fa1 r72ec6a4  
    11/**
    22 * Admin: global settings.
    3  *  - tenancy mode (Solo/Hub)
    43 *  - hub branding (name/tagline/intro/hero of the generic hub home page)
    54 *
     
    2019import { renderPage } from '../middleware/render.js';
    2120import { requireGod } from '../middleware/auth.js';
    22 import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
     21import { getSetting, setSetting } from '../services/SettingsService.js';
    2322import { SUPPORTED } from '../services/i18n.js';
    2423import { mailerStatus, sendMail } from '../config/mailer.js';
     
    6968    pageTitleKey: 'admin.t_settings',
    7069    bodyClass: 'on-admin',
    71     tenancy: getTenancy(),
    72     hubTitle: getSetting('hub_title') || '',
    7370    hubTagline: getSetting('hub_tagline') || '',
    7471    hubIntro: getSetting('hub_intro') || '',
     
    8784router.post('/', requireGod, (req, res) => {
    8885  // multer.single processes multipart (hub branding form). For a plain
    89   // urlencoded POST (tenancy form) multer does nothing and req.body stays intact.
     86  // urlencoded POST: multer does nothing and req.body stays intact.
    9087  heroUpload.single('hub_hero_file')(req, res, (err) => {
    9188    if (err) {
     
    9390    }
    9491
    95     if (typeof req.body.tenancy !== 'undefined') {
    96       // Hub mode is removed → setTenancy only accepts solo | circle (coerces the rest).
    97       setTenancy(req.body.tenancy);
    98     }
    9992    if (typeof req.body.default_lang !== 'undefined') {
    10093      // Default language for visitors (empty = follow env/browser). Validated against NL/EN/DE.
     
    109102      if (tz) { try { Intl.DateTimeFormat('en-US', { timeZone: tz }); valid = tz; } catch { valid = ''; } }
    110103      setSetting('timezone', valid);
    111     }
    112     if (typeof req.body.hub_title !== 'undefined') {
    113       setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
    114104    }
    115105    if (typeof req.body.hub_tagline !== 'undefined') {
  • src/routes/admin.js

    rbfa6fa1 r72ec6a4  
    99import { renderPage } from '../middleware/render.js';
    1010import { requireAuth } from '../middleware/auth.js';
    11 import { getTenancy, apEnabled } from '../services/SettingsService.js';
     11import { apEnabled } from '../services/SettingsService.js';
    1212import { getPrimarySite } from '../middleware/site.js';
    1313
     
    1717// Solves the problem that drafts (status != published) were not findable anywhere:
    1818// the timeline shows only published posts.
    19 function sitePosts(siteId, siteSlug, tenancy, limit = 60) {
    20   const base = tenancy === 'hub' ? `/user/${siteSlug}` : '';
     19function sitePosts(siteId, siteSlug, limit = 60) {
     20  const base = '';
    2121  return db.prepare(`
    2222    SELECT slug, title, status, published_at, created_at, updated_at
     
    5353      mySite,
    5454      mine,
    55       posts: sitePosts(mySite.id, mySite.slug, 'hub'), // my-site is hub-only
     55      posts: sitePosts(mySite.id, mySite.slug),
    5656    });
    5757  }
    5858
    59   const tenancy = getTenancy();
    60 
    61   // The primary/main site — in solo THE site, in hub the main site. Provides the
    62   // "Appearance" tile with its edit link + the posts/drafts list.
     59  // THE site: it provides the "Appearance" tile with its edit link and the
     60  // posts/drafts list.
    6361  const primarySite = getPrimarySite();
    6462
     
    7270  };
    7371
    74   // Sites/users tables are only relevant in hub mode; in solo we skip the query.
    75   const sites = tenancy === 'hub' ? db.prepare(`
    76     SELECT s.slug, s.title, s.created_at, u.username AS owner_username
    77     FROM sites s
    78     LEFT JOIN users u ON u.id = s.owner_id
    79     ORDER BY s.created_at DESC
    80     LIMIT 50
    81   `).all() : [];
    82 
    83   const users = tenancy === 'hub' ? db.prepare(`
    84     SELECT username, email, role, created_at
    85     FROM users
    86     ORDER BY created_at DESC
    87     LIMIT 50
    88   `).all() : [];
    89 
    90   // Posts/drafts of the primary site (in solo = the site; in hub = the admin's
    91   // main site). Drafts are listed first so they are easy to find.
    92   const posts = primarySite ? sitePosts(primarySite.id, primarySite.slug, tenancy) : [];
     72  // Posts/drafts of the site. Drafts are listed first so they are easy to find.
     73  const posts = primarySite ? sitePosts(primarySite.id, primarySite.slug) : [];
    9374
    9475  renderPage(req, res, 'pages/admin', {
    9576    pageTitleKey: 'admin.t_admin',
    9677    bodyClass: 'on-admin',
    97     tenancy,
    98     circlesOn: apEnabled(),   // solo vs cirkels tagline (federatie aan/uit)
     78    circlesOn: apEnabled(),   // federatie aan/uit in de tagline
    9979    primarySite,
    10080    stats,
    101     sites,
    10281    posts,
    103     users,
    10482  });
    10583});
     
    11189    pageTitleKey: 'admin.t_manual',
    11290    bodyClass: 'on-admin',
    113     tenancy: getTenancy(),
    11491  });
    11592});
  • src/routes/audio.js

    rbfa6fa1 r72ec6a4  
    156156  const id = String(req.params.id || '');
    157157  if (!/^[A-Za-z0-9_-]+$/.test(id)) return res.status(400).json({ error: 'bad id' });
    158   const isHub = res.locals.tenancy === 'hub';
    159158  const row = db.prepare(`
    160159    SELECT p.slug, s.slug AS site_slug
     
    164163  `).get('%[[track:' + id + ']]%');
    165164  if (!row) return res.status(404).json({ error: 'not found' });
    166   const url = isHub ? `/user/${row.site_slug}/${row.slug}` : `/${row.slug}`;
     165  const url = `/${row.slug}`;
    167166  res.json({ url });
    168167});
  • src/routes/download.js

    rbfa6fa1 r72ec6a4  
    3434  ).get(site.id);
    3535  if (!post) return {};
    36   try { return postNeighbors(site, post, res.locals.tenancy === 'hub'); } catch (e) { return {}; }
     36  try { return postNeighbors(site, post); } catch (e) { return {}; }
    3737}
    3838const __dirname = path.dirname(fileURLToPath(import.meta.url));
  • src/routes/guardian.js

    rbfa6fa1 r72ec6a4  
    1010 */
    1111import express from 'express';
    12 import crypto from 'crypto';
    13 import bcrypt from 'bcryptjs';
    1412import path from 'path';
    1513import { fileURLToPath } from 'url';
     
    595593});
    596594
    597 // ── Losse guardians (Guardian 2): uitnodigen en aansluiten ───────────────
    598 // De familie nodigt oma uit; zij kiest naam + wachtwoord en heeft daarmee een
    599 // guardian-only account: user + minimale site (guardian_only=1). Alles wat al
    600 // per slug werkt (actor, inbox, offers, push, deze PWA) werkt dan meteen.
    601 
    602 router.post('/invite', requireAuth, (req, res) => {
    603   const token = crypto.randomBytes(16).toString('base64url');
    604   db.prepare('INSERT INTO ap_guardian_invites (token, created_by) VALUES (?,?)')
    605     .run(token, req.session.user.id);
    606   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    607   const url = `${base}/guardian/join/${token}`;
    608   res.send(`<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif;max-width:480px;margin:40px auto">
    609     <h2>Invite a guardian</h2>
    610     <p>Share this link. It lets one person create a guardian account here:</p>
    611     <p><a href="${url}">${url}</a></p>
    612     <p><a href="/guardian">Back</a></p></body>`);
    613 });
    614 
    615 function joinForm(token, error) {
    616   return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
    617   <body style="font-family:sans-serif;max-width:420px;margin:40px auto">
    618   <h2>Become a guardian</h2>
    619   <p>Watch over someone you care about. Pick a name and a password; that is all.</p>
    620   ${error ? `<p style="color:#b00">${error}</p>` : ''}
    621   <form method="post" action="/guardian/join/${token}">
    622     <p><input name="name" placeholder="your name (grandma)" required pattern="[a-z0-9_-]{1,32}"
    623        style="width:100%;padding:10px" autocapitalize="none"></p>
    624     <p><input name="password" type="password" placeholder="password" required minlength="8"
    625        style="width:100%;padding:10px"></p>
    626     <p><button style="width:100%;padding:12px">Create my guardian account</button></p>
    627   </form></body>`;
    628 }
    629 
    630 router.get('/join/:token', (req, res) => {
    631   const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL')
    632     .get(req.params.token);
    633   if (!inv) return res.status(404).send('This invite is no longer valid.');
    634   res.send(joinForm(req.params.token));
    635 });
    636 
    637 router.post('/join/:token', express.urlencoded({ extended: false }), (req, res) => {
    638   const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL')
    639     .get(req.params.token);
    640   if (!inv) return res.status(404).send('This invite is no longer valid.');
    641   const name = String(req.body.name || '').trim().toLowerCase();
    642   const password = String(req.body.password || '');
    643   if (!/^[a-z0-9_-]{1,32}$/.test(name)) return res.status(400).send(joinForm(req.params.token, 'Only lowercase letters, digits, - and _.'));
    644   if (password.length < 8) return res.status(400).send(joinForm(req.params.token, 'Password: at least 8 characters.'));
    645   if (db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(name) || db.prepare('SELECT 1 FROM users WHERE username = ?').get(name)) {
    646     return res.status(409).send(joinForm(req.params.token, 'That name is taken, pick another.'));
    647   }
    648   const userId = crypto.randomUUID();
    649   db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
    650     .run(userId, name, `${name}@guardian.invalid`, bcrypt.hashSync(password, 10), 'member');
    651   db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, guardian_only) VALUES (?,?,?,?,0,1)')
    652     .run(crypto.randomUUID(), name, name, userId);
    653   db.prepare('UPDATE ap_guardian_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE token = ?')
    654     .run(userId, req.params.token);
    655   req.session.user = { id: userId, username: name, role: 'member' };
    656   res.redirect('/guardian');
    657 });
     595// Losse guardian-accounts (guardian-lite: /invite + /join, user + site met
     596// guardian_only=1) zijn verwijderd op 31-7-2026. Een instance is een eigenaar;
     597// zo'n account was de laatste multi-user-rest en zette bovendien andermans
     598// wachtwoordhash, sessie en PRIVATE actor-sleutel in jouw database, wat een
     599// verhuizing (shaer-qw6q) onmogelijk netjes maakte. Een guardian hoort een
     600// eigen Klonkt te hebben; de adoptie loopt dan gewoon over de federatie.
    658601
    659602export default router;
  • src/routes/posts.js

    rbfa6fa1 r72ec6a4  
    763763}
    764764
    765 function postNeighbors(site, post, isHub) {
    766   const urlBaseFor = (p) => (isHub && p && p.site_slug) ? `/user/${p.site_slug}` : '';
    767   const ordered = isHub
    768     ? db.prepare(`
    769         SELECT p.id, p.slug, p.title, p.pinned, s.slug AS site_slug
    770         FROM posts p JOIN sites s ON s.id = p.site_id
    771         WHERE p.status = 'published'
    772         ORDER BY p.published_at DESC
    773       `).all()
    774     : db.prepare(`
    775         SELECT id, slug, title, pinned FROM posts
    776         WHERE site_id = ? AND status = 'published'
    777         ORDER BY (pinned = 0) ASC, pinned ASC, published_at DESC
    778       `).all(site.id);
     765function postNeighbors(site, post) {
     766  const ordered = db.prepare(`
     767    SELECT id, slug, title, pinned FROM posts
     768    WHERE site_id = ? AND status = 'published'
     769    ORDER BY (pinned = 0) ASC, pinned ASC, published_at DESC
     770  `).all(site.id);
    779771  const idx = ordered.findIndex((p) => p.id === post.id);
    780772  const newerPost = idx > 0 ? ordered[idx - 1] : null;
    781773  const olderPost = (idx >= 0 && idx < ordered.length - 1) ? ordered[idx + 1] : null;
    782   if (newerPost) newerPost._urlBase = urlBaseFor(newerPost);
    783   if (olderPost) olderPost._urlBase = urlBaseFor(olderPost);
     774  if (newerPost) newerPost._urlBase = '';
     775  if (olderPost) olderPost._urlBase = '';
    784776  return { newerPost, olderPost };
    785777}
     
    13441336  const _unlocked = _u && _u.purpose === 'unlocked' && _u.siteId === site.id && String(_u.post) === String(post.slug);
    13451337  if (post.paid && !canEditThis && !_unlocked) {
    1346     const { newerPost, olderPost } = postNeighbors(site, post, res.locals.tenancy === 'hub');
     1338    const { newerPost, olderPost } = postNeighbors(site, post);
    13471339    return renderPage(req, res, 'pages/paid-gate', {
    13481340      pageTitle: post.title || 'Voor supporters',
     
    13641356    // Same Newer/Older navigation as on a normal post, so the visitor doesn't get
    13651357    // stuck on the fan gate but can keep browsing.
    1366     const { newerPost, olderPost } = postNeighbors(site, post, res.locals.tenancy === 'hub');
     1358    const { newerPost, olderPost } = postNeighbors(site, post);
    13671359    return renderPage(req, res, 'pages/fan-gate', {
    13681360      pageTitle: post.title || 'Alleen voor fans',
     
    13961388  // Prev / next chronological (kept for back-compat — "post-nav" feature
    13971389  // below the article still uses these as a simple linear navigation).
    1398   // Hub mode: Related posts + Newer/Older pull from ALL users (all sites),
    1399   // newest first. Solo mode: within the current site (old behaviour).
    1400   const isHub = res.locals.tenancy === 'hub';
    1401   // Per-post URL base: in hub a link points to /user/<site-slug>/<post-slug>.
    1402   const urlBaseFor = (p) => (isHub && p && p.site_slug) ? `/user/${p.site_slug}` : '';
     1390  const urlBaseFor = () => '';
    14031391
    14041392  // Newer/Older across ALL posts (shared helper — also used by the fan gate).
    1405   const { newerPost, olderPost } = postNeighbors(site, post, isHub);
     1393  const { newerPost, olderPost } = postNeighbors(site, post);
    14061394
    14071395  // ── Related posts: same-tag matching with recency fallback ─────
    14081396  // Fetch ~50 candidates, score by tag overlap, take top 3.
    14091397  // Excluding self via `id != ?`.
    1410   const candidates = isHub
    1411     ? db.prepare(`
    1412         SELECT p.id, p.slug, p.title, p.cover_image_url, p.cover_video_url, p.published_at, p.tags, p.nsfw, p.content_warning, s.slug AS site_slug
    1413         FROM posts p JOIN sites s ON s.id = p.site_id
    1414         WHERE p.status = 'published' AND p.id != ?
    1415         ORDER BY p.published_at DESC LIMIT 50
    1416       `).all(post.id)
    1417     : db.prepare(`
    1418         SELECT id, slug, title, cover_image_url, cover_video_url, published_at, tags, nsfw, content_warning
    1419         FROM posts
    1420         WHERE site_id = ? AND status = 'published' AND id != ?
    1421         ORDER BY published_at DESC LIMIT 50
    1422       `).all(site.id, post.id);
     1398  const candidates = db.prepare(`
     1399    SELECT id, slug, title, cover_image_url, cover_video_url, published_at, tags, nsfw, content_warning
     1400    FROM posts
     1401    WHERE site_id = ? AND status = 'published' AND id != ?
     1402    ORDER BY published_at DESC LIMIT 50
     1403  `).all(site.id, post.id);
    14231404
    14241405  // Parse tags JSON safely; missing/malformed → empty array.
  • src/routes/search.js

    rbfa6fa1 r72ec6a4  
    5151function searchSite(req, res, rawQ, lim) {
    5252  const site = res.locals.site;
    53   const isHub = res.locals.tenancy === 'hub';
    5453  const base = res.locals.siteUrlBase || '';
    55   const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
     54  const urlFor = (slug) => `/${slug}`;
    5655  // Eigen vertaler (werkt ook in de JSON-route, waar res.locals.t niet bestaat).
    5756  const lang = resolveLang(req);
     
    172171  if (rawQ.length < 2) return res.json({ posts: [], tracks: [], events: [], pages: [] });
    173172
    174   const isHub = res.locals.tenancy === 'hub';
    175   const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
     173  const urlFor = (slug) => `/${slug}`;
    176174  const r = searchSite(req, res, rawQ, { posts: 5, tracks: 4, events: 3, pages: 4 });
    177175  res.json({
Note: See TracChangeset for help on using the changeset viewer.