← back to Govarbitrage
src/app/api/newsletter/subscribe/route.ts
63 lines
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@/lib/db";
import { rateLimit, clientIp, tooManyRequests } from "@/lib/rate-limit";
import {
buildConfirmEmail,
newToken,
normalizeEmail,
sendNewsletterEmail,
} from "@/lib/newsletter";
export const dynamic = "force-dynamic";
// `website` is a honeypot: hidden in the form, humans leave it empty, naive
// bots fill it. Filled honeypot → pretend success, do nothing.
const Schema = z.object({
email: z.string().email().max(254),
website: z.string().optional(),
});
// Same body whether the email was new, already pending, or already confirmed —
// never reveals whether an address exists in the list.
const GENERIC_OK = {
ok: true,
message: "Check your inbox — if this address isn't already confirmed, we've sent a confirmation link.",
};
export async function POST(req: NextRequest) {
const ip = clientIp(req);
const ipLimit = rateLimit(`newsletter:ip:${ip}`, 8, 10 * 60_000);
if (!ipLimit.allowed) return tooManyRequests(ipLimit);
const parsed = Schema.safeParse(await req.json().catch(() => ({})));
if (!parsed.success) {
return NextResponse.json({ error: "A valid email address is required." }, { status: 400 });
}
if (parsed.data.website) return NextResponse.json(GENERIC_OK);
const email = normalizeEmail(parsed.data.email);
const emailLimit = rateLimit(`newsletter:email:${email}`, 3, 15 * 60_000);
if (!emailLimit.allowed) return tooManyRequests(emailLimit);
const existing = await prisma.subscriber.findUnique({ where: { email } });
if (existing?.status === "CONFIRMED") {
// Already on the list — idempotent no-op, identical response.
return NextResponse.json(GENERIC_OK);
}
const confirmToken = newToken();
const unsubscribeToken = newToken();
await prisma.subscriber.upsert({
where: { email },
create: { email, status: "PENDING", confirmToken, unsubscribeToken },
update: { status: "PENDING", confirmToken, unsubscribeToken, unsubscribedAt: null },
});
const baseUrl = process.env.NEWSLETTER_BASE_URL || req.nextUrl.origin;
await sendNewsletterEmail(buildConfirmEmail(email, baseUrl, confirmToken));
return NextResponse.json(GENERIC_OK);
}