diff --git a/.config/config-schema.json b/.config/config-schema.json
index 40da2df..ae77b28 100644
--- a/.config/config-schema.json
+++ b/.config/config-schema.json
@@ -52,6 +52,20 @@
"secret": {
"description": "The JWT secret used to sign tokens.",
"type": "string"
+ },
+ "turnstile": {
+ "additionalProperties": false,
+ "description": "Cloudflare turnstile configuration. Leave `undefined` to disable turnstile.",
+ "properties": {
+ "secret": {
+ "type": "string"
+ },
+ "siteKey": {
+ "type": "string"
+ }
+ },
+ "required": ["secret", "siteKey"],
+ "type": "object"
}
},
"required": ["secret"],
diff --git a/next.config.mjs b/next.config.mjs
index bf7e3b2..68661ea 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,5 +1,5 @@
-import { execSync } from "node:child_process";
// @ts-check
+import { execSync } from "node:child_process";
import NextBundleAnalyzer from "@next/bundle-analyzer";
/** @type {import('next').NextConfig} */
const nextConfig = {
@@ -16,6 +16,7 @@ const nextConfig = {
env: {
GIT_COMMIT: process.env.NODE_ENV === "production" ? execSync("git rev-parse HEAD").toString().trim() : "DEVELOP",
BUILD_DATE: Date.now().toString(),
+ TURNSTILE_SITEKEY: JSON.parse(process.env.CONFIG || "").auth.turnstile?.siteKey,
},
experimental: {
ppr: true,
@@ -50,5 +51,4 @@ const nextConfig = {
return config;
},
};
-
export default NextBundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(nextConfig);
diff --git a/package.json b/package.json
index 2888b03..e758afc 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"config:generate": "ts-json-schema-generator --tsconfig ./tsconfig.json --path ./src/types/config.d.ts --type Config > ./.config/config-schema.json"
},
"dependencies": {
+ "@marsidev/react-turnstile": "^0.7.2",
"@next/bundle-analyzer": "^14.2.3",
"@novnc/novnc": "^1.4.0",
"@paralleldrive/cuid2": "^2.2.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1d006a9..1f67a18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@marsidev/react-turnstile':
+ specifier: ^0.7.2
+ version: 0.7.2(react-dom@19.0.0-rc-163365a0-20240717(react@19.0.0-rc-163365a0-20240717))(react@19.0.0-rc-163365a0-20240717)
'@next/bundle-analyzer':
specifier: ^14.2.3
version: 14.2.3
@@ -937,6 +940,12 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ '@marsidev/react-turnstile@0.7.2':
+ resolution: {integrity: sha512-0jwLvAUkcLkaYaS6jBOZB3zzUiKi5dU3kZtlaeBX6yV7Y4CbFEtfHCY352ovphNz1v0ZjpOj6+3QUczJvD56VA==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
'@next/bundle-analyzer@14.2.3':
resolution: {integrity: sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA==}
@@ -3545,6 +3554,11 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
+ '@marsidev/react-turnstile@0.7.2(react-dom@19.0.0-rc-163365a0-20240717(react@19.0.0-rc-163365a0-20240717))(react@19.0.0-rc-163365a0-20240717)':
+ dependencies:
+ react: 19.0.0-rc-163365a0-20240717
+ react-dom: 19.0.0-rc-163365a0-20240717(react@19.0.0-rc-163365a0-20240717)
+
'@next/bundle-analyzer@14.2.3':
dependencies:
webpack-bundle-analyzer: 4.10.1
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 36ebb1f..99bab3c 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -1,4 +1,5 @@
import { StyledSubmit } from "@/components/submit-button";
+import Turnstile from "@/components/turnstile";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { CardContent, CardDescription } from "@/components/ui/card";
@@ -7,6 +8,7 @@ import { Label } from "@/components/ui/label";
import { auth, signIn } from "@/lib/auth";
import { providers } from "@/lib/auth.config";
import { getConfig } from "@/lib/config";
+import turnstileCheck from "@/lib/turnstile";
import { AlertCircle, Info } from "lucide-react";
import type { CredentialsSignin } from "next-auth";
import Link from "next/link";
@@ -43,9 +45,15 @@ export default async function Login({
action={async (data) => {
"use server";
try {
- await signIn("credentials", data);
+ if (await turnstileCheck(data)) {
+ await signIn("credentials", data);
+ } else {
+ throw new Error("Failed captcha");
+ }
} catch (error) {
- redirect(`/auth/login?error=${(error as CredentialsSignin).cause?.err?.message}`);
+ redirect(
+ `/auth/login?error=${(error as CredentialsSignin).cause?.err?.message || (error as Error).message}`,
+ );
}
}}
>
@@ -69,6 +77,7 @@ export default async function Login({
required
className="w-full"
/>
+