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 { 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 { trimTooLong } from "@/lib/strings";
import validator from "validator"; import validator from "validator";
@ -14,31 +14,24 @@ const validateInput = (data: any) => {
typeof data.name !== "string" || typeof data.name !== "string" ||
typeof data.email !== "string" typeof data.email !== "string"
) || ) ||
typeof data.message !== "string" typeof data.message !== "string" ||
typeof data["cf-turnstile-response"] !== "string"
) || ) ||
!data.anon && !data.anon &&
( (
!data.name.trim() || !data.name.trim() ||
!data.email.trim() || !data.email.trim() ||
!validator.isEmail(data.email) || !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) { export async function POST(req: NextRequest) {
const agent = req.headers.get("x-forwarded-for") ?? "damn"; 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; let data;
try { try {
data = await req.json(); 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)) { if (validateInput(data)) {
return NextResponse.json( return NextResponse.json(
{ error: "Invalid input" }, { error: "Invalid input" },

View File

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

View File

@ -5,6 +5,7 @@ import axios, { AxiosError } from "axios";
import { CheckboxInput, Input, Submit } from "./form"; import { CheckboxInput, Input, Submit } from "./form";
import { FloatingLabel } from "../floating-label"; import { FloatingLabel } from "../floating-label";
import { CloudflareTurnstile } from "../turnstile";
export default function ContactForm() { export default function ContactForm() {
const [anon, setAnon] = useState(false); const [anon, setAnon] = useState(false);
@ -32,7 +33,8 @@ export default function ContactForm() {
anon, anon,
name: formData.get('name')?.toString(), name: formData.get('name')?.toString(),
email: formData.get('email')?.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) { if ((!data.name || !data.email) && !data.anon) {
@ -45,6 +47,11 @@ export default function ContactForm() {
return; return;
} }
if (!data["cf-turnstile-response"]) {
setErrorMsg("Please fill in the captcha");
return;
}
setStatus('loading'); setStatus('loading');
try { try {
@ -59,6 +66,7 @@ export default function ContactForm() {
if (err instanceof AxiosError) { if (err instanceof AxiosError) {
if (err.status == 429) setErrorMsg("Limit reached, please try again later..."); 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" placeholder="Tell me something cool, or ask question"
/> />
</label> </label>
<CloudflareTurnstile />
<div className="text-primary">{errorMsg}</div> <div className="text-primary">{errorMsg}</div>
<Submit disabled={status === "loading"} className="w-full">{statusMsg[status]}</Submit> <Submit disabled={status === "loading"} className="w-full">{statusMsg[status]}</Submit>
</form> </form>

View File

@ -18,7 +18,7 @@ export function Submit ({ className, type, ...props }: React.ComponentProps<'but
return ( return (
<button <button
type={'submit'} 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} {...props}
/> />
) )

View File

@ -19,11 +19,10 @@ export async function rateLimited(clientId: string) {
export async function validateTurnstile(token: string, remoteip: string) { export async function validateTurnstile(token: string, remoteip: string) {
const body = new URLSearchParams({ const body = new URLSearchParams({
secret: process.env.CF_SECRET_KEY!, secret: process.env.CF_SECRET_KEY!,
token, response: token,
remoteip remoteip
}) })
try {
const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, { const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
@ -31,15 +30,11 @@ export async function validateTurnstile(token: string, remoteip: string) {
}); });
const result = await res.data; const result = await res.data;
console.log(result); return result.success === true;
} catch (error) {
console.error("Turnstile validation error:", error);
return { success: false, "error-codes": ["internal-error"] };
}
} }
export async function sendEmail(name: string, email: string, message: string) { 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)); const messageHTML = sanitize(escape(rawMessage));
await transporter.sendMail({ await transporter.sendMail({

View File

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