Add cloudflare turnstile
This commit is contained in:
parent
50461f1644
commit
15e4fd556c
@ -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" },
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #0F0A1F;
|
--background: #0F0A1F;
|
||||||
--primary: #F48120;
|
--primary: #F48120;
|
||||||
|
--primary-darker: #cc6d1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes click-bounce {
|
@keyframes click-bounce {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,27 +19,22 @@ 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"
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user