From e39bb8951d38d04f8ca120e4520ecf73b3c53729 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 11:54:00 +0100 Subject: [PATCH] auto-claude: subtask-2-2 - Create API route for contact form submission --- src/app/api/contact/route.ts | 127 +++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/app/api/contact/route.ts diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts new file mode 100644 index 0000000..8840ab4 --- /dev/null +++ b/src/app/api/contact/route.ts @@ -0,0 +1,127 @@ +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; + + 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 } + ); + } +}