source: Klonkt/src/routes/admin-paid.js@ 603d246

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

Feature: show the exact Patreon redirect URI in the paid admin

Bart hit Patreon's own error page ("Redirect URI .../paid/callback is not
supported by client") because the redirect URI our OAuth flow sends was
never whitelisted in his Patreon client, and the admin page told him
nowhere what that URI is. Once we redirect to patreon.com with an
unregistered redirect_uri, Patreon refuses to send the visitor back (open
redirect protection) and shows its own JSON error, which we cannot skin.

The only real defence is correct setup, so the admin now shows the exact
redirect URI to paste into the Patreon client, with a copy button. The URI
is built the same way paid.js builds it (PUBLIC_BASE_URL or the request
host + /paid/callback), so they always match.

Changed files:
src/routes/admin-paid.js

  • compute redirectUri (matches paid.js) and pass it to the view

src/views/pages/admin-paid.ejs

  • "Zet deze redirect-URI in je Patreon-client" block + copy button

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

  • Property mode set to 100644
File size: 3.1 KB
RevLine 
[61e3daf]1/**
2 * Admin: Paid posts (premium module, god-only). Slice 1 of klonkt-demo-aki.
3 *
4 * GET /admin/paid -> the owner's Patreon config form + status
5 * POST /admin/paid -> save config (secret/token stored encrypted)
6 * POST /admin/paid/disconnect -> forget the config
7 *
8 * Premium-gated via premiumUnlocked(), like stats/downloads. This is the site
9 * owner's OWN Patreon campaign, separate from Klonkt Premium's license flow.
10 */
11import express from 'express';
12import { renderPage } from '../middleware/render.js';
13import { requireGod } from '../middleware/auth.js';
14import { premiumUnlocked } from '../services/PatreonService.js';
15import { cryptoBoxReady } from '../services/CryptoBox.js';
16import PaidPatreon from '../services/PaidPatreonService.js';
17
18const router = express.Router();
19
[603d246]20// The redirect URI the owner MUST whitelist in their Patreon client. Must match
21// exactly what paid.js sends, or Patreon shows its own error page (which we
22// cannot skin) instead of returning the visitor to us.
23const redirectUri = (req) =>
24 (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '') + '/paid/callback';
25
[61e3daf]26function gate(req, res) {
27 if (!premiumUnlocked()) {
28 res.status(403).send('Betaalde posts is een premium-functie: koppel Patreon in Beheer, Instellingen.');
29 return false;
30 }
31 if (!res.locals.site) { res.status(400).send('Geen site.'); return false; }
32 return true;
33}
34
35router.get('/', requireGod, (req, res) => {
36 if (!gate(req, res)) return;
37 renderPage(req, res, 'pages/admin-paid', {
38 pageTitle: 'Betaalde posts',
39 bodyClass: 'on-admin',
40 status: PaidPatreon.ownerStatus(res.locals.site.id),
41 secretReady: cryptoBoxReady(),
[603d246]42 redirectUri: redirectUri(req),
[61e3daf]43 saved: req.query.saved === '1',
44 error: req.query.error || null,
45 });
46});
47
48router.post('/', requireGod, (req, res) => {
49 if (!gate(req, res)) return;
50 if (!cryptoBoxReady()) return res.redirect('/admin/paid?error=' + encodeURIComponent('PAID_SECRET ontbreekt in de serverconfig; secrets kunnen niet versleuteld worden opgeslagen.'));
51 const b = req.body || {};
52 const eur = String(b.default_min_eur || '').replace(',', '.').trim();
53 const cents = eur ? Math.round(parseFloat(eur) * 100) : undefined;
54 try {
55 PaidPatreon.saveOwnerConfig(res.locals.site.id, {
56 clientId: (b.client_id || '').trim() || undefined,
57 // Empty secret/token fields keep the stored value (no re-paste needed).
58 clientSecret: (b.client_secret || '').trim() || undefined,
59 campaignId: (b.campaign_id || '').trim() || undefined,
60 accessToken: (b.access_token || '').trim() || undefined,
61 refreshToken: (b.refresh_token || '').trim() || undefined,
62 defaultMinCents: Number.isFinite(cents) ? cents : undefined,
63 });
64 return res.redirect('/admin/paid?saved=1');
65 } catch (e) {
66 return res.redirect('/admin/paid?error=' + encodeURIComponent(e.message || 'Opslaan mislukt'));
67 }
68});
69
70router.post('/disconnect', requireGod, (req, res) => {
71 if (!gate(req, res)) return;
72 PaidPatreon.disconnect(res.locals.site.id);
73 res.redirect('/admin/paid?saved=1');
74});
75
76export default router;
Note: See TracBrowser for help on using the repository browser.