Add cloudflare turnstile

This commit is contained in:
Nomi Nonsense (Nonszy) 2026-01-01 17:46:33 +07:00
parent 50461f1644
commit 15e4fd556c
6 changed files with 55 additions and 29 deletions

View File

@ -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" },

View File

@ -5,6 +5,7 @@
:root {
--background: #0F0A1F;
--primary: #F48120;
--primary-darker: #cc6d1f;
}
@keyframes click-bounce {

View File

@ -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"
/>
</label>
<CloudflareTurnstile />
<div className="text-primary">{errorMsg}</div>
<Submit disabled={status === "loading"} className="w-full">{statusMsg[status]}</Submit>
</form>

View File

@ -18,7 +18,7 @@ export function Submit ({ className, type, ...props }: React.ComponentProps<'but
return (
<button
type={'submit'}
className={clsx("block py-3 px-4 bg-primary text-background", className)}
className={clsx("block py-3 px-4 bg-primary disabled:bg-primary-darker text-background", className)}
{...props}
/>
)

View File

@ -19,27 +19,22 @@ export async function rateLimited(clientId: string) {
export async function validateTurnstile(token: string, remoteip: string) {
const body = new URLSearchParams({
secret: process.env.CF_SECRET_KEY!,
token,
response: token,
remoteip
})
try {
const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
const result = await res.data;
console.log(result);
} catch (error) {
console.error("Turnstile validation error:", error);
return { success: false, "error-codes": ["internal-error"] };
}
const result = await res.data;
return result.success === true;
}
export async function sendEmail(name: string, email: string, message: string) {
const rawMessage = trimTooLong(message, 5000);
const rawMessage = `From: ${name}\nEmail: ${email}\n${trimTooLong(message, 5000)}`;
const messageHTML = sanitize(escape(rawMessage));
await transporter.sendMail({

View File

@ -12,6 +12,7 @@ const config: Config = {
background: "var(--background)",
foreground: "var(--foreground)",
primary: "var(--primary)",
'primary-darker': "var(--primary-darker)",
},
},
screens: {