Skip to content

Commit 2e5a785

Browse files
committed
feat: revamp to use AWS SES
1 parent 763f7d7 commit 2e5a785

File tree

6 files changed

+1019
-90
lines changed

6 files changed

+1019
-90
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,10 @@ ANALYZE=false
4949
# environment variables set to make api requests
5050
USE_MOCK_DATA=true
5151

52+
# AWS SES Configuration for Enterprise Contact Form
53+
# SES_ACCESS_KEY_ID=your_iam_access_key_id
54+
# SES_SECRET_ACCESS_KEY=your_iam_secret_access_key
55+
# SES_REGION=us-east-1
56+
5257
# Google Sheet ID for torch holders
5358
GOOGLE_SHEET_ID_TORCH_HOLDERS=

app/[locale]/enterprise/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import Visa from "@/components/icons/enterprise/visa.svg"
3636
import Walmart from "@/components/icons/enterprise/walmart.svg"
3737
import WFP from "@/components/icons/enterprise/wfp.svg"
3838
import MainArticle from "@/components/MainArticle"
39-
import NetlifyFormsDetection from "@/components/NetlifyFormsDetection"
4039
import Translation from "@/components/Translation"
4140
import { ButtonLink } from "@/components/ui/buttons/Button"
4241
import { Card } from "@/components/ui/card"
@@ -287,7 +286,6 @@ const Page = async ({ params }: { params: { locale: Lang } }) => {
287286

288287
return (
289288
<div className="mb-12 space-y-12 md:mb-20 md:space-y-20">
290-
<NetlifyFormsDetection />
291289
<HubHero
292290
header={t("page-enterprise-hero-title")}
293291
description={t("page-enterprise-hero-subtitle")}

app/api/enterprise-contact/route.ts

Lines changed: 89 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,44 @@
11
import { NextRequest, NextResponse } from "next/server"
2+
import { SendRawEmailCommand, SESClient } from "@aws-sdk/client-ses"
23

34
const ENTERPRISE_EMAIL = "[email protected]"
5+
const SES_FROM_EMAIL = "[email protected]"
46
const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute
57
const MAX_REQUESTS_PER_WINDOW = 3
68

7-
// Simple in-memory rate limiting (in production, use Redis or similar)
8-
const ipRequestHistory = new Map<string, number[]>()
9+
// Configure SES client
10+
const sesClient = new SESClient({
11+
region: process.env.SES_REGION || "us-east-1",
12+
credentials: {
13+
accessKeyId: process.env.SES_ACCESS_KEY_ID!,
14+
secretAccessKey: process.env.SES_SECRET_ACCESS_KEY!,
15+
},
16+
})
917

10-
function getClientIP(request: NextRequest): string {
11-
const forwarded = request.headers.get("x-forwarded-for")
12-
const realIP = request.headers.get("x-real-ip")
18+
// Log the region being used for debugging
19+
console.log("Using AWS SES region:", process.env.SES_REGION || "us-east-1")
1320

14-
if (forwarded) return forwarded.split(",")[0].trim()
15-
if (realIP) return realIP
16-
return "unknown"
17-
}
21+
// Simple in-memory rate limiting (in production, use Redis or similar)
22+
const requestHistory = new Map<string, number[]>()
1823

19-
function isRateLimited(ip: string): boolean {
24+
function isRateLimited(identifier: string): boolean {
2025
const now = Date.now()
21-
const requests = ipRequestHistory.get(ip) || []
26+
const requests = requestHistory.get(identifier) || []
2227

2328
// Filter out requests outside the current window
2429
const recentRequests = requests.filter(
2530
(timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS
2631
)
2732

2833
// Update the history
29-
ipRequestHistory.set(ip, recentRequests)
34+
requestHistory.set(identifier, recentRequests)
3035

3136
// Check if we're over the limit
3237
if (recentRequests.length >= MAX_REQUESTS_PER_WINDOW) return true
3338

3439
// Add this request
3540
recentRequests.push(now)
36-
ipRequestHistory.set(ip, recentRequests)
41+
requestHistory.set(identifier, recentRequests)
3742

3843
return false
3944
}
@@ -53,12 +58,72 @@ function validateEmail(email: string): boolean {
5358
return emailRegex.test(email)
5459
}
5560

61+
function createRawEmail(
62+
fromEmail: string,
63+
toEmail: string,
64+
replyToEmail: string,
65+
subject: string,
66+
textBody: string
67+
): string {
68+
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36)}`
69+
70+
return [
71+
`From: ${fromEmail}`,
72+
`To: ${toEmail}`,
73+
`Reply-To: ${replyToEmail}`,
74+
`Subject: ${subject}`,
75+
`MIME-Version: 1.0`,
76+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
77+
``,
78+
`--${boundary}`,
79+
`Content-Type: text/plain; charset=UTF-8`,
80+
`Content-Transfer-Encoding: 7bit`,
81+
``,
82+
textBody,
83+
``,
84+
`--${boundary}--`,
85+
].join("\r\n")
86+
}
87+
88+
async function sendEmail(userEmail: string, message: string): Promise<void> {
89+
const subject = "Enterprise Inquiry from ethereum.org"
90+
const textBody = `
91+
New enterprise inquiry received:
92+
93+
From: ${userEmail}
94+
Timestamp: ${new Date().toISOString()}
95+
96+
Message:
97+
${message}
98+
99+
---
100+
This message was sent via the enterprise contact form on ethereum.org/enterprise.
101+
Reply to this email to respond directly to the sender.
102+
`.trim()
103+
104+
const rawEmail = createRawEmail(
105+
SES_FROM_EMAIL,
106+
ENTERPRISE_EMAIL,
107+
userEmail,
108+
subject,
109+
textBody
110+
)
111+
112+
const command = new SendRawEmailCommand({
113+
RawMessage: {
114+
Data: new TextEncoder().encode(rawEmail),
115+
},
116+
})
117+
118+
await sesClient.send(command)
119+
}
120+
56121
export async function POST(request: NextRequest) {
57122
try {
58-
const clientIP = getClientIP(request)
123+
// Rate limiting based on a simple identifier (could be improved with real session tracking)
124+
const userAgent = request.headers.get("user-agent") || "unknown"
59125

60-
// Rate limiting
61-
if (isRateLimited(clientIP)) {
126+
if (isRateLimited(userAgent)) {
62127
return NextResponse.json(
63128
{ error: "Too many requests. Please try again later." },
64129
{ status: 429 }
@@ -88,56 +153,15 @@ export async function POST(request: NextRequest) {
88153
)
89154
}
90155

91-
// Create email content
92-
const emailSubject = "Enterprise Inquiry from ethereum.org"
93-
const emailBody = `
94-
New enterprise inquiry received:
95-
96-
From: ${sanitizedEmail}
97-
IP: ${clientIP}
98-
Timestamp: ${new Date().toISOString()}
99-
100-
Message:
101-
${sanitizedMessage}
102-
103-
---
104-
This message was sent via the enterprise contact form on ethereum.org/enterprise.
105-
`.trim()
106-
107-
// Submit to Netlify Forms
156+
// Send email via AWS SES
108157
try {
109-
const formData = new URLSearchParams()
110-
formData.append("form-name", "enterprise-contact")
111-
formData.append("email", sanitizedEmail)
112-
formData.append("message", sanitizedMessage)
113-
formData.append("subject", emailSubject)
114-
formData.append("ip", clientIP)
115-
formData.append("timestamp", new Date().toISOString())
116-
117-
const netlifyResponse = await fetch("/__forms.html", {
118-
method: "POST",
119-
headers: {
120-
"Content-Type": "application/x-www-form-urlencoded",
121-
},
122-
body: formData.toString(),
123-
})
124-
125-
if (!netlifyResponse.ok) {
126-
throw new Error(`Netlify Forms error: ${netlifyResponse.status}`)
127-
}
128-
} catch (netlifyError) {
129-
console.error("Netlify Forms submission failed:", netlifyError)
130-
// Log the submission details for manual follow-up
131-
console.log("Enterprise Contact Form Submission (Netlify failed):", {
132-
to: ENTERPRISE_EMAIL,
133-
subject: emailSubject,
134-
body: emailBody,
135-
from: sanitizedEmail,
136-
ip: clientIP,
137-
timestamp: new Date().toISOString(),
138-
})
139-
// Continue without throwing - we don't want to show user an error
140-
// if Netlify is down but the form validation passed
158+
await sendEmail(sanitizedEmail, sanitizedMessage)
159+
} catch (emailError) {
160+
console.error("AWS SES email sending failed:", emailError)
161+
return NextResponse.json(
162+
{ error: "Failed to send message. Please try again later." },
163+
{ status: 500 }
164+
)
141165
}
142166

143167
return NextResponse.json(

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"test:e2e:report": "playwright show-report tests/e2e/__report__"
3131
},
3232
"dependencies": {
33+
"@aws-sdk/client-ses": "^3.859.0",
3334
"@crowdin/crowdin-api-client": "^1.25.0",
3435
"@docsearch/react": "^3.5.2",
3536
"@hookform/resolvers": "^3.8.0",

0 commit comments

Comments
 (0)