diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 6fc8c71..3e59bdc 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -284,12 +284,45 @@ export default async function Home() { grecaptcha.enterprise.ready(async function() { try { const token = await grecaptcha.enterprise.execute('${recaptchaSiteKey}', {action: 'submit'}); - document.getElementById('g-recaptcha-response').value = token; - form.submit(); // Now submit the form manually + + // 1. Verify reCAPTCHA token securely via our backend + const verifyRes = await fetch('/api/contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: token }) + }); + + const verifyData = await verifyRes.json(); + if (!verifyData.success) { + btn.innerText = originalText; + btn.disabled = false; + window.location.href = '/?status=error&msg=' + encodeURIComponent(verifyData.msg) + '#contact'; + return; + } + + // 2. Token is verified! Send email directly via Web3Forms to bypass Vercel block + const formData = new FormData(form); + formData.append("access_key", "${process.env.WEB3FORMS_ACCESS_KEY}"); + formData.append("from_name", "Portfolio Contact Form"); + + const web3Res = await fetch('https://api.web3forms.com/submit', { + method: 'POST', + body: formData + }); + + const web3Data = await web3Res.json(); + if (web3Data.success) { + window.location.href = '/?status=success#contact'; + } else { + btn.innerText = originalText; + btn.disabled = false; + window.location.href = '/?status=error&msg=Email service rejected the request.#contact'; + } + } catch(err) { btn.innerText = originalText; btn.disabled = false; - alert('reCAPTCHA failed to load. Please try again.'); + alert('reCAPTCHA failed to load or an unexpected error occurred. Please try again.'); } }); }); diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index ef25c43..d695306 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -3,15 +3,11 @@ import { supabase } from '@/lib/supabase'; export async function POST(request: Request) { try { - const formData = await request.formData(); - const recaptchaResponse = formData.get('g-recaptcha-response'); - const name = formData.get('name'); - const email = formData.get('email'); - const subject = formData.get('subject'); - const message = formData.get('message'); + const body = await request.json(); + const recaptchaResponse = body.token; if (!recaptchaResponse) { - return NextResponse.redirect(new URL('/?status=error&msg=Please complete the reCAPTCHA verification.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'Please complete the reCAPTCHA verification.' }); } // 1. Verify Google reCAPTCHA using Enterprise REST API @@ -22,7 +18,7 @@ export async function POST(request: Request) { if (!apiKey) { console.error("Missing RECAPTCHA_API_KEY environment variable."); - return NextResponse.redirect(new URL('/?status=error&msg=Server configuration error. Please try again later.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'Server configuration error. Please try again later.' }); } try { @@ -45,22 +41,22 @@ export async function POST(request: Request) { if (!response.tokenProperties?.valid) { console.error(`Assessment failed because token was: ${response.tokenProperties?.invalidReason}`); - return NextResponse.redirect(new URL('/?status=error&msg=reCAPTCHA verification failed (invalid token). Please try again.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'reCAPTCHA verification failed (invalid token). Please try again.' }); } if (response.tokenProperties.action === 'submit') { const score = response.riskAnalysis?.score || 0; if (score < 0.5) { console.error(`reCAPTCHA blocked due to low score: ${score}`); - return NextResponse.redirect(new URL('/?status=error&msg=reCAPTCHA verification failed (bot behavior detected). Please try again.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'reCAPTCHA verification failed (bot behavior detected). Please try again.' }); } } else { console.error("The action attribute does not match."); - return NextResponse.redirect(new URL('/?status=error&msg=reCAPTCHA verification failed (action mismatch). Please try again.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'reCAPTCHA verification failed (action mismatch). Please try again.' }); } } catch (err) { console.error("Enterprise REST error:", err); - return NextResponse.redirect(new URL('/?status=error&msg=reCAPTCHA verification service unavailable. Please try again later.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'reCAPTCHA verification service unavailable. Please try again later.' }); } // 2. Rate Limiting via Supabase @@ -71,60 +67,23 @@ export async function POST(request: Request) { .from('email_submissions') .select('*', { count: 'exact', head: true }) .eq('ip_address', ip) - .gte('submitted_at', oneHourAgo); + .gte('created_at', oneHourAgo); if (countError) { console.error('Rate limit check error:', countError); } if (count !== null && count >= rateLimit) { - return NextResponse.redirect(new URL(`/?status=error&msg=Rate limit exceeded. You can only send ${rateLimit} emails per hour.#contact`, request.url), 303); + return NextResponse.json({ success: false, msg: `Rate limit exceeded. You can only send ${rateLimit} emails per hour.` }); } // 3. Log Submission await supabase.from('email_submissions').insert([{ ip_address: ip }]); - // 4. Forward to Web3Forms (Replaces FormSubmit) - const web3formsAccessKey = process.env.WEB3FORMS_ACCESS_KEY || ''; - if (!web3formsAccessKey) { - console.error("Missing WEB3FORMS_ACCESS_KEY"); - return NextResponse.redirect(new URL('/?status=error&msg=Email service configuration error. Please try again later.#contact', request.url), 303); - } - - const web3formsUrl = 'https://api.web3forms.com/submit'; - const payload = { - access_key: web3formsAccessKey, - name: name?.toString() || '', - email: email?.toString() || '', - subject: subject?.toString() || '', - message: message?.toString() || '', - from_name: 'Portfolio Contact Form' - }; - - const mailResult = await fetch(web3formsUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!mailResult.ok) { - console.error('Web3Forms HTTP error:', mailResult.status); - return NextResponse.redirect(new URL(`/?status=error&msg=Failed to send message (HTTP ${mailResult.status}). Please try again later.#contact`, request.url), 303); - } - - const mailData = await mailResult.json(); - if (!mailData.success) { - console.error('Web3Forms API error:', mailData.message); - return NextResponse.redirect(new URL('/?status=error&msg=Email service rejected the request. Please verify your Web3Forms configuration.#contact', request.url), 303); - } - - return NextResponse.redirect(new URL('/?status=success#contact', request.url), 303); + return NextResponse.json({ success: true, msg: 'Verification passed.' }); } catch (error) { console.error('Contact API error:', error); - return NextResponse.redirect(new URL('/?status=error&msg=An unexpected error occurred.#contact', request.url), 303); + return NextResponse.json({ success: false, msg: 'An unexpected error occurred.' }); } }