import { eq } from "drizzle-orm"; import sharp from "sharp"; import { getDb } from "@/db"; import { users } from "@/db/schema"; import { requireCommissionUser } from "@/lib/commission/server"; import { inspectImage } from "@/lib/media/inspect"; import { getMediaStorage, publicMediaUrl } from "@/lib/media/storage"; import { boundedBody, errorResponse, HttpError, requireSameOrigin, withUploadSlot } from "@/lib/security/http"; import { limitRequest } from "@/lib/security/rate-limit"; const MAX_PROFILE_IMAGE_BYTES = 6 * 1024 * 1024; const IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp"] as const; export async function POST(request: Request) { try { requireSameOrigin(request); const user = await requireCommissionUser(); await limitRequest("profile-update", user.id, 20); if (!request.headers.get("content-type")?.startsWith("multipart/form-data;")) throw new HttpError(415, "expected-multipart"); return await withUploadSlot(async () => { const form = await boundedBody(request, MAX_PROFILE_IMAGE_BYTES + 64 * 1024).formData(); const rawName = form.get("name"); const name = typeof rawName === "string" ? rawName.trim() : ""; if (name.length < 2 || name.length > 80) throw new HttpError(400, "invalid-display-name"); const image = form.get("image"); if (image !== null && !(image instanceof File)) throw new HttpError(400, "invalid-profile-image"); let objectKey: string | null = null; if (image instanceof File) { if (image.size === 0 || image.size > MAX_PROFILE_IMAGE_BYTES || !IMAGE_TYPES.includes(image.type as typeof IMAGE_TYPES[number])) throw new HttpError(415, "invalid-profile-image"); const bytes = new Uint8Array(await image.arrayBuffer()); await inspectImage(bytes, image.type as typeof IMAGE_TYPES[number]); const output = await sharp(bytes).rotate().resize(256, 256, { fit: "cover" }).webp({ quality: 82 }).toBuffer(); objectKey = `profiles/${crypto.randomUUID()}.webp`; await (await getMediaStorage()).write(objectKey, output, { type: "image/webp", acl: "public-read" }); } try { const [updated] = await getDb().update(users).set({ name, ...(objectKey ? { image: publicMediaUrl(objectKey) } : {}) }) .where(eq(users.id, user.id)).returning({ name: users.name, image: users.image }); if (!updated) throw new HttpError(401, "unauthorized"); if (objectKey && user.image) { const prefix = publicMediaUrl("profiles/"); if (user.image.startsWith(prefix)) { const previousKey = `profiles/${user.image.slice(prefix.length)}`; if (/^profiles\/[0-9a-f-]{36}\.webp$/u.test(previousKey)) await (await getMediaStorage()).delete(previousKey).catch(() => undefined); } } return Response.json(updated); } catch (cause) { if (objectKey) await (await getMediaStorage()).delete(objectKey).catch(() => undefined); throw cause; } }); } catch (cause) { return errorResponse(cause); } }