Files
buzz-sheet/app/api/profile/route.ts
T
2026-10-02 17:33:14 +07:00

60 lines
3.0 KiB
TypeScript

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