Changeset c3d12a6 in Klonkt
- Timestamp:
- 07/21/2026 02:07:18 AM (7 weeks ago)
- Branches:
- main
- Children:
- 9019a3e
- Parents:
- 603d246
- git-author:
- Robin <roboburr@…> (07/21/2026 02:07:07 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 02:07:18 AM)
- Files:
-
- 9 edited
-
src/config/database.js (modified) (1 diff)
-
src/routes/admin-paid.js (modified) (1 diff)
-
src/routes/paid.js (modified) (2 diffs)
-
src/routes/posts.js (modified) (2 diffs)
-
src/services/PaidPatreonService.js (modified) (5 diffs)
-
src/views/pages/admin-paid.ejs (modified) (1 diff)
-
src/views/pages/paid-gate.ejs (modified) (2 diffs)
-
src/views/pages/paid-result.ejs (modified) (4 diffs)
-
test/paid-patron.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r603d246 rc3d12a6 469 469 ensureColumn('posts', 'paid', 'INTEGER DEFAULT 0'); // paid post (klonkt-demo-aki) 470 470 ensureColumn('posts', 'paid_min_cents', 'INTEGER'); // required support; null = owner default 471 ensureColumn('paid_patreon', 'patreon_url', 'TEXT'); // owner's public Patreon page → "Word supporter" link (klonkt-demo-aki) 471 472 ensureColumn('ap_outbox', 'visibility', 'TEXT'); // 'direct' = private mention, never Public (shaer-tqc) 472 473 ensureColumn('ap_outbox', 'to_actors', 'TEXT'); // JSON array of recipient actor URIs for direct notes -
src/routes/admin-paid.js
r603d246 rc3d12a6 60 60 accessToken: (b.access_token || '').trim() || undefined, 61 61 refreshToken: (b.refresh_token || '').trim() || undefined, 62 // Empty clears it (null), a value sets it. Unlike secrets, this is not 63 // sensitive and there's a clear "remove the link" intent. 64 patreonUrl: (b.patreon_url || '').trim() || null, 62 65 defaultMinCents: Number.isFinite(cents) ? cents : undefined, 63 66 }); -
src/routes/paid.js
r603d246 rc3d12a6 54 54 const payload = verifyBlob(String(req.query.state || '')); 55 55 if (!payload || payload.purpose !== 'patron' || payload.siteId !== r.site.id) { 56 return res.status(400).send('Ongeldige of verlopen aanvraag. Probeer opnieuw vanaf de post.'); 56 return renderPage(req, res, 'pages/paid-result', { 57 pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false, reason: 'expired', 58 }); 57 59 } 60 const patronUrl = PaidPatreon.patreonUrl(r.site.id); 58 61 const code = String(req.query.code || ''); 59 62 if (req.query.error || !code) { 60 return renderPage(req, res, 'pages/paid-result', { pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false, reason: 'declined', postSlug: payload.post });63 return renderPage(req, res, 'pages/paid-result', { pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false, reason: 'declined', postSlug: payload.post, patronUrl }); 61 64 } 62 65 const membership = await PaidPatreon.verifyPatron(r.site.id, code, baseUrl(req) + '/paid/callback').catch(() => null); … … 66 69 return renderPage(req, res, 'pages/paid-result', { 67 70 pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false, 68 reason: active ? 'tier' : 'notpatron', neededCents: payload.cents, haveCents: cents, postSlug: payload.post, 71 reason: active ? 'tier' : 'notpatron', neededCents: payload.cents, haveCents: cents, postSlug: payload.post, patronUrl, 69 72 }); 70 73 } -
src/routes/posts.js
r603d246 rc3d12a6 21 21 import ActivityPubService from '../services/ActivityPubService.js'; 22 22 import { premiumUnlocked } from '../services/PatreonService.js'; 23 import { defaultMinCents as paidDefaultMinCents } from '../services/PaidPatreonService.js';23 import { defaultMinCents as paidDefaultMinCents, patreonUrl as paidPatronUrl } from '../services/PaidPatreonService.js'; 24 24 import MusicMeta from '../services/MusicMeta.js'; 25 25 … … 1247 1247 pgCents: post.paid_min_cents || paidDefaultMinCents(site.id), 1248 1248 pgSlug: post.slug, 1249 pgPatronUrl: paidPatronUrl(site.id), 1249 1250 newerPost, 1250 1251 olderPost, -
src/services/PaidPatreonService.js
r603d246 rc3d12a6 21 21 tokenExp: row.token_exp || 0, 22 22 defaultMinCents: row.default_min_cents || 0, 23 patreonUrl: row.patreon_url || null, 23 24 }; 24 25 } … … 36 37 tokenExp: c.tokenExp || 0, 37 38 hasSecret: !!c.clientSecret, 39 patreonUrl: c.patreonUrl || null, 38 40 }; 41 } 42 43 // The owner's public Patreon page, for the "Word supporter" link. Null when unset. 44 export function patreonUrl(siteId) { 45 const c = getOwnerConfig(siteId); 46 return c && c.patreonUrl ? c.patreonUrl : null; 39 47 } 40 48 … … 52 60 tokenExp: patch.tokenExp ?? cur.tokenExp ?? 0, 53 61 defaultMinCents: patch.defaultMinCents ?? cur.defaultMinCents ?? 0, 62 // undefined = keep (e.g. token refresh doesn't touch it); null/'' = clear. 63 patreonUrl: patch.patreonUrl !== undefined ? (patch.patreonUrl || null) : (cur.patreonUrl ?? null), 54 64 }; 55 65 db.prepare(`INSERT INTO paid_patreon 56 (site_id, client_id, client_secret_enc, campaign_id, access_token_enc, refresh_token_enc, token_exp, default_min_cents, updated_at)57 VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)66 (site_id, client_id, client_secret_enc, campaign_id, access_token_enc, refresh_token_enc, token_exp, default_min_cents, patreon_url, updated_at) 67 VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP) 58 68 ON CONFLICT(site_id) DO UPDATE SET 59 69 client_id=excluded.client_id, client_secret_enc=excluded.client_secret_enc, 60 70 campaign_id=excluded.campaign_id, access_token_enc=excluded.access_token_enc, 61 71 refresh_token_enc=excluded.refresh_token_enc, token_exp=excluded.token_exp, 62 default_min_cents=excluded.default_min_cents, updated_at=CURRENT_TIMESTAMP`)72 default_min_cents=excluded.default_min_cents, patreon_url=excluded.patreon_url, updated_at=CURRENT_TIMESTAMP`) 63 73 .run( 64 74 siteId, … … 70 80 merged.tokenExp || 0, 71 81 Math.max(0, parseInt(merged.defaultMinCents, 10) || 0), 82 merged.patreonUrl || null, 72 83 ); 73 84 } … … 171 182 export default { 172 183 getOwnerConfig, ownerStatus, saveOwnerConfig, disconnect, 173 defaultMinCents, needsRefresh, refreshCreatorToken, creatorAccessToken,184 defaultMinCents, patreonUrl, needsRefresh, refreshCreatorToken, creatorAccessToken, 174 185 pickCampaignMembership, verifyPatron, 175 186 }; -
src/views/pages/admin-paid.ejs
r603d246 rc3d12a6 47 47 <input type="text" name="campaign_id" value="<%= status.campaignId || '' %>" autocomplete="off" style="width:100%"> 48 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> 49 53 <label>Creator access token 50 54 <input type="password" name="access_token" placeholder="blijft ongewijzigd" autocomplete="off" style="width:100%"> -
src/views/pages/paid-gate.ejs
r603d246 rc3d12a6 20 20 <button type="button" id="pg-unlock" class="pg-btn">Ontgrendelen</button> 21 21 <p id="pg-status" class="pg-status" hidden></p> 22 <% if (typeof pgPatronUrl !== 'undefined' && pgPatronUrl) { %> 23 <p class="pg-join">Nog geen supporter? <a href="<%= pgPatronUrl %>" target="_blank" rel="noopener">Word het op Patreon</a> en ontgrendel daarna met je passkey.</p> 24 <% } %> 22 25 </section> 23 26 </article> … … 88 91 .pg-status { margin: 12px 0 0; opacity: .9; } 89 92 .pg-status.is-err { color: #c0392b; } 93 .pg-join { margin: 14px 0 0; font-size: .92rem; opacity: .8; } 90 94 </style> -
src/views/pages/paid-result.ejs
r603d246 rc3d12a6 1 <% 2 var _slug = (typeof postSlug !== 'undefined' && postSlug) ? postSlug : ''; 3 var _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; 4 var _patron = (typeof patronUrl !== 'undefined' && patronUrl) ? patronUrl : ''; 5 %> 1 6 <section class="pr"> 2 7 <div class="pr-card"> … … 4 9 <% if (reason === 'notpatron') { %> 5 10 <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>11 <p class="pr-sub">Je bent (nog) geen actieve supporter van deze site op Patreon. Word supporter en probeer het daarna opnieuw vanaf de post.</p> 7 12 <% } else if (reason === 'tier') { %> 8 13 <h1 class="pr-h1">Een niveau hoger nodig</h1> … … 11 16 Jouw steun is nu €<%= ((typeof haveCents !== 'undefined' ? haveCents : 0)/100).toFixed(2) %>. Verhoog je steun en probeer opnieuw. 12 17 </p> 18 <% } else if (reason === 'expired') { %> 19 <h1 class="pr-h1">Aanvraag verlopen</h1> 20 <p class="pr-sub">Deze ontgrendel-link is verlopen of al gebruikt. Ga terug naar de post en probeer het opnieuw.</p> 13 21 <% } else { %> 14 22 <h1 class="pr-h1">Ontgrendelen afgebroken</h1> 15 23 <p class="pr-sub">Er is niets gekoppeld. Je kunt het opnieuw proberen vanaf de post.</p> 16 24 <% } %> 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 <% } %> 25 26 <div class="pr-actions"> 27 <% if ((reason === 'notpatron' || reason === 'tier') && _patron) { %> 28 <a class="pr-btn" href="<%= _patron %>" target="_blank" rel="noopener">Word supporter op Patreon</a> 29 <% } %> 30 <% if (_slug) { %> 31 <a class="pr-btn pr-btn-ghost" href="<%= _base %>/<%= _slug %>">Terug naar de post</a> 32 <% } else { %> 33 <a class="pr-btn pr-btn-ghost" href="<%= _base %>/">Terug naar de site</a> 34 <% } %> 35 </div> 20 36 </div> 21 37 </section> … … 26 42 .pr-ic { font-size: 38px; margin-bottom: 6px; } 27 43 .pr-h1 { font-size: 23px; margin: 0 0 8px; } 28 .pr-sub { opacity: .85; line-height: 1.6; margin: 0 0 18px; } 44 .pr-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; } 45 .pr-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; } 29 46 .pr-btn { display: inline-block; padding: 11px 20px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; } 47 .pr-btn-ghost { background: transparent; color: var(--ink,#222); border: 1px solid color-mix(in srgb, var(--ink,#000) 22%, transparent); } 30 48 </style> -
test/paid-patron.test.js
r603d246 rc3d12a6 73 73 }); 74 74 75 test('patreonUrl: set, kept on unrelated save, cleared on empty', () => { 76 PP.saveOwnerConfig('s2', { clientId: 'c', clientSecret: 's', campaignId: '7', patreonUrl: 'https://patreon.com/x' }); 77 assert.equal(PP.patreonUrl('s2'), 'https://patreon.com/x'); 78 // a save that does NOT mention patreonUrl (e.g. token refresh) keeps it 79 PP.saveOwnerConfig('s2', { defaultMinCents: 200 }); 80 assert.equal(PP.patreonUrl('s2'), 'https://patreon.com/x'); 81 // an explicit empty value clears it 82 PP.saveOwnerConfig('s2', { patreonUrl: '' }); 83 assert.equal(PP.patreonUrl('s2'), null); 84 }); 85 75 86 test('deleteEntitlement removes the row (forget-passkey path)', () => { 76 87 Passkey.storeEntitlement({ credentialId: 'cred3', siteId: 's1', publicKey: 'PK', minCents: 100 });
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)