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:
43
app/templates/emails/contact_notification.html
Normal file
43
app/templates/emails/contact_notification.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{#
|
||||
HTML body for the admin contact-form notification email.
|
||||
|
||||
Deliberately plain — email clients are hostile to CSS. ``message`` is
|
||||
rendered inside a <pre> so newlines submitted by the visitor are
|
||||
preserved without us having to manually convert them to <br>s (which
|
||||
bleach would strip anyway).
|
||||
|
||||
Jinja2 autoescape is on for .html templates; every field below is
|
||||
rendered via ``{{ ... }}`` without ``| safe``, so user input cannot
|
||||
escape into the DOM.
|
||||
|
||||
Context:
|
||||
- submission_name : str
|
||||
- submission_email : str
|
||||
- message : str
|
||||
- submitted_at : ISO-8601 str
|
||||
- ip : str
|
||||
#}<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>New contact submission — Chicken Babies R Us</title>
|
||||
</head>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #2B3A42; max-width: 560px; margin: 0 auto; padding: 24px;">
|
||||
<h1 style="font-size: 20px; margin: 0 0 16px;">New contact submission</h1>
|
||||
<p style="margin: 0 0 8px;"><strong>Name:</strong> {{ submission_name }}</p>
|
||||
<p style="margin: 0 0 8px;">
|
||||
<strong>Email:</strong>
|
||||
<a href="mailto:{{ submission_email }}" style="color: #5D8AA8;">{{ submission_email }}</a>
|
||||
</p>
|
||||
<p style="margin: 0 0 8px;"><strong>Submitted at:</strong> {{ submitted_at }}</p>
|
||||
<p style="margin: 0 0 16px;"><strong>IP:</strong> {{ ip }}</p>
|
||||
|
||||
<h2 style="font-size: 16px; margin: 16px 0 8px;">Message</h2>
|
||||
<pre style="white-space: pre-wrap; word-break: break-word; font-family: inherit; font-size: 14px; background: #FAF3E7; padding: 12px 16px; border-radius: 6px; margin: 0;">{{ message }}</pre>
|
||||
|
||||
<p style="color: #6b7a80; font-size: 13px; margin-top: 24px;">
|
||||
Replying to this email will respond directly to the sender
|
||||
({{ submission_email }}).
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
17
app/templates/emails/contact_notification.txt
Normal file
17
app/templates/emails/contact_notification.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
{# Plaintext body for the admin contact-form notification email.
|
||||
Mirrors the HTML version so clients that reject HTML still get a
|
||||
readable message. #}New contact submission
|
||||
======================
|
||||
|
||||
Name: {{ submission_name }}
|
||||
Email: {{ submission_email }}
|
||||
Submitted at: {{ submitted_at }}
|
||||
IP: {{ ip }}
|
||||
|
||||
Message
|
||||
-------
|
||||
{{ message }}
|
||||
|
||||
--
|
||||
Replying to this email will respond directly to the sender
|
||||
({{ submission_email }}).
|
||||
Reference in New Issue
Block a user