From 15e4fd556c90f81442da143f200ffc3c4076ffd5 Mon Sep 17 00:00:00 2001 From: nomi-nonsz Date: Thu, 1 Jan 2026 17:46:33 +0700 Subject: [PATCH] Add cloudflare turnstile --- src/app/api/contact/route.ts | 46 ++++++++++++++++++++-------- src/app/globals.css | 1 + src/components/form/contact-form.tsx | 11 ++++++- src/components/form/form.tsx | 2 +- src/lib/server-utils.ts | 23 ++++++-------- tailwind.config.ts | 1 + 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index 7c4f101..3ab2617 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { rateLimited, sendEmail } from "@/lib/server-utils"; +import { rateLimited, sendEmail, validateTurnstile } from "@/lib/server-utils"; import { trimTooLong } from "@/lib/strings"; import validator from "validator"; @@ -14,31 +14,24 @@ const validateInput = (data: any) => { typeof data.name !== "string" || typeof data.email !== "string" ) || - typeof data.message !== "string" + typeof data.message !== "string" || + typeof data["cf-turnstile-response"] !== "string" ) || !data.anon && ( !data.name.trim() || !data.email.trim() || !validator.isEmail(data.email) || - data.email.length > 30 + data.email.length > 50 ) || - !data.message.trim() + !data.message.trim(), + !data["cf-turnstile-response"].trim() ) } export async function POST(req: NextRequest) { const agent = req.headers.get("x-forwarded-for") ?? "damn"; - - const isRateLimited = await rateLimited(agent); - if (isRateLimited) { - return NextResponse.json( - { error: "Too many requests" }, - { status: 429 } - ); - } - let data; try { data = await req.json(); @@ -49,6 +42,33 @@ export async function POST(req: NextRequest) { ); } + const cf_token = data["cf-turnstile-response"]; + + if (typeof cf_token !== 'string' || !cf_token.trim()) { + return NextResponse.json( + { error: "Captcha Failed" }, + { status: 403 } + ); + } + + const captchaVerified = await validateTurnstile(cf_token, agent); + + if (!captchaVerified) { + return NextResponse.json( + { error: "Captcha Failed" }, + { status: 403 } + ); + } + + const isRateLimited = await rateLimited(agent); + + if (isRateLimited) { + return NextResponse.json( + { error: "Too many requests" }, + { status: 429 } + ); + } + if (validateInput(data)) { return NextResponse.json( { error: "Invalid input" }, diff --git a/src/app/globals.css b/src/app/globals.css index 3f4af8d..d3b13de 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,6 +5,7 @@ :root { --background: #0F0A1F; --primary: #F48120; + --primary-darker: #cc6d1f; } @keyframes click-bounce { diff --git a/src/components/form/contact-form.tsx b/src/components/form/contact-form.tsx index 8a4c3b5..698d08e 100644 --- a/src/components/form/contact-form.tsx +++ b/src/components/form/contact-form.tsx @@ -5,6 +5,7 @@ import axios, { AxiosError } from "axios"; import { CheckboxInput, Input, Submit } from "./form"; import { FloatingLabel } from "../floating-label"; +import { CloudflareTurnstile } from "../turnstile"; export default function ContactForm() { const [anon, setAnon] = useState(false); @@ -32,7 +33,8 @@ export default function ContactForm() { anon, name: formData.get('name')?.toString(), email: formData.get('email')?.toString(), - message: formData.get('message')!.toString() + message: formData.get('message')!.toString(), + "cf-turnstile-response": formData.get('cf-turnstile-response')?.toString() } if ((!data.name || !data.email) && !data.anon) { @@ -44,6 +46,11 @@ export default function ContactForm() { setErrorMsg("What do you want to tell me???"); return; } + + if (!data["cf-turnstile-response"]) { + setErrorMsg("Please fill in the captcha"); + return; + } setStatus('loading'); @@ -59,6 +66,7 @@ export default function ContactForm() { if (err instanceof AxiosError) { if (err.status == 429) setErrorMsg("Limit reached, please try again later..."); + if (err.status == 403) setErrorMsg("Captcha failed or expired, please try again later"); } } } @@ -100,6 +108,7 @@ export default function ContactForm() { placeholder="Tell me something cool, or ask question" /> +
{errorMsg}
{statusMsg[status]} diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 7f4bdd2..90241c6 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -18,7 +18,7 @@ export function Submit ({ className, type, ...props }: React.ComponentProps<'but return (