128 lines
3.4 KiB
TypeScript
128 lines
3.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { supabaseRateLimiter } from '@/utils/rateLimitSupabase';
|
|
|
|
interface ContactFormData {
|
|
name: string;
|
|
email: string;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Extract client IP address from Next.js request headers
|
|
* Handles Vercel's forwarding headers and fallback scenarios
|
|
*/
|
|
function getClientIp(request: NextRequest): string {
|
|
// Check Vercel's forwarded IP header first
|
|
const forwardedFor = request.headers.get('x-forwarded-for');
|
|
if (forwardedFor) {
|
|
// x-forwarded-for may contain multiple IPs, take the first one
|
|
return forwardedFor.split(',')[0].trim();
|
|
}
|
|
|
|
// Check for real IP header
|
|
const realIp = request.headers.get('x-real-ip');
|
|
if (realIp) {
|
|
return realIp;
|
|
}
|
|
|
|
// Fallback to a default identifier for development
|
|
return 'unknown-ip';
|
|
}
|
|
|
|
/**
|
|
* Validate contact form data
|
|
*/
|
|
function validateFormData(data: unknown): data is ContactFormData {
|
|
if (!data || typeof data !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const formData = data as Record<string, unknown>;
|
|
|
|
return (
|
|
typeof formData.name === 'string' &&
|
|
formData.name.trim().length > 0 &&
|
|
typeof formData.email === 'string' &&
|
|
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
|
|
typeof formData.message === 'string' &&
|
|
formData.message.trim().length > 0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* POST /api/contact
|
|
* Handle contact form submissions with rate limiting
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// Extract client IP for rate limiting
|
|
const clientIp = getClientIp(request);
|
|
|
|
// Check rate limit
|
|
const isRateLimited = await supabaseRateLimiter.isRateLimited(clientIp);
|
|
|
|
if (isRateLimited) {
|
|
const timeToReset = await supabaseRateLimiter.getTimeToReset(clientIp);
|
|
const retryAfterSeconds = Math.ceil(timeToReset / 1000);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Too many requests. Please try again later.',
|
|
retryAfter: retryAfterSeconds,
|
|
},
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
'X-RateLimit-Remaining': '0',
|
|
'Retry-After': retryAfterSeconds.toString(),
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// Parse and validate request body
|
|
const body = await request.json();
|
|
|
|
if (!validateFormData(body)) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid form data. Please check all fields.' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const formData = body as ContactFormData;
|
|
|
|
// Get remaining attempts for response headers
|
|
const remainingAttempts = await supabaseRateLimiter.getRemainingAttempts(clientIp);
|
|
|
|
// TODO: In a real implementation, you would:
|
|
// 1. Send email via a service like SendGrid, Resend, or AWS SES
|
|
// 2. Store the message in a database
|
|
// 3. Send a confirmation email to the user
|
|
// For now, we'll just simulate success
|
|
|
|
// Simulate processing delay
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
message: 'Your message has been received. We will get back to you soon!',
|
|
},
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
'X-RateLimit-Remaining': remainingAttempts.toString(),
|
|
},
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error('Error processing contact form:', error);
|
|
|
|
return NextResponse.json(
|
|
{ error: 'An unexpected error occurred. Please try again later.' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|