From e8080be405fbe514085cc6585440f426a4861cf7 Mon Sep 17 00:00:00 2001 From: Andil Date: Sun, 20 Sep 2026 00:37:37 +0000 Subject: [PATCH 1/4] fix: explicitly load Cloudflare Turnstile --- frontend/src/components/Captcha.jsx | 121 +++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Captcha.jsx b/frontend/src/components/Captcha.jsx index c56e189..87626cf 100644 --- a/frontend/src/components/Captcha.jsx +++ b/frontend/src/components/Captcha.jsx @@ -1,19 +1,114 @@ -import { Turnstile } from '@marsidev/react-turnstile' -import { CAPTCHA_ENABLED, TURNSTILE_SITE_KEY } from '../config/captcha' +import { useEffect, useRef } from 'react' +import { TURNSTILE_SITE_KEY } from '../config/captcha' -// `ref` expose reset() : un jeton Turnstile n'est valable qu'une fois, il faut -// en redemander un après chaque envoi refusé. -export default function Captcha({ ref, onToken }) { - if (!CAPTCHA_ENABLED) return null +const TURNSTILE_SCRIPT_ID = 'cf-turnstile-script' +const TURNSTILE_SCRIPT_URL = + 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit' + +export default function Captcha({ onToken }) { + const containerRef = useRef(null) + const widgetIdRef = useRef(null) + + useEffect(() => { + if (!TURNSTILE_SITE_KEY || !containerRef.current) return + + let cancelled = false + + const renderWidget = () => { + if ( + cancelled || + !containerRef.current || + !window.turnstile + ) { + return + } + + if (widgetIdRef.current !== null) { + try { + window.turnstile.remove(widgetIdRef.current) + } catch { + // Rien à faire + } + + widgetIdRef.current = null + } + + containerRef.current.innerHTML = '' + + widgetIdRef.current = window.turnstile.render( + containerRef.current, + { + sitekey: TURNSTILE_SITE_KEY, + language: 'fr', + size: 'flexible', + + callback: (token) => { + onToken(token) + }, + + 'expired-callback': () => { + onToken('') + }, + + 'error-callback': () => { + onToken('') + }, + } + ) + } + + const existingScript = document.getElementById( + TURNSTILE_SCRIPT_ID + ) + + if (window.turnstile) { + renderWidget() + return + } + + const previousCallback = window.onloadTurnstileCallback + + window.onloadTurnstileCallback = () => { + previousCallback?.() + renderWidget() + } + + if (!existingScript) { + const script = document.createElement('script') + + script.id = TURNSTILE_SCRIPT_ID + script.src = `${TURNSTILE_SCRIPT_URL}&onload=onloadTurnstileCallback` + script.async = true + script.defer = true + + document.head.appendChild(script) + } + + return () => { + cancelled = true + + if ( + widgetIdRef.current !== null && + window.turnstile + ) { + try { + window.turnstile.remove(widgetIdRef.current) + } catch { + // Rien à faire + } + } + + widgetIdRef.current = null + } + }, [onToken]) + + if (!TURNSTILE_SITE_KEY) return null return ( - onToken('')} - onError={() => onToken('')} +
) } From 3a5eed6b9644c9c65a197f1dcc84cc281bdf1a9c Mon Sep 17 00:00:00 2001 From: Andil Date: Sun, 20 Sep 2026 00:49:03 +0000 Subject: [PATCH 2/4] fix: load Turnstile without onload callback --- frontend/src/components/Captcha.jsx | 34 +++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Captcha.jsx b/frontend/src/components/Captcha.jsx index 87626cf..6c2df87 100644 --- a/frontend/src/components/Captcha.jsx +++ b/frontend/src/components/Captcha.jsx @@ -27,7 +27,7 @@ export default function Captcha({ onToken }) { try { window.turnstile.remove(widgetIdRef.current) } catch { - // Rien à faire + // Widget déjà supprimé } widgetIdRef.current = null @@ -41,6 +41,7 @@ export default function Captcha({ onToken }) { sitekey: TURNSTILE_SITE_KEY, language: 'fr', size: 'flexible', + appearance: 'always', callback: (token) => { onToken(token) @@ -66,23 +67,28 @@ export default function Captcha({ onToken }) { return } - const previousCallback = window.onloadTurnstileCallback + if (existingScript) { + existingScript.addEventListener('load', renderWidget, { + once: true, + }) - window.onloadTurnstileCallback = () => { - previousCallback?.() - renderWidget() + return () => { + cancelled = true + } } - if (!existingScript) { - const script = document.createElement('script') + const script = document.createElement('script') - script.id = TURNSTILE_SCRIPT_ID - script.src = `${TURNSTILE_SCRIPT_URL}&onload=onloadTurnstileCallback` - script.async = true - script.defer = true + script.id = TURNSTILE_SCRIPT_ID + script.src = TURNSTILE_SCRIPT_URL + script.async = true + script.defer = true - document.head.appendChild(script) - } + script.addEventListener('load', renderWidget, { + once: true, + }) + + document.head.appendChild(script) return () => { cancelled = true @@ -94,7 +100,7 @@ export default function Captcha({ onToken }) { try { window.turnstile.remove(widgetIdRef.current) } catch { - // Rien à faire + // Widget déjà supprimé } } From 7c1e4cf3e2772397a13929e8242aed96bd43854c Mon Sep 17 00:00:00 2001 From: Andil Date: Sun, 20 Sep 2026 01:07:37 +0000 Subject: [PATCH 3/4] fix: load Turnstile site key in frontend build --- Jenkinsfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 02fe649..b416518 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -334,6 +334,13 @@ stage('SonarQube Quality Gate') { steps { sh ''' + set -a + . /opt/furious-ducks/secrets/tiptop-mail.env + set +a + + test -n "${TURNSTILE_SITE_KEY}" + echo "Turnstile site key chargée (longueur: ${#TURNSTILE_SITE_KEY})" + docker build \ -t "${BACKEND_IMAGE}:${IMAGE_TAG}" \ backend From d56b91479f2a7f8bb592c3d2d3e8fb1c76a88bb9 Mon Sep 17 00:00:00 2001 From: Andil Date: Sun, 20 Sep 2026 01:25:06 +0000 Subject: [PATCH 4/4] fix: prevent turnstile during prerender --- frontend/src/components/Captcha.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Captcha.jsx b/frontend/src/components/Captcha.jsx index 6c2df87..47d13e5 100644 --- a/frontend/src/components/Captcha.jsx +++ b/frontend/src/components/Captcha.jsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react' -import { TURNSTILE_SITE_KEY } from '../config/captcha' +import { CAPTCHA_ENABLED, TURNSTILE_SITE_KEY } from '../config/captcha' const TURNSTILE_SCRIPT_ID = 'cf-turnstile-script' const TURNSTILE_SCRIPT_URL = @@ -10,7 +10,9 @@ export default function Captcha({ onToken }) { const widgetIdRef = useRef(null) useEffect(() => { - if (!TURNSTILE_SITE_KEY || !containerRef.current) return + if (!CAPTCHA_ENABLED || !TURNSTILE_SITE_KEY || !containerRef.current) { + return + } let cancelled = false @@ -108,7 +110,9 @@ export default function Captcha({ onToken }) { } }, [onToken]) - if (!TURNSTILE_SITE_KEY) return null + if (!CAPTCHA_ENABLED || !TURNSTILE_SITE_KEY) { + return null + } return (