source: Klonkt/src/services/PostAccessService.js@ f5cf299

main
Last change on this file since f5cf299 was 8b65e67, checked in by Bart <bart@…>, 3 weeks ago

Een gesloten bericht houdt zijn plek, en zijn tekst blijft thuis

Tweede stap voor de tijdlijn (Barts idee, naar bartoverkamp.nl): hele berichten
in de stroom in plaats van kaartjes. Een poort kan dan geen pagina meer zijn --
een betaald of fan-bericht moet ter plekke zeggen wat er is, met behoud van zijn
plek. Weglaten zou erger zijn: dan zien een supporter en een vreemde een andere
tijdlijn met andere buren, en klopt "vorige/volgende" voor niemand.

postEntry() is met opzet een functie en geen sjabloonconditie. Het lijf wordt
alleen GERENDERD als het ook getoond mag worden, dus bij een dichte poort reist
de tekst niet mee. Een <% if %> in een sjabloon krijgt hem wel in het model,
en dan staat er één vergeten conditie -- of één partial die net iets anders
samenstelt -- tussen de betaalmuur en de tekst erachter.

partials/post-body.ejs kiest alleen de vorm en gebruikt de woorden van de
bestaande poorten (pgate./fgate.), zodat ze niet uiteen gaan lopen na de eerste
tekstwijziging. Het ontgrendelen zelf blijft op de eigen pagina, want daar zit
de passkey-code; één knop ernaartoe is eerlijker dan een halve poort.

Vijf toetsen erbij, waaronder dat er bij een dichte poort niets van de tweede
alinea in het model zit.

Co-Authored-By: Claude Opus 5 <claude@…>

  • Property mode set to 100644
File size: 4.4 KB
Line 
1/**
2 * Wat mag DEZE bezoeker van DIT bericht zien?
3 *
4 * De vraag stond tot nu toe alleen in de postroute, en daar werd hij beantwoord
5 * door een ANDERE PAGINA te renderen: paid-gate of fan-gate in plaats van het
6 * bericht. Dat werkt zolang een bericht een pagina is. Zodra de tijdlijn hele
7 * berichten toont, moet hetzelfde besluit een STUK opleveren dat tussen de
8 * andere berichten past -- en twee plekken die allebei zelf beslissen wie wat
9 * mag zien, zijn een lek dat wacht op een gelegenheid.
10 *
11 * Dus één functie, en de route en de tijdlijn lezen er allebei uit. Deze module
12 * bepaalt alleen WAT er mag; hoe het eruitziet is aan de sjablonen.
13 */
14import PermissionsService from './PermissionsService.js';
15
16/**
17 * De teaser van een betaald bericht: nooit meer dan de eerste alinea.
18 *
19 * Stond als paidTeaser() in routes/posts.js. Hier neergezet omdat de tijdlijn
20 * hem ook nodig heeft, en een tweede kopie vroeg of laat meer prijsgeeft dan
21 * deze -- precies de fout die je bij een betaalmuur niet wilt maken.
22 */
23export function paidTeaser(post, max = 280) {
24 if (post && post.excerpt && String(post.excerpt).trim()) return String(post.excerpt).trim();
25 const html = String((post && post.content) || '');
26 const firstP = (html.match(/<p[^>]*>([\s\S]*?)<\/p>/i) || [null, html])[1] || '';
27 const text = firstP.replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\s+/g, ' ').trim();
28 return text.length > max ? text.slice(0, max).replace(/\s+\S*$/, '') + '…' : text;
29}
30
31/**
32 * Het besluit, als één waarde.
33 *
34 * 'full' → de hele inhoud
35 * 'paid' → betaalmuur: teaser + ontgrendelknop
36 * 'fan' → alleen voor fans: teaser + inloggen
37 * 'forbidden' → niet gepubliceerd en jij mag het niet bewerken
38 *
39 * De VOLGORDE is niet vrijblijvend. Betaald wordt vóór fan-only getoetst, want
40 * een bericht dat allebei is gaat open met een passkey en niet met een
41 * Klonkt-login; andersom belandt een anonieme supporter op het inlogscherm en
42 * ziet hij de ontgrendelknop nooit. Die regel stond als commentaar in de route
43 * en verhuist hier mee, want hij hoort bij het besluit en niet bij de pagina.
44 *
45 * @param post de rij uit `posts`
46 * @param viewer { user, site, unlockedSlug } -- unlockedSlug is de slug uit
47 * een vers ?u=-bewijs van /paid/unlock, al geverifieerd door de
48 * aanroeper. Deze module doet geen crypto.
49 */
50export function postAccess(post, { user = null, site = null, unlockedSlug = null } = {}) {
51 if (!post) return 'forbidden';
52
53 const canEdit = !!(user && PermissionsService.canEditPost(user, post, site));
54
55 // Een concept is van de maker. Dit stond vóór beide poorten in de route en
56 // hoort dat te blijven: over een ongepubliceerd bericht valt niets te kopen
57 // en niets te ontgrendelen.
58 if (post.status !== 'published' && !canEdit) return 'forbidden';
59
60 // De eigenaar/redacteur ziet altijd zijn eigen werk, betaald of niet.
61 if (canEdit) return 'full';
62
63 if (post.paid && String(unlockedSlug || '') !== String(post.slug)) return 'paid';
64 if (post.fan_only && !user) return 'fan';
65 return 'full';
66}
67
68/** Handig voor sjablonen: mag de bezoeker de echte inhoud zien? */
69export function canReadBody(post, viewer) { return postAccess(post, viewer) === 'full'; }
70
71/**
72 * Eén bericht, klaar voor de tijdlijn.
73 *
74 * DE HELE REDEN DAT DIT EEN FUNCTIE IS: het lijf wordt alleen GERENDERD als het
75 * ook getoond mag worden. Een sjabloon dat zelf `<% if (access === 'full') %>`
76 * doet krijgt de volledige tekst tóch mee in het model, en dan is het één
77 * vergeten conditie -- of één `?partial=1` die net iets anders samenstelt --
78 * tussen een betaalmuur en de tekst erachter. Hier komt hij niet eens mee.
79 *
80 * `renderBody` wordt ingespoten (posts.js levert renderPostBodyHtml), zodat
81 * deze module niets van routes of sjablonen hoeft te weten.
82 */
83export function postEntry(post, viewer, { renderBody } = {}) {
84 const access = postAccess(post, viewer);
85 const full = access === 'full';
86 return {
87 slug: post && post.slug,
88 title: (post && post.title) || '',
89 access,
90 // Alleen bij een gesloten poort een teaser, en nooit allebei: de teaser is
91 // de vervanging van het lijf, niet een voorproefje ernaast.
92 teaser: full || access === 'forbidden' ? null : paidTeaser(post),
93 content_html: full && typeof renderBody === 'function' ? renderBody(post) : null,
94 };
95}
96
97export default { postAccess, canReadBody, paidTeaser, postEntry };
Note: See TracBrowser for help on using the repository browser.