import { NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/auth"; import { MediaType, UserRole } from "@/generated/prisma/enums"; import { prisma } from "@/lib/prisma"; import { classifyMime } from "@/lib/uploads"; import { recordAudit } from "@/lib/admin/audit"; import { generateImageVariants } from "@/lib/variants-server"; export const runtime = "nodejs"; const schema = z.object({ carbetId: z.string().min(1), s3Key: z.string().min(5).max(500), s3Url: z.string().url(), mime: z.string().min(3).max(100), }); export async function POST(req: Request) { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); } const parsed = schema.safeParse(await req.json().catch(() => ({}))); if (!parsed.success) { return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Payload invalide" }, { status: 400 }); } const kind = classifyMime(parsed.data.mime); if (!kind) return NextResponse.json({ error: "Type non supporté" }, { status: 400 }); const carbet = await prisma.carbet.findUnique({ where: { id: parsed.data.carbetId }, select: { id: true, ownerId: true }, }); if (!carbet) return NextResponse.json({ error: "Carbet introuvable" }, { status: 404 }); const isOwner = carbet.ownerId === session.user.id; const isAdmin = session.user.role === UserRole.ADMIN; if (!isOwner && !isAdmin) { return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); } // S3Key doit appartenir au carbet — verrou pour éviter qu'un user finalise une key étrangère. if (!parsed.data.s3Key.startsWith(`carbets/${carbet.id}/`)) { return NextResponse.json({ error: "s3Key invalide pour ce carbet" }, { status: 400 }); } const existingCount = await prisma.media.count({ where: { carbetId: carbet.id } }); const media = await prisma.media.create({ data: { carbetId: carbet.id, type: kind === "photo" ? MediaType.PHOTO : MediaType.VIDEO, s3Key: parsed.data.s3Key, s3Url: parsed.data.s3Url, sortOrder: existingCount, }, select: { id: true, type: true, s3Url: true, s3Key: true, sortOrder: true }, }); await recordAudit({ scope: "uploads", event: "media.finalize", target: media.id, actorEmail: session.user.email ?? null, details: { carbetId: carbet.id, kind }, }); // Génération des variantes responsives (best-effort, n'échoue pas la requête). // L'utilisateur attend quelques secondes mais l'expérience derrière est bien meilleure. try { const variants = await generateImageVariants({ originalS3Key: parsed.data.s3Key, mime: parsed.data.mime, }); if (!variants.skipped) { const okCount = variants.results.filter((r) => r.ok).length; await recordAudit({ scope: "uploads", event: "media.variants", target: media.id, actorEmail: session.user.email ?? null, details: { generated: okCount, total: variants.results.length }, }); } } catch (e) { console.error("[uploads] variants generation error:", e); } return NextResponse.json({ media }); }