source: Klonkt/src/views/pages/admin-paid.ejs@ 4590e66

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

Feature: auto-generate the paid-posts encryption key (no env needed)

Nobody should have to hand-edit the env to use paid posts. CryptoBox now
resolves its key as: PAID_SECRET (env) if set, else a persisted key file
next to the database, auto-generated (0600) on first use. New installs and
existing users after an update get a key with zero config; a self-hoster
who set PAID_SECRET (Bart already did) keeps working unchanged, env wins.

The key lives OUTSIDE the sqlite DB on purpose: encrypting the Patreon
secrets is pointless if the key sits in the same file a DB dump would leak.
If the file can't be persisted (read-only fs) the box stays "not ready"
rather than using an ephemeral key that a restart would lose, so ciphertext
never becomes undecryptable.

Admin copy that referenced PAID_SECRET is updated: the warning now describes
the real remaining failure (key can't be created/read), not a missing env.

Changed files:
src/services/CryptoBox.js

  • resolve secret: env, else auto-generated 0600 key file by the database

src/services/PaidPatreonService.js

  • save error no longer names PAID_SECRET

src/routes/admin-paid.js, src/views/pages/admin-paid.ejs

  • not-ready copy describes the key file, not a missing env var

New file:
test/paid-secret.test.js

  • without PAID_SECRET: key file generated (0600), encrypt/decrypt roundtrips, key persists

remarks: the generated storage/.paid-secret must be backed up alongside the
DB, or the stored Patreon secrets can't be decrypted after a restore.

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

  • Property mode set to 100644
File size: 5.0 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: de encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?). Zonder sleutel kunnen secrets niet veilig 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 <div style="border:1px solid var(--border,#333);border-radius:10px;padding:.9rem 1rem;margin:0 0 1.3rem;background:color-mix(in srgb,var(--accent,#6b8f71) 8%,transparent)">
27 <p style="margin:0 0 .4rem;font-weight:600">Zet deze redirect-URI in je Patreon-client</p>
28 <p style="margin:0 0 .6rem;color:var(--ink-soft,#888);font-size:.9rem">
29 Bij je Patreon API-client, onder <em>Redirect URIs</em>, moet exact deze regel staan.
30 Klopt hij niet, dan geeft Patreon een foutmelding in plaats van je supporters terug te sturen.
31 </p>
32 <div style="display:flex;gap:.5rem;align-items:center">
33 <input id="pd-redirect" type="text" readonly value="<%= redirectUri %>" style="flex:1;min-width:0;font-family:monospace;font-size:.9rem">
34 <button type="button" id="pd-copy" class="btn">Kopieer</button>
35 </div>
36 </div>
37
38 <form method="post" action="/admin/paid" style="display:flex;flex-direction:column;gap:.9rem">
39 <label>Patreon client id
40 <input type="text" name="client_id" value="<%= status.clientId || '' %>" autocomplete="off" style="width:100%">
41 </label>
42 <label>Patreon client secret
43 <input type="password" name="client_secret" placeholder="<%= status.hasSecret ? 'blijft ongewijzigd' : '' %>" autocomplete="off" style="width:100%">
44 <small style="color:var(--ink-soft,#888)">Leeg laten = huidige waarde behouden.</small>
45 </label>
46 <label>Campagne-id
47 <input type="text" name="campaign_id" value="<%= status.campaignId || '' %>" autocomplete="off" style="width:100%">
48 </label>
49 <label>Openbare Patreon-pagina
50 <input type="url" name="patreon_url" value="<%= status.patreonUrl || '' %>" placeholder="https://www.patreon.com/jouwnaam" autocomplete="off" style="width:100%">
51 <small style="color:var(--ink-soft,#888)">De link waar bezoekers supporter kunnen worden. Getoond als "Word supporter" wanneer iemand nog niet doneert.</small>
52 </label>
53 <label>Creator access token
54 <input type="password" name="access_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
55 </label>
56 <label>Creator refresh token
57 <input type="password" name="refresh_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%">
58 <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>
59 </label>
60 <label>Standaard-steunbedrag voor een betaalde post (euro)
61 <input type="text" name="default_min_eur" value="<%= status.defaultMinCents ? (status.defaultMinCents/100).toFixed(2) : '' %>" inputmode="decimal" style="width:120px">
62 </label>
63 <div style="display:flex;gap:.6rem;align-items:center">
64 <button type="submit" class="btn btn-primary">Opslaan</button>
65 </div>
66 </form>
67
68 <% if (status.configured || status.connected) { %>
69 <form method="post" action="/admin/paid/disconnect" data-confirm="Patreon-koppeling verwijderen?" style="margin-top:1rem">
70 <button type="submit" class="btn btn-danger">Koppeling verwijderen</button>
71 </form>
72 <% } %>
73
74 <p style="margin-top:1.5rem"><a href="/admin">&larr; Terug naar Beheer</a></p>
75</div>
76
77<script>
78(function () {
79 var field = document.getElementById('pd-redirect');
80 var btn = document.getElementById('pd-copy');
81 if (!field || !btn) return;
82 field.addEventListener('focus', function () { field.select(); });
83 btn.addEventListener('click', function () {
84 field.select();
85 var done = function () { var t = btn.textContent; btn.textContent = 'Gekopieerd'; setTimeout(function () { btn.textContent = t; }, 1400); };
86 if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(field.value).then(done, function () { try { document.execCommand('copy'); done(); } catch (e) {} }); }
87 else { try { document.execCommand('copy'); done(); } catch (e) {} }
88 });
89})();
90</script>
Note: See TracBrowser for help on using the repository browser.