Enhance Vereinsmeisterschaften and Vorstand pages with image support for players and board members. Implement lightbox functionality for player images in Vereinsmeisterschaften. Update CSV handling to include image filenames for better data management. Refactor components to utilize PersonCard for board members, improving code readability and maintainability.
This commit is contained in:
108
server/api/personen/[filename].get.js
Normal file
108
server/api/personen/[filename].get.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import sharp from 'sharp'
|
||||
|
||||
// Handle both dev and production paths
|
||||
const getDataPath = (filename) => {
|
||||
const cwd = process.cwd()
|
||||
if (cwd.endsWith('.output')) {
|
||||
return path.join(cwd, '../server/data', filename)
|
||||
}
|
||||
return path.join(cwd, 'server/data', filename)
|
||||
}
|
||||
|
||||
const PERSONEN_DIR = getDataPath('personen')
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const filename = getRouterParam(event, 'filename')
|
||||
|
||||
if (!filename) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Dateiname erforderlich'
|
||||
})
|
||||
}
|
||||
|
||||
// Sicherheitsprüfung: Nur erlaubte Dateinamen (UUID-Format)
|
||||
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(jpg|jpeg|png|gif|webp)$/i.test(filename)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Ungültiger Dateiname'
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = path.join(PERSONEN_DIR, filename)
|
||||
|
||||
// Prüfe ob Datei existiert
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Bild nicht gefunden'
|
||||
})
|
||||
}
|
||||
|
||||
// MIME-Type bestimmen
|
||||
const ext = path.extname(filename).toLowerCase()
|
||||
const mimeTypes = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp'
|
||||
}
|
||||
const contentType = mimeTypes[ext] || 'application/octet-stream'
|
||||
|
||||
// Optional: Query-Parameter für Größe
|
||||
const query = getQuery(event)
|
||||
const width = query.width ? parseInt(query.width) : null
|
||||
const height = query.height ? parseInt(query.height) : null
|
||||
|
||||
let imageBuffer = await fs.readFile(filePath)
|
||||
|
||||
// Bild verarbeiten falls Größe angegeben
|
||||
if (width || height) {
|
||||
const resizeOptions = {}
|
||||
if (width && height) {
|
||||
resizeOptions.width = width
|
||||
resizeOptions.height = height
|
||||
resizeOptions.fit = 'cover'
|
||||
} else if (width) {
|
||||
resizeOptions.width = width
|
||||
resizeOptions.fit = 'inside'
|
||||
resizeOptions.withoutEnlargement = true
|
||||
} else if (height) {
|
||||
resizeOptions.height = height
|
||||
resizeOptions.fit = 'inside'
|
||||
resizeOptions.withoutEnlargement = true
|
||||
}
|
||||
|
||||
imageBuffer = await sharp(imageBuffer)
|
||||
.rotate() // EXIF-Orientierung korrigieren
|
||||
.resize(resizeOptions)
|
||||
.toBuffer()
|
||||
} else {
|
||||
// Nur EXIF-Orientierung korrigieren
|
||||
imageBuffer = await sharp(imageBuffer).rotate().toBuffer()
|
||||
}
|
||||
|
||||
setHeader(event, 'Content-Type', contentType)
|
||||
setHeader(event, 'Cache-Control', 'public, max-age=31536000')
|
||||
|
||||
return imageBuffer
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Personenfotos:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Laden des Bildes'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
127
server/api/personen/upload.post.js
Normal file
127
server/api/personen/upload.post.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import multer from 'multer'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import sharp from 'sharp'
|
||||
import { getUserFromToken, verifyToken } from '../../utils/auth.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// Handle both dev and production paths
|
||||
const getDataPath = (filename) => {
|
||||
const cwd = process.cwd()
|
||||
if (cwd.endsWith('.output')) {
|
||||
return path.join(cwd, '../server/data', filename)
|
||||
}
|
||||
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 || (user.role !== 'admin' && user.role !== '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
|
||||
const ext = path.extname(file.originalname)
|
||||
const newFilename = `${randomUUID()}${ext}`
|
||||
const newPath = path.join(PERSONEN_DIR, newFilename)
|
||||
|
||||
// Bild verarbeiten: EXIF-Orientierung korrigieren
|
||||
await sharp(originalPath)
|
||||
.rotate()
|
||||
.toFile(newPath)
|
||||
|
||||
// Temporäre Datei löschen
|
||||
await fs.unlink(originalPath).catch(() => {})
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user