From b7eef9d8e7275cb5ca8de0b8841ef7b90ae283df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Tam=C3=A1s?= Date: Tue, 28 Jan 2025 13:46:24 +0100 Subject: [PATCH] Added next js api route based newsletter implementation --- src/app/api/newsletter/route.js | 138 ++++++++++++++++++++++++ src/components/Newsletter/Newsletter.js | 9 +- 2 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 src/app/api/newsletter/route.js diff --git a/src/app/api/newsletter/route.js b/src/app/api/newsletter/route.js new file mode 100644 index 00000000..b8a3d24c --- /dev/null +++ b/src/app/api/newsletter/route.js @@ -0,0 +1,138 @@ +import { NextResponse } from "next/server"; +import mailchimp from "@mailchimp/mailchimp_marketing"; + +// Validate required environment variables +const requiredEnvVars = ["MAILCHIMP_API_KEY", "MAILCHIMP_LIST_ID", "MAILCHIMP_SERVER_PREFIX"]; +for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + console.error(`Missing required environment variable: ${envVar}`); + throw new Error(`Missing required environment variable: ${envVar}`); + } +} + +// Configure Mailchimp +mailchimp.setConfig({ + apiKey: process.env.MAILCHIMP_API_KEY, + server: process.env.MAILCHIMP_SERVER_PREFIX, +}); + +function isValidEmail(email) { + // More comprehensive email validation + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return ( + typeof email === "string" && + email.length <= 320 && // Max email length + email.length >= 3 && // Min reasonable length + emailRegex.test(email) + ); +} + +async function verifyRecaptcha(token) { + try { + const response = await fetch("https://www.google.com/recaptcha/api/siteverify", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`, + }); + + const data = await response.json(); + return data.success; + } catch (error) { + console.error("reCAPTCHA verification failed:", error); + return false; + } +} + +async function subscribeToMailchimp(email) { + try { + // Check if member exists + try { + const subscriberHash = mailchimp.helpers.getMemberHash(email.toLowerCase()); + const response = await mailchimp.lists.getListMember(process.env.MAILCHIMP_LIST_ID, subscriberHash); + + if (response.status === "subscribed") { + return { + success: false, + status: 400, + error: "Already subscribed", + }; + } + } catch (error) { + // Member not found - continue with subscription + if (error.status !== 404) { + throw error; + } + } + + // Subscribe the member + const response = await mailchimp.lists.addListMember(process.env.MAILCHIMP_LIST_ID, { + email_address: email, + status: "subscribed", + }); + + return { + success: true, + status: 200, + data: { + id: response.id, + email: response.email_address, + status: response.status, + }, + }; + } catch (error) { + console.error("Mailchimp API Error:", error); + + if (error.status === 400) { + return { + success: false, + status: 400, + error: error.response?.body?.detail || "Invalid email format", + }; + } + + if (error.status === 401) { + console.error("Mailchimp authentication failed"); + return { + success: false, + status: 500, // Return 500 to hide API issues from client + error: "Internal server error", + }; + } + + if (error.status === 429) { + return { + success: false, + status: 429, + error: "Too many requests, please try again later", + }; + } + + throw error; // Let the main handler catch other errors + } +} + +export async function POST(request) { + try { + const { email, token } = await request.json(); + + // Validate reCAPTCHA first + if (!token || !(await verifyRecaptcha(token))) { + return NextResponse.json({ success: false, error: "Invalid reCAPTCHA" }, { status: 400 }); + } + + if (!email || !isValidEmail(email)) { + return NextResponse.json({ success: false, error: "Invalid email format" }, { status: 400 }); + } + + // Subscribe to Mailchimp + const result = await subscribeToMailchimp(email); + + return NextResponse.json({ success: result.success, error: result.error }, { status: result.status }); + } catch (error) { + console.error("API Error:", error); + return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/components/Newsletter/Newsletter.js b/src/components/Newsletter/Newsletter.js index 507f23f2..622a4d9c 100644 --- a/src/components/Newsletter/Newsletter.js +++ b/src/components/Newsletter/Newsletter.js @@ -52,14 +52,15 @@ const Newsletter = () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); - const response = await fetch("https://eff999e9-celestia-newsletter-worker.infra-admin-749.workers.dev/", { + const response = await fetch("/api/newsletter", { method: "POST", headers: { "Content-Type": "application/json", - Accept: "application/json", }, - body: JSON.stringify({ email }), - mode: "cors", + body: JSON.stringify({ + email, + token, + }), signal: controller.signal, });