source: Klonkt/src/views/pages/admin-paid.ejs@ 928d1c7

main
Last change on this file since 928d1c7 was 61e3daf, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: paid posts slice 1, owner Patreon config (encrypted)

The site owner can connect their OWN Patreon campaign for paid posts
(klonkt-demo-aki), premium-gated in Beheer. Client id/secret, campaign
id and the creator access/refresh token are stored ENCRYPTED at rest
(new CryptoBox AES-256-GCM helper, key from PAID_SECRET), so a database
dump leaks nothing usable; the token auto-refreshes. Separate from
Klonkt Premium's license flow, which is untouched. Degrades gracefully:
without PAID_SECRET the admin page refuses to save rather than storing
plaintext. Nothing patron-facing yet (posts.paid + unlock come in
slices 2 to 4), so no changelog entry.

CryptoBox also carries the cookie-less signed-blob helper (signBlob/
verifyBlob) that slices 3 and 4 reuse for the OAuth state and the
WebAuthn challenge.

Changed files:
src/config/database.js

  • paid_patreon table (site_id PK, secrets encrypted)

src/server.js

  • mount /admin/paid

src/views/pages/admin.ejs

  • "Betaalde posts" button in Beheer

New file:
src/services/CryptoBox.js

  • aes-256-gcm encrypt/decrypt + HMAC signBlob/verifyBlob

src/services/PaidPatreonService.js

  • owner config CRUD (encrypted), token refresh, creatorAccessToken

src/routes/admin-paid.js

  • premium-gated config form (GET/POST/disconnect)

src/views/pages/admin-paid.ejs

  • the form + status

test/paid-patreon.test.js

  • crypto roundtrip, no-plaintext-in-DB, refresh, blob signing

docs/paid-posts-design.md, docs/privacy-betaalde-posts.md

  • concurrency property documented

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

  • Property mode set to 100644
File size: 3.1 KB
Line 
1<div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
2 <h1 style="margin:0 0 .3rem">Betaalde posts</h1>
3 <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
4 Koppel je eigen Patreon-campagne. Supporters ontgrendelen betaalde posts met een passkey,
5 zonder account en zonder cookie. Wij bewaren geen namen of e-mailadressen van supporters,
6 alleen het versleutelde token van jouw campagne.
7 </p>
8
9 <% if (saved) { %><p style="color:var(--accent);font-weight:600">Opgeslagen.</p><% } %>
10 <% if (error) { %><p style="color:#c0392b"><%= error %></p><% } %>
11 <% if (!secretReady) { %>
12 <p style="color:#c0392b">Let op: <code>PAID_SECRET</code> staat niet in de serverconfig. Zonder die sleutel kunnen secrets niet versleuteld worden opgeslagen.</p>
13 <% } %>
14
15 <p style="margin:.2rem 0 1.2rem">
16 Status:
17 <% if (status.connected) { %>
18 <strong style="color:var(--accent)">verbonden</strong> (campagne <%= status.campaignId %>)
19 <% } else if (status.configured) { %>
20 <strong>ingesteld, nog niet verbonden</strong> (vul een token in)
21 <% } else { %>
22 <strong>nog niet ingesteld</strong>
23 <% } %>
24 </p>
25
26 <form method="post" action="/admin/paid" style="display:flex;flex-direction:column;gap:.9rem">
27 <label>Patreon client id
28 <input type="text" name="client_id" value="<%= status.clientId || '' %>" autocomplete="off" style="width:100%">
29 </label>
30 <label>Patreon client secret
31 <input type="password" name="client_secret" placeholder="<%= status.hasSecret ? 'blijft ongewijzigd' : '' %>" autocomplete="off" style="width:100%">
32 <small style="color:var(--ink-soft,#888)">Leeg laten = huidige waarde behouden.</small>
33 </label>
34 <label>Campagne-id
35 <input type="text" name="campaign_id" value="<%= status.campaignId || '' %>" autocomplete="off" style="width:100%">
36 </label>
37 <label>Creator access token
38 <input type="password" name="access_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
39 </label>
40 <label>Creator refresh token
41 <input type="password" name="refresh_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
42 <small style="color:var(--ink-soft,#888)">De access + refresh token krijg je op je Patreon API-clientpagina. Wij versleutelen ze en verversen automatisch.</small>
43 </label>
44 <label>Standaard-steunbedrag voor een betaalde post (euro)
45 <input type="text" name="default_min_eur" value="<%= status.defaultMinCents ? (status.defaultMinCents/100).toFixed(2) : '' %>" inputmode="decimal" style="width:120px">
46 </label>
47 <div style="display:flex;gap:.6rem;align-items:center">
48 <button type="submit" class="btn btn-primary">Opslaan</button>
49 </div>
50 </form>
51
52 <% if (status.configured || status.connected) { %>
53 <form method="post" action="/admin/paid/disconnect" data-confirm="Patreon-koppeling verwijderen?" style="margin-top:1rem">
54 <button type="submit" class="btn btn-danger">Koppeling verwijderen</button>
55 </form>
56 <% } %>
57
58 <p style="margin-top:1.5rem"><a href="/admin">&larr; Terug naar Beheer</a></p>
59</div>
Note: See TracBrowser for help on using the repository browser.