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

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

fix(i18n): translate the premium-gate 403 messages (newsletter/agenda)

Two hardcoded Dutch res.send(403) messages now use a generic aset.premium_gate key
(with {feature}, reusing the admin.t_* title keys), resolved to the request language.

  • services/i18n.js — aset.premium_gate (nl/en/de)
  • routes/admin-newsletter.js, admin-shows.js — translate the 403
  • Property mode set to 100644
File size: 3.9 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';
19import { t, resolveLang } from '../services/i18n.js';
20import { getSetting } from '../services/SettingsService.js';
21
22const router = express.Router();
23
24function esc(s) {
25 return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
26}
27function fullUrl(req, p) {
28 const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
29 return base + (res_siteUrlBase(req)) + p;
30}
31function res_siteUrlBase(req) {
32 return req.res && req.res.locals ? (req.res.locals.siteUrlBase || '') : '';
33}
34
35function premiumGate(req, res, next) {
36 if (!premiumUnlocked()) {
37 const lang = resolveLang(req, { defaultLang: getSetting('default_lang') });
38 return res.status(403).send(t(lang, 'aset.premium_gate', { feature: t(lang, 'admin.t_newsletter') }));
39 }
40 next();
41}
42
43function renderCompose(req, res, extra = {}) {
44 const site = res.locals.site;
45 const c = counts(site.id);
46 const history = db.prepare(
47 'SELECT subject, sent_at, recipient_count FROM newsletters WHERE site_id = ? ORDER BY sent_at DESC LIMIT 10'
48 ).all(site.id);
49 const subscribeUrl = fullUrl(req, '/nieuwsbrief');
50 renderPage(req, res, 'pages/admin-newsletter', {
51 pageTitleKey: 'admin.t_newsletter',
52 bodyClass: 'on-admin',
53 nlCounts: c,
54 nlHistory: history,
55 nlSubscribeUrl: subscribeUrl,
56 nlSmtp: mailerConfigured(),
57 ...extra,
58 });
59}
60
61router.get('/', requireSiteManager, premiumGate, (req, res) => {
62 if (!res.locals.site) return res.status(404).send('Geen site.');
63 renderCompose(req, res);
64});
65
66router.post('/send', requireSiteManager, premiumGate, async (req, res) => {
67 const site = res.locals.site;
68 if (!site) return res.status(404).send('Geen site.');
69 if (!mailerConfigured()) return renderCompose(req, res, { nlMsg: 'SMTP is niet ingesteld — versturen kan nog niet.', nlMsgKind: 'bad' });
70
71 const subject = (req.body.subject || '').trim();
72 const body = (req.body.body || '').trim();
73 if (!subject || !body) return renderCompose(req, res, { nlMsg: 'Onderwerp en bericht zijn verplicht.', nlMsgKind: 'bad', nlSubject: subject, nlBody: body });
74
75 const subs = confirmedFor(site.id);
76 const bodyHtml = esc(body).replace(/\n/g, '<br>');
77 let sent = 0;
78 for (const s of subs) {
79 const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
80 try {
81 await sendMail({
82 to: s.email,
83 subject,
84 text: body + '\n\n—\nUitschrijven: ' + unsub,
85 html: '<div>' + bodyHtml + '</div>' +
86 '<hr style="margin-top:24px;border:none;border-top:1px solid #ddd">' +
87 '<p style="color:#888;font-size:12px">Je ontvangt dit omdat je je aanmeldde voor de nieuwsbrief van ' +
88 esc(site.title) + '. <a href="' + unsub + '">Uitschrijven</a>.</p>',
89 });
90 sent++;
91 } catch (e) { /* skip this recipient, continue */ }
92 }
93 db.prepare('INSERT INTO newsletters (id, site_id, subject, body, recipient_count) VALUES (?,?,?,?,?)')
94 .run(uuid(), site.id, subject, body, sent);
95
96 renderCompose(req, res, { nlMsg: 'Verstuurd naar ' + sent + ' van ' + subs.length + ' abonnee(s).', nlMsgKind: 'ok' });
97});
98
99export default router;
Note: See TracBrowser for help on using the repository browser.