From df1efb488159cdfceaf110192a4900b628e29ff8 Mon Sep 17 00:00:00 2001 From: pkupuk Date: Sun, 1 Mar 2026 14:06:44 +0800 Subject: [PATCH] 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. --- apps/frontend/dev.vars | 7 ++ apps/frontend/src/pages/api/contact.ts | 82 ++++++++++++++++++++++++ apps/frontend/src/pages/contact-us.astro | 46 +++++++++++-- 3 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 apps/frontend/src/pages/api/contact.ts diff --git a/apps/frontend/dev.vars b/apps/frontend/dev.vars index 0dd31aa..e1e6c92 100644 --- a/apps/frontend/dev.vars +++ b/apps/frontend/dev.vars @@ -3,3 +3,10 @@ # Production Payload CMS URL (for SSR fetch) PAYLOAD_CMS_URL=https://enchun-admin.anlstudio.cc + +# Cloudflare Turnstile +CF_TURNSTILE_SITE_KEY=0x4AAAAAACkOUZK2u7Fo8IZ- +CF_TURNSTILE_SECRET_KEY=0x4AAAAAACkOUV9TDUOwVdcdLUakEVxJjww + +# n8n webhook +N8N_WEBHOOK_URL=https://n8n.anlstudio.cc/webhook/contact diff --git a/apps/frontend/src/pages/api/contact.ts b/apps/frontend/src/pages/api/contact.ts new file mode 100644 index 0000000..60d8fab --- /dev/null +++ b/apps/frontend/src/pages/api/contact.ts @@ -0,0 +1,82 @@ +import type { APIRoute } from "astro"; + +// Your Cloudflare Turnstile Secret Key +// In production, this should be set in environment variables +const TURNSTILE_SECRET_KEY = import.meta.env.CF_TURNSTILE_SECRET_KEY || import.meta.env.TURNSTILE_SECRET_KEY; +// The n8n webhook URL +const WEBHOOK_URL = import.meta.env.N8N_WEBHOOK_URL || "https://n8n.anlstudio.cc/webhook/contact"; + +export const POST: APIRoute = async ({ request }) => { + try { + const body = await request.json(); + const { name, phone, email, message, "cf-turnstile-response": token } = body; + + // Basic validation + if (!name || !phone || !email || !message) { + return new Response(JSON.stringify({ error: "Missing required fields" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + if (!token) { + return new Response(JSON.stringify({ error: "[Turnstile] No token provided" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + // Verify Turnstile token + if (TURNSTILE_SECRET_KEY) { + const turnstileVerify = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + secret: TURNSTILE_SECRET_KEY, + response: token, + }).toString(), + } + ); + + const turnstileResult = await turnstileVerify.json(); + + if (!turnstileResult.success) { + return new Response(JSON.stringify({ error: "[Turnstile] Token verification failed" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + } else { + console.warn("TURNSTILE_SECRET_KEY is not defined. Skipping verification."); + } + + // After successful verification, send data to n8n + const n8nResponse = await fetch(WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, phone, email, message }), + }); + + if (!n8nResponse.ok) { + throw new Error(`n8n webhook failed with status ${n8nResponse.status}`); + } + + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + + } catch (error) { + console.error("API proxy error:", error); + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } +}; diff --git a/apps/frontend/src/pages/contact-us.astro b/apps/frontend/src/pages/contact-us.astro index 3b7d00e..5028bd0 100644 --- a/apps/frontend/src/pages/contact-us.astro +++ b/apps/frontend/src/pages/contact-us.astro @@ -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 = --- +
+ +
+
+
+ +
+