karbe/src/app/api/uploads/finalize/route.ts
Claude Integration e2d3b6a686
All checks were successful
CI / test (pull_request) Successful in 2m21s
feat: variantes responsives 320/800/1600 via sharp + srcset partout (Reels, cards, galerie, favoris)
2026-06-02 01:05:25 +00:00

89 lines
3.1 KiB
TypeScript

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 });
}