import { Router, Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import rateLimit from 'express-rate-limit'; import RedisStore from 'rate-limit-redis'; import { redis } from '../../config/redis'; import { env } from '../../config/env'; import { listmonkClient } from '../../services/listmonk.client'; import { logger } from '../../utils/logger'; const router = Router(); const subscribeRateLimit = rateLimit({ windowMs: 60 * 1000, max: 5, standardHeaders: true, legacyHeaders: false, store: new RedisStore({ sendCommand: (command: string, ...args: string[]) => redis.call(command, ...args) as Promise, prefix: 'rl:newsletter-subscribe:', }), message: { error: { message: 'Too many subscribe attempts, please try again later', code: 'NEWSLETTER_RATE_LIMIT_EXCEEDED' } }, }); const subscribeSchema = z.object({ email: z.string().email(), name: z.string().optional(), }); const PUBLIC_LIST_NAME = 'Public Updates'; // POST /api/newsletter/subscribe router.post('/subscribe', subscribeRateLimit, async (req: Request, res: Response, next: NextFunction) => { try { // Check if Listmonk is enabled if (env.LISTMONK_SYNC_ENABLED !== 'true') { res.status(503).json({ error: { message: 'Newsletter subscriptions are not currently available' } }); return; } const body = subscribeSchema.parse(req.body); // Find or create the "Public Updates" list let lists = await listmonkClient.getLists(); let publicList = lists.find(l => l.name === PUBLIC_LIST_NAME); if (!publicList) { publicList = await listmonkClient.createList(PUBLIC_LIST_NAME, 'public', ['public', 'auto']); } // Create or update subscriber (double opt-in via Listmonk's optin flow) try { await listmonkClient.createSubscriber( body.email, body.name || '', [publicList.id], { source: 'public_signup' }, ); } catch (err: any) { // If subscriber already exists, that's fine if (err?.message?.includes('already exists') || err?.statusCode === 409) { // subscriber already exists, success } else { throw err; } } res.json({ success: true, message: 'Check your email to confirm your subscription' }); } catch (err) { if (err instanceof z.ZodError) { res.status(400).json({ error: { message: 'Please provide a valid email address' } }); return; } logger.error('Newsletter subscribe error:', err); next(err); } }); export { router as newsletterPublicRouter };