import { randomUUID } from "node:crypto" import { hasPermission } from "@cms/content/rbac" import { createMediaAsset } from "@cms/db" import { auth, resolveRoleFromAuthSession } from "@/lib/auth/server" import { storeUpload } from "@/lib/media/storage" export const runtime = "nodejs" const MAX_UPLOAD_BYTES = Number(process.env.CMS_MEDIA_UPLOAD_MAX_BYTES ?? 25 * 1024 * 1024) type AllowedRule = { mimePrefix?: string mimeExact?: string[] } const ALLOWED_MIME_BY_TYPE: Record = { artwork: { mimePrefix: "image/", }, banner: { mimePrefix: "image/", }, promotion: { mimePrefix: "image/", }, video: { mimePrefix: "video/", }, gif: { mimeExact: ["image/gif"], }, generic: { mimePrefix: "", }, } function parseTextField(formData: FormData, field: string): string { const value = formData.get(field) return typeof value === "string" ? value.trim() : "" } function parseOptionalField(formData: FormData, field: string): string | undefined { const value = parseTextField(formData, field) return value.length > 0 ? value : undefined } function parseTags(formData: FormData): string[] { const value = parseTextField(formData, "tags") if (!value) { return [] } return value .split(",") .map((item) => item.trim()) .filter((item) => item.length > 0) } function deriveTitleFromFilename(fileName: string): string { const trimmed = fileName.trim() if (!trimmed) { return "Untitled media" } const dotIndex = trimmed.lastIndexOf(".") const base = dotIndex > 0 ? trimmed.slice(0, dotIndex) : trimmed const normalized = base.trim() return normalized.length > 0 ? normalized : "Untitled media" } function isMimeAllowed(mediaType: string, mimeType: string): boolean { const rule = ALLOWED_MIME_BY_TYPE[mediaType] if (!rule) { return false } if (rule.mimeExact?.includes(mimeType)) { return true } if (rule.mimePrefix === "") { return true } return rule.mimePrefix ? mimeType.startsWith(rule.mimePrefix) : false } function badRequest(message: string): Response { return Response.json( { message, }, { status: 400 }, ) } export async function POST(request: Request): Promise { const session = await auth.api .getSession({ headers: request.headers, }) .catch(() => null) const role = resolveRoleFromAuthSession(session) if (!role) { return Response.json( { message: "Unauthorized", }, { status: 401 }, ) } if (!hasPermission(role, "media:write", "team")) { return Response.json( { message: "Missing permission: media:write", }, { status: 403 }, ) } const formData = await request.formData().catch(() => null) if (!formData) { return badRequest("Invalid form payload.") } const type = parseTextField(formData, "type") const fileEntry = formData.get("file") if (!type) { return badRequest("Type is required.") } if (!(fileEntry instanceof File)) { return badRequest("File is required.") } if (fileEntry.size === 0) { return badRequest("File is empty.") } if (fileEntry.size > MAX_UPLOAD_BYTES) { return badRequest( `File is too large. Maximum upload is ${Math.floor(MAX_UPLOAD_BYTES / 1024 / 1024)} MB.`, ) } if (!isMimeAllowed(type, fileEntry.type)) { return badRequest(`File type ${fileEntry.type || "unknown"} is not allowed for ${type}.`) } const title = parseTextField(formData, "title") || deriveTitleFromFilename(fileEntry.name) const mediaAssetId = randomUUID() const variant = "original" const fileRole = "original" try { const stored = await storeUpload({ file: fileEntry, assetId: mediaAssetId, variant, fileRole, }) const created = await createMediaAsset({ id: mediaAssetId, title, type, description: parseOptionalField(formData, "description"), altText: parseOptionalField(formData, "altText"), source: parseOptionalField(formData, "source"), copyright: parseOptionalField(formData, "copyright"), author: parseOptionalField(formData, "author"), tags: parseTags(formData), storageKey: stored.storageKey, mimeType: fileEntry.type || undefined, sizeBytes: fileEntry.size, isPublished: parseTextField(formData, "isPublished") === "true", }) return Response.json( { id: created.id, provider: stored.provider, warning: stored.fallbackReason, notice: "Media uploaded successfully.", }, { status: 201 }, ) } catch (error) { const message = error instanceof Error ? error.message : "Upload failed. Please try again." return Response.json( { message, }, { status: 500 }, ) } }