import { randomBytes } from "crypto"; import { mkdir, unlink, writeFile } from "fs/promises"; import path from "path"; import { NextResponse } from "next/server"; import { requireCurrentUser, UnauthorizedError } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; export const runtime = "nodejs"; const MAX_LOGO_SIZE_BYTES = 2 * 1024 * 1024; const LOGO_UPLOAD_DIR = process.env.LOGO_UPLOAD_DIR ?? "/data/uploads/logos"; type ImageType = { extension: "png" | "jpg" | "webp"; contentType: "image/png" | "image/jpeg" | "image/webp"; }; function detectImageType(buffer: Buffer): ImageType | null { const isPng = buffer.length >= 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 && buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a; if (isPng) { return { extension: "png", contentType: "image/png" }; } const isJpeg = buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff; if (isJpeg) { return { extension: "jpg", contentType: "image/jpeg" }; } const isWebp = buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP"; if (isWebp) { return { extension: "webp", contentType: "image/webp" }; } return null; } function getUploadedLogoFilename(logoUrl: string | null): string | null { if (!logoUrl) { return null; } const prefix = "/api/uploads/logo/"; if (!logoUrl.startsWith(prefix)) { return null; } const filename = logoUrl.slice(prefix.length); if (!/^[a-zA-Z0-9._-]+$/.test(filename)) { return null; } return filename; } async function deleteOldUploadedLogo(logoUrl: string | null, newFilename: string): Promise { const oldFilename = getUploadedLogoFilename(logoUrl); if (!oldFilename || oldFilename === newFilename) { return; } const oldPath = path.join(LOGO_UPLOAD_DIR, oldFilename); try { await unlink(oldPath); } catch { // Altes Logo muss nicht existieren. Der Upload soll dadurch nicht fehlschlagen. } } export async function POST(request: Request) { try { const user = await requireCurrentUser(); const formData = await request.formData(); const uploadedFile = formData.get("logo"); if (!(uploadedFile instanceof File)) { return NextResponse.json({ error: "Keine Logo-Datei erhalten." }, { status: 400 }); } if (uploadedFile.size <= 0) { return NextResponse.json({ error: "Die Datei ist leer." }, { status: 400 }); } if (uploadedFile.size > MAX_LOGO_SIZE_BYTES) { return NextResponse.json({ error: "Das Logo darf maximal 2 MB groß sein." }, { status: 400 }); } const arrayBuffer = await uploadedFile.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const imageType = detectImageType(buffer); if (!imageType) { return NextResponse.json( { error: "Ungültiges Dateiformat. Erlaubt sind PNG, JPG und WebP." }, { status: 400 } ); } const existingSettings = await prisma.settings.upsert({ where: { userId: user.id }, create: { userId: user.id, dashboardTitle: "Personal Dashboard", logoUrl: "/logo.svg" }, update: {} }); await mkdir(LOGO_UPLOAD_DIR, { recursive: true }); const filename = `${user.id}-${Date.now()}-${randomBytes(8).toString("hex")}.${imageType.extension}`; const absolutePath = path.join(LOGO_UPLOAD_DIR, filename); await writeFile(absolutePath, buffer, { flag: "wx" }); const logoUrl = `/api/uploads/logo/${filename}`; const settings = await prisma.settings.update({ where: { userId: user.id }, data: { logoUrl } }); await deleteOldUploadedLogo(existingSettings.logoUrl, filename); return NextResponse.json({ settings }); } catch (error) { if (error instanceof UnauthorizedError) { return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }); } return NextResponse.json({ error: "Logo konnte nicht hochgeladen werden." }, { status: 500 }); } }