import { NextResponse } from "next/server" import { auth } from "@/lib/auth" import { toNextJsHandler } from "better-auth/next-js" import { verifyTurnstile } from "@/lib/turnstile" const handlers = toNextJsHandler(auth) // The auth pages post to server actions, which call `auth.api.*` in-process and // run their own verifyTurnstile() check. This route is the *other* door into the // same endpoints — a direct HTTP POST — and without this gate it accepts // unlimited credential guesses and email sends with no bot protection at all. // // Only credential-bearing / email-triggering POSTs are gated. GET is untouched // (OAuth callbacks, verify-email links, get-session), and `/sign-in/social` is // left open because it only starts a redirect to the provider. const CAPTCHA_PROTECTED = new Set([ "/sign-in/email", "/sign-up/email", "/request-password-reset", "/reset-password", "/send-verification-email", ]) /** * Turnstile token from a header (preferred — leaves the body stream untouched) * or, for clients that submit it inline, from a cloned JSON body. */ async function captchaToken(request: Request): Promise { const header = request.headers.get("x-captcha-response") ?? request.headers.get("cf-turnstile-response") if (header) return header try { const body = (await request.clone().json()) as Record const inline = body?.["cf-turnstile-response"] ?? body?.captchaToken return typeof inline === "string" ? inline : null } catch { // Not JSON, or no body — treated as a missing token, which fails closed. return null } } export const GET = handlers.GET export async function POST(request: Request) { const path = new URL(request.url).pathname.replace(/^\/api\/auth/, "") if (CAPTCHA_PROTECTED.has(path)) { const ok = await verifyTurnstile( await captchaToken(request), request.headers.get("x-forwarded-for") ) if (!ok) { return NextResponse.json( { message: "Verification challenge required.", code: "CAPTCHA_VERIFICATION_FAILED", }, { status: 403 } ) } } return handlers.POST(request) }