changemaker.lite/api/src/modules/newsletter/newsletter-public.routes.ts

78 lines
2.5 KiB
TypeScript

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<any>,
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 };