From bc95af8027d9d48bd8ed5d41d2bf74b6f8f19c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B6rdle=20Bot?= Date: Sun, 14 Dec 2025 14:11:02 +0100 Subject: [PATCH] =?UTF-8?q?Cover-Bilder=20=C3=BCber=20API-Route=20laden=20?= =?UTF-8?q?statt=20direkt=20aus=20Dateisystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/covers/[filename]/route.ts | 95 ++++++++++++++++++++++++++++++ app/curator/CuratorPageClient.tsx | 25 ++++++-- package.json | 2 +- 3 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 app/api/covers/[filename]/route.ts diff --git a/app/api/covers/[filename]/route.ts b/app/api/covers/[filename]/route.ts new file mode 100644 index 0000000..7c7bda7 --- /dev/null +++ b/app/api/covers/[filename]/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { stat } from 'fs/promises'; +import { createReadStream } from 'fs'; +import path from 'path'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ filename: string }> } +) { + try { + const { filename } = await params; + + // Security: Prevent path traversal attacks + // Allow alphanumeric, hyphens, underscores, and dots for image filenames + // Support common image formats: jpg, jpeg, png, gif, webp + const safeFilenamePattern = /^[a-zA-Z0-9_\-\.]+\.(jpg|jpeg|png|gif|webp)$/i; + if (!safeFilenamePattern.test(filename)) { + return new NextResponse('Invalid filename', { status: 400 }); + } + + // Additional check: ensure no path separators + if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) { + return new NextResponse('Invalid filename', { status: 400 }); + } + + const filePath = path.join(process.cwd(), 'public/uploads/covers', filename); + + // Security: Verify the resolved path is still within covers directory + const coversDir = path.join(process.cwd(), 'public/uploads/covers'); + const resolvedPath = path.resolve(filePath); + if (!resolvedPath.startsWith(coversDir)) { + return new NextResponse('Forbidden', { status: 403 }); + } + + const stats = await stat(filePath); + const fileSize = stats.size; + + // Determine content type based on file extension + const ext = filename.toLowerCase().split('.').pop(); + const contentTypeMap: Record = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'webp': 'image/webp', + }; + const contentType = contentTypeMap[ext || ''] || 'image/jpeg'; + + const stream = createReadStream(filePath); + + // Convert Node stream to Web stream + const readable = new ReadableStream({ + start(controller) { + let isClosed = false; + + stream.on('data', (chunk: any) => { + if (isClosed) return; + try { + controller.enqueue(chunk); + } catch (e) { + isClosed = true; + stream.destroy(); + } + }); + + stream.on('end', () => { + if (isClosed) return; + isClosed = true; + controller.close(); + }); + + stream.on('error', (err: any) => { + if (isClosed) return; + isClosed = true; + controller.error(err); + }); + }, + cancel() { + stream.destroy(); + } + }); + + return new NextResponse(readable, { + status: 200, + headers: { + 'Content-Length': fileSize.toString(), + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=3600, must-revalidate', + }, + }); + } catch (error) { + console.error('Error serving cover image:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/app/curator/CuratorPageClient.tsx b/app/curator/CuratorPageClient.tsx index bf829f2..3f3af3e 100644 --- a/app/curator/CuratorPageClient.tsx +++ b/app/curator/CuratorPageClient.tsx @@ -1615,7 +1615,7 @@ export default function CuratorPageClient() { )} -
+
- + @@ -1701,12 +1711,13 @@ export default function CuratorPageClient() { const isSelected = selectedSongIds.has(song.id); + const rowBackgroundColor = isSelected ? '#eff6ff' : 'white'; return (
{t('columnExcludeGlobal')}{t('columnActions')} + {t('columnActions')} +
@@ -1810,7 +1821,7 @@ export default function CuratorPageClient() { }} > {`Cover {isEditing ? ( @@ -2025,6 +2040,7 @@ export default function CuratorPageClient() { border: 'none', borderRadius: '0.25rem', cursor: 'pointer', + whiteSpace: 'nowrap', }} > 💾 @@ -2038,6 +2054,7 @@ export default function CuratorPageClient() { border: 'none', borderRadius: '0.25rem', cursor: 'pointer', + whiteSpace: 'nowrap', }} > ✖ diff --git a/package.json b/package.json index 16d7505..73f876c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hoerdle", - "version": "0.1.6.31", + "version": "0.1.6.32", "private": true, "scripts": { "dev": "next dev",