source: Klonkt/src/views/pages/paid-result.ejs@ e685f55

main
Last change on this file since e685f55 was 9e9e6f9, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: paid posts slice 3, patron link + passkey (cookie-less)

The registration leg of the paid-posts flow (klonkt-demo-aki). A
visitor links once via Patreon and gets a pseudonymous passkey entitlement,
with no session and no patron identity stored.

  • Dependency (approved): @simplewebauthn/server for verification, plus @simplewebauthn/browser vendored (UMD) so the page loads it with no CDN.
  • New paid_entitlements table: {passkey, site, proven cents, expiry}. No name, e-mail or Patreon id, ever.
  • Cookie-less throughout: the OAuth state and the WebAuthn challenge travel in signed blobs (CryptoBox), so nothing is kept between requests.
  • Flow: GET /paid/link -> Patreon authorize; GET /paid/callback verifies the patron (verifyPatron exchanges the code, reads identity?include=memberships.campaign, checks patron_status + currently_entitled_amount_cents against the post's price), then hands out registration options + a signed blob carrying the challenge and proven cents; POST /paid/register verifies the passkey and stores the entitlement. The patron token is used once and discarded.

The gate button and the per-post unlock (assertion) are slice 4; this
leg is what that flow calls to register a passkey on demand.

Changed files:
package.json, package-lock.json

  • @simplewebauthn/server + @simplewebauthn/browser

src/assets/vendor/simplewebauthn-browser.umd.min.js

  • vendored browser UMD (no CDN)

src/config/database.js

  • paid_entitlements table (no patron identity)

src/services/PaidPatreonService.js

  • pickCampaignMembership (pure), verifyPatron (exchange + identity)

src/server.js

  • mount /paid before the /:slug catch-all

New file:
src/services/PasskeyService.js

  • registration options + verify (lib) + entitlement store/prune

src/routes/paid.js

  • link / callback / register (cookie-less)

src/views/pages/paid-passkey.ejs, paid-result.ejs

  • passkey creation + not-a-supporter pages

test/paid-patron.test.js

  • membership parse, patron exchange (mock), options challenge, entitlement store/expiry/prune/delete, no-identity-columns

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 1.6 KB
Line 
1<section class="pr">
2 <div class="pr-card">
3 <div class="pr-ic">🔒</div>
4 <% if (reason === 'notpatron') { %>
5 <h1 class="pr-h1">Nog geen supporter</h1>
6 <p class="pr-sub">Je bent (nog) geen actieve supporter van deze site op Patreon. Word supporter en probeer het opnieuw.</p>
7 <% } else if (reason === 'tier') { %>
8 <h1 class="pr-h1">Een niveau hoger nodig</h1>
9 <p class="pr-sub">
10 Deze post vraagt vanaf &euro;<%= ((typeof neededCents !== 'undefined' ? neededCents : 0)/100).toFixed(2) %>.
11 Jouw steun is nu &euro;<%= ((typeof haveCents !== 'undefined' ? haveCents : 0)/100).toFixed(2) %>. Verhoog je steun en probeer opnieuw.
12 </p>
13 <% } else { %>
14 <h1 class="pr-h1">Ontgrendelen afgebroken</h1>
15 <p class="pr-sub">Er is niets gekoppeld. Je kunt het opnieuw proberen vanaf de post.</p>
16 <% } %>
17 <% if (typeof postSlug !== 'undefined' && postSlug) { %>
18 <p><a class="pr-btn" href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>/<%= postSlug %>">Terug naar de post</a></p>
19 <% } %>
20 </div>
21</section>
22
23<style>
24 .pr { max-width: 460px; margin: 0 auto; padding: 56px 18px; text-align: center; }
25 .pr-card { border: 1px solid color-mix(in srgb, var(--ink,#000) 16%, transparent); border-radius: 18px; padding: 34px 26px; }
26 .pr-ic { font-size: 38px; margin-bottom: 6px; }
27 .pr-h1 { font-size: 23px; margin: 0 0 8px; }
28 .pr-sub { opacity: .85; line-height: 1.6; margin: 0 0 18px; }
29 .pr-btn { display: inline-block; padding: 11px 20px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; }
30</style>
Note: See TracBrowser for help on using the repository browser.