import type { UploadedFile } from '@admin/types' import type { NextRequest } from 'next/server' import { getAuditRequestMetadata } from '@admin/actions/audit/get-audit-request-metadata' import { mediaCacheTags } from '@admin/actions/media/types' import { saveUpload } from '@admin/actions/storage' import { requireRole, UserRole } from '@admin/auth/middleware' import db from '@admin/db' import { adminMedia } from '@admin/db/schema' import { runAdminEventHooks } from '@admin/lib/lifecycle-hooks' import { getTouchedFieldNames } from '@admin/utils/audit/get-touched-field-names' import { toActionError } from '@admin/utils/error/to-action-error' import { mapMediaRow } from '@admin/utils/media/map-media-row' import { selectMedia } from '@admin/utils/media/select-media' import { validateFiles } from '@admin/utils/validation/validate-files' import { eq } from 'drizzle-orm' import { revalidateTag } from 'next/cache' import { NextResponse } from 'next/server' export async function POST(request: NextRequest) { const actor = await requireRole([UserRole.ADMIN, UserRole.EDITOR]) try { const formData = await request.formData() const prefix = formData.get('prefix')?.toString() || 'uploads' const parsedMaxSizeInBytes = Number(formData.get('maxSizeInBytes')) const maxSizeInBytes = Number.isFinite(parsedMaxSizeInBytes) && parsedMaxSizeInBytes > 0 ? parsedMaxSizeInBytes : undefined const parsedMaxFiles = Number(formData.get('maxFiles')) const maxFiles = Number.isFinite(parsedMaxFiles) && parsedMaxFiles > 0 ? parsedMaxFiles : undefined const allowedTypesRaw = formData.get('allowedTypes')?.toString() const allowedTypes = allowedTypesRaw ? allowedTypesRaw.split(',').filter(Boolean) : [] const files: File[] = [] for (const [key, value] of formData.entries()) { if (value instanceof File && key.startsWith('file')) { files.push(value) } } if (files.length === 0) { return NextResponse.json({ success: false, error: 'No files provided' }, { status: 400 }) } const validation = validateFiles(files, { maxSizeInBytes, allowedTypes, maxFiles }) if (!validation.valid) { return NextResponse.json( { success: false, error: validation.errors .map((e) => (e.filename ? `${e.filename}: ${e.error}` : e.error)) .join('; ') }, { status: 400 } ) } const uploadedFiles: UploadedFile[] = [] const auditRequestMetadata = await getAuditRequestMetadata() for (const [index, file] of files.entries()) { const arrayBuffer = await file.arrayBuffer() const buffer = Buffer.from(arrayBuffer) const { key, url } = await saveUpload({ buffer, filename: file.name, contentType: file.type, contentLength: file.size, prefix }) const widthStr = formData.get(`width${index}`)?.toString() const heightStr = formData.get(`height${index}`)?.toString() const now = new Date().toISOString() const mediaValues = { url, filename: file.name, contentType: file.type, size: file.size, width: widthStr ? Number(widthStr) : null, height: heightStr ? Number(heightStr) : null, alt: null, tags: null } const [created] = await db .insert(adminMedia) .values({ ...mediaValues, createdAt: now, updatedAt: now, createdBy: actor.id, updatedBy: actor.id }) .returning({ id: adminMedia.id }) if (!created?.id) { return NextResponse.json( { success: false, error: 'Failed to create media record' }, { status: 500 } ) } // Route handlers cannot call updateTag(), so revalidateTag() with an // immediate expiry is the equivalent invalidation used by the actions. revalidateTag(mediaCacheTags.all, { expire: 0 }) revalidateTag(mediaCacheTags.byId(created.id), { expire: 0 }) const [mediaRow] = await selectMedia().where(eq(adminMedia.id, created.id)).limit(1) if (!mediaRow) { return NextResponse.json( { success: false, error: 'Failed to create media record' }, { status: 500 } ) } const media = mapMediaRow(mediaRow) runAdminEventHooks({ category: 'media', action: 'create', targetType: 'media', targetId: media.id, targetLabel: media.filename, user: actor, timestamp: now, sourceType: 'admin_ui' as const, changedFields: getTouchedFieldNames(mediaValues), metadata: { contentType: media.contentType, size: media.size }, ...auditRequestMetadata }) uploadedFiles.push({ id: media.id, key, url, filename: file.name, size: file.size, contentType: file.type, width: media.width, height: media.height }) } return NextResponse.json({ success: true, files: uploadedFiles }) } catch (error) { const message = toActionError(error, 'Failed to upload files') return NextResponse.json({ success: false, error: message }, { status: 500 }) } }