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' import { clamp } from '../../utils/upload-validation.js' // 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 ('personen'), 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 PERSONEN_DIR = getDataPath('personen') // Multer-Konfiguration für Bild-Uploads const storage = multer.diskStorage({ destination: async (req, file, cb) => { try { await fs.mkdir(PERSONEN_DIR, { recursive: true }) cb(null, PERSONEN_DIR) } catch (error) { cb(error) } }, filename: (req, file, cb) => { const ext = path.extname(file.originalname) const filename = `${randomUUID()}${ext}` cb(null, filename) } }) 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 } }) 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 if (!file) { throw createError({ statusCode: 400, statusMessage: 'Keine Bilddatei hochgeladen' }) } // Bild mit sharp verarbeiten (EXIF-Orientierung korrigieren und optional resize) const originalPath = file.path // 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 newFilename = `${randomUUID()}${ext}` // Zusätzliche Sicherheit: Validiere generierten Dateinamen const sanitizedFilename = path.basename(path.normalize(newFilename)) if (sanitizedFilename !== newFilename) { throw createError({ statusCode: 500, statusMessage: 'Fehler beim Generieren des Dateinamens' }) } // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const newPath = path.join(PERSONEN_DIR, sanitizedFilename) // Bild verarbeiten: EXIF-Orientierung korrigieren const maxPixels = Number(process.env.IMAGE_MAX_PIXELS || 20_000_000) // 20MP const maxDim = Number(process.env.IMAGE_MAX_DIM || 2500) // px const meta = await sharp(originalPath).metadata() const w = meta.width || 0 const h = meta.height || 0 let pipeline = sharp(originalPath).rotate() if (w > 0 && h > 0) { const pixels = w * h // Falls extrem groß: auf maxPixels runter skalieren if (pixels > maxPixels) { const scale = Math.sqrt(maxPixels / pixels) const nw = clamp(Math.floor(w * scale), 1, maxDim) const nh = clamp(Math.floor(h * scale), 1, maxDim) pipeline = pipeline.resize(nw, nh, { fit: 'inside', withoutEnlargement: true }) } else if (w > maxDim || h > maxDim) { pipeline = pipeline.resize(maxDim, maxDim, { fit: 'inside', withoutEnlargement: true }) } } else { // Unbekannte Dimensionen: dennoch hartes Größenlimit pipeline = pipeline.resize(maxDim, maxDim, { fit: 'inside', withoutEnlargement: true }) } // toFile re-encodiert => EXIF/Metadata wird entfernt (sofern nicht withMetadata() genutzt wird) await pipeline.toFile(newPath) // Temporäre Datei löschen await fs.unlink(originalPath).catch(() => { // Datei bereits gelöscht oder nicht vorhanden, ignorieren }) return { success: true, message: 'Bild erfolgreich hochgeladen', filename: newFilename } } catch (error) { console.error('Fehler beim Hochladen des Personenfotos:', error) if (error.statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: error.message || 'Fehler beim Hochladen des Bildes' }) } })