Changeset ca507ef in Klonkt


Ignore:
Timestamp:
06/19/2026 08:29:28 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
7eb35ae
Parents:
977f65d
Message:

feat: SMTP configurable via Admin → Settings (app_settings, env fallback) + test email

  • config/mailer.js: reads SMTP from app_settings (smtp_host/port/user/pass/from), with env as fallback; transport rebuilds automatically on change (no restart needed); mailerStatus() for the UI (without leaking the password).
  • admin-settings: POST /smtp (save; password only updated when a new value is given; clear) + POST /smtp/test (test email). GET includes smtp status.
  • admin-settings.ejs: Email (SMTP) card with host/port/user/pass/from + save/ clear + test-email button (only shown when configured).

Once SMTP is filled in, newsletter sending, show-notify and double opt-in work.

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

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/mailer.js

    r977f65d rca507ef  
    1 // E-mail versturen (wachtwoord-reset). Optioneel: alleen actief als de self-hoster
    2 // SMTP heeft ingesteld. Niet ingesteld? Dan valt de reset terug op de CLI
    3 // (`npm run reset-admin`) — zie README.
     1// E-mail versturen (wachtwoord-reset, nieuwsbrief, notify). Optioneel: alleen actief
     2// als SMTP is ingesteld — via Beheer → Instellingen (app_settings) OF env-vars.
    43//
    5 // Config via env:
    6 //   SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS
    7 //   SMTP_FROM (afzender; default = SMTP_USER)
     4// Config-bron (in deze volgorde): app_settings (ingesteld in de UI), anders env:
     5//   SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS, SMTP_FROM (default = USER)
     6// Niet ingesteld → versturen valt terug op CLI (reset-admin) / wordt overgeslagen.
    87
    98import nodemailer from 'nodemailer';
     9import { getSetting } from '../services/SettingsService.js';
    1010
    11 const HOST = process.env.SMTP_HOST || '';
    12 const PORT = parseInt(process.env.SMTP_PORT || '587', 10);
    13 const USER = process.env.SMTP_USER || '';
    14 const PASS = process.env.SMTP_PASS || '';
    15 const FROM = process.env.SMTP_FROM || USER;
     11function cfg() {
     12  const host = getSetting('smtp_host', '') || process.env.SMTP_HOST || '';
     13  const port = parseInt(getSetting('smtp_port', '') || process.env.SMTP_PORT || '587', 10) || 587;
     14  const user = getSetting('smtp_user', '') || process.env.SMTP_USER || '';
     15  const pass = getSetting('smtp_pass', '') || process.env.SMTP_PASS || '';
     16  const from = getSetting('smtp_from', '') || process.env.SMTP_FROM || user;
     17  return { host, port, user, pass, from };
     18}
    1619
    1720export function mailerConfigured() {
    18   return !!(HOST && USER && PASS);
     21  const c = cfg();
     22  return !!(c.host && c.user && c.pass);
    1923}
    2024
    21 let _transport = null;
     25// Status voor de UI (zonder het wachtwoord te lekken).
     26export function mailerStatus() {
     27  const c = cfg();
     28  return {
     29    configured: mailerConfigured(),
     30    host: c.host,
     31    port: c.port,
     32    user: c.user,
     33    from: c.from,
     34    passSet: !!c.pass,
     35    // bron: handig om te tonen dat env nog actief is
     36    fromEnv: !getSetting('smtp_host', '') && !!process.env.SMTP_HOST,
     37  };
     38}
     39
     40// Transport cachen, maar herbouwen zodra de config wijzigt (UI-edit zonder herstart).
     41let _transport = null, _key = null;
    2242function transport() {
    23   if (!_transport) {
     43  const c = cfg();
     44  const key = [c.host, c.port, c.user, c.pass].join('|');
     45  if (!_transport || _key !== key) {
    2446    _transport = nodemailer.createTransport({
    25       host: HOST,
    26       port: PORT,
    27       secure: PORT === 465, // 465 = impliciete TLS; 587 = STARTTLS
    28       auth: { user: USER, pass: PASS },
     47      host: c.host,
     48      port: c.port,
     49      secure: c.port === 465, // 465 = impliciete TLS; 587 = STARTTLS
     50      auth: { user: c.user, pass: c.pass },
    2951    });
     52    _key = key;
    3053  }
    3154  return _transport;
     
    3457export async function sendMail({ to, subject, text, html }) {
    3558  if (!mailerConfigured()) throw new Error('SMTP niet geconfigureerd');
    36   return transport().sendMail({ from: FROM, to, subject, text, html });
     59  const c = cfg();
     60  return transport().sendMail({ from: c.from, to, subject, text, html });
    3761}
  • src/routes/admin-settings.js

    r977f65d rca507ef  
    2222import { requireGod } from '../middleware/auth.js';
    2323import { getTenancy, setTenancy, getSetting, setSetting } from '../services/SettingsService.js';
     24import { mailerStatus, sendMail } from '../config/mailer.js';
    2425import { entitlementStatus, premiumUnlocked } from '../services/PatreonService.js';
    2526import { googleConfigured, redirectUri, currentClientId, clientSecretSet } from '../config/google.js';
     
    8384      secretSet: clientSecretSet(),
    8485    },
     86    smtp: mailerStatus(),
    8587    success: req.query.success || null,
    8688    error: req.query.error || null,
     
    151153});
    152154
     155// ── SMTP / e-mail-instellingen ────────────────────────────────────
     156router.post('/smtp', requireGod, (req, res) => {
     157  const b = req.body || {};
     158  if (b.clear === '1') {
     159    ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_from'].forEach((k) => setSetting(k, ''));
     160    return res.redirect('/admin/settings?success=' + encodeURIComponent('SMTP-instellingen gewist'));
     161  }
     162  setSetting('smtp_host', (b.smtp_host || '').toString().trim());
     163  setSetting('smtp_port', (b.smtp_port || '').toString().trim());
     164  setSetting('smtp_user', (b.smtp_user || '').toString().trim());
     165  setSetting('smtp_from', (b.smtp_from || '').toString().trim());
     166  // Wachtwoord alleen overschrijven als er een nieuwe waarde is ingevoerd.
     167  const pass = (b.smtp_pass || '').toString();
     168  if (pass) setSetting('smtp_pass', pass);
     169  res.redirect('/admin/settings?success=' + encodeURIComponent('SMTP-instellingen opgeslagen'));
     170});
     171
     172// Testmail sturen naar een opgegeven adres (of de ingelogde gebruiker).
     173router.post('/smtp/test', requireGod, async (req, res) => {
     174  const to = ((req.body && req.body.to) || (req.session.user && req.session.user.email) || '').toString().trim();
     175  if (!to || to.indexOf('@') === -1) {
     176    return res.redirect('/admin/settings?error=' + encodeURIComponent('Geef een geldig test-e-mailadres op.'));
     177  }
     178  try {
     179    await sendMail({
     180      to,
     181      subject: 'Klonkt — SMTP-test',
     182      text: 'Gelukt! Je SMTP-instellingen werken. Dit is een testbericht van je Klonkt-site.',
     183      html: '<p>Gelukt! Je <strong>SMTP-instellingen werken</strong>. Dit is een testbericht van je Klonkt-site.</p>',
     184    });
     185    res.redirect('/admin/settings?success=' + encodeURIComponent('Testmail verstuurd naar ' + to));
     186  } catch (e) {
     187    res.redirect('/admin/settings?error=' + encodeURIComponent('Testmail mislukt: ' + (e.message || e)));
     188  }
     189});
     190
    153191export default router;
  • src/views/pages/admin-settings.ejs

    r977f65d rca507ef  
    110110    <% if (typeof premiumEnabled !== 'undefined' && premiumEnabled && !(premium && premium.premium)) { %>
    111111      <p class="set-help" style="margin-top:.7rem; color:var(--accent)">Premium-laag staat aan zonder Patreon — de Google-knop blijft verborgen tot premium ontgrendeld is.</p>
     112    <% } %>
     113  </section>
     114
     115  <%# E-mail / SMTP — nodig voor nieuwsbrief versturen, notify-mails en wachtwoord-reset. %>
     116  <section class="set-card" style="margin-top:1rem">
     117    <h2>E-mail (SMTP)</h2>
     118    <p class="set-help">Nodig om de <strong>nieuwsbrief te versturen</strong>, <strong>show-notify</strong>-mails te sturen en wachtwoord-reset per mail te laten werken. Vul de gegevens van je mailprovider in (bv. je hostingmail, Gmail-app-wachtwoord, Brevo, Mailgun…).
     119      <% if (smtp && smtp.configured) { %><br><strong>Status: ingesteld&nbsp;✓</strong><% if (smtp.fromEnv) { %> <small>(via .env)</small><% } %><% } else { %><br>Nog niet ingesteld — versturen werkt nog niet.<% } %></p>
     120
     121    <form method="post" action="/admin/settings/smtp" class="set-form">
     122      <label><span>SMTP-host</span>
     123        <input type="text" name="smtp_host" value="<%= smtp ? smtp.host : '' %>" placeholder="smtp.jouwprovider.nl" autocomplete="off">
     124      </label>
     125      <label><span>Poort <small style="font-weight:400">— 587 (STARTTLS) of 465 (SSL)</small></span>
     126        <input type="text" name="smtp_port" value="<%= (smtp && smtp.port) ? smtp.port : '' %>" placeholder="587" autocomplete="off" inputmode="numeric">
     127      </label>
     128      <label><span>Gebruikersnaam</span>
     129        <input type="text" name="smtp_user" value="<%= smtp ? smtp.user : '' %>" placeholder="jij@jouwprovider.nl" autocomplete="off">
     130      </label>
     131      <label><span>Wachtwoord <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— ingesteld; leeg laten = ongewijzigd</small><% } %></span>
     132        <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? '•••••••• (ingesteld)' : 'app-wachtwoord' %>" autocomplete="off">
     133      </label>
     134      <label><span>Afzender <small style="font-weight:400">— optioneel; standaard = gebruikersnaam</small></span>
     135        <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder="Jouw Naam &lt;jij@jouwprovider.nl&gt;" autocomplete="off">
     136      </label>
     137      <div class="set-actions">
     138        <button type="submit" class="btn btn-primary">SMTP opslaan</button>
     139        <% if (smtp && (smtp.host || smtp.passSet)) { %>
     140          <button type="submit" name="clear" value="1" class="btn" formnovalidate>Wissen</button>
     141        <% } %>
     142      </div>
     143    </form>
     144
     145    <% if (smtp && smtp.configured) { %>
     146      <form method="post" action="/admin/settings/smtp/test" class="set-form" style="margin-top:.8rem">
     147        <label><span>Testmail sturen naar</span>
     148          <input type="email" name="to" placeholder="jij@email.nl" autocomplete="off">
     149        </label>
     150        <div class="set-actions"><button type="submit" class="btn">✉ Testmail sturen</button></div>
     151      </form>
    112152    <% } %>
    113153  </section>
Note: See TracChangeset for help on using the changeset viewer.