Changeset ec288dc in Klonkt


Ignore:
Timestamp:
07/21/2026 03:11:39 AM (7 weeks ago)
Author:
Robin <roboburr@…>
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)
Message:

Debug: show why a patron is rejected on the "Nog geen supporter" page

We are debugging boiert.eu blind (it's a self-hosted instance we can't see
the logs of), so surface the non-identifying diagnosis on the result page
itself. verifyPatron now always returns { status, cents, diag } after it
reaches Patreon, where diag is a breadcrumb: the configured campaign_id, the
memberships Patreon returned (campaign:status:cents each), and what got
picked. The paid-result page prints it small under the buttons so the tester
can read it and report back. No identity (name/email) is included or stored.

Once the cause is known this can be removed again.

Changed files:
src/services/PaidPatreonService.js

  • verifyPatron returns {status,cents,diag}; null only on hard misconfig

src/routes/paid.js

  • pass diag as debug to the notpatron/tier result page

src/views/pages/paid-result.ejs

  • small "diagnose:" line when debug is present

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

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/paid.js

    rc7a211d rec288dc  
    7070      pageTitle: 'Ontgrendelen', bodyClass: 'on-special', ok: false,
    7171      reason: active ? 'tier' : 'notpatron', neededCents: payload.cents, haveCents: cents, postSlug: payload.post, patronUrl,
     72      debug: membership ? membership.diag : 'no_response',
    7273    });
    7374  }
  • src/services/PaidPatreonService.js

    rc7a211d rec288dc  
    164164
    165165// 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.
    168170export async function verifyPatron(siteId, code, redirectUri, fetchImpl = fetch) {
    169171  const c = getOwnerConfig(siteId);
    170172  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}`);
    180186  const tok = await tokenRes.json();
    181   if (!tok || !tok.access_token) return null;
     187  if (!tok || !tok.access_token) return none('no_access_token');
    182188  const url = 'https://www.patreon.com/api/oauth2/v2/identity'
    183189    + '?include=memberships.campaign'
    184190    + '&fields%5Bmember%5D=patron_status,currently_entitled_amount_cents';
    185191  const idRes = await fetchImpl(url, { headers: { Authorization: `Bearer ${tok.access_token}` } });
    186   if (!idRes.ok) return null;
     192  if (!idRes.ok) return none(`identity_http_${idRes.status}`);
    187193  const identity = await idRes.json();
    188194  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 };
    203205}
    204206
  • src/views/pages/paid-result.ejs

    rc7a211d rec288dc  
    3434      <% } %>
    3535    </div>
     36
     37    <% if (typeof debug !== 'undefined' && debug) { %>
     38      <p class="pr-diag">diagnose: <code><%= debug %></code></p>
     39    <% } %>
    3640  </div>
    3741</section>
     
    4650  .pr-btn { display: inline-block; padding: 11px 20px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; }
    4751  .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; }
    4854</style>
Note: See TracChangeset for help on using the changeset viewer.