58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
|
|
import { createPasswordResetToken } from "@/lib/password-reset";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { sendPasswordReset } from "@/lib/email";
|
|
import { recordAudit } from "@/lib/admin/audit";
|
|
import { rateLimitRequest } from "@/lib/rate-limit";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const schema = z.object({
|
|
email: z.string().trim().toLowerCase().email(),
|
|
});
|
|
|
|
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://karbe.cosmolan.fr";
|
|
|
|
export async function POST(req: Request) {
|
|
const rl = rateLimitRequest(req, "password-reset", 60 * 60 * 1000, 3);
|
|
if (!rl.ok) {
|
|
return NextResponse.json(
|
|
{ error: `Trop de tentatives. Réessayez dans ${rl.retryAfter}s.` },
|
|
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } },
|
|
);
|
|
}
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Corps JSON invalide." }, { status: 400 });
|
|
}
|
|
const parsed = schema.safeParse(body);
|
|
if (!parsed.success) {
|
|
// Réponse générique pour ne pas leak la validité du format à un attaquant.
|
|
return NextResponse.json({ ok: true });
|
|
}
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: parsed.data.email },
|
|
select: { id: true, email: true, firstName: true, isActive: true },
|
|
});
|
|
|
|
if (user && user.isActive) {
|
|
const token = await createPasswordResetToken(user.id);
|
|
const resetUrl = `${SITE_URL}/mot-de-passe-oublie/${token}`;
|
|
sendPasswordReset(user.email, resetUrl).catch(() => {});
|
|
await recordAudit({
|
|
scope: "public.password",
|
|
event: "reset.request",
|
|
target: user.id,
|
|
actorEmail: user.email,
|
|
details: {},
|
|
});
|
|
}
|
|
|
|
// Réponse identique que l'email existe ou non (énumération-safe).
|
|
return NextResponse.json({ ok: true });
|
|
}
|