Changeset ec288dc in Klonkt
- Timestamp:
- 07/21/2026 03:11:39 AM (7 weeks ago)
- Branches:
- main
- Children:
- dede82e
- Parents:
- c7a211d
- git-author:
- Robin <roboburr@…> (07/21/2026 03:11:37 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 03:11:39 AM)
- Location:
- src
- Files:
-
- 3 edited
-
routes/paid.js (modified) (1 diff)
-
services/PaidPatreonService.js (modified) (1 diff)
-
views/pages/paid-result.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/paid.js
rc7a211d rec288dc 70 70 pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false, 71 71 reason: active ? 'tier' : 'notpatron', neededCents: payload.cents, haveCents: cents, postSlug: payload.post, patronUrl, 72 debug: membership ? membership.diag : 'no_response', 72 73 }); 73 74 } -
src/services/PaidPatreonService.js
rc7a211d rec288dc 164 164 165 165 // Exchange a patron's auth code and read their membership of the owner's 166 // campaign. Returns { status, cents } or null. The patron token is used once 167 // and discarded here: nothing identifying is stored (design decision). 166 // campaign. Returns { status, cents, diag } (status null = not a patron); the 167 // `diag` string is a NON-identifying breadcrumb (campaign ids + status + cents) 168 // so a stuck owner can see why. Returns null only on hard misconfig. The patron 169 // token is used once and discarded here: nothing identifying is stored. 168 170 export async function verifyPatron(siteId, code, redirectUri, fetchImpl = fetch) { 169 171 const c = getOwnerConfig(siteId); 170 172 if (!c || !c.clientId || !c.clientSecret || !c.campaignId) return null; 171 const tokenRes = await fetchImpl(TOKEN_URL, { 172 method: 'POST', 173 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 174 body: new URLSearchParams({ 175 grant_type: 'authorization_code', code, 176 client_id: c.clientId, client_secret: c.clientSecret, redirect_uri: redirectUri, 177 }).toString(), 178 }); 179 if (!tokenRes.ok) return null; 173 const none = (diag) => ({ status: null, cents: 0, diag }); 174 let tokenRes; 175 try { 176 tokenRes = await fetchImpl(TOKEN_URL, { 177 method: 'POST', 178 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 179 body: new URLSearchParams({ 180 grant_type: 'authorization_code', code, 181 client_id: c.clientId, client_secret: c.clientSecret, redirect_uri: redirectUri, 182 }).toString(), 183 }); 184 } catch { return none('token_fetch_error'); } 185 if (!tokenRes.ok) return none(`token_http_${tokenRes.status}`); 180 186 const tok = await tokenRes.json(); 181 if (!tok || !tok.access_token) return n ull;187 if (!tok || !tok.access_token) return none('no_access_token'); 182 188 const url = 'https://www.patreon.com/api/oauth2/v2/identity' 183 189 + '?include=memberships.campaign' 184 190 + '&fields%5Bmember%5D=patron_status,currently_entitled_amount_cents'; 185 191 const idRes = await fetchImpl(url, { headers: { Authorization: `Bearer ${tok.access_token}` } }); 186 if (!idRes.ok) return n ull;192 if (!idRes.ok) return none(`identity_http_${idRes.status}`); 187 193 const identity = await idRes.json(); 188 194 const membership = pickCampaignMembership(identity, c.campaignId); // token goes out of scope, discarded 189 // Diagnostic (no identity stored, only shapes): why did a real supporter get 190 // rejected? Logs the memberships Patreon returned vs the configured campaign. 191 // Nothing here is persisted; it's a one-line server log for the owner. 192 if (!membership || membership.status !== 'active_patron') { 193 const seen = ((identity && identity.included) || []) 194 .filter((it) => it.type === 'member') 195 .map((it) => { 196 const camp = it.relationships && it.relationships.campaign && it.relationships.campaign.data; 197 const a = it.attributes || {}; 198 return `${camp ? camp.id : '?'}:${a.patron_status || 'null'}:${a.currently_entitled_amount_cents || 0}c`; 199 }); 200 console.warn(`[paid] verifyPatron: config campaign=${c.campaignId} → memberships seen=[${seen.join(', ') || 'none'}] picked=${membership ? membership.status + '/' + membership.cents + 'c' : 'null'}`); 201 } 202 return membership; 195 const seen = ((identity && identity.included) || []) 196 .filter((it) => it.type === 'member') 197 .map((it) => { 198 const camp = it.relationships && it.relationships.campaign && it.relationships.campaign.data; 199 const a = it.attributes || {}; 200 return `${camp ? camp.id : '?'}:${a.patron_status || 'null'}:${a.currently_entitled_amount_cents || 0}c`; 201 }); 202 const diag = `campaign=${c.campaignId} seen=[${seen.join(', ') || 'none'}] picked=${membership ? membership.status + '/' + membership.cents + 'c' : 'null'}`; 203 if (!membership || membership.status !== 'active_patron') console.warn(`[paid] verifyPatron: ${diag}`); 204 return { status: membership ? membership.status : null, cents: membership ? membership.cents : 0, diag }; 203 205 } 204 206 -
src/views/pages/paid-result.ejs
rc7a211d rec288dc 34 34 <% } %> 35 35 </div> 36 37 <% if (typeof debug !== 'undefined' && debug) { %> 38 <p class="pr-diag">diagnose: <code><%= debug %></code></p> 39 <% } %> 36 40 </div> 37 41 </section> … … 46 50 .pr-btn { display: inline-block; padding: 11px 20px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; } 47 51 .pr-btn-ghost { background: transparent; color: var(--ink,#222); border: 1px solid color-mix(in srgb, var(--ink,#000) 22%, transparent); } 52 .pr-diag { margin: 18px 0 0; font-size: 12px; opacity: .6; word-break: break-all; } 53 .pr-diag code { font-family: ui-monospace, monospace; } 48 54 </style>
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)