Files
old.cms.fellies.org/apps/admin/src/app/api/media/upload/route.ts

229 lines
5.3 KiB
TypeScript

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<string, AllowedRule> = {
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 parseOptionalDateField(formData: FormData, field: string): Date | undefined {
const value = parseTextField(formData, field)
if (!value) {
return undefined
}
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) {
return undefined
}
return parsed
}
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<Response> {
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"),
licenseType: parseOptionalField(formData, "licenseType"),
licenseUrl: parseOptionalField(formData, "licenseUrl"),
usageContext: parseOptionalField(formData, "usageContext"),
location: parseOptionalField(formData, "location"),
capturedAt: parseOptionalDateField(formData, "capturedAt"),
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 },
)
}
}