From 17acf5085d7e4a307c0c9cf4148ed79d54953528 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 16 Apr 2025 16:54:52 -0400 Subject: [PATCH] feat: replace turnstile with altcha --- app/account/signup/page.tsx | 82 ++++++++++++++++++--------------- app/api/captcha/create/route.ts | 22 +++++++++ app/api/users/create/route.ts | 2 +- components/custom/Altcha.tsx | 57 +++++++++++++++++++++++ components/custom/Turnstile.tsx | 36 --------------- lib/utils.ts | 22 ++++----- package.json | 3 +- 7 files changed, 135 insertions(+), 89 deletions(-) create mode 100644 app/api/captcha/create/route.ts create mode 100644 components/custom/Altcha.tsx delete mode 100644 components/custom/Turnstile.tsx diff --git a/app/account/signup/page.tsx b/app/account/signup/page.tsx index 60124a7..950ef60 100644 --- a/app/account/signup/page.tsx +++ b/app/account/signup/page.tsx @@ -15,14 +15,7 @@ import { useRouter } from "next/navigation" import { validateEmail, validatePassword } from "@/lib/utils" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import EmailField from "@/components/custom/signup/EmailField" -import Turnstile from "@/components/custom/Turnstile" - -declare global { - interface Window { - onTurnstileSuccess?: (token: string) => void - onloadTurnstileCallback?: () => void - } -} +import Altcha from "@/components/custom/Altcha" export default function Signup() { const router = useRouter() @@ -40,10 +33,11 @@ export default function Signup() { const [isValid, setIsValid] = useState(false) const [validationMessage, setValidationMessage] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) - const [turnstileStatus, setTurnstileStatus] = useState<"success" | "error" | "expired" | "required">("required") + const [altchaStatus, setAltchaStatus] = useState<"success" | "error" | "expired" | "required">("required") const formRef = useRef(null) const [errorAlert, setErrorAlert] = useState(null) const [forceRefresh, setForceRefresh] = useState(false) + const [altchaToken, setAltchaToken] = useState(null) const fadeInOut = { initial: { opacity: 0, y: 20 }, @@ -69,17 +63,15 @@ export default function Signup() { } } - const turnstileCallback = () => { - console.log("[i] Turnstile token received") - } - - useEffect(() => { - window.onTurnstileSuccess = turnstileCallback - - return () => { - delete window.onTurnstileSuccess + const handleAltchaStateChange = (e: Event | CustomEvent) => { + if ('detail' in e && e.detail?.payload) { + setAltchaToken(e.detail.payload) + setAltchaStatus("success") + } else { + setAltchaToken(null) + setAltchaStatus("required") } - }, []) + } useEffect(() => { if (formType === "create") { @@ -110,7 +102,7 @@ export default function Signup() { return } - if (turnstileStatus !== "success") { + if (altchaStatus !== "success") { setIsValid(false) setValidationMessage("Please verify you are not a robot") return @@ -146,7 +138,7 @@ export default function Signup() { return } - if (turnstileStatus !== "success") { + if (altchaStatus !== "success") { setIsValid(false) setValidationMessage("Please verify you are not a robot") return @@ -155,7 +147,7 @@ export default function Signup() { setIsValid(true) setValidationMessage("Migrate Account") } - }, [formData, formType, turnstileStatus]) + }, [formData, formType, altchaStatus]) const getButtonIcon = () => { if (isValid) return @@ -173,7 +165,7 @@ export default function Signup() { setErrorAlert(null) try { - if (turnstileStatus !== "success") { + if (altchaStatus !== "success") { setValidationMessage("Please verify you are not a robot") setIsSubmitting(false) return @@ -181,10 +173,10 @@ export default function Signup() { const email = `${formData.emailUsername}@${formData.emailDomain}` const formDataObj = new FormData(formRef.current as HTMLFormElement) - const token = formDataObj.get("cf-turnstile-response") as string + const token = formDataObj.get("altcha-token") as string if (!token) { - setErrorAlert("Cloudflare Turnstile token is missing. Please refresh") + setErrorAlert("Altcha token is missing. Please refresh") setIsSubmitting(false) setForceRefresh(true) return @@ -246,11 +238,11 @@ export default function Signup() { {formType === "initial" && ( - - @@ -319,10 +311,17 @@ export default function Signup() { {!forceRefresh && ( - + + )} + {!forceRefresh && ( + <> +
+ A CAPTCHA box. You must solve the challenge to make an account. +
+ + )} )} @@ -389,10 +388,17 @@ export default function Signup() { {!forceRefresh && ( - + + )} + {!forceRefresh && ( + <> +
+ A CAPTCHA box. You must solve the challenge to make an account. +
+ + )} )} @@ -405,7 +411,7 @@ export default function Signup() { - @@ -428,7 +434,7 @@ export default function Signup() { ) ) : ( - diff --git a/app/api/captcha/create/route.ts b/app/api/captcha/create/route.ts new file mode 100644 index 0000000..a5b8ae8 --- /dev/null +++ b/app/api/captcha/create/route.ts @@ -0,0 +1,22 @@ +import { createChallenge } from 'altcha-lib' +import { NextResponse } from 'next/server' + +const hmacKey = process.env.ALTCHA_SECRETKEY + +async function getChallenge() { + if (!hmacKey) { + console.error("ALTCHA_SECRETKEY is not set") + return NextResponse.json({ error: "ALTCHA_SECRETKEY is not set" }, { status: 500 }) + } + + const challenge = await createChallenge({ + hmacKey, + maxNumber: 1200000, // Max random number + }) + + return NextResponse.json(challenge) +} + +export async function GET(request: Request) { + return getChallenge() +} \ No newline at end of file diff --git a/app/api/users/create/route.ts b/app/api/users/create/route.ts index 3a8104b..2968d04 100644 --- a/app/api/users/create/route.ts +++ b/app/api/users/create/route.ts @@ -61,7 +61,7 @@ export async function POST(request: Request) { const tokenValidation = await validateToken(token) if (!tokenValidation.success) { - console.error("Turnstile validation failed:", tokenValidation.error) + console.error("Altcha validation failed:", tokenValidation.error) return NextResponse.json({ success: false, message: "Robot check failed, try refreshing" }, { status: 400 }) } diff --git a/components/custom/Altcha.tsx b/components/custom/Altcha.tsx new file mode 100644 index 0000000..10e4353 --- /dev/null +++ b/components/custom/Altcha.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react' + +interface AltchaProps { + onStateChange?: (ev: Event | CustomEvent) => void +} + +const Altcha = forwardRef<{ value: string | null }, AltchaProps>(({ onStateChange }, ref) => { + const widgetRef = useRef(null) + const [value, setValue] = useState(null) + + useEffect(() => { + import('altcha') + }, []) + + useImperativeHandle(ref, () => { + return { + get value() { + return value + } + } + }, [value]) + + useEffect(() => { + const handleStateChange = (ev: Event | CustomEvent) => { + if ('detail' in ev) { + setValue(ev.detail.payload || null) + onStateChange?.(ev) + } + } + + const { current } = widgetRef + + if (current) { + current.addEventListener('statechange', handleStateChange) + return () => current.removeEventListener('statechange', handleStateChange) + } + }, [onStateChange]) + + return ( + + ) +}) + +Altcha.displayName = 'Altcha' + +export default Altcha diff --git a/components/custom/Turnstile.tsx b/components/custom/Turnstile.tsx deleted file mode 100644 index 78e7f51..0000000 --- a/components/custom/Turnstile.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type React from "react" -import { Turnstile as CloudflareTS } from "next-turnstile" - -interface TurnstileProps { - setTurnstileStatus: React.Dispatch> - setValidationMessage: (message: string) => void -} - -export default function Turnstile({ setTurnstileStatus, setValidationMessage }: TurnstileProps) { - return ( - { - setTurnstileStatus("error") - setValidationMessage("Security check failed. Please try again.") - console.error("[!] Turnstile error occurred") - }} - onExpire={() => { - setTurnstileStatus("expired") - setValidationMessage("Security check expired. Please verify again.") - console.warn("[!] Turnstile token expired") - }} - onLoad={() => { - setTurnstileStatus("required") - }} - onVerify={() => { - setTurnstileStatus("success") - console.log("[S] Turnstile verification successful") - }} - className="flex justify-center" - /> - ) -} - diff --git a/lib/utils.ts b/lib/utils.ts index b9ddcea..bd2d57a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,6 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" -import { validateTurnstileToken } from "next-turnstile" +import { verifySolution } from "altcha-lib" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -9,29 +9,25 @@ export function cn(...inputs: ClassValue[]) { export async function validateToken(token: string) { try { if (!token) { - console.error("Validation failed: No token provided") + console.error("Altcha error: No token provided") return { success: false, error: "No token provided" } } - if (!process.env.CF_SECRETKEY) { - console.error("Validation failed: Missing CF_SECRETKEY environment variable") + if (!process.env.ALTCHA_SECRETKEY) { + console.error("Altcha error: Missing ALTCHA_SECRETKEY environment variable") return { success: false, error: "Server configuration error" } } - const result = await validateTurnstileToken({ - token, - secretKey: process.env.CF_SECRETKEY, - }) - - if (result.success) { + const ok = await verifySolution(token, process.env.ALTCHA_SECRETKEY) + if (ok) { return { success: true } } else { - console.error("Validation failed:", result) + console.error("Altcha error: Invalid token") return { success: false, error: "Invalid token" } } } catch (error) { - console.error("Turnstile validation error:", error) - return { success: false, error: "Validation service error" } + console.error("Altcha error:", error) + return { success: false, error: "An error occurred with Altcha" } } } diff --git a/package.json b/package.json index f09acff..57672c7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.2.0", "@web3icons/react": "^4.0.13", + "altcha": "^1.4.2", + "altcha-lib": "^1.2.0", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -38,7 +40,6 @@ "next": "^15.3.0", "next-auth": "^5.0.0-beta.25", "next-themes": "^0.4.6", - "next-turnstile": "^1.0.2", "password-validator": "^5.3.0", "prisma": "^6.6.0", "react": "^19.1.0",