| 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 | */
|
|---|
| 11 | import express from 'express';
|
|---|
| 12 | import { renderPage } from '../middleware/render.js';
|
|---|
| 13 | import { requireGod } from '../middleware/auth.js';
|
|---|
| 14 | import { premiumUnlocked } from '../services/PatreonService.js';
|
|---|
| 15 | import { cryptoBoxReady } from '../services/CryptoBox.js';
|
|---|
| 16 | import PaidPatreon from '../services/PaidPatreonService.js';
|
|---|
| 17 |
|
|---|
| 18 | const router = express.Router();
|
|---|
| 19 |
|
|---|
| 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.
|
|---|
| 23 | const redirectUri = (req) =>
|
|---|
| 24 | (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '') + '/paid/callback';
|
|---|
| 25 |
|
|---|
| 26 | function 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 |
|
|---|
| 35 | router.get('/', requireGod, (req, res) => {
|
|---|
| 36 | if (!gate(req, res)) return;
|
|---|
| 37 | renderPage(req, res, 'pages/admin-paid', {
|
|---|
| 38 | pageTitleKey: 'apaid.t',
|
|---|
| 39 | bodyClass: 'on-admin',
|
|---|
| 40 | status: PaidPatreon.ownerStatus(res.locals.site.id),
|
|---|
| 41 | secretReady: cryptoBoxReady(),
|
|---|
| 42 | redirectUri: redirectUri(req),
|
|---|
| 43 | saved: req.query.saved === '1',
|
|---|
| 44 | error: req.query.error || null,
|
|---|
| 45 | });
|
|---|
| 46 | });
|
|---|
| 47 |
|
|---|
| 48 | router.post('/', requireGod, (req, res) => {
|
|---|
| 49 | if (!gate(req, res)) return;
|
|---|
| 50 | if (!cryptoBoxReady()) return res.redirect('/admin/paid?error=' + encodeURIComponent('De encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?); secrets kunnen niet veilig 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 | // Empty clears it (null), a value sets it. Unlike secrets, this is not
|
|---|
| 63 | // sensitive and there's a clear "remove the link" intent.
|
|---|
| 64 | patreonUrl: (b.patreon_url || '').trim() || null,
|
|---|
| 65 | defaultMinCents: Number.isFinite(cents) ? cents : undefined,
|
|---|
| 66 | });
|
|---|
| 67 | return res.redirect('/admin/paid?saved=1');
|
|---|
| 68 | } catch (e) {
|
|---|
| 69 | return res.redirect('/admin/paid?error=' + encodeURIComponent(e.message || 'Opslaan mislukt'));
|
|---|
| 70 | }
|
|---|
| 71 | });
|
|---|
| 72 |
|
|---|
| 73 | router.post('/disconnect', requireGod, (req, res) => {
|
|---|
| 74 | if (!gate(req, res)) return;
|
|---|
| 75 | PaidPatreon.disconnect(res.locals.site.id);
|
|---|
| 76 | res.redirect('/admin/paid?saved=1');
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | export default router;
|
|---|