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

main
Last change on this file since 3487567 was 3487567, checked in by Robin Genis <roboburr@…>, 2 months ago

fix(i18n): translate admin page titles (pageTitleKey instead of hardcoded strings)

Admin page/tab titles were hardcoded (mostly Dutch: Beheer, Instellingen, Nieuwsbrief, …).
renderPage now accepts pageTitleKey (+ pageTitleVars) and translates it with the resolved
language; the 16 admin routes pass keys. Adds admin.t_* keys in nl/en/de.

  • middleware/render.js — pageTitleKey support
  • services/i18n.js — admin.t_* title keys (nl/en/de)
  • routes/admin*.js — pageTitle string -> pageTitleKey
  • Property mode set to 100644
File size: 3.7 KB
Line 
1/**
2 * Newsletter — admin side (premium feature #1).
3 *
4 * GET /admin/newsletter -> compose + subscriber counts + history
5 * POST /admin/newsletter/send -> send to all CONFIRMED subscribers (SMTP)
6 *
7 * Premium-gated + site manager. Sending requires configured SMTP; without SMTP
8 * sign-ups are still collected (single opt-in), only sending is unavailable.
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 pageTitleKey: 'admin.t_newsletter',
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++;
88 } catch (e) { /* skip this recipient, continue */ }
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.