Changeset 61e3daf in Klonkt for src/views


Ignore:
Timestamp:
07/21/2026 12:38:08 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
928d1c7
Parents:
4a08bfc
git-author:
Robin <roboburr@…> (07/21/2026 12:38:07 AM)
git-committer:
Robin <roboburr@…> (07/21/2026 12:38:08 AM)
Message:

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@…>

Location:
src/views/pages
Files:
1 added
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/admin.ejs

    r4a08bfc r61e3daf  
    4646    <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
    4747      <a href="/admin/stats" class="btn"><%= t('admin.b_stats') %></a>
     48      <a href="/admin/paid" class="btn">Betaalde posts</a>
    4849      <a href="/admin/newsletter" class="btn"><%= t('admin.b_newsletter') %></a>
    4950      <% if (tenancy !== 'hub' && primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
Note: See TracChangeset for help on using the changeset viewer.