import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { NextRequest, NextResponse } from "next/server"; import { requireFamily } from "@/lib/auth"; import sharp from "sharp"; import { randomUUID } from "crypto"; const ALLOWED_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/heic"]; const MAX_BYTES = 8 * 1024 * 1024; // 8MB function getR2Config() { return { accountId: process.env.R2_ACCOUNT_ID!, accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretKey: process.env.R2_SECRET_ACCESS_KEY!, bucket: process.env.R2_BUCKET_NAME!, publicUrl: process.env.R2_PUBLIC_URL, }; } function makeClient(R2: ReturnType) { return new S3Client({ region: "auto", endpoint: `https://${R2.accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: R2.accessKeyId, secretAccessKey: R2.secretKey }, }); } // POST /api/garments/upload // Accepts multipart/form-data with a single "file" field. // Returns { imageKey, thumbKey, imageUrl, thumbUrl } export async function POST(req: NextRequest) { const auth = await requireFamily(); if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status }); // family_id comes from session — never from request params const familyId = auth.session!.familyId!; const R2 = getR2Config(); if (!R2.accountId || !R2.accessKeyId || !R2.secretKey || !R2.bucket) { return NextResponse.json({ error: "R2 not configured" }, { status: 500 }); } let formData: FormData; try { formData = await req.formData(); } catch { return NextResponse.json({ error: "Invalid form data" }, { status: 400 }); } const file = formData.get("file") as File | null; if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 }); if (!ALLOWED_TYPES.includes(file.type)) { return NextResponse.json({ error: "Unsupported file type" }, { status: 400 }); } if (file.size > MAX_BYTES) { return NextResponse.json({ error: "File too large (max 8MB)" }, { status: 400 }); } const ext = file.name.split(".").pop()?.toLowerCase() || "jpg"; const id = randomUUID(); const imageKey = `garments/${familyId}/${id}-original.${ext}`; const thumbKey = `garments/${familyId}/${id}-thumb.webp`; const arrayBuffer = await file.arrayBuffer(); const originalBuffer = Buffer.from(arrayBuffer); const thumbBuffer = await sharp(originalBuffer) .resize(400, 400, { fit: "inside", withoutEnlargement: true }) .webp({ quality: 70 }) .toBuffer(); const client = makeClient(R2); await Promise.all([ client.send(new PutObjectCommand({ Bucket: R2.bucket, Key: imageKey, Body: originalBuffer, ContentType: file.type, })), client.send(new PutObjectCommand({ Bucket: R2.bucket, Key: thumbKey, Body: thumbBuffer, ContentType: "image/webp", })), ]); const baseUrl = R2.publicUrl || `https://pub-${R2.accountId}.r2.dev`; return NextResponse.json({ imageKey, thumbKey, imageUrl: `${baseUrl}/${imageKey}`, thumbUrl: `${baseUrl}/${thumbKey}`, }); }