| [b1b7652] | 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 | */
|
|---|
| 14 | import 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 | */
|
|---|
| 23 | export 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 | */
|
|---|
| 50 | export 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? */
|
|---|
| 69 | export function canReadBody(post, viewer) { return postAccess(post, viewer) === 'full'; }
|
|---|
| 70 |
|
|---|
| [8b65e67] | 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 | */
|
|---|
| 83 | export 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 |
|
|---|
| 97 | export default { postAccess, canReadBody, paidTeaser, postEntry };
|
|---|