Changeset 8afbdd6 in Klonkt for src/routes


Ignore:
Timestamp:
06/14/2026 06:07:51 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
c869272
Parents:
b94c08e
Message:

feat: viewer role + artists directory + Klonkt Hub Beta rebrand

Viewer role (replaces the separate view-mode/readonly toggle):

  • 'kijker' is now a real role (VALID_ROLES) selectable in Admin. May view EVERYTHING including Admin, but cannot modify anything.
  • isViewer(user) (= role kijker or legacy readonly flag) is the source; requireGod/requireSiteManager(BySlug) let a viewer through (viewing), the global guard 403s every write. Existing readonly accounts are migrated on each role change (readonly=0).
  • Write leaks via GET patched: /prutter/new (INSERT) blocks for viewers, /prutter/:id skips markAsRead (UPDATE); WS upgrade rejects viewers (the HTTP guard doesn't cover WebSockets).
  • Clean "Viewer mode" page (viewer-blocked) instead of raw 403 text; styled sticky banner; account page shows read-only UI instead of an upload button that silently 403s. canMutate hides write buttons.

Scalability (>50 artists):

  • Hub home shows max 24 (most active first) + "All N artists ->".
  • New searchable, paginated /artiesten directory (hub only).
  • 'user' + 'artiesten' reserved as slugs.

Rebrand PrutFolio v1 -> Klonkt Hub Beta (footer, PWA manifest, account/
admin texts, default page title, startup, README; internal package +
PWA id 'prutfolio' remain for stability).

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

Location:
src/routes
Files:
1 added
6 edited

Legend:

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

    rb94c08e r8afbdd6  
    9898  'auth', 'admin', 'login', 'register', 'logout', 'archive', 'search',
    9999  'account', 'sites', 'comments', 'posts', 'media', 'audio', 'prutter',
    100   'forum', 'tag', 'users', 'feed.xml', 'atom.xml', 'sitemap.xml',
     100  'forum', 'tag', 'user', 'users', 'artiesten', 'feed.xml', 'atom.xml', 'sitemap.xml',
    101101  'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
    102102]);
  • src/routes/admin-users.js

    rb94c08e r8afbdd6  
    2020const router = express.Router();
    2121
    22 const VALID_ROLES = new Set(['member', 'admin', 'god']);
     22// 'kijker' = alleen-lezen demonstratie/audit-account: mag alles bekijken (incl.
     23// Beheer), maar de globale guard blokkeert elke wijziging. Vervangt de oude
     24// losse 'kijk-modus'-vlag (readonly), die nu door deze rol wordt afgedekt.
     25const VALID_ROLES = new Set(['kijker', 'member', 'admin', 'god']);
    2326
    2427function godCount() {
     
    6669  }
    6770
    68   db.prepare('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
     71  // readonly=0: de alleen-lezen-status zit nu volledig in de 'kijker'-rol, dus
     72  // bij elke rolwijziging ruimen we de legacy-vlag op (geen dubbele bron).
     73  db.prepare('UPDATE users SET role = ?, readonly = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
    6974    .run(newRole, userId);
    7075  res.redirect('/admin/users?success=' + encodeURIComponent('Role updated'));
    71 });
    72 
    73 // ==================== READ-ONLY (kijk-account) ====================
    74 router.post('/:id/readonly', requireGod, (req, res) => {
    75   const target = db.prepare('SELECT id FROM users WHERE id = ?').get(req.params.id);
    76   if (!target) return res.redirect('/admin/users?error=User+not+found');
    77   // Niet op jezelf — voorkomt dat een god zichzelf alleen-lezen maakt en vastloopt.
    78   if (target.id === req.session.user.id) {
    79     return res.redirect('/admin/users?error=' + encodeURIComponent('Kan jezelf niet op alleen-lezen zetten'));
    80   }
    81   const ro = req.body.readonly === '1' ? 1 : 0;
    82   db.prepare('UPDATE users SET readonly = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
    83     .run(ro, req.params.id);
    84   res.redirect('/admin/users?success=' + encodeURIComponent(ro ? 'Kijk-modus aan' : 'Kijk-modus uit'));
    8576});
    8677
  • src/routes/admin.js

    rb94c08e r8afbdd6  
    1616  const user = req.session.user;
    1717
    18   // Niet-god: een artiest die een eigen site bezit krijgt een "Mijn PrutFolio"-
    19   // dashboard, gescopet op z'n eigen site. Bezit 'ie geen site -> geen beheer.
    20   if (user.role !== 'god') {
     18  // Een kijker mag het volledige (god-)Beheer alleen-lezen inzien — net als god
     19  // dus, alleen schrijven is globaal geblokkeerd. Een gewone artiest die een
     20  // eigen site bezit krijgt een "Mijn Klonkt Hub"-dashboard, gescopet op z'n
     21  // eigen site. Bezit 'ie geen site -> geen beheer.
     22  if (user.role !== 'god' && user.role !== 'kijker') {
    2123    const mySite = db.prepare(
    2224      'SELECT * FROM sites WHERE owner_id = ? ORDER BY created_at ASC LIMIT 1'
     
    2931    };
    3032    return renderPage(req, res, 'pages/my-site', {
    31       pageTitle: 'Mijn PrutFolio',
     33      pageTitle: 'Mijn Klonkt Hub',
    3234      bodyClass: 'on-admin',
    3335      mySite,
  • src/routes/hub.js

    rb94c08e r8afbdd6  
    3535  `).all();
    3636
    37   // ALLE PrutFolio's (elke user, incl. de admin z'n eigen site) voor de roster.
     37  // Uitgelichte PrutFolio's voor de home-roster: meest-actief eerst (aantal
     38  // gepubliceerde posts), dan nieuwste. Beperkt tot HOME_ROSTER_LIMIT zodat de
     39  // home schaalt — de volledige, doorzoekbare lijst staat op /artiesten.
     40  const HOME_ROSTER_LIMIT = 24;
    3841  const artists = db.prepare(`
    3942    SELECT s.slug, s.title, s.tagline, s.profile_photo, s.accent,
     
    4245    FROM sites s
    4346    LEFT JOIN users u ON u.id = s.owner_id
    44     ORDER BY s.created_at ASC
    45   `).all();
     47    ORDER BY post_count DESC, s.created_at DESC
     48    LIMIT ?
     49  `).all(HOME_ROSTER_LIMIT);
     50  const totalArtists = db.prepare('SELECT COUNT(*) AS c FROM sites').get().c;
    4651
    4752  // De hub-pagina is GENERIEK (van geen enkele user) — branding komt uit globale
     
    6065    hub,
    6166    artists,
     67    totalArtists,
     68    rosterLimit: HOME_ROSTER_LIMIT,
    6269    posts,
    6370  });
  • src/routes/posts.js

    rb94c08e r8afbdd6  
    6262  'archive', 'search', 'account', 'sites', 'comments',
    6363  'posts', 'media', 'audio', 'prutter', 'forum',
    64   'tag', 'type', 'users', 'feed.xml', 'atom.xml', 'sitemap.xml',
     64  'tag', 'type', 'user', 'users', 'artiesten', 'feed.xml', 'atom.xml', 'sitemap.xml',
    6565  'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
    6666]);
  • src/routes/prutter.js

    rb94c08e r8afbdd6  
    1818import db from '../config/database.js';
    1919import { renderPage } from '../middleware/render.js';
    20 import { requireAuth } from '../middleware/auth.js';
     20import { requireAuth, isViewer } from '../middleware/auth.js';
    2121import PermissionsService from '../services/PermissionsService.js';
    2222
     
    5555// ==================== START / RESUME CONVERSATION ====================
    5656router.get('/new', requirePrutter, (req, res) => {
     57  // Een gesprek starten is een schrijf-actie (INSERT) — een kijker mag dat niet.
     58  // De globale guard pakt dit niet omdat het een GET is, dus expliciet blokkeren.
     59  if (isViewer(req.session.user)) {
     60    res.status(403);
     61    return renderPage(req, res, 'pages/viewer-blocked', { pageTitle: 'Kijker-modus', bodyClass: 'on-special' });
     62  }
    5763  const targetUsername = (req.query.to || '').toString().trim();
    5864  if (!targetUsername) {
     
    9096  const messages = prutter.getMessages(conv.id, 200, 0).reverse();
    9197
    92   // Mark inbound messages as read
    93   prutter.markAsRead(conv.id, me);
     98  // Mark inbound messages as read — sla over voor kijkers (markAsRead is een
     99  // UPDATE; een GET valt buiten de globale guard, dus hier expliciet skippen).
     100  if (!isViewer(req.session.user)) prutter.markAsRead(conv.id, me);
    94101
    95102  renderPage(req, res, 'pages/prutter-conversation', {
Note: See TracChangeset for help on using the changeset viewer.