Skip to content

Commit

Permalink
Merge pull request #66 from celestiaorg/feature/api-route-based-imple…
Browse files Browse the repository at this point in the history
…mentation-of-newsletter

Added next js api route based newsletter implementation
  • Loading branch information
sysrex authored Jan 29, 2025
2 parents 80bf0ff + b7eef9d commit f17049e
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 4 deletions.
138 changes: 138 additions & 0 deletions src/app/api/newsletter/route.js
Original file line number Diff line number Diff line change
@@ -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 });
}
}
9 changes: 5 additions & 4 deletions src/components/Newsletter/Newsletter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down

0 comments on commit f17049e

Please sign in to comment.