From d190afbd19f8f398b4d128e65d4c94b948a0cb6b Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Mon, 16 Feb 2026 23:27:25 -0600 Subject: [PATCH] feat: add ALTCHA widget to admin login page - Create Altcha component for admin frontend - Add ALTCHA verification to login flow - Disable login button until ALTCHA is verified - Use admin ALTCHA challenge endpoint --- admin/src/app/login/page.tsx | 30 ++++++++++++++--- admin/src/components/Altcha.tsx | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 admin/src/components/Altcha.tsx diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx index b01d499..62bef29 100644 --- a/admin/src/app/login/page.tsx +++ b/admin/src/app/login/page.tsx @@ -3,22 +3,38 @@ import { useState, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/lib/auth'; +import Altcha from '@/components/Altcha'; export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [altchaToken, setAltchaToken] = useState(''); + const [altchaVerified, setAltchaVerified] = useState(false); const emailRef = useRef(''); const passwordRef = useRef(''); const { login } = useAuth(); const router = useRouter(); + const handleAltchaStateChange = useCallback((state: any) => { + if (state.state === 'verified' && state.payload) { + setAltchaToken(state.payload); + setAltchaVerified(true); + } else { + setAltchaToken(''); + setAltchaVerified(false); + } + }, []); + const performLogin = useCallback(async () => { + if (!altchaVerified) { + setError('Please complete the security verification'); + return; + } + setLoading(true); try { - // Use development bypass if in development mode - const altchaToken = process.env.NODE_ENV === 'development' ? 'BYPASS_DEV_MODE' : ''; await login(emailRef.current, passwordRef.current, altchaToken); router.push('/'); } catch (err: any) { @@ -26,7 +42,7 @@ export default function LoginPage() { } finally { setLoading(false); } - }, [login, router]); + }, [login, router, altchaToken, altchaVerified]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -85,10 +101,16 @@ export default function LoginPage() { required /> +
+ +
diff --git a/admin/src/components/Altcha.tsx b/admin/src/components/Altcha.tsx new file mode 100644 index 0000000..a4614ac --- /dev/null +++ b/admin/src/components/Altcha.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +interface AltchaProps { + challengeurl: string; + onStateChange?: (state: any) => void; +} + +export default function Altcha({ challengeurl, onStateChange }: AltchaProps) { + const widgetRef = useRef(null); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + // Load ALTCHA widget script + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/altcha@0.5.0/dist/altcha.min.js'; + script.type = 'module'; + script.async = true; + script.onload = () => setLoaded(true); + document.head.appendChild(script); + + return () => { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + }; + }, []); + + useEffect(() => { + if (!loaded || !widgetRef.current) return; + + const widget = widgetRef.current.querySelector('altcha-widget'); + if (!widget) return; + + const handleStateChange = (event: any) => { + if (onStateChange) { + onStateChange(event.detail); + } + }; + + widget.addEventListener('statechange', handleStateChange); + + return () => { + widget.removeEventListener('statechange', handleStateChange); + }; + }, [loaded, onStateChange]); + + return ( +
+ +
+ ); +}