feat(contact): implement Turnstile protection via API proxy

- Add `pages/api/contact.ts` to proxy n8n webhook and verify Turnstile tokens.
- Update `contact-us.astro` form to include Turnstile widget and validation logic.
- Replace hardcoded sitekey with `PUBLIC_TURNSTILE_SITE_KEY` from environment variables.
- Update `dev.vars` to include Cloudflare Turnstile keys.
This commit is contained in:
2026-03-01 14:06:44 +08:00
parent 84b5a498e6
commit df1efb4881
3 changed files with 131 additions and 4 deletions

View File

@@ -5,6 +5,7 @@
* Includes form validation, submission handling, and responsive layout
*/
import Layout from "../layouts/Layout.astro";
import HeaderBg from "../components/HeaderBg.astro";
// Metadata for SEO
const title = "聯絡我們 | 恩群數位行銷";
@@ -13,6 +14,7 @@ const description =
---
<Layout title={title} description={description}>
<HeaderBg />
<section class="py-20 bg-[#f4f5f7] scroll-mt-20 lg:py-10" id="contact">
<div
class="grid grid-cols-2 gap-16 items-start max-w-3xl mx-auto px-5 lg:grid-cols-2 lg:gap-10"
@@ -144,6 +146,20 @@ const description =
id="Message-error"></span>
</div>
<!-- Turnstile Widget -->
<div class="flex flex-col mt-2">
<div
class="cf-turnstile"
data-sitekey={import.meta.env.PUBLIC_TURNSTILE_SITE_KEY ||
"0x4AAAAAACkOUZK2u7Fo8IZ-"}
data-theme="light"
>
</div>
<span
class="error-message text-[#dc3545] text-sm mt-1 hidden"
id="turnstile-error"></span>
</div>
<!-- Submit Button -->
<div class="flex justify-end mt-2">
<button
@@ -163,6 +179,8 @@ const description =
</section>
</Layout>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer
></script>
<script>
// Form validation and submission handler
function initContactForm() {
@@ -172,6 +190,9 @@ const description =
) as HTMLButtonElement;
const successMsg = document.getElementById("form-success") as HTMLElement;
const errorMsg = document.getElementById("form-error") as HTMLElement;
const turnstileError = document.getElementById(
"turnstile-error",
) as HTMLElement;
if (!form) return;
@@ -281,10 +302,28 @@ const description =
}
});
// Collect form data to check turnstile token
const formData = new FormData(form);
const turnstileToken = formData.get("cf-turnstile-response");
// Validate Turnstile
if (!turnstileToken) {
isFormValid = false;
if (turnstileError) {
turnstileError.textContent = "請完成驗證";
turnstileError.style.display = "block";
}
} else {
if (turnstileError) {
turnstileError.textContent = "";
turnstileError.style.display = "none";
}
}
if (!isFormValid) {
// Scroll to first error
const firstError = form.querySelector(
".input_field.error",
".input_field.error, #turnstile-error[style*='display: block']",
) as HTMLElement;
if (firstError) {
firstError.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -305,17 +344,16 @@ const description =
successMsg.style.display = "none";
errorMsg.style.display = "none";
// Collect form data
const formData = new FormData(form);
const data = {
name: formData.get("Name"),
phone: formData.get("Phone"),
email: formData.get("Email"),
message: formData.get("Message"),
"cf-turnstile-response": turnstileToken,
};
try {
// Submit to backend (via API proxy)
// Submit to API proxy
const response = await fetch("/api/contact", {
method: "POST",
headers: {