TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import { NextRequest, NextResponse } from "next/server";2import { z } from "zod";3import { cookies } from "next/headers";4import { verifyCode } from "@/lib/auth/otp";5import { assertSameOrigin, clientIp, createSession, cookieOptions, COOKIE, userAgent } from "@/lib/auth/session";6import { handle, parseBody } from "@/lib/api";7import { hit } from "@/lib/auth/ratelimit";8import { ipHash } from "@/lib/auth/crypto";9export const dynamic = "force-dynamic";10const Body = z.object({ challengeId: z.string().uuid(), code: z.string().regex(/^\d{6}$/), acceptTerms: z.boolean().optional().default(false) });11const MSG: Record<string, string> = { invalid: "Code invalide.", expired: "Ce code a expiré. Demandez-en un nouveau.", too_many_attempts: "Trop d'essais. Demandez un nouveau code.", not_found: "Demande introuvable. Recommencez.", terms_required: "Nouveau compte : acceptez les conditions puis confirmez le code." };12export async function POST(req: NextRequest) {13 try {14 await assertSameOrigin();15 const { challengeId, code, acceptTerms } = await parseBody(req, Body);16 const ip = await clientIp();17 const rl = await hit(`otp:verify:${ipHash(ip)}`, 30, 600); if (!rl.ok) return NextResponse.json({ error: { code: "rate_limited", message: "Trop de tentatives. Réessayez plus tard." } }, { status: 429 });18 const r = await verifyCode(challengeId, code, ip, acceptTerms);19 if (!r.ok) return NextResponse.json({ error: { code: r.reason, message: MSG[r.reason] } }, { status: r.reason === "terms_required" ? 409 : 400 });20 const s = await createSession(r.userId, { ip, device: await userAgent() });21 (await cookies()).set(COOKIE, s.token, cookieOptions(s.expiresAt));22 return NextResponse.json({ ok: true, created: r.created, user: { id: r.userId, email: r.email } });23 } catch (e) { return handle(e); }24}25