Changeset 072a242 in Klonkt for src/routes/paid.js


Ignore:
Timestamp:
07/21/2026 03:26:24 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
b9beb16
Parents:
dede82e
git-author:
Robin <roboburr@…> (07/21/2026 03:26:22 AM)
git-committer:
Robin <roboburr@…> (07/21/2026 03:26:24 AM)
Message:

Fix: unlocked post renders via its own page; no duplicate supporter button

Two things the tester hit once the passkey flow worked:

  1. Duplicate "Word supporter". The gate's no-WebAuthn fallback relabelled the unlock button to "Word supporter", which sat next to the real "Word supporter op Patreon" button. In the two-button layout that fallback now hides the (unusable) unlock button instead; the single-button layout keeps the relabel since it's the only button.
  1. The unlocked content sat in a bare <div>, so it lost the post page's layout and scoped typography ("div niet in een goeie div"). Instead of injecting HTML into the gate, /paid/unlock now returns a short-lived (120s) signed unlock capability and the client reloads the real post URL with it (?u=). GET /:slug renders the FULL post through its normal template when the capability is valid: correct wrapper, scoped styles, and working audio players (which bind on load and couldn't init in injected HTML). Not a cookie and not stored: the token lives only in that one URL and expires.

Changed files:
src/routes/paid.js

  • /unlock returns { ok, redirect } with a 120s unlock blob; drop the renderPostBodyHtml import (no longer injected here)

src/routes/posts.js

  • a valid ?u= capability bypasses the paid gate and renders the post

src/views/pages/paid-gate.ejs

  • no-WebAuthn fallback hides the unlock button when a Patreon page exists; success reloads the real post via the redirect

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/paid.js

    rdede82e r072a242  
    1515import PaidPatreon from '../services/PaidPatreonService.js';
    1616import Passkey from '../services/PasskeyService.js';
    17 import { renderPostBodyHtml } from './posts.js';
    1817
    1918const router = express.Router();
     
    128127  const post = db.prepare("SELECT * FROM posts WHERE site_id = ? AND slug = ? AND status = 'published'").get(r.site.id, String(payload.post || ''));
    129128  if (!post || !post.paid) return res.status(404).json({ error: 'gone' });
    130   res.json({ ok: true, title: post.title || '', html: renderPostBodyHtml(r.site, post, req) });
     129  // Hand back a short-lived, single-post unlock capability. The client reloads
     130  // the real post page with it (?u=), so the post renders through its normal
     131  // template: correct layout, scoped styles, working audio. Not a cookie and
     132  // not stored: a 120s signed blob that lives only in that one URL.
     133  const token = signBlob({ purpose: 'unlocked', siteId: r.site.id, post: post.slug }, 120);
     134  res.json({ ok: true, redirect: `${res.locals.siteUrlBase || ''}/${encodeURIComponent(post.slug)}?u=${encodeURIComponent(token)}` });
    131135});
    132136
Note: See TracChangeset for help on using the changeset viewer.