| [746d98a] | 1 | /**
|
|---|
| 2 | * De wardmodus: een besloten Klonkt waarvan alleen het profiel openbaar is
|
|---|
| 3 | * (Robins ontwerp, 30-9).
|
|---|
| 4 | *
|
|---|
| 5 | * AAN ZOLANG DE SITE GUARDIANS HEEFT, en dat wordt niet opgeslagen maar
|
|---|
| 6 | * afgeleid. listGuardians telt alleen GEACCEPTEERDE guardianships: de modus gaat
|
|---|
| 7 | * aan zodra een koppeling rond is (niet bij een voorstel), en uit zodra de set
|
|---|
| 8 | * leeg is -- en de set leegmaken is emancipatie (FEP-633c 3.4). Dat is precies
|
|---|
| 9 | * waar het ontwerp de terugweg naar een gewone Klonkt legt. Een schakelaar zou
|
|---|
| 10 | * een tweede waarheid naast de guardianship zijn, die kan vergeten worden of
|
|---|
| 11 | * uit de pas kan lopen.
|
|---|
| 12 | *
|
|---|
| 13 | * WAT EEN BEZOEKER ZIET: de voorpagina wordt het profiel (naam, foto, bio,
|
|---|
| 14 | * links), en verder niets. Elke andere pagina stuurt terug naar dat profiel;
|
|---|
| 15 | * feeds, zoeken en alles wat geen pagina is geeft 404. STANDAARD DICHT: wat hier
|
|---|
| 16 | * niet op de lijst staat komt er niet langs, zodat een nieuwe route niet vanzelf
|
|---|
| 17 | * een kind openzet.
|
|---|
| 18 | *
|
|---|
| 19 | * WAT GEWOON DOORGAAT, en waarom:
|
|---|
| 20 | * - de federatie (ActivityPub, WebFinger, OAuth voor de Shaer-app) en /media,
|
|---|
| 21 | * /assets, /og: die hangen in server.js VOOR deze poort. Het kind post en
|
|---|
| 22 | * volgt via Shaer, en de volgpoort van de guardians regelt wie er meeleest;
|
|---|
| 23 | * - inloggen, het account, beheer en het guardianpaneel: die hebben hun eigen
|
|---|
| 24 | * bewaking, en de ward moet erbij kunnen;
|
|---|
| 25 | * - /audio/stream/: bewaakt elk bestand zelf, en een track die de ward op de
|
|---|
| 26 | * fediverse opende moet daar speelbaar blijven.
|
|---|
| 27 | *
|
|---|
| 28 | * WIE ALLES ZIET: de ward zelf (eigenaar), en wie de site mag beheren of
|
|---|
| 29 | * bekijken (god, kijker, sitebeheer) -- dezelfde grens als elders.
|
|---|
| 30 | */
|
|---|
| 31 |
|
|---|
| 32 | import * as Guardianship from '../services/guardianship/index.js';
|
|---|
| 33 | import PermissionsService from '../services/PermissionsService.js';
|
|---|
| 34 | import { parseProfileLinks } from '../services/PlatformIcons.js';
|
|---|
| 35 | import { renderPage } from './render.js';
|
|---|
| 36 |
|
|---|
| 37 | // Wat een bezoeker in de wardmodus WEL bereikt. Een map telt met alles eronder.
|
|---|
| 38 | const OPEN_DIRS = ['/auth', '/account', '/admin', '/guardian', '/audio/stream', '/lang',
|
|---|
| 39 | // De federatie hangt in server.js al VOOR deze poort en komt hier dus nooit
|
|---|
| 40 | // langs. Ze staan er toch, voor als iemand de volgorde ooit omgooit: dan zou
|
|---|
| 41 | // een ward stil van de fediverse verdwijnen, en precies dat mag een
|
|---|
| 42 | // opruimactie niet kunnen veroorzaken.
|
|---|
| 43 | '/ap', '/.well-known', '/nodeinfo', '/oauth'];
|
|---|
| 44 | const OPEN_FILES = new Set(['/manifest.webmanifest', '/favicon.svg', '/favicon.ico', '/sw.js', '/robots.txt']);
|
|---|
| 45 |
|
|---|
| 46 | function isOpenPath(p) {
|
|---|
| 47 | if (OPEN_FILES.has(p)) return true;
|
|---|
| 48 | return OPEN_DIRS.some((d) => p === d || p.startsWith(d + '/'));
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | /** Heeft deze site guardians? Dan is het een ward, en staat de modus aan. */
|
|---|
| 52 | export function isWardSite(site) {
|
|---|
| 53 | if (!site || !site.slug) return false;
|
|---|
| 54 | try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; }
|
|---|
| 55 | }
|
|---|
| 56 |
|
|---|
| 57 | /** Ziet deze gebruiker de hele site, ook in de wardmodus? */
|
|---|
| 58 | export function seesWholeSite(user, site) {
|
|---|
| 59 | if (!user) return false;
|
|---|
| 60 | if (user.role === 'god' || user.role === 'kijker') return true;
|
|---|
| 61 | try { return !!PermissionsService.canAdminSite(user, site); } catch { return false; }
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | /** De fediverse-handle van de site: @slug@host. */
|
|---|
| 65 | function handleOf(req, site) {
|
|---|
| 66 | let host = '';
|
|---|
| 67 | try { host = new URL(process.env.PUBLIC_BASE_URL || '').host; } catch { /* terugval hieronder */ }
|
|---|
| 68 | if (!host) host = req.get('host') || '';
|
|---|
| 69 | return host ? `@${site.slug}@${host}` : `@${site.slug}`;
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 | function renderWardProfile(req, res) {
|
|---|
| 73 | const site = res.locals.site;
|
|---|
| 74 | return renderPage(req, res, 'pages/ward-profile', {
|
|---|
| 75 | pageTitle: site.profile_name || site.title || site.slug,
|
|---|
| 76 | bodyClass: 'on-ward-profile',
|
|---|
| 77 | wardHandle: handleOf(req, site),
|
|---|
| 78 | wardLinks: parseProfileLinks(site.profile_links),
|
|---|
| 79 | });
|
|---|
| 80 | }
|
|---|
| 81 |
|
|---|
| 82 | export function wardProfileGate(req, res, next) {
|
|---|
| 83 | const site = res.locals.site;
|
|---|
| 84 | res.locals.wardProfile = false;
|
|---|
| 85 | if (!isWardSite(site)) return next();
|
|---|
| 86 | if (seesWholeSite(req.session && req.session.user, site)) return next();
|
|---|
| 87 |
|
|---|
| 88 | // Vanaf hier: een bezoeker van een ward-site. De views lezen deze vlag om
|
|---|
| 89 | // niets aan te bieden wat toch dicht is (de zoekknop bijvoorbeeld).
|
|---|
| 90 | res.locals.wardProfile = true;
|
|---|
| 91 | const p = req.path;
|
|---|
| 92 | if (isOpenPath(p)) return next();
|
|---|
| 93 |
|
|---|
| 94 | const isRead = req.method === 'GET' || req.method === 'HEAD';
|
|---|
| 95 | if (isRead && p === '/') return renderWardProfile(req, res);
|
|---|
| 96 |
|
|---|
| 97 | const base = res.locals.siteUrlBase || '';
|
|---|
| 98 | if (isRead && req.get('hx-request') === 'true') {
|
|---|
| 99 | // Een htmx-navigatie zou het profiel als brokstuk in de pagina zetten.
|
|---|
| 100 | // HX-Redirect laat de browser echt naar het profiel gaan.
|
|---|
| 101 | res.set('HX-Redirect', base + '/');
|
|---|
| 102 | return res.status(204).end();
|
|---|
| 103 | }
|
|---|
| 104 | if (isRead && req.accepts(['html', 'json']) === 'html') return res.redirect(302, base + '/');
|
|---|
| 105 | return res.status(404).end();
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | export default { wardProfileGate, isWardSite, seesWholeSite };
|
|---|