feat: reset password + page mon-compte (RGPD) + facettes recherche (prix max, équipements)
All checks were successful
CI / test (pull_request) Successful in 2m19s

This commit is contained in:
Claude Integration 2026-06-01 10:16:37 +00:00
parent 0b5e5408e8
commit a6df96db7e
19 changed files with 922 additions and 0 deletions

View file

@ -0,0 +1,103 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { recordAudit } from "@/lib/admin/audit";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** RGPD article 20 — droit à la portabilité. Renvoie un JSON avec toutes les données utilisateur. */
export async function GET() {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
const userId = session.user.id;
const [user, bookings, reviews, carbets, subscriptions] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
role: true,
avatarUrl: true,
isActive: true,
createdAt: true,
updatedAt: true,
organizationId: true,
},
}),
prisma.booking.findMany({
where: { tenantId: userId },
select: {
id: true,
carbetId: true,
startDate: true,
endDate: true,
guestCount: true,
status: true,
paymentStatus: true,
amount: true,
currency: true,
createdAt: true,
},
}),
prisma.review.findMany({
where: { authorId: userId },
select: {
id: true,
bookingId: true,
carbetId: true,
rating: true,
comment: true,
createdAt: true,
},
}),
prisma.carbet.findMany({
where: { ownerId: userId },
select: { id: true, slug: true, title: true, status: true, createdAt: true },
}),
prisma.subscription.findMany({
where: { ownerId: userId },
select: { id: true, carbetId: true, status: true, provider: true, startedAt: true },
}),
]);
await recordAudit({
scope: "public.profile",
event: "data.export",
target: userId,
actorEmail: session.user.email ?? null,
details: {},
});
const filename = `karbe-mes-donnees-${new Date().toISOString().slice(0, 10)}.json`;
return new NextResponse(
JSON.stringify(
{
exportedAt: new Date().toISOString(),
rgpdNotice:
"Conformément à l'article 20 du RGPD. Pour exercer vos autres droits, contactez contact@karbe.cosmolan.fr.",
user,
bookings,
reviews,
carbets,
subscriptions,
},
null,
2,
),
{
status: 200,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Content-Disposition": `attachment; filename="${filename}"`,
},
},
);
}

View file

@ -0,0 +1,50 @@
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";
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) {
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 });
}

View file

@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { consumePasswordResetToken } from "@/lib/password-reset";
import { recordAudit } from "@/lib/admin/audit";
export const runtime = "nodejs";
const schema = z.object({
token: z.string().min(20).max(200),
password: z.string().min(8).max(200),
});
export async function POST(req: Request) {
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) {
return NextResponse.json(
{ error: parsed.error.issues.map((i) => i.message).join(" · ") },
{ status: 400 },
);
}
const result = await consumePasswordResetToken(parsed.data.token, parsed.data.password);
if (!result.ok) {
return NextResponse.json({ error: result.reason }, { status: 400 });
}
await recordAudit({
scope: "public.password",
event: "reset.success",
target: result.userId,
actorEmail: null,
details: {},
});
return NextResponse.json({ ok: true });
}