import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { eq } from "drizzle-orm"; import { db } from "@/db/client"; import { users } from "@/db/schema"; import { assertSameOrigin, requireUser, clientIp, revokeAllSessions } from "@/lib/auth/session"; import { requestCode, verifyCode } from "@/lib/auth/otp"; import { normalizeEmail } from "@/lib/auth/crypto"; import { audit } from "@/lib/auth/audit"; import { apiError, handle, parseBody } from "@/lib/api"; export const dynamic = "force-dynamic"; /** Étape 1 : code envoyé à la NOUVELLE adresse (l'utilisateur est déjà authentifié sur l'ancienne). */ export async function POST(req: NextRequest) { try { await assertSameOrigin(); const u = await requireUser(); const { newEmail } = await parseBody(req, z.object({ newEmail: z.string().max(254) })); const e = normalizeEmail(newEmail); if (!e) return apiError(400, "invalid_email", "Adresse invalide"); if (e === u.email) return apiError(400, "same_email", "C'est déjà votre adresse"); const r = await requestCode(e, await clientIp(), "EMAIL_CHANGE", { userId: u.id }); if (!r.ok) return apiError(429, "rate_limited", "Trop de demandes, réessayez plus tard"); return NextResponse.json({ ok: true, challengeId: r.challengeId }); } catch (e) { return handle(e); } } /** Étape 2 : vérification du code puis changement, si l'adresse est libre ; déconnexion des autres sessions. */ export async function PATCH(req: NextRequest) { try { await assertSameOrigin(); const u = await requireUser(); const b = await parseBody(req, z.object({ challengeId: z.string().uuid(), code: z.string().regex(/^\d{6}$/) })); const r = await verifyCode(b.challengeId, b.code, await clientIp(), true); if (!r.ok) return apiError(400, r.reason, "Code invalide ou expiré"); if (r.userId !== u.id) return apiError(403, "forbidden", "Ce code ne correspond pas à votre compte"); const [taken] = await db().select({ id: users.id }).from(users).where(eq(users.email, r.email)).limit(1); if (taken) return apiError(409, "email_taken", "Cette adresse est déjà utilisée"); await db().update(users).set({ email: r.email, emailVerifiedAt: new Date(), updatedAt: new Date() }).where(eq(users.id, u.id)); await revokeAllSessions(u.id, u.sessionId); await audit("me.email_change", { actorId: u.id }); return NextResponse.json({ ok: true }); } catch (e) { return handle(e); } }