Created
March 13, 2026 19:55
-
-
Save Nooshu/f4655e77defa1cfa875c376e2f85de24 to your computer and use it in GitHub Desktop.
This is the Cloudflare Turnstile version of the contact.js file that sits in the functions/api directory.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { escapeHtml } from '../../_helpers/escape-html.js'; | |
| export const onRequestPost = async ({ request, env }) => { | |
| try { | |
| // Accept standard form submissions | |
| const contentType = request.headers.get('content-type') || ''; | |
| if (!contentType.includes('application/x-www-form-urlencoded') && !contentType.includes('multipart/form-data')) { | |
| return new Response(JSON.stringify({ error: 'Unsupported content type' }), { | |
| status: 415, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const form = await request.formData(); | |
| // Honeypot - reject if set | |
| if (form.get('_akjhaskjdkjhakjshdadjknjnkdsa')) { | |
| const hpUrl = new URL('/', request.url); | |
| return Response.redirect(hpUrl.toString(), 303); | |
| } | |
| const name = (form.get('name') || '').toString().trim(); | |
| const email = (form.get('email') || '').toString().trim(); | |
| const message = (form.get('message') || '').toString().trim(); | |
| const redirectUrl = (form.get('_redirect') || '/contact/thanks/').toString(); | |
| if (!name || !email || !message) { | |
| return new Response(JSON.stringify({ error: 'Missing required fields' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Basic validation | |
| if (name.length > 200 || email.length > 320 || message.length > 5000) { | |
| return new Response(JSON.stringify({ error: 'Invalid field lengths' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Verify Cloudflare Turnstile token - REQUIRED for all submissions | |
| const turnstileToken = form.get('cf-turnstile-response'); | |
| if (!turnstileToken) { | |
| return new Response(JSON.stringify({ error: 'Turnstile token is required' }), { | |
| status: 400, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const secret = env.TURNSTILE_SECRET_KEY; | |
| if (!secret) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'Server not configured for CAPTCHA (missing TURNSTILE_SECRET_KEY)', | |
| }), | |
| { status: 500, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| // Include remote IP for additional verification (best practice) | |
| const remoteIp = request.headers.get('CF-Connecting-IP') || request.cf?.clientAddress || null; | |
| const verifyBody = { | |
| secret, | |
| response: turnstileToken.toString(), | |
| }; | |
| if (remoteIp) { | |
| verifyBody.remoteip = remoteIp; | |
| } | |
| const verifyResp = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/json' }, | |
| body: JSON.stringify(verifyBody), | |
| }); | |
| if (!verifyResp.ok) { | |
| return new Response(JSON.stringify({ error: 'Failed to verify Turnstile' }), { | |
| status: 502, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| const verifyJson = await verifyResp.json(); | |
| // Check if verification was successful | |
| if (!verifyJson.success) { | |
| const errorCodes = verifyJson['error-codes'] || []; | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'CAPTCHA verification failed', | |
| details: errorCodes.length > 0 ? errorCodes.join(', ') : 'Unknown error', | |
| }), | |
| { status: 400, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| // Optional: Verify hostname matches (helps prevent token reuse from other domains) | |
| const requestHost = new URL(request.url).hostname; | |
| if ( | |
| verifyJson.hostname && | |
| verifyJson.hostname !== requestHost && | |
| !requestHost.endsWith(`.${verifyJson.hostname}`) | |
| ) { | |
| console.warn(`Turnstile hostname mismatch: expected ${requestHost}, got ${verifyJson.hostname}`); | |
| } | |
| // Prepare email via Resend | |
| const fromEmail = 'website@nooshu.com'; | |
| const subject = form.get('_email.subject')?.toString() || 'New Message from Nooshu.com'; | |
| const textBody = `New contact form submission on nooshu.com\n\nName: ${name}\nEmail: ${email}\n\nMessage:\n${message}`; | |
| const htmlBody = `<h2>New contact form submission on nooshu.com</h2><p><strong>Name:</strong> ${escapeHtml(name)}<br/><strong>Email:</strong> ${escapeHtml(email)}</p><p><strong>Message:</strong></p><pre style="white-space:pre-wrap">${escapeHtml(message)}</pre>`; | |
| const apiKey = env.RESEND_API_KEY; | |
| if (!apiKey) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: 'Server not configured for email (missing RESEND_API_KEY)', | |
| }), | |
| { status: 500, headers: { 'content-type': 'application/json' } } | |
| ); | |
| } | |
| const resendResp = await fetch('https://api.resend.com/emails', { | |
| method: 'POST', | |
| headers: { | |
| 'content-type': 'application/json', | |
| Authorization: `Bearer ${apiKey}`, | |
| }, | |
| body: JSON.stringify({ | |
| from: `Nooshu Contact <${fromEmail}>`, | |
| to: ['test@example.com'], | |
| reply_to: email, | |
| subject, | |
| text: textBody, | |
| html: htmlBody, | |
| }), | |
| }); | |
| if (!resendResp.ok) { | |
| const text = await resendResp.text(); | |
| return new Response(JSON.stringify({ error: 'Email send failed', details: text }), { | |
| status: 502, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| // Redirect to thank you page (absolute URL required) | |
| const finalRedirect = new URL(redirectUrl, request.url); | |
| return Response.redirect(finalRedirect.toString(), 303); | |
| } catch (err) { | |
| return new Response(JSON.stringify({ error: 'Server error', details: String(err) }), { | |
| status: 500, | |
| headers: { 'content-type': 'application/json' }, | |
| }); | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment