'use strict'; /** * THE VYNDR REPORT — public subscribe endpoint (Session S7, a1 board). * * POST /api/newsletter/subscribe { email, website? } * * Forwards to the self-hosted Listmonk's subscriber API with double opt-in * (preconfirm_subscriptions: false → Listmonk sends the confirmation email; * nothing lands in the list until the visitor clicks it). * * Grace notes: * - Without LISTMONK_* env this is a calm HTTP 200 * { ok: false, reason: 'not configured' } — the UI renders * "signups open soon", never a scary error. * - `website` is a honeypot (same trick as /api/waitlist): bots fill the * hidden field, humans don't. Filled → silent { ok: true }. * - Listmonk 409 (already subscribed) is { ok: true } — idempotent, and the * response never reveals whether an address exists. */ const express = require('express'); const { createRateLimit } = require('../middleware/rateLimit'); const newsletterService = require('../services/newsletterService'); const router = express.Router(); // Public + writes upstream → tight throttle (10/min per IP). router.use(createRateLimit({ windowMs: 60_000, max: 10 })); const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; router.post('/subscribe', async (req, res) => { const body = (req.body && typeof req.body === 'object') ? req.body : {}; // Honeypot — silently accept and drop. if (body.website) return res.json({ ok: true }); const email = typeof body.email === 'string' ? body.email.trim() : ''; if (!email || email.length > 254 || !EMAIL_RE.test(email)) { return res.status(400).json({ ok: false, error: 'Enter a valid email.' }); } const result = await newsletterService.subscribe(email); if (result.ok) return res.json({ ok: true }); if (result.reason === 'not configured') { return res.json({ ok: false, reason: 'not configured' }); } // Upstream hiccup — log it, keep the visitor's response calm. console.error('[newsletter/subscribe] listmonk error:', result.reason); return res.json({ ok: false, reason: 'unavailable' }); }); module.exports = router;