source: Klonkt/src/routes/admin-newsletter.js@ abdcf33

main
Last change on this file since abdcf33 was 834bcc3, checked in by Robin Genis <roboburr@…>, 3 months ago

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 3.7 KB
RevLine 
[2e247e4]1/**
[834bcc3]2 * Newsletter — admin side (premium feature #1).
[2e247e4]3 *
[834bcc3]4 * GET /admin/newsletter -> compose + subscriber counts + history
5 * POST /admin/newsletter/send -> send to all CONFIRMED subscribers (SMTP)
[2e247e4]6 *
[834bcc3]7 * Premium-gated + site manager. Sending requires configured SMTP; without SMTP
8 * sign-ups are still collected (single opt-in), only sending is unavailable.
[2e247e4]9 */
10
11import express from 'express';
12import db from '../config/database.js';
13import { v4 as uuid } from 'uuid';
14import { renderPage } from '../middleware/render.js';
15import { requireSiteManager } from '../middleware/auth.js';
16import { premiumUnlocked } from '../services/PatreonService.js';
17import { mailerConfigured, sendMail } from '../config/mailer.js';
18import { confirmedFor, counts } from '../services/SubscriberService.js';
19
20const router = express.Router();
21
22function esc(s) {
23 return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
24}
25function fullUrl(req, p) {
26 const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
27 return base + (res_siteUrlBase(req)) + p;
28}
29function res_siteUrlBase(req) {
30 return req.res && req.res.locals ? (req.res.locals.siteUrlBase || '') : '';
31}
32
33function premiumGate(req, res, next) {
34 if (!premiumUnlocked()) {
35 return res.status(403).send('Nieuwsbrief is premium — koppel Patreon in Beheer → Instellingen.');
36 }
37 next();
38}
39
40function renderCompose(req, res, extra = {}) {
41 const site = res.locals.site;
42 const c = counts(site.id);
43 const history = db.prepare(
44 'SELECT subject, sent_at, recipient_count FROM newsletters WHERE site_id = ? ORDER BY sent_at DESC LIMIT 10'
45 ).all(site.id);
46 const subscribeUrl = fullUrl(req, '/nieuwsbrief');
47 renderPage(req, res, 'pages/admin-newsletter', {
48 pageTitle: 'Nieuwsbrief',
49 bodyClass: 'on-admin',
50 nlCounts: c,
51 nlHistory: history,
52 nlSubscribeUrl: subscribeUrl,
53 nlSmtp: mailerConfigured(),
54 ...extra,
55 });
56}
57
58router.get('/', requireSiteManager, premiumGate, (req, res) => {
59 if (!res.locals.site) return res.status(404).send('Geen site.');
60 renderCompose(req, res);
61});
62
63router.post('/send', requireSiteManager, premiumGate, async (req, res) => {
64 const site = res.locals.site;
65 if (!site) return res.status(404).send('Geen site.');
66 if (!mailerConfigured()) return renderCompose(req, res, { nlMsg: 'SMTP is niet ingesteld — versturen kan nog niet.', nlMsgKind: 'bad' });
67
68 const subject = (req.body.subject || '').trim();
69 const body = (req.body.body || '').trim();
70 if (!subject || !body) return renderCompose(req, res, { nlMsg: 'Onderwerp en bericht zijn verplicht.', nlMsgKind: 'bad', nlSubject: subject, nlBody: body });
71
72 const subs = confirmedFor(site.id);
73 const bodyHtml = esc(body).replace(/\n/g, '<br>');
74 let sent = 0;
75 for (const s of subs) {
76 const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
77 try {
78 await sendMail({
79 to: s.email,
80 subject,
81 text: body + '\n\n—\nUitschrijven: ' + unsub,
82 html: '<div>' + bodyHtml + '</div>' +
83 '<hr style="margin-top:24px;border:none;border-top:1px solid #ddd">' +
84 '<p style="color:#888;font-size:12px">Je ontvangt dit omdat je je aanmeldde voor de nieuwsbrief van ' +
85 esc(site.title) + '. <a href="' + unsub + '">Uitschrijven</a>.</p>',
86 });
87 sent++;
[834bcc3]88 } catch (e) { /* skip this recipient, continue */ }
[2e247e4]89 }
90 db.prepare('INSERT INTO newsletters (id, site_id, subject, body, recipient_count) VALUES (?,?,?,?,?)')
91 .run(uuid(), site.id, subject, body, sent);
92
93 renderCompose(req, res, { nlMsg: 'Verstuurd naar ' + sent + ' van ' + subs.length + ' abonnee(s).', nlMsgKind: 'ok' });
94});
95
96export default router;
Note: See TracBrowser for help on using the repository browser.