Skip to content

Commit 91ecdd7

Browse files
committed
feat: add netlify form integration
1 parent 0ea5775 commit 91ecdd7

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed

app/[locale]/enterprise/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ 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"
3940
import Translation from "@/components/Translation"
4041
import { ButtonLink } from "@/components/ui/buttons/Button"
4142
import { Card } from "@/components/ui/card"
@@ -284,6 +285,7 @@ const Page = async ({ params }: { params: { locale: Lang } }) => {
284285

285286
return (
286287
<div className="mb-12 space-y-12 md:mb-20 md:space-y-20">
288+
<NetlifyFormsDetection />
287289
<HubHero
288290
header={t("page-enterprise-hero-title")}
289291
description={t("page-enterprise-hero-subtitle")}

app/api/enterprise-contact/route.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
3+
import { CONTACT_FORM_CHAR_MIN } from "../../[locale]/enterprise/constants"
4+
5+
const ENTERPRISE_EMAIL = "[email protected]"
6+
const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute
7+
const MAX_REQUESTS_PER_WINDOW = 3
8+
9+
// Simple in-memory rate limiting (in production, use Redis or similar)
10+
const ipRequestHistory = new Map<string, number[]>()
11+
12+
function getClientIP(request: NextRequest): string {
13+
const forwarded = request.headers.get("x-forwarded-for")
14+
const realIP = request.headers.get("x-real-ip")
15+
16+
if (forwarded) return forwarded.split(",")[0].trim()
17+
if (realIP) return realIP
18+
return "unknown"
19+
}
20+
21+
function isRateLimited(ip: string): boolean {
22+
const now = Date.now()
23+
const requests = ipRequestHistory.get(ip) || []
24+
25+
// Filter out requests outside the current window
26+
const recentRequests = requests.filter(
27+
(timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS
28+
)
29+
30+
// Update the history
31+
ipRequestHistory.set(ip, recentRequests)
32+
33+
// Check if we're over the limit
34+
if (recentRequests.length >= MAX_REQUESTS_PER_WINDOW) return true
35+
36+
// Add this request
37+
recentRequests.push(now)
38+
ipRequestHistory.set(ip, recentRequests)
39+
40+
return false
41+
}
42+
43+
function sanitizeInput(input: string): string {
44+
return input
45+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
46+
.replace(/javascript:/gi, "")
47+
.replace(/on\w+\s*=/gi, "")
48+
.replace(/&lt;script/gi, "")
49+
.replace(/&lt;\/script/gi, "")
50+
.trim()
51+
}
52+
53+
function validateEmail(email: string): boolean {
54+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
55+
return emailRegex.test(email)
56+
}
57+
58+
export async function POST(request: NextRequest) {
59+
try {
60+
const clientIP = getClientIP(request)
61+
62+
// Rate limiting
63+
if (isRateLimited(clientIP)) {
64+
return NextResponse.json(
65+
{ error: "Too many requests. Please try again later." },
66+
{ status: 429 }
67+
)
68+
}
69+
70+
const body = await request.json()
71+
const { email, message } = body
72+
73+
// Validate input
74+
if (!email || !message) {
75+
return NextResponse.json(
76+
{ error: "Email and message are required" },
77+
{ status: 400 }
78+
)
79+
}
80+
81+
// Sanitize inputs
82+
const sanitizedEmail = sanitizeInput(email)
83+
const sanitizedMessage = sanitizeInput(message)
84+
85+
// Validate email format
86+
if (!validateEmail(sanitizedEmail)) {
87+
return NextResponse.json(
88+
{ error: "Invalid email format" },
89+
{ status: 400 }
90+
)
91+
}
92+
93+
// Validate message length
94+
if (sanitizedMessage.length < CONTACT_FORM_CHAR_MIN) {
95+
return NextResponse.json(
96+
{
97+
error: `Message must be at least ${CONTACT_FORM_CHAR_MIN} characters`,
98+
},
99+
{ status: 400 }
100+
)
101+
}
102+
103+
// Create email content
104+
const emailSubject = "Enterprise Inquiry from ethereum.org"
105+
const emailBody = `
106+
New enterprise inquiry received:
107+
108+
From: ${sanitizedEmail}
109+
IP: ${clientIP}
110+
Timestamp: ${new Date().toISOString()}
111+
112+
Message:
113+
${sanitizedMessage}
114+
115+
---
116+
This message was sent via the enterprise contact form on ethereum.org/enterprise.
117+
`.trim()
118+
119+
// Submit to Netlify Forms
120+
try {
121+
const formData = new URLSearchParams()
122+
formData.append("form-name", "enterprise-contact")
123+
formData.append("email", sanitizedEmail)
124+
formData.append("message", sanitizedMessage)
125+
formData.append("subject", emailSubject)
126+
formData.append("ip", clientIP)
127+
formData.append("timestamp", new Date().toISOString())
128+
129+
const netlifyResponse = await fetch("/__forms.html", {
130+
method: "POST",
131+
headers: {
132+
"Content-Type": "application/x-www-form-urlencoded",
133+
},
134+
body: formData.toString(),
135+
})
136+
137+
if (!netlifyResponse.ok) {
138+
throw new Error(`Netlify Forms error: ${netlifyResponse.status}`)
139+
}
140+
} catch (netlifyError) {
141+
console.error("Netlify Forms submission failed:", netlifyError)
142+
// Log the submission details for manual follow-up
143+
console.log("Enterprise Contact Form Submission (Netlify failed):", {
144+
to: ENTERPRISE_EMAIL,
145+
subject: emailSubject,
146+
body: emailBody,
147+
from: sanitizedEmail,
148+
ip: clientIP,
149+
timestamp: new Date().toISOString(),
150+
})
151+
// Continue without throwing - we don't want to show user an error
152+
// if Netlify is down but the form validation passed
153+
}
154+
155+
return NextResponse.json(
156+
{ message: "Message sent successfully" },
157+
{ status: 200 }
158+
)
159+
} catch (error) {
160+
console.error("Enterprise contact form error:", error)
161+
return NextResponse.json(
162+
{ error: "Internal server error" },
163+
{ status: 500 }
164+
)
165+
}
166+
}
167+
168+
// Only allow POST requests
169+
export async function GET() {
170+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 })
171+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Hidden form for Netlify Forms detection during build time.
3+
* This form is never visible to users and is only used so Netlify
4+
* can detect and register the enterprise contact form during deployment.
5+
*/
6+
export default function NetlifyFormsDetection() {
7+
return (
8+
<form
9+
name="enterprise-contact"
10+
data-netlify="true"
11+
data-netlify-honeypot="bot-field"
12+
hidden
13+
aria-hidden="true"
14+
>
15+
{/* Honeypot field for spam protection */}
16+
<input type="text" name="bot-field" />
17+
18+
{/* Form fields must match the React form exactly */}
19+
<input type="email" name="email" />
20+
<textarea name="message"></textarea>
21+
</form>
22+
)
23+
}

0 commit comments

Comments
 (0)