source: Klonkt/src/middleware/auth.js@ ec2e358

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

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

  • Property mode set to 100644
File size: 3.1 KB
Line 
1/**
2 * Auth middleware
3 */
4
5import db from '../config/database.js';
6
7/**
8 * Validate a "next" URL for safe redirect after login.
9 * Returns the URL if safe, otherwise null.
10 *
11 * Rules:
12 * - Must be a string, max 256 chars (prevent abuse).
13 * - Must start with "/" but NOT "//" or "/\" (no protocol-relative open redirects).
14 * - Must not point back at /auth/* (prevents login → login loop).
15 */
16export function safeNext(raw) {
17 if (typeof raw !== 'string' || !raw.length || raw.length > 256) return null;
18 if (raw[0] !== '/' || raw[1] === '/' || raw[1] === '\\') return null;
19 if (/^\/auth(\/|$)/i.test(raw)) return null;
20 return raw;
21}
22
23function loginRedirect(req, res) {
24 // Preserve the originally-requested URL so login can return us there.
25 const next = encodeURIComponent(req.originalUrl || req.url || '/');
26 const target = `/auth/login?next=${next}`;
27 if (req.headers['hx-request'] === 'true') {
28 res.setHeader('HX-Redirect', target);
29 return res.status(401).send('Login required');
30 }
31 return res.redirect(target);
32}
33
34export function requireAuth(req, res, next) {
35 if (!req.session?.user) return loginRedirect(req, res);
36 next();
37}
38
39// Een 'kijker' mag ALLES bekijken (incl. Beheer) maar NIETS wijzigen. De
40// schrijf-blokkade zit in de globale guard in server.js; deze helper bepaalt
41// alleen "is dit een alleen-lezen account?". `readonly` is de legacy-vlag die
42// we nog meenemen zodat niet-gemigreerde demo-accounts geblokkeerd blijven.
43export function isViewer(user) {
44 return !!user && (user.role === 'kijker' || !!user.readonly);
45}
46
47export function requireGod(req, res, next) {
48 if (!req.session?.user) return loginRedirect(req, res);
49 const role = req.session.user.role;
50 // god beheert; een kijker mág het Beheer-paneel zien (alleen-lezen) — de
51 // globale guard 403't elke write, dus dit geeft enkel kijk-toegang.
52 if (role !== 'god' && role !== 'kijker') {
53 return res.status(403).send('God role required');
54 }
55 next();
56}
57
58// Mag de ingelogde user de HUIDIGE site (res.locals.site) beheren? god altijd;
59// anders alleen de owner van die site. Gebruikt voor site-gescopete beheerroutes
60// die een artiest via /user/<eigen-slug>/admin/... bereikt (res.locals.site is dan
61// z'n eigen site; een vreemde slug levert een andere site -> 403).
62export function requireSiteManager(req, res, next) {
63 if (!req.session?.user) return loginRedirect(req, res);
64 const u = req.session.user;
65 if (u.role === 'god' || u.role === 'kijker') return next(); // kijker = alleen-lezen kijk-toegang
66 const site = res.locals.site;
67 if (site && site.owner_id === u.id) return next();
68 return res.status(403).send('Geen toegang tot deze site.');
69}
70
71// Idem, maar de site wordt bepaald door de :slug-parameter (bv. site-edit).
72export function requireSiteManagerBySlug(req, res, next) {
73 if (!req.session?.user) return loginRedirect(req, res);
74 const u = req.session.user;
75 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 return res.status(403).send('Geen toegang tot deze site.');
79}
Note: See TracBrowser for help on using the repository browser.