// Verifica del codice OTP: se corretto, emette il cookie di sessione (90 giorni). import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { rateLimit } from "@/lib/rate-limit"; import { getClientIdentityByToken } from "@/lib/client-view"; import { verifyOtp } from "@/lib/otp"; import { SESSION_MAX_AGE_SECONDS, createSessionValue, sessionCookieName, } from "@/lib/client-session"; const schema = z.object({ token: z.string().min(1), email: z.string().trim().toLowerCase().email(), code: z.string().trim().regex(/^\d{6}$/, "Il codice ha 6 cifre"), }); // Codice sbagliato, scaduto o inesistente danno lo stesso messaggio: dire // "scaduto" a un codice mai emesso confermerebbe che l'email è in whitelist. const INVALID = "Codice non valido o scaduto. Richiedine uno nuovo."; export async function POST(request: NextRequest) { const ip = request.headers.get("x-forwarded-for") ?? request.headers.get("x-real-ip") ?? "unknown"; try { const parsed = schema.safeParse(await request.json()); if (!parsed.success) { return NextResponse.json({ error: INVALID }, { status: 400 }); } const { token, email, code } = parsed.data; // 5 tentativi ogni 15 minuti per IP+cliente: con 1M di codici possibili e // TTL 15 min, indovinare per forza bruta è fuori portata. if (!rateLimit(`otp-vrf:${ip}:${token}`, 5, 15 * 60_000)) { return NextResponse.json( { error: "Troppi tentativi. Riprova tra qualche minuto." }, { status: 429 } ); } const client = await getClientIdentityByToken(token); if (!client) return NextResponse.json({ error: INVALID }, { status: 400 }); const result = await verifyOtp(client.id, email, code); if (!result.ok) { return NextResponse.json({ error: INVALID }, { status: 400 }); } const response = NextResponse.json({ success: true }, { status: 200 }); response.cookies.set({ name: sessionCookieName(client.id), value: await createSessionValue(client.id, email), httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/client", maxAge: SESSION_MAX_AGE_SECONDS, }); return response; } catch (err) { console.error("/api/client/otp/verify error:", err); return NextResponse.json({ error: "Errore interno" }, { status: 500 }); } }