Cloudflare Turnstile en login y registro (Next.js)

- lib/turnstile.ts: verifyTurnstile(token, ip) contra siteverify (fail-closed).
- components/Turnstile.tsx: widget cliente (carga el script de Cloudflare, callback
  con el token). Site key por NEXT_PUBLIC_TURNSTILE_SITE_KEY; secreto server-only.
- LoginForm/RegisterForm: widget + token en el POST; submit deshabilitado hasta
  resolver el captcha (si hay site key).
- Routes login/register: verifican el token antes de continuar (error captchaFailed).
- Catálogos: captchaFailed.

Verificado: POST sin token -> captchaFailed en login y registro (enforcement server).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:04:22 +00:00
parent 4a783d165f
commit aa61f79a55
8 changed files with 134 additions and 9 deletions
+8
View File
@@ -1,18 +1,26 @@
import { authenticate, getGameAccounts } from '@/lib/auth'
import { getSession } from '@/lib/session'
import { setGameAccountSession } from '@/lib/account-session'
import { verifyTurnstile } from '@/lib/turnstile'
export async function POST(request: Request) {
let email = ''
let password = ''
let turnstileToken = ''
try {
const body = await request.json()
email = String(body.email ?? '').trim()
password = String(body.password ?? '')
turnstileToken = String(body.turnstileToken ?? '')
} catch {
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
}
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim()
if (!(await verifyTurnstile(turnstileToken, ip))) {
return Response.json({ success: false, error: 'captchaFailed' })
}
if (!email || !password) {
return Response.json({ success: false, error: 'missingFields' })
}
+7
View File
@@ -1,4 +1,5 @@
import { registerAccount } from '@/lib/register'
import { verifyTurnstile } from '@/lib/turnstile'
export async function POST(request: Request) {
let body: Record<string, string>
@@ -7,6 +8,12 @@ export async function POST(request: Request) {
} catch {
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
}
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim()
if (!(await verifyTurnstile(body.turnstileToken ?? '', ip))) {
return Response.json({ success: false, error: 'captchaFailed' })
}
const result = await registerAccount({
password: body.password ?? '',
confPassword: body.confPassword ?? '',