Changeset 98ecf51 in Klonkt


Ignore:
Timestamp:
06/18/2026 03:19:09 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
7881080
Parents:
6a2abb1
Message:

Hub #1+#2: site admin permissions work + assign owner to another user

#1 — canAdminSite was reading user.siteRoles which was NEVER populated (dead
code) → only god and the literal owner passed admin checks. Now canAdminSite
queries site_members directly (an assigned co-admin). requireSiteManager/BySlug
use canAdminSite so collaborators can access site management too.

#2 — owner_id was hard-coded to the creating god → you couldn't assign a site to
another user (the core of hub mode was missing). Now: god-only "Owner" select in
the site form; create/save set owner_id + a site_members-admin row for that owner
(upsert). Non-god doesn't see the field and cannot change the owner.

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

Location:
src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/middleware/auth.js

    r6a2abb1 r98ecf51  
    44
    55import db from '../config/database.js';
     6import PermissionsService from '../services/PermissionsService.js';
    67
    78/**
     
    6566  if (u.role === 'god' || u.role === 'kijker') return next(); // kijker = alleen-lezen kijk-toegang
    6667  const site = res.locals.site;
    67   if (site && site.owner_id === u.id) return next();
     68  // owner OF toegewezen mede-beheerder (site_members) — canAdminSite dekt beide.
     69  if (site && PermissionsService.canAdminSite(u, site)) return next();
    6870  return res.status(403).send('Geen toegang tot deze site.');
    6971}
     
    7476  const u = req.session.user;
    7577  if (u.role === 'god' || u.role === 'kijker') return next(); // kijker = alleen-lezen kijk-toegang
    76   const site = db.prepare('SELECT owner_id FROM sites WHERE slug = ?').get(req.params.slug);
    77   if (site && site.owner_id === u.id) return next();
     78  const site = db.prepare('SELECT id, owner_id FROM sites WHERE slug = ?').get(req.params.slug);
     79  if (site && PermissionsService.canAdminSite(u, site)) return next();
    7880  return res.status(403).send('Geen toegang tot deze site.');
    7981}
  • src/routes/admin-sites.js

    r6a2abb1 r98ecf51  
    137137}
    138138
     139/** Geldige user-id voor owner-toewijzing, of null bij leeg/onbekend. */
     140function validOwnerId(raw) {
     141  const id = (raw || '').toString().trim();
     142  if (!id) return null;
     143  return db.prepare('SELECT 1 FROM users WHERE id = ?').get(id) ? id : null;
     144}
     145
     146/** Geef een user admin-rechten op een site (idempotent upsert). */
     147function grantSiteAdmin(siteId, userId) {
     148  db.prepare(`
     149    INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')
     150    ON CONFLICT(site_id, user_id) DO UPDATE SET role = 'admin'
     151  `).run(siteId, userId);
     152}
     153
     154/** Kandidaat-owners voor het owner-keuzeveld (god-only). */
     155function listOwnerCandidates() {
     156  return db.prepare('SELECT id, username, role FROM users ORDER BY username').all();
     157}
     158
    139159// ==================== LIST ====================
    140160router.get('/', requireGod, (req, res) => {
     
    163183    bodyClass: 'on-admin',
    164184    isNew: true,
    165     site: { slug: '', ...siteEditableFields() },
     185    site: { slug: '', owner_id: req.session.user.id, ...siteEditableFields() },
     186    users: listOwnerCandidates(),
    166187    palettes: ThemeService.listPalettes(),
    167188    accents: ThemeService.listAccents(),
     
    187208
    188209  const f = { ...siteEditableFields(), ...req.body };
     210
     211  // Owner: god mag de site aan een ANDERE gebruiker toewijzen — dit is de kern
     212  // van hub-modus (elke gebruiker z'n eigen, zelf te beheren Klonkt). Leeg of
     213  // ongeldig → de aanmakende god zelf.
     214  const ownerId = validOwnerId(req.body.owner_id) || req.session.user.id;
     215
    189216  const siteId = uuid();
    190217  db.prepare(`
     
    200227    (f.description || '').slice(0, 500),
    201228    (f.tagline || '').slice(0, 200),
    202     req.session.user.id,
     229    ownerId,
    203230    f.language || 'nl',
    204231    f.palette || 'sage',
     
    213240  );
    214241
    215   // The site_members entry lets the god/owner show up in canAdminSite checks.
    216   db.prepare(`
    217     INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')
    218   `).run(siteId, req.session.user.id);
    219 
    220   res.redirect(`/admin/sites/${slug}/edit?success=` + encodeURIComponent('Site created'));
     242  // De OWNER (niet per se de aanmaker) krijgt een site_members-admin-rij → zo komt
     243  // 'ie door canAdminSite + de requireSiteManager-gates en beheert 'ie z'n site.
     244  grantSiteAdmin(siteId, ownerId);
     245
     246  res.redirect(`/admin/sites/${slug}/edit?success=` + encodeURIComponent('Site aangemaakt'));
    221247});
    222248
     
    236262    isNew: false,
    237263    site,
     264    users: listOwnerCandidates(),
    238265    palettes: ThemeService.listPalettes(),
    239266    accents: ThemeService.listAccents(),
     
    314341  );
    315342
    316   res.redirect(`/admin/sites/${req.params.slug}/edit?success=` + encodeURIComponent('Saved'));
     343  // Owner (her)toewijzen — ALLEEN god. Een site-owner die z'n eigen site bewerkt
     344  // kan de eigenaar niet wijzigen (het veld wordt voor niet-god ook niet getoond).
     345  if (req.session.user.role === 'god') {
     346    const newOwner = validOwnerId(req.body.owner_id);
     347    if (newOwner) {
     348      db.prepare('UPDATE sites SET owner_id = ? WHERE id = ?').run(newOwner, site.id);
     349      grantSiteAdmin(site.id, newOwner);
     350    }
     351  }
     352
     353  res.redirect(`/admin/sites/${req.params.slug}/edit?success=` + encodeURIComponent('Opgeslagen'));
    317354});
    318355
  • src/services/PermissionsService.js

    r6a2abb1 r98ecf51  
    33 * Used in templates to show/hide edit buttons, delete buttons, etc.
    44 */
     5
     6import db from '../config/database.js';
    57
    68class PermissionsService {
     
    5456   */
    5557  static canAdminSite(user, site) {
    56     if (!user) return false;
     58    if (!user || !site) return false;
    5759    if (user.role === 'god') return true;
    5860    if (user.id === site.owner_id) return true;
    59     // Check site_members table
    60     return user.siteRoles && user.siteRoles[site.id] === 'admin';
     61    // Toegewezen mede-beheerder (collaborator) via site_members. Dit werd
     62    // voorheen via een nooit-gevulde user.siteRoles gelezen → dode code; nu
     63    // direct op de tabel (paar checks per pagina, indexed = goedkoop).
     64    return !!db.prepare(
     65      "SELECT 1 FROM site_members WHERE site_id = ? AND user_id = ? AND role = 'admin' LIMIT 1"
     66    ).get(site.id, user.id);
    6167  }
    6268
  • src/views/pages/admin-site-edit.ejs

    r6a2abb1 r98ecf51  
    2424        <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
    2525      </label>
     26      <%# Eigenaar (her)toewijzen — ALLEEN god. In hub-modus geef je zo elke
     27          gebruiker z'n eigen, zelf te beheren Klonkt. Niet-god ziet dit niet. %>
     28      <% if (user && user.role === 'god') { %>
     29      <label>
     30        <span>Eigenaar <small class="form-hint-inline">— wie deze Klonkt zelf mag beheren</small></span>
     31        <select name="owner_id">
     32          <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %>
     33            <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? ' (god)' : '' %></option>
     34          <% }); %>
     35        </select>
     36      </label>
     37      <% } %>
    2638      <label>
    2739        <span>Tagline <small class="form-hint-inline">— korte oneliner</small></span>
Note: See TracChangeset for help on using the changeset viewer.