import multer from 'multer' import fs from 'fs/promises' import path from 'path' import sharp from 'sharp' import { getUserFromToken, verifyToken, hasAnyRole } from '../../utils/auth.js' import { randomUUID } from 'crypto' // Handle both dev and production paths // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // filename is always a hardcoded constant (e.g., 'galerie-metadata.json'), never user input const getDataPath = (filename) => { const cwd = process.cwd() if (cwd.endsWith('.output')) { // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal return path.join(cwd, '../server/data', filename) } // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal return path.join(cwd, 'server/data', filename) } const GALERIE_DIR = getDataPath('galerie') const GALERIE_METADATA = getDataPath('galerie-metadata.json') // Multer-Konfiguration für Bild-Uploads // Temporärer Dateiname, wird später basierend auf Titel umbenannt const storage = multer.diskStorage({ destination: async (req, file, cb) => { try { await fs.mkdir(GALERIE_DIR, { recursive: true }) await fs.mkdir(path.join(GALERIE_DIR, 'originals'), { recursive: true }) await fs.mkdir(path.join(GALERIE_DIR, 'previews'), { recursive: true }) cb(null, path.join(GALERIE_DIR, 'originals')) } catch (error) { cb(error) } }, filename: (req, file, cb) => { // Temporärer Dateiname, wird später umbenannt const ext = path.extname(file.originalname) const tempFilename = `temp_${randomUUID()}${ext}` cb(null, tempFilename) } }) const upload = multer({ storage, fileFilter: (req, file, cb) => { const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] if (allowedMimes.includes(file.mimetype)) { cb(null, true) } else { cb(new Error('Nur Bilddateien sind erlaubt (JPEG, PNG, GIF, WebP)'), false) } }, limits: { fileSize: 10 * 1024 * 1024 // 10MB Limit } }) async function readGalerieMetadata() { try { const data = await fs.readFile(GALERIE_METADATA, 'utf-8') return JSON.parse(data) } catch (error) { if (error.code === 'ENOENT') { return [] } throw error } } async function writeGalerieMetadata(metadata) { await fs.writeFile(GALERIE_METADATA, JSON.stringify(metadata, null, 2), 'utf-8') } export default defineEventHandler(async (event) => { try { // Authentifizierung prüfen const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace('Bearer ', '') if (!token) { throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' }) } const decoded = verifyToken(token) if (!decoded) { throw createError({ statusCode: 401, statusMessage: 'Ungültiges Token' }) } const user = await getUserFromToken(token) if (!user || !hasAnyRole(user, 'admin', 'vorstand')) { throw createError({ statusCode: 403, statusMessage: 'Keine Berechtigung zum Hochladen von Bildern' }) } // Multer-Middleware für multipart/form-data await new Promise((resolve, reject) => { upload.single('image')(event.node.req, event.node.res, (err) => { if (err) reject(err) else resolve() }) }) const file = event.node.req.file const body = event.node.req.body || {} const isPublic = body.isPublic === 'true' || body.isPublic === true if (!file) { throw createError({ statusCode: 400, statusMessage: 'Keine Bilddatei hochgeladen' }) } // Titel ist Pflichtfeld if (!body.title || !body.title.trim()) { // Lösche die hochgeladene Datei await fs.unlink(file.path).catch(() => { // Datei bereits gelöscht oder nicht vorhanden, ignorieren }) throw createError({ statusCode: 400, statusMessage: 'Titel ist ein Pflichtfeld' }) } // Generiere Dateinamen basierend auf Titel const titleSlug = body.title.trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 100) // Max 100 Zeichen // Validiere Dateiendung const ext = path.extname(file.originalname).toLowerCase() const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] if (!allowedExtensions.includes(ext)) { await fs.unlink(file.path).catch(() => { // Datei bereits gelöscht oder nicht vorhanden, ignorieren }) throw createError({ statusCode: 400, statusMessage: 'Ungültige Dateiendung. Nur Bilddateien sind erlaubt.' }) } const filename = `${titleSlug}_${randomUUID().substring(0, 8)}${ext}` const previewFilename = `preview_${filename}` // Verschiebe die Datei zum neuen Namen // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const originalPath = path.join(GALERIE_DIR, 'originals', filename) await fs.rename(file.path, originalPath) // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const previewPath = path.join(GALERIE_DIR, 'previews', previewFilename) // Thumbnail erstellen (150x150px) mit automatischer EXIF-Orientierungskorrektur await sharp(originalPath) .rotate() // Korrigiert automatisch EXIF-Orientierung .resize(150, 150, { fit: 'inside', withoutEnlargement: true }) .toFile(previewPath) // Metadaten speichern const metadata = await readGalerieMetadata() const newImage = { id: randomUUID(), filename, previewFilename, title: body.title.trim(), description: body.description || '', isPublic, uploadedBy: user.id, uploadedAt: new Date().toISOString(), originalName: file.originalname } metadata.push(newImage) await writeGalerieMetadata(metadata) return { success: true, message: 'Bild erfolgreich hochgeladen', image: { id: newImage.id, title: newImage.title, isPublic: newImage.isPublic } } } catch (error) { console.error('Fehler beim Bild-Upload:', error) if (error.statusCode) { throw error } if (error.code === 'LIMIT_FILE_SIZE') { throw createError({ statusCode: 413, statusMessage: 'Datei zu groß (max. 10MB)' }) } if (error.message && error.message.includes('Nur Bilddateien')) { throw createError({ statusCode: 400, statusMessage: error.message }) } throw createError({ statusCode: 500, statusMessage: 'Fehler beim Hochladen des Bildes' }) } })