Skip to content

Commit 310f94a

Browse files
committed
feat: integrate Turnstile captcha for enhanced login security & enhance metadata for blogs & whole project :)
1 parent 2026727 commit 310f94a

File tree

6 files changed

+126
-15
lines changed

6 files changed

+126
-15
lines changed

app/api/login/route.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,55 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { verifyAdminPassword, createSession } from "@/lib/auth";
3+
import { error } from "console";
4+
5+
async function verifyTurnstile(token: string, remoteip?: string) {
6+
const secret = process.env.TURNSTILE_SECRET_KEY;
7+
if (!secret) return false;
8+
9+
const body = new URLSearchParams();
10+
body.append("secret", secret);
11+
body.append("response", token);
12+
if (remoteip) body.append("remoteip", remoteip);
13+
14+
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
15+
method: "POST",
16+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
17+
body,
18+
});
19+
20+
if (!res.ok) return false;
21+
const data = await res.json();
22+
return data.success === true;
23+
}
324

425
export async function POST(request: NextRequest) {
526
try {
6-
const { password } = await request.json();
27+
const { password, turnstileToken } = await request.json();
728

8-
if (!password) {
29+
if (!password || !turnstileToken) {
930
return NextResponse.json(
10-
{ error: "Password is required" },
31+
error({ error: "Password and Turnstile token are required" }),
1132
{ status: 400 }
12-
);
33+
)
1334
}
1435

15-
const isValid = await verifyAdminPassword(password);
36+
const ip = request.headers.get("x-forwarder-for")?.split(",")[0].trim();
37+
const turnstileOk = await verifyTurnstile(turnstileToken, ip);
1638

17-
if (!isValid) {
18-
return NextResponse.json(
19-
{ error: "Invalid password" },
20-
{ status: 401 }
21-
);
39+
if (!turnstileOk) {
40+
return NextResponse.json({
41+
error: "Capcha verification failed. Please try again."
42+
}, { status: 401 })
2243
}
2344

45+
const isValid = await verifyAdminPassword(password);
46+
if (!isValid) {
47+
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
48+
}
2449
await createSession();
2550

2651
return NextResponse.json({ success: true });
2752
} catch (error) {
28-
console.error("Login error:", error);
2953
return NextResponse.json(
3054
{ error: "Internal server error" },
3155
{ status: 500 }

app/blog/[slug]/page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,49 @@ import { notFound } from "next/navigation";
44
import { prisma } from "@/lib/prisma"
55
import Link from "next/link";
66
import { ArrowBigLeftDash } from "lucide-react";
7+
import { Metadata } from "next";
78

89
type BlogPageProps = {
910
params: Promise<{
1011
slug: string;
1112
}>;
1213
};
1314

15+
export async function generateMetadata({
16+
params,
17+
}: {
18+
params: Promise<{ slug: string }>;
19+
}): Promise<Metadata> {
20+
const { slug } = await params;
21+
const blog = await prisma.blog.findFirst({
22+
where: { slug, published: true },
23+
});
24+
25+
if (!blog) {
26+
return { title: "Blog not found" };
27+
}
28+
29+
const description = blog.summary || blog.content?.slice(0, 160) || "";
30+
31+
return {
32+
title: blog.title,
33+
description,
34+
openGraph: {
35+
title: blog.title,
36+
description,
37+
images: blog.thumbnail ? [{ url: blog.thumbnail }] : [],
38+
type: "article",
39+
publishedTime: blog.createdAt.toISOString(),
40+
},
41+
twitter: {
42+
card: "summary_large_image",
43+
title: blog.title,
44+
description,
45+
images: blog.thumbnail ? [blog.thumbnail] : [],
46+
},
47+
};
48+
}
49+
1450
export default async function BlogDetailPage({ params }: BlogPageProps) {
1551
const { slug } = await params;
1652

app/layout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,24 @@ const geistMono = Geist_Mono({
1414

1515
export const metadata: Metadata = {
1616
title: "sander.tf",
17-
description: "sander.tf",
17+
description: "This is my personal website! Explore my projects, blogs, and more.",
18+
keywords: ["blog", "projects"],
19+
20+
openGraph: {
21+
title: "sander.tf",
22+
description: "This is my personal website! Explore my projects, blogs, and more.",
23+
url: "https://sander.tf/",
24+
siteName: "sander.tf",
25+
images: [{ url: "https://sander.tf/sander-tf.png" }],
26+
type: "website",
27+
},
28+
29+
twitter: {
30+
card: "summary_large_image",
31+
title: "sander.tf",
32+
description: "This is my personal website! Explore my projects, blogs, and more.",
33+
images: ["https://sander.tf/sander-tf.png"]
34+
},
1835
};
1936

2037
export default function RootLayout({

app/login/page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import { FormEvent, useState } from "react";
44
import { useRouter } from "next/navigation";
55
import { motion } from "framer-motion";
6+
import { Turnstile } from "@marsidev/react-turnstile";
67

78
export default function LoginPage() {
89
const router = useRouter();
9-
10+
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
1011
const [password, setPassword] = useState("");
1112
const [error, setError] = useState<string | null>(null);
1213
const [loading, setLoading] = useState(false);
@@ -16,11 +17,17 @@ export default function LoginPage() {
1617
setLoading(true);
1718
setError(null);
1819

20+
if(!turnstileToken) {
21+
setError("Please complete the captcha.");
22+
setLoading(false);
23+
return;
24+
}
25+
1926
try {
2027
const response = await fetch("/api/login", {
2128
method: "POST",
2229
headers: { "Content-Type": "application/json" },
23-
body: JSON.stringify({ password }),
30+
body: JSON.stringify({ password, turnstileToken }),
2431
});
2532

2633
const data = await response.json();
@@ -84,7 +91,22 @@ export default function LoginPage() {
8491
</div>
8592

8693
{error ? <p className="text-sm text-red-600 dark:text-red-400">{error}</p> : null}
87-
94+
95+
<div className="rounded-2xl border border-slate-200/80 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-900/40">
96+
<Turnstile
97+
className="mx-auto"
98+
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
99+
options={{
100+
size: "flexible",
101+
theme: "auto",
102+
appearance: "interaction-only",
103+
}}
104+
onSuccess={(token) => setTurnstileToken(token)}
105+
onExpire={() => setTurnstileToken(null)}
106+
onError={() => setTurnstileToken(null)}
107+
/>
108+
</div>
109+
88110
<motion.button
89111
type="submit"
90112
disabled={loading}

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"reset-admin-password": "tsx scripts/reset-admin-password.ts"
1212
},
1313
"dependencies": {
14+
"@marsidev/react-turnstile": "^1.4.2",
1415
"@prisma/client": "^6.19.2",
1516
"bcryptjs": "^3.0.3",
1617
"framer-motion": "^12.35.0",

0 commit comments

Comments
 (0)