feat: reset password + page mon-compte (RGPD) + facettes recherche (prix max, équipements)
All checks were successful
CI / test (pull_request) Successful in 2m19s
All checks were successful
CI / test (pull_request) Successful in 2m19s
This commit is contained in:
parent
0b5e5408e8
commit
a6df96db7e
19 changed files with 922 additions and 0 deletions
103
src/app/api/me/export/route.ts
Normal file
103
src/app/api/me/export/route.ts
Normal 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}"`,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
50
src/app/api/password/reset-request/route.ts
Normal file
50
src/app/api/password/reset-request/route.ts
Normal 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 });
|
||||
}
|
||||
40
src/app/api/password/reset/route.ts
Normal file
40
src/app/api/password/reset/route.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue