From aec14bc97df7ddb573124a33bcc03fd09a872655 Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Mon, 16 Feb 2026 12:24:20 -0600 Subject: [PATCH] Harden admin login Turnstile flow --- admin/src/app/login/page.tsx | 14 ++++++++++++++ go-backend/internal/handlers/admin_handler.go | 1 + 2 files changed, 15 insertions(+) diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx index 27e7f71..02b99cd 100644 --- a/admin/src/app/login/page.tsx +++ b/admin/src/app/login/page.tsx @@ -14,6 +14,7 @@ export default function LoginPage() { const [loading, setLoading] = useState(false); const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileReady, setTurnstileReady] = useState(false); + const [turnstileWidgetRendered, setTurnstileWidgetRendered] = useState(false); const turnstileRef = useRef(null); const widgetIdRef = useRef(null); const tokenRef = useRef(''); @@ -57,6 +58,7 @@ export default function LoginPage() { 'error-callback': () => { setTurnstileToken(''); tokenRef.current = ''; setTurnstileReady(false); }, 'expired-callback': () => { setTurnstileToken(''); tokenRef.current = ''; setTurnstileReady(false); }, }); + setTurnstileWidgetRendered(true); }, [performLogin]); useEffect(() => { @@ -69,6 +71,7 @@ export default function LoginPage() { setTurnstileToken(''); tokenRef.current = ''; setTurnstileReady(false); + setTurnstileWidgetRendered(false); setError(''); if (widgetIdRef.current && (window as any).turnstile) { (window as any).turnstile.reset(widgetIdRef.current); @@ -82,6 +85,11 @@ export default function LoginPage() { // Invisible Turnstile flow: // - If we don't have a token yet, execute Turnstile first. // - If we already have a token, proceed with login. + if (TURNSTILE_SITE_KEY && !widgetIdRef.current) { + setError('Security check is still loading. Please wait a moment and try again.'); + return; + } + if (TURNSTILE_SITE_KEY && widgetIdRef.current && !tokenRef.current) { setLoading(true); try { @@ -95,6 +103,12 @@ export default function LoginPage() { return; } + // If Turnstile is enabled, we must have a token at this point. + if (TURNSTILE_SITE_KEY && !tokenRef.current) { + setError('Security verification failed. Please try again.'); + return; + } + await performLogin(); }; diff --git a/go-backend/internal/handlers/admin_handler.go b/go-backend/internal/handlers/admin_handler.go index b4d2825..ead1055 100644 --- a/go-backend/internal/handlers/admin_handler.go +++ b/go-backend/internal/handlers/admin_handler.go @@ -84,6 +84,7 @@ func (h *AdminHandler) AdminLogin(c *gin.Context) { // Verify Turnstile token if h.turnstileSecret != "" { if strings.TrimSpace(req.TurnstileToken) == "" { + log.Warn().Str("email", req.Email).Msg("Admin login: missing Turnstile token") c.JSON(http.StatusBadRequest, gin.H{"error": "Security verification failed"}) return }