source: Klonkt/src/routes/admin-patreon.js@ bb82e7f

main
Last change on this file since bb82e7f was 1b4d5dd, checked in by roboburr <roboburr@…>, 3 months ago

Patreon premium gate (Phase 1): entitlement verification + Connect Patreon + isPremium

Klonkt-side of the Patreon model. The license server signs an Ed25519 JWT
entitlement token; this instance verifies it OFFLINE with Node-crypto
against /pubkey (no jose dependency). PatreonService: verify/store/isPremium.
Routes /admin/patreon/connect|callback|disconnect. Premium card in
Admin → Settings.

Premium is OFF BY DEFAULT (env KLONKT_PREMIUM_ENABLED != on) → no premium
UI, nothing gated. Inert deployment until the self-hoster enables it and
connects Patreon. Gating Hub/Statistics/Fan login follows in Phases 2-4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 2.4 KB
Line 
1/**
2 * Admin: Patreon koppelen voor de premium-laag (god-only).
3 *
4 * GET /admin/patreon/connect -> stuur de beheerder naar de license-server
5 * (oauth/start) met onze callback als return.
6 * GET /admin/patreon/callback -> license-server keert terug met ?klonkt_token
7 * (of ?klonkt_error). Verifieer + sla op.
8 * GET /admin/patreon/disconnect -> entitlement wissen.
9 *
10 * Het echte verdienmodel-slot zit in het ondertekende token (alleen de
11 * license-server kan tekenen). Zie PatreonService.js.
12 */
13
14import express from 'express';
15import { requireGod } from '../middleware/auth.js';
16import {
17 licenseBase, premiumEnabled, verifyEntitlementToken, storeEntitlement, clearEntitlement,
18} from '../services/PatreonService.js';
19
20const router = express.Router();
21router.use(requireGod);
22
23function baseUrl(req) {
24 return (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/$/, '');
25}
26
27router.get('/connect', (req, res) => {
28 if (!premiumEnabled()) return res.redirect('/admin/settings');
29 const ret = baseUrl(req) + '/admin/patreon/callback';
30 res.redirect(`${licenseBase()}/oauth/start?return=${encodeURIComponent(ret)}`);
31});
32
33router.get('/callback', async (req, res) => {
34 if (!premiumEnabled()) return res.redirect('/admin/settings');
35 const { klonkt_token, klonkt_error, klonkt_support_cents } = req.query;
36 if (klonkt_error) {
37 const cents = Number(klonkt_support_cents || 0);
38 const msg = klonkt_error === 'not_entitled'
39 ? `Patreon gekoppeld, maar nog geen $10 lifetime (nu $${(cents / 100).toFixed(2)}). Steun de campagne en koppel opnieuw.`
40 : 'Patreon-koppeling mislukt.';
41 return res.redirect('/admin/settings?error=' + encodeURIComponent(msg));
42 }
43 try {
44 const payload = await verifyEntitlementToken(String(klonkt_token || ''));
45 if (!payload.entitled) throw new Error('not entitled');
46 storeEntitlement(payload, String(klonkt_token));
47 res.redirect('/admin/settings?success=' + encodeURIComponent('Patreon gekoppeld — premium is actief.'));
48 } catch (e) {
49 console.error('[patreon/callback]', e.message);
50 res.redirect('/admin/settings?error=' + encodeURIComponent('Patreon-token kon niet geverifieerd worden.'));
51 }
52});
53
54router.get('/disconnect', (req, res) => {
55 clearEntitlement();
56 res.redirect('/admin/settings?success=' + encodeURIComponent('Patreon ontkoppeld.'));
57});
58
59export default router;
Note: See TracBrowser for help on using the repository browser.