Skip to content

Protect contact requests with Turnstile and Resend - #9

Open
frgfm wants to merge 5 commits into
mainfrom
codex/contact-form-turnstile
Open

frgfm wants to merge 5 commits into
mainfrom
codex/contact-form-turnstile

Conversation

@frgfm

@frgfm frgfm commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Replace the bypassable arithmetic CAPTCHA with server-verified Cloudflare Turnstile. GitHub Pages continues to host the website.

Form → Worker (validation + Turnstile verification) → Resend → inquiries@pyronear.org

Changes

  • Validate email, input limits and honeypot; enforce token hostname/action and 5 attempts/minute/IP per Cloudflare location. Secrets stay server-side.
  • Send from forms@pyronear.org to inquiries@pyronear.org; Reply-To is the visitor, subject starts with [Contact form], body contains only the message.
  • Use native HTML validation and Turnstile rendering. Preserve failed submissions and deduplicate unchanged retries. Browser JS: 63 lines (+1).
  • Keep all four languages aligned; setup instructions in README.md.

Validation

  • CI passed: 9 Node checks, Worker bundle check and Hugo build for all four languages.
  • Browser email validation checked; local email delivery verified inbox receipt, SPF/DKIM/DMARC and Reply-To.

Deployment follow-up

  • Configure the production Worker secrets, Turnstile widget and GitHub variables CONTACT_FORM_URL / TURNSTILE_SITE_KEY. Production deployment stops if those variables are missing.
  • Retest the complete form before cutover, then disable the old Apps Script endpoint. Cloudflare hosting migration is a separate phase.
image

@frgfm frgfm self-assigned this Sep 28, 2026
@frgfm
frgfm marked this pull request as ready for review September 30, 2026 16:11
@frgfm frgfm added the type: enhancement New feature or request label Sep 30, 2026

@MateoLostanlen MateoLostanlen left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @frgfm for the PR! Nice work overall, the Worker validates everything before sending and the tests are thorough. Two things to fix before merging:

1. The form only works on website.pyronear.org

ALLOWED_ORIGINS in worker/wrangler.toml and the Turnstile widget are both tied to website.pyronear.org, but hugo.toml already uses https://pyronear.org/. When the Pages domain moves, the Worker will reject every submission with origin_denied and visitors will only see the generic "Sending failed" error. The README also still says a domain change needs no code change.

Suggestion: add https://pyronear.org to ALLOWED_ORIGINS and to the widget hostnames now, and update the README note.

2. Line breaks count twice against the 5000 char limit

The textarea's maxlength="5000" counts line breaks as \n, but they are sent as \r\n. So a long message with several paragraphs passes in the browser and is then rejected by the Worker (message.length > 5000), with a misleading "Check your email address and message" error.

Suggestion: normalize before checking in worker/index.mjs:

const message = (form.get("message") || "").replace(/\r\n/g, "\n").trim();

@frgfm

frgfm commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

All addressed ✅

@frgfm
frgfm requested a review from MateoLostanlen October 1, 2026 14:29

@MateoLostanlen MateoLostanlen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @frgfm, ok for the PR ! Where are you on the setup (Turnstile widget, Worker secrets, Resend key, GitHub variables)? We should validate that before merging, right?

@fe51 fe51 mentioned this pull request Oct 19, 2026
5 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants