From 942fe2e397d56245d5e2b09df3fdfa1148fdc6fe Mon Sep 17 00:00:00 2001 From: adevopg Date: Tue, 14 Jul 2026 21:17:37 +0000 Subject: [PATCH] Fix captcha: resetear Turnstile tras un fallo (token de un solo uso) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Al fallar (p.ej. contraseña incorrecta) el token de Turnstile ya se consumió en el servidor; el reintento reenviaba el mismo token y daba "captcha fallida" aunque estuviera resuelto. Se resetea el widget (setCaptcha('') + captchaKey++ con , mismo patrón que recover/restore/quest) en login, create-account y trade-points (venta y canje). El check de código no usa captcha, no se resetea ahí. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/[locale]/create-account/RegisterForm.tsx | 8 +++++++- web-next/app/[locale]/log-in/LoginForm.tsx | 9 ++++++++- web-next/components/TradePointsForm.tsx | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web-next/app/[locale]/create-account/RegisterForm.tsx b/web-next/app/[locale]/create-account/RegisterForm.tsx index a99b7aa..deb473d 100644 --- a/web-next/app/[locale]/create-account/RegisterForm.tsx +++ b/web-next/app/[locale]/create-account/RegisterForm.tsx @@ -62,6 +62,7 @@ export function RegisterForm() { const [notUs, setNotUs] = useState(false) const [busy, setBusy] = useState(false) const [captcha, setCaptcha] = useState('') + const [captchaKey, setCaptchaKey] = useState(0) // remonta el widget para pedir un token nuevo const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) const canSubmit = accepted && notUs && !busy @@ -102,9 +103,14 @@ export function RegisterForm() { } else { const key = (ERROR_KEYS as readonly string[]).includes(data.error ?? '') ? data.error! : 'genericError' setMessage({ ok: false, text: t(key) }) + // El token de Turnstile es de un solo uso: resetear tras un fallo. + setCaptcha('') + setCaptchaKey((k) => k + 1) } } catch { setMessage({ ok: false, text: t('genericError') }) + setCaptcha('') + setCaptchaKey((k) => k + 1) } finally { setBusy(false) } @@ -162,7 +168,7 @@ export function RegisterForm() { - + diff --git a/web-next/app/[locale]/log-in/LoginForm.tsx b/web-next/app/[locale]/log-in/LoginForm.tsx index ef71b60..c8885b3 100644 --- a/web-next/app/[locale]/log-in/LoginForm.tsx +++ b/web-next/app/[locale]/log-in/LoginForm.tsx @@ -14,6 +14,7 @@ export function LoginForm() { const [showPw, setShowPw] = useState(false) const [busy, setBusy] = useState(false) const [captcha, setCaptcha] = useState('') + const [captchaKey, setCaptchaKey] = useState(0) // remonta el widget para pedir un token nuevo const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) async function handleSubmit(e: React.FormEvent) { @@ -43,10 +44,16 @@ export function LoginForm() { } else { const key = (ERROR_KEYS as readonly string[]).includes(data.error ?? '') ? data.error! : 'genericError' setMessage({ ok: false, text: t(key) }) + // El token de Turnstile es de un solo uso: tras un fallo hay que resetear + // el widget para emitir uno nuevo, o el reintento daría "captcha ya usado". + setCaptcha('') + setCaptchaKey((k) => k + 1) setBusy(false) } } catch { setMessage({ ok: false, text: t('genericError') }) + setCaptcha('') + setCaptchaKey((k) => k + 1) setBusy(false) } } @@ -93,7 +100,7 @@ export function LoginForm() { - + diff --git a/web-next/components/TradePointsForm.tsx b/web-next/components/TradePointsForm.tsx index 727301f..c0b95c0 100644 --- a/web-next/components/TradePointsForm.tsx +++ b/web-next/components/TradePointsForm.tsx @@ -89,6 +89,7 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible: const [password, setPassword] = useState('') const [token, setToken] = useState('') const [captcha, setCaptcha] = useState('') + const [captchaKey, setCaptchaKey] = useState(0) const [busy, setBusy] = useState(false) const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) const [created, setCreated] = useState<{ code: string; points: number; gold: number } | null>(null) @@ -123,9 +124,13 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible: setToken('') } else { setMessage({ ok: false, text: tradeError(data.error) }) + setCaptcha('') + setCaptchaKey((k) => k + 1) } } catch { setMessage({ ok: false, text: tradeError() }) + setCaptcha('') + setCaptchaKey((k) => k + 1) } finally { setBusy(false) } @@ -146,7 +151,7 @@ function SellForm({ characters, visible }: { characters: CharOption[]; visible:
setToken(e.target.value)} placeholder={t('trade.tokenPlaceholder')} required /> - + @@ -178,6 +183,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b const [password, setPassword] = useState('') const [token, setToken] = useState('') const [captcha, setCaptcha] = useState('') + const [captchaKey, setCaptchaKey] = useState(0) const [busy, setBusy] = useState(false) const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) const [checkInfo, setCheckInfo] = useState<{ points: number; gold: number } | null>(null) @@ -230,9 +236,13 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b setCheckInfo(null) } else { setMessage({ ok: false, text: tradeError(data.error) }) + setCaptcha('') + setCaptchaKey((k) => k + 1) } } catch { setMessage({ ok: false, text: tradeError() }) + setCaptcha('') + setCaptchaKey((k) => k + 1) } finally { setBusy(false) } @@ -254,7 +264,7 @@ function BuyForm({ characters, visible }: { characters: CharOption[]; visible: b
setToken(e.target.value)} placeholder={t('trade.tokenPlaceholder')} required /> - +