fix: migrate Web3Forms submission to frontend to bypass Vercel Cloudflare block, while keeping reCAPTCHA on backend

This commit is contained in:
ssbg04 committed 2026-06-06 21:39:56 +08:00
1 parent 54932b9c72
commit 8b485b5103
2 files changed
+48 -56

No files matched your search

+36 -3
View File
@@ -284,12 +284,45 @@ export default async function Home() {
grecaptcha.enterprise.ready(async function() { grecaptcha.enterprise.ready(async function() {
try { try {
const token = await grecaptcha.enterprise.execute('${recaptchaSiteKey}', {action: 'submit'}); 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) { } catch(err) {
btn.innerText = originalText; btn.innerText = originalText;
btn.disabled = false; btn.disabled = false;
alert('reCAPTCHA failed to load. Please try again.'); alert('reCAPTCHA failed to load or an unexpected error occurred. Please try again.');
} }
}); });
}); });
+12 -53
View File
@@ -3,15 +3,11 @@ import { supabase } from '@/lib/supabase';
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const formData = await request.formData(); const body = await request.json();
const recaptchaResponse = formData.get('g-recaptcha-response'); const recaptchaResponse = body.token;
const name = formData.get('name');
const email = formData.get('email');
const subject = formData.get('subject');
const message = formData.get('message');
if (!recaptchaResponse) { 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 // 1. Verify Google reCAPTCHA using Enterprise REST API
@@ -22,7 +18,7 @@ export async function POST(request: Request) {
if (!apiKey) { if (!apiKey) {
console.error("Missing RECAPTCHA_API_KEY environment variable."); 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 { try {
@@ -45,22 +41,22 @@ export async function POST(request: Request) {
if (!response.tokenProperties?.valid) { if (!response.tokenProperties?.valid) {
console.error(`Assessment failed because token was: ${response.tokenProperties?.invalidReason}`); 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') { if (response.tokenProperties.action === 'submit') {
const score = response.riskAnalysis?.score || 0; const score = response.riskAnalysis?.score || 0;
if (score < 0.5) { if (score < 0.5) {
console.error(`reCAPTCHA blocked due to low score: ${score}`); 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 { } else {
console.error("The action attribute does not match."); 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) { } catch (err) {
console.error("Enterprise REST error:", 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 // 2. Rate Limiting via Supabase
@@ -71,60 +67,23 @@ export async function POST(request: Request) {
.from('email_submissions') .from('email_submissions')
.select('*', { count: 'exact', head: true }) .select('*', { count: 'exact', head: true })
.eq('ip_address', ip) .eq('ip_address', ip)
.gte('submitted_at', oneHourAgo); .gte('created_at', oneHourAgo);
if (countError) { if (countError) {
console.error('Rate limit check error:', countError); console.error('Rate limit check error:', countError);
} }
if (count !== null && count >= rateLimit) { 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 // 3. Log Submission
await supabase.from('email_submissions').insert([{ ip_address: ip }]); await supabase.from('email_submissions').insert([{ ip_address: ip }]);
// 4. Forward to Web3Forms (Replaces FormSubmit) return NextResponse.json({ success: true, msg: 'Verification passed.' });
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);
} catch (error) { } catch (error) {
console.error('Contact API error:', 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.' });
} }
} }