| 1 | /**
|
|---|
| 2 | * Newsletter — public side (premium feature #1).
|
|---|
| 3 | *
|
|---|
| 4 | * GET /nieuwsbrief -> sign-up form (premium; 404 otherwise)
|
|---|
| 5 | * POST /nieuwsbrief -> subscribe (double opt-in if SMTP configured)
|
|---|
| 6 | * GET /nieuwsbrief/bevestigen/:token -> confirm opt-in
|
|---|
| 7 | * GET /nieuwsbrief/uitschrijven/:token -> unsubscribe (ALWAYS allowed)
|
|---|
| 8 | *
|
|---|
| 9 | * In hub mode this runs via /user/:slug/nieuwsbrief (resolveSite sets siteUrlBase).
|
|---|
| 10 | * Confirm/unsub links in the mail are absolute (PUBLIC_BASE_URL + siteUrlBase).
|
|---|
| 11 | */
|
|---|
| 12 |
|
|---|
| 13 | import express from 'express';
|
|---|
| 14 | import { renderPage } from '../middleware/render.js';
|
|---|
| 15 | import { premiumUnlocked } from '../services/PatreonService.js';
|
|---|
| 16 | import { mailerConfigured, sendMail } from '../config/mailer.js';
|
|---|
| 17 | import { addSubscriber, confirm, unsubscribe } from '../services/SubscriberService.js';
|
|---|
| 18 |
|
|---|
| 19 | const router = express.Router();
|
|---|
| 20 |
|
|---|
| 21 | function esc(s) {
|
|---|
| 22 | return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|---|
| 23 | }
|
|---|
| 24 | function fullUrl(req, siteUrlBase, p) {
|
|---|
| 25 | const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
|
|---|
| 26 | return base + (siteUrlBase || '') + p;
|
|---|
| 27 | }
|
|---|
| 28 | function show(req, res, state, extra = {}) {
|
|---|
| 29 | renderPage(req, res, 'pages/newsletter', {
|
|---|
| 30 | pageTitle: 'Nieuwsbrief' + (res.locals.site ? ' — ' + res.locals.site.title : ''),
|
|---|
| 31 | bodyClass: 'on-newsletter',
|
|---|
| 32 | nlState: state,
|
|---|
| 33 | ...extra,
|
|---|
| 34 | });
|
|---|
| 35 | }
|
|---|
| 36 |
|
|---|
| 37 | router.get('/nieuwsbrief', (req, res, next) => {
|
|---|
| 38 | if (!premiumUnlocked()) return next();
|
|---|
| 39 | if (!res.locals.site) return next();
|
|---|
| 40 | const fan = req.session && req.session.user;
|
|---|
| 41 | const prefill = (fan && fan.email && fan.email.includes('@')) ? fan.email : '';
|
|---|
| 42 | show(req, res, 'form', { nlPrefill: prefill });
|
|---|
| 43 | });
|
|---|
| 44 |
|
|---|
| 45 | router.post('/nieuwsbrief', async (req, res, next) => {
|
|---|
| 46 | if (!premiumUnlocked()) return next();
|
|---|
| 47 | const site = res.locals.site;
|
|---|
| 48 | if (!site) return next();
|
|---|
| 49 | const email = (req.body.email || '').trim();
|
|---|
| 50 | const doubleOptin = mailerConfigured();
|
|---|
| 51 | const r = addSubscriber(site.id, email, 'widget', { doubleOptin });
|
|---|
| 52 | if (!r.ok) return show(req, res, r.error === 'invalid_email' ? 'invalid' : 'error', { nlPrefill: email });
|
|---|
| 53 |
|
|---|
| 54 | if (r.status === 'pending') {
|
|---|
| 55 | // Double opt-in: send the confirmation email.
|
|---|
| 56 | const link = fullUrl(req, res.locals.siteUrlBase, '/nieuwsbrief/bevestigen/' + r.token);
|
|---|
| 57 | const unsub = fullUrl(req, res.locals.siteUrlBase, '/nieuwsbrief/uitschrijven/' + r.token);
|
|---|
| 58 | try {
|
|---|
| 59 | await sendMail({
|
|---|
| 60 | to: email,
|
|---|
| 61 | subject: 'Bevestig je inschrijving — ' + (site.title || 'nieuwsbrief'),
|
|---|
| 62 | text: 'Bevestig je inschrijving op de nieuwsbrief van ' + (site.title || '') + ':\n' + link +
|
|---|
| 63 | '\n\nNiet aangevraagd? Negeer deze mail. Uitschrijven: ' + unsub,
|
|---|
| 64 | html: '<p>Bevestig je inschrijving op de nieuwsbrief van <strong>' + esc(site.title) + '</strong>:</p>' +
|
|---|
| 65 | '<p><a href="' + link + '">Inschrijving bevestigen</a></p>' +
|
|---|
| 66 | '<p style="color:#888;font-size:12px">Niet aangevraagd? Negeer deze mail. ' +
|
|---|
| 67 | '<a href="' + unsub + '">Uitschrijven</a></p>',
|
|---|
| 68 | });
|
|---|
| 69 | } catch (e) {
|
|---|
| 70 | return show(req, res, 'smtperror');
|
|---|
| 71 | }
|
|---|
| 72 | return show(req, res, 'check', { nlEmail: email });
|
|---|
| 73 | }
|
|---|
| 74 | return show(req, res, 'done', { nlEmail: email });
|
|---|
| 75 | });
|
|---|
| 76 |
|
|---|
| 77 | router.get('/nieuwsbrief/bevestigen/:token', (req, res, next) => {
|
|---|
| 78 | if (!premiumUnlocked()) return next();
|
|---|
| 79 | const ok = confirm(req.params.token);
|
|---|
| 80 | show(req, res, ok ? 'confirmed' : 'badtoken');
|
|---|
| 81 | });
|
|---|
| 82 |
|
|---|
| 83 | // Unsubscribe is always allowed (even if the premium layer is later disabled): a
|
|---|
| 84 | // subscriber must always be able to opt out. Not premium-gated.
|
|---|
| 85 | router.get('/nieuwsbrief/uitschrijven/:token', (req, res) => {
|
|---|
| 86 | const ok = unsubscribe(req.params.token);
|
|---|
| 87 | show(req, res, ok ? 'unsubbed' : 'badtoken');
|
|---|
| 88 | });
|
|---|
| 89 |
|
|---|
| 90 | export default router;
|
|---|