feat: phase 5 contact form — hCaptcha, honeypot, rate limit, notify

Working /contact POST flow: honeypot → hCaptcha server-verify →
field validation → SlowAPI 3/hr IP rate limit → contact_submissions
row → best-effort Resend notification (Reply-To = submitter) →
generic success page. Spam paths don't persist and render the same
success page (anti-enumeration). Send failures don't break the
request path — the row is already durable.

New services: HCaptchaService (async httpx + dev fallback),
ContactService. EmailService gains send_contact_notification.
Production config validator now requires ADMIN_CONTACT_EMAIL,
HCAPTCHA_SECRET, HCAPTCHA_SITE_KEY. 23 new tests, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 06:47:06 -05:00
parent 67c848f329
commit d9090f5055
16 changed files with 1671 additions and 39 deletions

View File

@@ -183,11 +183,22 @@ class Settings(BaseSettings):
missing.append("RESEND_FROM")
if not self.admin_emails or not self.admin_emails_list:
missing.append("ADMIN_EMAILS")
# Phase 5: the public contact form needs a destination inbox
# and a real hCaptcha key pair. Missing any of these would
# either drop submissions on the floor (no recipient) or open
# the form to unverified bot traffic (no captcha), so we
# enforce them at boot.
if not self.admin_contact_email:
missing.append("ADMIN_CONTACT_EMAIL")
if not self.hcaptcha_secret:
missing.append("HCAPTCHA_SECRET")
if not self.hcaptcha_site_key:
missing.append("HCAPTCHA_SITE_KEY")
if missing:
raise ValueError(
"Production configuration is missing required values: "
+ ", ".join(missing)
+ ". These are needed for magic-link admin auth."
+ ". These are needed for admin auth and the contact form."
)
return self