Changeset 072a242 in Klonkt
- Timestamp:
- 07/21/2026 03:26:24 AM (7 weeks ago)
- Branches:
- main
- Children:
- b9beb16
- Parents:
- dede82e
- git-author:
- Robin <roboburr@…> (07/21/2026 03:26:22 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 03:26:24 AM)
- Location:
- src
- Files:
-
- 3 edited
-
routes/paid.js (modified) (2 diffs)
-
routes/posts.js (modified) (2 diffs)
-
views/pages/paid-gate.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/paid.js
rdede82e r072a242 15 15 import PaidPatreon from '../services/PaidPatreonService.js'; 16 16 import Passkey from '../services/PasskeyService.js'; 17 import { renderPostBodyHtml } from './posts.js';18 17 19 18 const router = express.Router(); … … 128 127 const post = db.prepare("SELECT * FROM posts WHERE site_id = ? AND slug = ? AND status = 'published'").get(r.site.id, String(payload.post || '')); 129 128 if (!post || !post.paid) return res.status(404).json({ error: 'gone' }); 130 res.json({ ok: true, title: post.title || '', html: renderPostBodyHtml(r.site, post, req) }); 129 // Hand back a short-lived, single-post unlock capability. The client reloads 130 // the real post page with it (?u=), so the post renders through its normal 131 // template: correct layout, scoped styles, working audio. Not a cookie and 132 // not stored: a 120s signed blob that lives only in that one URL. 133 const token = signBlob({ purpose: 'unlocked', siteId: r.site.id, post: post.slug }, 120); 134 res.json({ ok: true, redirect: `${res.locals.siteUrlBase || ''}/${encodeURIComponent(post.slug)}?u=${encodeURIComponent(token)}` }); 131 135 }); 132 136 -
src/routes/posts.js
rdede82e r072a242 22 22 import { premiumUnlocked } from '../services/PatreonService.js'; 23 23 import { defaultMinCents as paidDefaultMinCents, patreonUrl as paidPatronUrl } from '../services/PaidPatreonService.js'; 24 import { verifyBlob } from '../services/CryptoBox.js'; 24 25 import MusicMeta from '../services/MusicMeta.js'; 25 26 … … 1238 1239 // never see the unlock button). 1239 1240 const canEditThis = req.session?.user && PermissionsService.canEditPost(req.session.user, post, site); 1240 if (post.paid && !canEditThis) { 1241 // A fresh unlock capability (?u=) from /paid/unlock lets a just-verified 1242 // supporter render the FULL post through this normal template (correct layout, 1243 // scoped styles, working audio). Short-lived signed blob, single post, not a 1244 // cookie and not stored. 1245 const _u = req.query.u ? verifyBlob(String(req.query.u)) : null; 1246 const _unlocked = _u && _u.purpose === 'unlocked' && _u.siteId === site.id && String(_u.post) === String(post.slug); 1247 if (post.paid && !canEditThis && !_unlocked) { 1241 1248 const { newerPost, olderPost } = postNeighbors(site, post, res.locals.tenancy === 'hub'); 1242 1249 return renderPage(req, res, 'pages/paid-gate', { -
src/views/pages/paid-gate.ejs
rdede82e r072a242 37 37 var base = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>"; 38 38 var slug = "<%= pgSlug %>"; 39 var hasPatron = <%= _hasPatron ? 'true' : 'false' %>; 39 40 var btn = document.getElementById('pg-unlock'); 40 41 var status = document.getElementById('pg-status'); 41 var page = document.getElementById('pg-page');42 42 function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); } 43 43 function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); } 44 44 45 if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) { btn.textContent = 'Word supporter'; btn.addEventListener('click', toLink); return; } 45 // No WebAuthn here: an assertion is impossible. With a Patreon page there is 46 // already a "Word supporter" button, so hide the (dead) unlock button rather 47 // than turn it into a second "Word supporter". Without one, this IS the button. 48 if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) { 49 if (hasPatron) { btn.style.display = 'none'; } 50 else { btn.textContent = 'Word supporter'; btn.addEventListener('click', toLink); } 51 return; 52 } 46 53 47 54 btn.addEventListener('click', function () { … … 61 68 .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); }) 62 69 .then(function (res) { 63 if (res.j && res.j.ok) { 64 // Swap the gate for the full post, client-side (no cookie kept). 65 var h = document.createElement('div'); 66 h.innerHTML = (res.j.title ? '<h1 class="post-title">' + res.j.title + '</h1>' : '') + 67 '<div class="post-content">' + res.j.html + '</div>'; 68 page.replaceWith(h); 70 if (res.j && res.j.ok && res.j.redirect) { 71 // Reload the real post page via the one-shot unlock capability, so it 72 // renders through its normal template (layout, styles, audio). 73 location.href = res.j.redirect; 69 74 } else if (res.status === 403) { 70 75 toLink(); // no valid passkey yet (or lapsed tier): link via Patreon
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)