diff --git a/src/app/api/newsletter/route.js b/src/app/api/newsletter/route.js deleted file mode 100644 index dddb71eb..00000000 --- a/src/app/api/newsletter/route.js +++ /dev/null @@ -1,155 +0,0 @@ -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", "RECAPTCHA_SECRET_KEY"]; -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) { - if (!process.env.RECAPTCHA_SECRET_KEY) { - console.error("Missing RECAPTCHA_SECRET_KEY"); - return false; - } - - 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(); - - if (!data.success) { - console.error("reCAPTCHA verification failed:", data["error-codes"]); - } - - 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", - }; - } - - // Detailed logging for unexpected errors - console.error("Unexpected Mailchimp error:", { - message: error.message, - stack: error.stack, - response: error.response, - }); - - 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/app/api/subscribe/route.js b/src/app/api/subscribe/route.js index 15475805..7017c15f 100644 --- a/src/app/api/subscribe/route.js +++ b/src/app/api/subscribe/route.js @@ -1,10 +1,45 @@ import { NextResponse } from "next/server"; +// Add validation check for required env vars +const requiredEnvVars = ["MAILCHIMP_API_KEY", "MAILCHIMP_LIST_ID", "MAILCHIMP_SERVER_PREFIX", "RECAPTCHA_SECRET_KEY"]; +for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + console.error(`Missing required environment variable: ${envVar}`); + throw new Error(`Missing required environment variable: ${envVar}`); + } +} + +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(); + if (!data.success) { + console.error("reCAPTCHA verification failed:", data["error-codes"]); + } + return data.success; + } catch (error) { + console.error("reCAPTCHA verification failed:", error); + return false; + } +} + export async function POST(req) { try { - const { email } = await req.json(); + const { email, token } = await req.json(); + + // Verify reCAPTCHA first + if (!token || !(await verifyRecaptcha(token))) { + return NextResponse.json({ error: "Invalid reCAPTCHA" }, { status: 400 }); + } - if (!email || !email.includes("@")) { + if (!isValidEmail(email)) { return NextResponse.json({ error: "Invalid email address" }, { status: 400 }); } @@ -38,17 +73,34 @@ export async function POST(req) { } } + if (response.status === 429) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }); + } + const data = await response.json(); if (!response.ok) { + console.error("Mailchimp API Error:", data); return NextResponse.json({ error: data.detail || "Failed to subscribe" }, { status: response.status }); } return NextResponse.json({ success: true }); } catch (error) { + console.error("API Error:", error); if (error.name === "TimeoutError") { return NextResponse.json({ error: "Request timed out" }, { status: 408 }); } return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } } + +function isValidEmail(email) { + 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) + ); +} diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index 101e37fb..bf22de57 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -8,8 +8,7 @@ import Link from "@/macros/Link/Link"; import Icon from "@/macros/Icons/Icon"; import ArrowLongSVG from "@/macros/SVGs/ArrowLongSVG"; import { isInternalLink } from "@/utils/isInternalLink"; -// import Newsletter from "@/components/Newsletter/Newsletter"; -import NewsletterSimple from "@/components/NewsletterSimple/NewsletterSimple"; +import Newsletter from "@/components/Newsletter/Newsletter"; const Footer = () => { const columns = footerData(); @@ -31,8 +30,7 @@ const Footer = () => { Build whatever with Celestia underneath - {/* */} - +
{columns.map((column, index) => { diff --git a/src/components/Newsletter/Newsletter.js b/src/components/Newsletter/Newsletter.js index 4834a1bb..a3df8e86 100644 --- a/src/components/Newsletter/Newsletter.js +++ b/src/components/Newsletter/Newsletter.js @@ -33,9 +33,6 @@ const Newsletter = () => { const handleSubmit = async (e) => { e.preventDefault(); - - console.log("Submitting with token:", token); - if (isSubmitting) return; if (!email) { setStatus("Error"); @@ -54,15 +51,12 @@ const Newsletter = () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); - const response = await fetch("/api/newsletter", { + const response = await fetch("/api/subscribe", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - email, - token, - }), + body: JSON.stringify({ email, token }), signal: controller.signal, }); @@ -137,7 +131,6 @@ const Newsletter = () => { type={"submit"} disabled={isSubmitting} onClick={(e) => { - console.log("Button clicked"); // Debug log if (!isSubmitting) { handleSubmit(e); } @@ -146,11 +139,13 @@ const Newsletter = () => { {isSubmitting ? "Subscribing..." : "Subscribe"} + {siteKey && ( )} + {captchaError && ( diff --git a/src/components/NewsletterSimple/NewsletterSimple.js b/src/components/NewsletterSimple/NewsletterSimple.js deleted file mode 100644 index bbb30944..00000000 --- a/src/components/NewsletterSimple/NewsletterSimple.js +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState } from "react"; -import PrimaryButton from "@/macros/Buttons/PrimaryButton"; -import { Row } from "@/macros/Grids"; -import { Body } from "@/macros/Copy"; - -const NewsletterSimple = () => { - const [email, setEmail] = useState(""); - const [status, setStatus] = useState(null); - const [msg, setMsg] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isFocused, setIsFocused] = useState(false); - - const handleChange = (e) => { - setEmail(e.target.value); - setStatus(null); - setMsg(""); - }; - - const handleFocus = () => setIsFocused(true); - const handleBlur = () => setIsFocused(false); - - const handleSubmit = async (e) => { - e.preventDefault(); - if (isSubmitting) return; - if (!email) { - setStatus("Error"); - setMsg("Please enter your email address."); - return; - } - - try { - setIsSubmitting(true); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - - const response = await fetch("/api/subscribe", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email }), - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - const data = await response.json(); - - if (response.status === 429) { - setStatus("Error"); - setMsg("Too many requests. Please try again later."); - return; - } - - if (data.success) { - setStatus("Success"); - setMsg("Thank you for subscribing!"); - setEmail(""); - } else { - setStatus("Error"); - setMsg( - data.error === "Already subscribed" - ? "You're already subscribed to our newsletter!" - : data.error || "An error occurred. Please try again." - ); - } - } catch (error) { - setStatus("Error"); - const errorMessage = - error.name === "AbortError" ? "Request timed out. Please try again." : "Unable to subscribe at this time. Please try again later."; - setMsg(errorMessage); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
- -
- - -
- - - {isSubmitting ? "Subscribing..." : "Subscribe"} - -
- - {status === "Error" && ( - - - {msg} - - - )} - {status === "Success" && ( - - - {msg} - - - )} -
-
- ); -}; - -export default NewsletterSimple;