fix: migrate Web3Forms submission to frontend to bypass Vercel Cloudflare block, while keeping reCAPTCHA on backend
This commit is contained in:
1 parent
54932b9c72
commit
8b485b5103
2 files changed
+48
-56
No files matched your search
+36
-3
@@ -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.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user