SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
5 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.4 KB · 29 lines typescript
Raw Blame History
1import { NextRequest, NextResponse } from "next/server";2import { z } from "zod";3import { eq } from "drizzle-orm";4import { db } from "@/db/client";5import { users } from "@/db/schema";6import { assertSameOrigin, requireUser, clientIp, revokeAllSessions } from "@/lib/auth/session";7import { requestCode, verifyCode } from "@/lib/auth/otp";8import { normalizeEmail } from "@/lib/auth/crypto";9import { audit } from "@/lib/auth/audit";10import { apiError, handle, parseBody } from "@/lib/api";11export const dynamic = "force-dynamic";12/** Étape 1 : code envoyé à la NOUVELLE adresse (l'utilisateur est déjà authentifié sur l'ancienne). */13export async function POST(req: NextRequest) {14  try { await assertSameOrigin(); const u = await requireUser(); const { newEmail } = await parseBody(req, z.object({ newEmail: z.string().max(254) }));15    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");16    const r = await requestCode(e, await clientIp(), "EMAIL_CHANGE", { userId: u.id });17    if (!r.ok) return apiError(429, "rate_limited", "Trop de demandes, réessayez plus tard");18    return NextResponse.json({ ok: true, challengeId: r.challengeId }); } catch (e) { return handle(e); }19}20/** Étape 2 : vérification du code puis changement, si l'adresse est libre ; déconnexion des autres sessions. */21export async function PATCH(req: NextRequest) {22  try { await assertSameOrigin(); const u = await requireUser(); const b = await parseBody(req, z.object({ challengeId: z.string().uuid(), code: z.string().regex(/^\d{6}$/) }));23    const r = await verifyCode(b.challengeId, b.code, await clientIp(), true);24    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");25    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");26    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 });27    return NextResponse.json({ ok: true }); } catch (e) { return handle(e); }28}29