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 { validateEmail, validatePassword } from "@/lib/utils"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import EmailField from "@/components/custom/signup/EmailField"
|
import EmailField from "@/components/custom/signup/EmailField"
|
||||||
import Turnstile from "@/components/custom/Turnstile"
|
import Altcha from "@/components/custom/Altcha"
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
onTurnstileSuccess?: (token: string) => void
|
|
||||||
onloadTurnstileCallback?: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Signup() {
|
export default function Signup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -40,10 +33,11 @@ export default function Signup() {
|
|||||||
const [isValid, setIsValid] = useState(false)
|
const [isValid, setIsValid] = useState(false)
|
||||||
const [validationMessage, setValidationMessage] = useState("")
|
const [validationMessage, setValidationMessage] = useState("")
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
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 formRef = useRef<HTMLFormElement>(null)
|
||||||
const [errorAlert, setErrorAlert] = useState<string | null>(null)
|
const [errorAlert, setErrorAlert] = useState<string | null>(null)
|
||||||
const [forceRefresh, setForceRefresh] = useState(false)
|
const [forceRefresh, setForceRefresh] = useState(false)
|
||||||
|
const [altchaToken, setAltchaToken] = useState<string | null>(null)
|
||||||
|
|
||||||
const fadeInOut = {
|
const fadeInOut = {
|
||||||
initial: { opacity: 0, y: 20 },
|
initial: { opacity: 0, y: 20 },
|
||||||
@ -69,17 +63,15 @@ export default function Signup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const turnstileCallback = () => {
|
const handleAltchaStateChange = (e: Event | CustomEvent) => {
|
||||||
console.log("[i] Turnstile token received")
|
if ('detail' in e && e.detail?.payload) {
|
||||||
}
|
setAltchaToken(e.detail.payload)
|
||||||
|
setAltchaStatus("success")
|
||||||
useEffect(() => {
|
} else {
|
||||||
window.onTurnstileSuccess = turnstileCallback
|
setAltchaToken(null)
|
||||||
|
setAltchaStatus("required")
|
||||||
return () => {
|
|
||||||
delete window.onTurnstileSuccess
|
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formType === "create") {
|
if (formType === "create") {
|
||||||
@ -110,7 +102,7 @@ export default function Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (turnstileStatus !== "success") {
|
if (altchaStatus !== "success") {
|
||||||
setIsValid(false)
|
setIsValid(false)
|
||||||
setValidationMessage("Please verify you are not a robot")
|
setValidationMessage("Please verify you are not a robot")
|
||||||
return
|
return
|
||||||
@ -146,7 +138,7 @@ export default function Signup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (turnstileStatus !== "success") {
|
if (altchaStatus !== "success") {
|
||||||
setIsValid(false)
|
setIsValid(false)
|
||||||
setValidationMessage("Please verify you are not a robot")
|
setValidationMessage("Please verify you are not a robot")
|
||||||
return
|
return
|
||||||
@ -155,7 +147,7 @@ export default function Signup() {
|
|||||||
setIsValid(true)
|
setIsValid(true)
|
||||||
setValidationMessage("Migrate Account")
|
setValidationMessage("Migrate Account")
|
||||||
}
|
}
|
||||||
}, [formData, formType, turnstileStatus])
|
}, [formData, formType, altchaStatus])
|
||||||
|
|
||||||
const getButtonIcon = () => {
|
const getButtonIcon = () => {
|
||||||
if (isValid) return <CheckCircle2 size={30} />
|
if (isValid) return <CheckCircle2 size={30} />
|
||||||
@ -173,7 +165,7 @@ export default function Signup() {
|
|||||||
setErrorAlert(null)
|
setErrorAlert(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (turnstileStatus !== "success") {
|
if (altchaStatus !== "success") {
|
||||||
setValidationMessage("Please verify you are not a robot")
|
setValidationMessage("Please verify you are not a robot")
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
return
|
return
|
||||||
@ -181,10 +173,10 @@ export default function Signup() {
|
|||||||
|
|
||||||
const email = `${formData.emailUsername}@${formData.emailDomain}`
|
const email = `${formData.emailUsername}@${formData.emailDomain}`
|
||||||
const formDataObj = new FormData(formRef.current as HTMLFormElement)
|
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) {
|
if (!token) {
|
||||||
setErrorAlert("Cloudflare Turnstile token is missing. Please refresh")
|
setErrorAlert("Altcha token is missing. Please refresh")
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setForceRefresh(true)
|
setForceRefresh(true)
|
||||||
return
|
return
|
||||||
@ -246,11 +238,11 @@ export default function Signup() {
|
|||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{formType === "initial" && (
|
{formType === "initial" && (
|
||||||
<motion.div key="initial" {...fadeInOut} className="space-y-4">
|
<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" />
|
<UserPlus className="mr-2" />
|
||||||
Create New Account
|
Create New Account
|
||||||
</Button>
|
</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" />
|
<UserCog className="mr-2" />
|
||||||
Migrate p0ntus mail Account
|
Migrate p0ntus mail Account
|
||||||
</Button>
|
</Button>
|
||||||
@ -319,10 +311,17 @@ export default function Signup() {
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{!forceRefresh && (
|
{!forceRefresh && (
|
||||||
<Turnstile
|
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||||
setTurnstileStatus={setTurnstileStatus}
|
)}
|
||||||
setValidationMessage={setValidationMessage}
|
{!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>
|
</motion.form>
|
||||||
)}
|
)}
|
||||||
@ -389,10 +388,17 @@ export default function Signup() {
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{!forceRefresh && (
|
{!forceRefresh && (
|
||||||
<Turnstile
|
<input type="hidden" name="altcha-token" value={altchaToken ?? ""} />
|
||||||
setTurnstileStatus={setTurnstileStatus}
|
)}
|
||||||
setValidationMessage={setValidationMessage}
|
{!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>
|
</motion.form>
|
||||||
)}
|
)}
|
||||||
@ -405,7 +411,7 @@ export default function Signup() {
|
|||||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full mb-4"
|
className="w-full mb-4 cursor-pointer"
|
||||||
disabled={!isValid || isSubmitting}
|
disabled={!isValid || isSubmitting}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
>
|
>
|
||||||
@ -416,7 +422,7 @@ export default function Signup() {
|
|||||||
)}
|
)}
|
||||||
{isSubmitting ? "Submitting..." : validationMessage}
|
{isSubmitting ? "Submitting..." : validationMessage}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" className="w-full" onClick={() => setFormType("initial")}>
|
<Button variant="outline" className="w-full cursor-pointer" onClick={() => setFormType("initial")}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -428,7 +434,7 @@ export default function Signup() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<motion.div key="buttons" {...fadeInOut} className="w-full space-y-2">
|
<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} />
|
<ArrowLeft size={30} />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</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)
|
const tokenValidation = await validateToken(token)
|
||||||
if (!tokenValidation.success) {
|
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 })
|
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 { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { validateTurnstileToken } from "next-turnstile"
|
import { verifySolution } from "altcha-lib"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
@ -9,29 +9,25 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
export async function validateToken(token: string) {
|
export async function validateToken(token: string) {
|
||||||
try {
|
try {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error("Validation failed: No token provided")
|
console.error("Altcha error: No token provided")
|
||||||
return { success: false, error: "No token provided" }
|
return { success: false, error: "No token provided" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.CF_SECRETKEY) {
|
if (!process.env.ALTCHA_SECRETKEY) {
|
||||||
console.error("Validation failed: Missing CF_SECRETKEY environment variable")
|
console.error("Altcha error: Missing ALTCHA_SECRETKEY environment variable")
|
||||||
return { success: false, error: "Server configuration error" }
|
return { success: false, error: "Server configuration error" }
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await validateTurnstileToken({
|
const ok = await verifySolution(token, process.env.ALTCHA_SECRETKEY)
|
||||||
token,
|
if (ok) {
|
||||||
secretKey: process.env.CF_SECRETKEY,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} else {
|
} else {
|
||||||
console.error("Validation failed:", result)
|
console.error("Altcha error: Invalid token")
|
||||||
return { success: false, error: "Invalid token" }
|
return { success: false, error: "Invalid token" }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Turnstile validation error:", error)
|
console.error("Altcha error:", error)
|
||||||
return { success: false, error: "Validation service error" }
|
return { success: false, error: "An error occurred with Altcha" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.4",
|
"@radix-ui/react-tabs": "^1.1.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@web3icons/react": "^4.0.13",
|
"@web3icons/react": "^4.0.13",
|
||||||
|
"altcha": "^1.4.2",
|
||||||
|
"altcha-lib": "^1.2.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -38,7 +40,6 @@
|
|||||||
"next": "^15.3.0",
|
"next": "^15.3.0",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"next-turnstile": "^1.0.2",
|
|
||||||
"password-validator": "^5.3.0",
|
"password-validator": "^5.3.0",
|
||||||
"prisma": "^6.6.0",
|
"prisma": "^6.6.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user