From 913fbdb8f7979b9cca0aa49d134a1dcf2cf86927 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Mon, 16 Feb 2026 23:51:46 -0600 Subject: [PATCH] fix: rewrite ALTCHA component for reliable event binding - Use refs for callbacks to avoid stale closures - Create widget via innerHTML and bind events immediately - Remove dev bypass - use real ALTCHA token - Add console logging for debugging --- admin/src/app/login/page.tsx | 11 ++--- admin/src/components/Altcha.tsx | 81 +++++++++++++-------------------- 2 files changed, 36 insertions(+), 56 deletions(-) diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx index e6c5b9f..c2a81ac 100644 --- a/admin/src/app/login/page.tsx +++ b/admin/src/app/login/page.tsx @@ -28,24 +28,21 @@ export default function LoginPage() { }, []); const performLogin = useCallback(async () => { - // Use development bypass for now - const token = process.env.NODE_ENV === 'development' ? 'BYPASS_DEV_MODE' : altchaToken; - - if (!token && !altchaVerified) { + if (!altchaToken) { setError('Please complete the security verification'); return; } setLoading(true); try { - await login(emailRef.current, passwordRef.current, token); + await login(emailRef.current, passwordRef.current, altchaToken); router.push('/'); } catch (err: any) { setError(err.message || 'Login failed. Check your credentials.'); } finally { setLoading(false); } - }, [login, router, altchaToken, altchaVerified]); + }, [login, router, altchaToken]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -114,7 +111,7 @@ export default function LoginPage() { diff --git a/admin/src/components/Altcha.tsx b/admin/src/components/Altcha.tsx index cb1a7e8..af864d8 100644 --- a/admin/src/components/Altcha.tsx +++ b/admin/src/components/Altcha.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef } from 'react'; interface AltchaProps { challengeurl: string; @@ -9,63 +9,46 @@ interface AltchaProps { } export default function Altcha({ challengeurl, onVerified, onError }: AltchaProps) { - const widgetRef = useRef(null); - const scriptLoaded = useRef(false); + const containerRef = useRef(null); + const callbacksRef = useRef({ onVerified, onError }); + callbacksRef.current = { onVerified, onError }; - const handleStateChange = useCallback((e: Event) => { - const detail = (e as CustomEvent).detail; - if (detail?.state === 'verified' && detail?.payload) { - onVerified?.(detail.payload); - } else if (detail?.state === 'error') { - onError?.(); + useEffect(() => { + // Load script if not already loaded + if (!document.querySelector('script[data-altcha]')) { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/altcha@2.3.0/dist/altcha.min.js'; + script.type = 'module'; + script.async = true; + script.setAttribute('data-altcha', 'true'); + document.head.appendChild(script); } - }, [onVerified, onError]); - useEffect(() => { - if (scriptLoaded.current) return; - scriptLoaded.current = true; - - const script = document.createElement('script'); - script.src = 'https://cdn.jsdelivr.net/npm/altcha@2.3.0/dist/altcha.min.js'; - script.type = 'module'; - script.async = true; - document.head.appendChild(script); - }, []); - - useEffect(() => { - const container = widgetRef.current; + const container = containerRef.current; if (!container) return; - const observer = new MutationObserver(() => { - const widget = container.querySelector('altcha-widget'); - if (widget) { - widget.addEventListener('statechange', handleStateChange); - observer.disconnect(); - } - }); - - observer.observe(container, { childList: true, subtree: true }); - - // Also try immediately in case widget already exists + // Create the widget element + container.innerHTML = ``; const widget = container.querySelector('altcha-widget'); - if (widget) { - widget.addEventListener('statechange', handleStateChange); - observer.disconnect(); - } + if (!widget) return; - return () => { - observer.disconnect(); - const w = container.querySelector('altcha-widget'); - if (w) { - w.removeEventListener('statechange', handleStateChange); + const handler = (e: Event) => { + const detail = (e as CustomEvent).detail; + console.log('[ALTCHA] statechange:', detail); + if (detail?.state === 'verified' && detail?.payload) { + callbacksRef.current.onVerified?.(detail.payload); + } else if (detail?.state === 'error') { + callbacksRef.current.onError?.(); } }; - }, [handleStateChange]); - return ( -
` - }} /> - ); + widget.addEventListener('statechange', handler); + + return () => { + widget.removeEventListener('statechange', handler); + }; + }, [challengeurl]); + + return
; }