78 lines
2.5 KiB
TypeScript
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 };
|