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:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user