feat: replace turnstile with altcha
This commit is contained in:
parent
b38347f6d9
commit
17acf5085d
@ -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<HTMLFormElement>(null)
|
||||
const [errorAlert, setErrorAlert] = useState<string | null>(null)
|
||||
const [forceRefresh, setForceRefresh] = useState(false)
|
||||
const [altchaToken, setAltchaToken] = useState<string | null>(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 <CheckCircle2 size={30} />
|
||||
@ -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() {
|
||||
<AnimatePresence mode="wait">
|
||||
{formType === "initial" && (
|
||||
<motion.div key="initial" {...fadeInOut} className="space-y-4">
|
||||
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg">
|
||||
<Button onClick={() => setFormType("create")} className="w-full h-16 text-lg cursor-pointer">
|
||||
<UserPlus className="mr-2" />
|
||||
Create New Account
|
||||
</Button>
|
||||
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg">
|
||||
<Button onClick={() => setFormType("migrate")} className="w-full h-16 text-lg cursor-pointer">
|
||||
<UserCog className="mr-2" />
|
||||
Migrate p0ntus mail Account
|
||||
</Button>
|
||||
@ -319,10 +311,17 @@ export default function Signup() {
|
||||
</Label>
|
||||
</div>
|
||||
{!forceRefresh && (
|
||||
<Turnstile
|
||||
setTurnstileStatus={setTurnstileStatus}
|
||||
setValidationMessage={setValidationMessage}
|
||||
/>
|
||||
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||
)}
|
||||
{!forceRefresh && (
|
||||
<>
|
||||
<div id="altcha-description" className="sr-only">
|
||||
A CAPTCHA box. You must solve the challenge to make an account.
|
||||
</div>
|
||||
<Altcha
|
||||
onStateChange={handleAltchaStateChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
)}
|
||||
@ -389,10 +388,17 @@ export default function Signup() {
|
||||
</Label>
|
||||
</div>
|
||||
{!forceRefresh && (
|
||||
<Turnstile
|
||||
setTurnstileStatus={setTurnstileStatus}
|
||||
setValidationMessage={setValidationMessage}
|
||||
/>
|
||||
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||
)}
|
||||
{!forceRefresh && (
|
||||
<>
|
||||
<div id="altcha-description" className="sr-only">
|
||||
A CAPTCHA box. You must solve the challenge to make an account.
|
||||
</div>
|
||||
<Altcha
|
||||
onStateChange={handleAltchaStateChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.form>
|
||||
)}
|
||||
@ -405,7 +411,7 @@ export default function Signup() {
|
||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full mb-4"
|
||||
className="w-full mb-4 cursor-pointer"
|
||||
disabled={!isValid || isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
@ -416,7 +422,7 @@ export default function Signup() {
|
||||
)}
|
||||
{isSubmitting ? "Submitting..." : validationMessage}
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
|
||||
<Button variant="outline" className="w-full cursor-pointer" onClick={() => setFormType("initial")}>
|
||||
Back
|
||||
</Button>
|
||||
</motion.div>
|
||||
@ -428,7 +434,7 @@ export default function Signup() {
|
||||
)
|
||||
) : (
|
||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
||||
<Button className="w-full" onClick={() => { setFormType("initial"); setForceRefresh(false); setErrorAlert(null) }}>
|
||||
<Button className="w-full cursor-pointer" onClick={() => { setFormType("initial"); setForceRefresh(false); setErrorAlert(null) }}>
|
||||
<ArrowLeft size={30} />
|
||||
Back
|
||||
</Button>
|
||||
|
22
app/api/captcha/create/route.ts
Normal file
22
app/api/captcha/create/route.ts
Normal file
@ -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()
|
||||
}
|
@ -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 })
|
||||
}
|
||||
|
||||
|
57
components/custom/Altcha.tsx
Normal file
57
components/custom/Altcha.tsx
Normal file
@ -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<AltchaWidget & AltchaWidgetMethods & HTMLElement>(null)
|
||||
const [value, setValue] = useState<string | null>(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-widget
|
||||
challengeurl="/api/captcha/create"
|
||||
ref={widgetRef}
|
||||
style={{
|
||||
'--altcha-max-width': '100%',
|
||||
}}
|
||||
debug={process.env.NODE_ENV === "development"}
|
||||
aria-label="Security verification"
|
||||
aria-describedby="altcha-description"
|
||||
></altcha-widget>
|
||||
)
|
||||
})
|
||||
|
||||
Altcha.displayName = 'Altcha'
|
||||
|
||||
export default Altcha
|
@ -1,36 +0,0 @@
|
||||
import type React from "react"
|
||||
import { Turnstile as CloudflareTS } from "next-turnstile"
|
||||
|
||||
interface TurnstileProps {
|
||||
setTurnstileStatus: React.Dispatch<React.SetStateAction<"error" | "expired" | "required" | "success">>
|
||||
setValidationMessage: (message: string) => void
|
||||
}
|
||||
|
||||
export default function Turnstile({ setTurnstileStatus, setValidationMessage }: TurnstileProps) {
|
||||
return (
|
||||
<CloudflareTS
|
||||
siteKey={process.env.NEXT_PUBLIC_CF_SITEKEY!}
|
||||
retry="auto"
|
||||
refreshExpired="auto"
|
||||
onError={() => {
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
22
lib/utils.ts
22
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" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user