diff --git a/backups/users-1766060412221/users.json b/backups/users-1766060412221/users.json
new file mode 100644
index 0000000..5f5658c
--- /dev/null
+++ b/backups/users-1766060412221/users.json
@@ -0,0 +1 @@
+vt5myp1IVj2hMck3wi+hrAym+ZAIGNkg5zeSZcHwpt8NV9ZIj3KD1bPEbzTT7LhmlgspNL/HmTYwdUYN/yoxOxZ5d3usU+/q690XcuP4j4PzMtRc+xXVlA2oZT2lszkZtw0sm9auHI7NCAIViCqfpmnAtjsJPy9Pguni/9BH5hMJtNzR1zg0wIgigqA0eYLatRyMusk+hq0Bv2qodwOH0V6kQ9NHAj6lR6Dehs/nO8R+qjgtvWgYjxPR8RMtn62s8zFki3YcXi8Zweb/I0XUTS9VV4EukyZXpEGDs7ECiN6nesYNAHSB/PhC8rqrPjUPPna2s2sZjVgfY8WueuODw5oArRGfgzDhCz/eqpTS5pjMSrGJ8AygrC7R+l5KSSsMN2hHn/AwY6PAhUtbLe3mmQ==
\ No newline at end of file
diff --git a/components/ImageUpload.vue b/components/ImageUpload.vue
new file mode 100644
index 0000000..a0d4a30
--- /dev/null
+++ b/components/ImageUpload.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
{{ error }}
+
+
+
+
+
diff --git a/components/PersonCard.vue b/components/PersonCard.vue
new file mode 100644
index 0000000..9b7567f
--- /dev/null
+++ b/components/PersonCard.vue
@@ -0,0 +1,35 @@
+
+
+
+
![]()
+
+
{{ title }}
+
{{ name }}
+
+
+
+
+
+
+
+
diff --git a/pages/cms/einstellungen.vue b/pages/cms/einstellungen.vue
index d978f4e..da3dacd 100644
--- a/pages/cms/einstellungen.vue
+++ b/pages/cms/einstellungen.vue
@@ -295,6 +295,12 @@
+
@@ -170,12 +189,15 @@
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click.self="closeModal"
>
-
-
- {{ editingResult ? 'Ergebnis bearbeiten' : 'Neues Ergebnis hinzufügen' }}
-
+
+
+
+ {{ editingResult ? 'Ergebnis bearbeiten' : 'Neues Ergebnis hinzufügen' }}
+
+
-
@@ -330,7 +369,9 @@ const formData = ref({
platz: '',
spieler1: '',
spieler2: '',
- bemerkung: ''
+ bemerkung: '',
+ imageFilename1: '',
+ imageFilename2: ''
})
const loadResults = async () => {
@@ -370,6 +411,7 @@ const loadResults = async () => {
}
values.push(current.trim())
+ // Mindestens 6 Spalten erforderlich (die neuen Bildspalten sind optional)
if (values.length < 6) return null
return {
@@ -378,7 +420,9 @@ const loadResults = async () => {
platz: values[2].trim(),
spieler1: values[3].trim(),
spieler2: values[4].trim(),
- bemerkung: values[5].trim()
+ bemerkung: values[5].trim(),
+ imageFilename1: values[6]?.trim() || '',
+ imageFilename2: values[7]?.trim() || ''
}
}).filter(result => result !== null)
} catch (error) {
@@ -445,7 +489,9 @@ const addNewResult = () => {
platz: '',
spieler1: '',
spieler2: '',
- bemerkung: ''
+ bemerkung: '',
+ imageFilename1: '',
+ imageFilename2: ''
}
showModal.value = true
}
@@ -461,7 +507,9 @@ const addResultForYear = (jahr) => {
platz: '',
spieler1: '',
spieler2: '',
- bemerkung: ''
+ bemerkung: '',
+ imageFilename1: '',
+ imageFilename2: ''
}
showModal.value = true
}
@@ -477,7 +525,9 @@ const addResultForKategorie = (jahr, kategorie) => {
platz: '',
spieler1: '',
spieler2: '',
- bemerkung: ''
+ bemerkung: '',
+ imageFilename1: '',
+ imageFilename2: ''
}
showModal.value = true
}
@@ -493,7 +543,9 @@ const editResult = (result, jahr, kategorie, index) => {
platz: result.platz,
spieler1: result.spieler1,
spieler2: result.spieler2,
- bemerkung: result.bemerkung
+ bemerkung: result.bemerkung,
+ imageFilename1: result.imageFilename1 || '',
+ imageFilename2: result.imageFilename2 || ''
}
showModal.value = true
}
@@ -649,7 +701,7 @@ const closeBemerkungModal = () => {
const save = async () => {
try {
// CSV generieren
- const csvHeader = 'Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung'
+ const csvHeader = 'Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung,imageFilename1,imageFilename2'
const csvRows = results.value.map(result => {
return [
result.jahr,
@@ -657,7 +709,9 @@ const save = async () => {
result.platz,
result.spieler1,
result.spieler2,
- result.bemerkung
+ result.bemerkung,
+ result.imageFilename1 || '',
+ result.imageFilename2 || ''
].map(field => `"${field}"`).join(',')
})
diff --git a/pages/training/trainer.vue b/pages/training/trainer.vue
index 17ead0b..ed78b27 100644
--- a/pages/training/trainer.vue
+++ b/pages/training/trainer.vue
@@ -16,6 +16,14 @@
:key="trainer.id"
class="bg-white p-8 rounded-xl shadow-lg"
>
+
+
![]()
+
{{ trainer.lizenz }}
{{ trainer.name }}
diff --git a/pages/vereinsmeisterschaften.vue b/pages/vereinsmeisterschaften.vue
index aa7a636..f7b60a5 100644
--- a/pages/vereinsmeisterschaften.vue
+++ b/pages/vereinsmeisterschaften.vue
@@ -88,13 +88,37 @@
>
{{ ergebnis.platz }}
-
-
-
Vorsitzender
-
- {{ config.vorstand.vorsitzender.vorname }} {{ config.vorstand.vorsitzender.nachname }}
-
-
-
{{ config.vorstand.vorsitzender.strasse }}
-
{{ config.vorstand.vorsitzender.plz }} {{ config.vorstand.vorsitzender.ort }}
-
Tel. {{ config.vorstand.vorsitzender.telefon }}
-
-
- {{ config.vorstand.vorsitzender.email }}
-
-
-
-
+
+ {{ config.vorstand.vorsitzender.strasse }}
+ {{ config.vorstand.vorsitzender.plz }} {{ config.vorstand.vorsitzender.ort }}
+ Tel. {{ config.vorstand.vorsitzender.telefon }}
+
+
+ {{ config.vorstand.vorsitzender.email }}
+
+
+
-
-
Stellvertreter
-
- {{ config.vorstand.stellvertreter.vorname }} {{ config.vorstand.stellvertreter.nachname }}
-
-
-
{{ config.vorstand.stellvertreter.strasse }}
-
{{ config.vorstand.stellvertreter.plz }} {{ config.vorstand.stellvertreter.ort }}
-
Tel. {{ config.vorstand.stellvertreter.telefon }}
-
-
- {{ config.vorstand.stellvertreter.email }}
-
-
-
-
+
+ {{ config.vorstand.stellvertreter.strasse }}
+ {{ config.vorstand.stellvertreter.plz }} {{ config.vorstand.stellvertreter.ort }}
+ Tel. {{ config.vorstand.stellvertreter.telefon }}
+
+
+ {{ config.vorstand.stellvertreter.email }}
+
+
+
-
-
Kassenwart
-
- {{ config.vorstand.kassenwart.vorname }} {{ config.vorstand.kassenwart.nachname }}
-
-
-
{{ config.vorstand.kassenwart.strasse }}
-
{{ config.vorstand.kassenwart.plz }} {{ config.vorstand.kassenwart.ort }}
-
Tel. {{ config.vorstand.kassenwart.telefon }}
-
-
- {{ config.vorstand.kassenwart.email }}
-
-
-
-
+
+ {{ config.vorstand.kassenwart.strasse }}
+ {{ config.vorstand.kassenwart.plz }} {{ config.vorstand.kassenwart.ort }}
+ Tel. {{ config.vorstand.kassenwart.telefon }}
+
+
+ {{ config.vorstand.kassenwart.email }}
+
+
+
-
-
Schriftführer
-
- {{ config.vorstand.schriftfuehrer.vorname }} {{ config.vorstand.schriftfuehrer.nachname }}
-
-
-
{{ config.vorstand.schriftfuehrer.strasse }}
-
{{ config.vorstand.schriftfuehrer.plz }} {{ config.vorstand.schriftfuehrer.ort }}
-
Tel. {{ config.vorstand.schriftfuehrer.telefon }}
-
-
- {{ config.vorstand.schriftfuehrer.email }}
-
-
-
-
+
+ {{ config.vorstand.schriftfuehrer.strasse }}
+ {{ config.vorstand.schriftfuehrer.plz }} {{ config.vorstand.schriftfuehrer.ort }}
+ Tel. {{ config.vorstand.schriftfuehrer.telefon }}
+
+
+ {{ config.vorstand.schriftfuehrer.email }}
+
+
+
-
-
Sportwart
-
- {{ config.vorstand.sportwart.vorname }} {{ config.vorstand.sportwart.nachname }}
-
-
-
{{ config.vorstand.sportwart.strasse }}
-
{{ config.vorstand.sportwart.plz }} {{ config.vorstand.sportwart.ort }}
-
Tel. {{ config.vorstand.sportwart.telefon }}
-
-
- {{ config.vorstand.sportwart.email }}
-
-
-
-
+
+ {{ config.vorstand.sportwart.strasse }}
+ {{ config.vorstand.sportwart.plz }} {{ config.vorstand.sportwart.ort }}
+ Tel. {{ config.vorstand.sportwart.telefon }}
+
+
+ {{ config.vorstand.sportwart.email }}
+
+
+
-
-
Jugendwart
-
- {{ config.vorstand.jugendwart.vorname }} {{ config.vorstand.jugendwart.nachname }}
-
-
-
{{ config.vorstand.jugendwart.strasse }}
-
{{ config.vorstand.jugendwart.plz }} {{ config.vorstand.jugendwart.ort }}
-
Tel. {{ config.vorstand.jugendwart.telefon }}
-
-
- {{ config.vorstand.jugendwart.email }}
-
-
-
-
+
+ {{ config.vorstand.jugendwart.strasse }}
+ {{ config.vorstand.jugendwart.plz }} {{ config.vorstand.jugendwart.ort }}
+ Tel. {{ config.vorstand.jugendwart.telefon }}
+
+
+ {{ config.vorstand.jugendwart.email }}
+
+
+
diff --git a/public/data/vereinsmeisterschaften.csv b/public/data/vereinsmeisterschaften.csv
index 80c7547..645db4c 100644
--- a/public/data/vereinsmeisterschaften.csv
+++ b/public/data/vereinsmeisterschaften.csv
@@ -1,49 +1,49 @@
-Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung
-"2024","Einzel","1","Michael Koch","",""
-"2024","Einzel","2","Olaf Nüßlein","",""
-"2024","Einzel","3","Bernd Meyer","",""
-"2024","Doppel","1","Sven Baublies","Johannes Binder",""
-"2024","Doppel","2","Bernd Meyer","Jürgen Dichmann",""
-"2024","Doppel","3","Michael Koch","Jacob Waltenberger",""
-"2023","Einzel","1","André Gilzinger","",""
-"2023","Einzel","2","Olaf Nüßlein","",""
-"2023","Einzel","3","Michael Koch","",""
-"2023","Doppel","1","Olaf Nüßlein","Johannes Binder",""
-"2023","Doppel","2","Renate Nebel","André Gilzinger",""
-"2023","Doppel","3","Ute Puschmann","Jürgen Kratz",""
-"2022","Einzel","1","Sven Baublies","",""
-"2022","Einzel","2","Thomas Steinbrech","",""
-"2022","Einzel","3","André Gilzinger","",""
-"2022","Doppel","1","Sven Baublies","Kristin von Rauchhaupt",""
-"2022","Doppel","2","Michael Weber","Johannes Binder",""
-"2022","Doppel","3","Michael Koch","Renate Nebel",""
-"2021","","","","","coronabedingter Ausfall"
-"2020","","","","","coronabedingter Ausfall"
-"2019","Einzel","1","André Gilzinger","",""
-"2019","Einzel","2","Thomas Steinbrech","",""
-"2019","Einzel","3","Jürgen Kratz","",""
-"2019","Doppel","1","André Gilzinger","Volker Marx",""
-"2019","Doppel","2","Jürgen Kratz","Marko Wiedau",""
-"2019","Doppel","3","Bernd Meyer","Kristin von Rauchhaupt",""
-"2018","Einzel","1","André Gilzinger","",""
-"2018","Einzel","2","Jürgen Kratz","",""
-"2018","Einzel","3","Sven Baublies","",""
-"2018","Doppel","1","André Gilzinger","Volker Marx",""
-"2018","Doppel","2","Sven Baublies","Helge Stefan",""
-"2018","Doppel","3","Jürgen Kratz","Renate Nebel",""
-"2017","Einzel","1","André Gilzinger","",""
-"2017","Einzel","2","Sven Baublies","",""
-"2017","Einzel","3","Olaf Nüßlein","",""
-"2017","Doppel","1","Olaf Nüßlein","Helge Stefan",""
-"2017","Doppel","2","André Gilzinger","Renate Nebel",""
-"2017","Doppel","3","Jürgen Kratz","Kristin von Rauchhaupt",""
-"2016","Herren-Einzel","1","André Gilzinger","",""
-"2016","Herren-Einzel","2","Sven Baublies","",""
-"2016","Herren-Einzel","3","Olaf Nüßlein","",""
-"2016","Damen-Einzel","1","Birgit Haas-Schrödter","",""
-"2016","Damen-Einzel","2","Kristin von Rauchhaupt","",""
-"2016","Damen-Einzel","3","Renate Nebel","",""
-"2016","Doppel","1","Jürgen Kratz","Matthias Schmidt",""
-"2016","Doppel","2","André Gilzinger","Bernd Meyer",""
-"2016","Doppel","3","Sven Baublies","Dagmar Bereksasi",""
-"2025","Doppel","1","a","b",""
\ No newline at end of file
+Jahr,Kategorie,Platz,Spieler1,Spieler2,Bemerkung,imageFilename1,imageFilename2
+"2024","Einzel","1","Michael Koch","","","",""
+"2024","Einzel","2","Olaf Nüßlein","","","",""
+"2024","Einzel","3","Bernd Meyer","","","",""
+"2024","Doppel","1","Sven Baublies","Johannes Binder","","",""
+"2024","Doppel","2","Bernd Meyer","Jürgen Dichmann","","",""
+"2024","Doppel","3","Michael Koch","Jacob Waltenberger","","",""
+"2023","Einzel","1","André Gilzinger","","","",""
+"2023","Einzel","2","Olaf Nüßlein","","","",""
+"2023","Einzel","3","Michael Koch","","","",""
+"2023","Doppel","1","Olaf Nüßlein","Johannes Binder","","",""
+"2023","Doppel","2","Renate Nebel","André Gilzinger","","",""
+"2023","Doppel","3","Ute Puschmann","Jürgen Kratz","","",""
+"2022","Einzel","1","Sven Baublies","","","",""
+"2022","Einzel","2","Thomas Steinbrech","","","",""
+"2022","Einzel","3","André Gilzinger","","","",""
+"2022","Doppel","1","Sven Baublies","Kristin von Rauchhaupt","","",""
+"2022","Doppel","2","Michael Weber","Johannes Binder","","",""
+"2022","Doppel","3","Michael Koch","Renate Nebel","","",""
+"2021","","","","","coronabedingter Ausfall","",""
+"2020","","","","","coronabedingter Ausfall","",""
+"2019","Einzel","1","André Gilzinger","","","",""
+"2019","Einzel","2","Thomas Steinbrech","","","",""
+"2019","Einzel","3","Jürgen Kratz","","","",""
+"2019","Doppel","1","André Gilzinger","Volker Marx","","",""
+"2019","Doppel","2","Jürgen Kratz","Marko Wiedau","","",""
+"2019","Doppel","3","Bernd Meyer","Kristin von Rauchhaupt","","",""
+"2018","Einzel","1","André Gilzinger","","","",""
+"2018","Einzel","2","Jürgen Kratz","","","",""
+"2018","Einzel","3","Sven Baublies","","","",""
+"2018","Doppel","1","André Gilzinger","Volker Marx","","",""
+"2018","Doppel","2","Sven Baublies","Helge Stefan","","",""
+"2018","Doppel","3","Jürgen Kratz","Renate Nebel","","",""
+"2017","Einzel","1","André Gilzinger","","","",""
+"2017","Einzel","2","Sven Baublies","","","",""
+"2017","Einzel","3","Olaf Nüßlein","","","",""
+"2017","Doppel","1","Olaf Nüßlein","Helge Stefan","","",""
+"2017","Doppel","2","André Gilzinger","Renate Nebel","","",""
+"2017","Doppel","3","Jürgen Kratz","Kristin von Rauchhaupt","","",""
+"2016","Herren-Einzel","1","André Gilzinger","","","",""
+"2016","Herren-Einzel","2","Sven Baublies","","","",""
+"2016","Herren-Einzel","3","Olaf Nüßlein","","","",""
+"2016","Damen-Einzel","1","Birgit Haas-Schrödter","","","",""
+"2016","Damen-Einzel","2","Kristin von Rauchhaupt","","","",""
+"2016","Damen-Einzel","3","Renate Nebel","","","",""
+"2016","Doppel","1","Jürgen Kratz","Matthias Schmidt","","",""
+"2016","Doppel","2","André Gilzinger","Bernd Meyer","","",""
+"2016","Doppel","3","Sven Baublies","Dagmar Bereksasi","","",""
+"2025","Doppel","1","a","b","","7f6c46f8-b93f-4807-b369-b26e0bba2da5.png","4f51e2e9-8cb0-4ce0-9395-ea5080361dd5.png"
\ No newline at end of file
diff --git a/server/api/personen/[filename].get.js b/server/api/personen/[filename].get.js
new file mode 100644
index 0000000..526b635
--- /dev/null
+++ b/server/api/personen/[filename].get.js
@@ -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'
+ })
+ }
+})
+
diff --git a/server/api/personen/upload.post.js b/server/api/personen/upload.post.js
new file mode 100644
index 0000000..e235bd2
--- /dev/null
+++ b/server/api/personen/upload.post.js
@@ -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'
+ })
+ }
+})
+
diff --git a/server/data/config.json b/server/data/config.json
index bdc914e..5bee3e7 100644
--- a/server/data/config.json
+++ b/server/data/config.json
@@ -36,7 +36,8 @@
"name": "Torsten Schulz",
"lizenz": "C-Trainer",
"schwerpunkt": "Nachwuchsförderung",
- "zusatz": "Erwachsenen bei Wunsch zur Verfügung"
+ "zusatz": "Erwachsenen bei Wunsch zur Verfügung",
+ "imageFilename": "8f79a5b9-bfba-43c4-9ab8-81192337bd8f.png"
},
{
"id": "2",
@@ -99,7 +100,8 @@
"plz": "60437",
"ort": "Frankfurt",
"telefon": "06101-9953015",
- "email": "rogerdichmann@gmx.de"
+ "email": "rogerdichmann@gmx.de",
+ "imageFilename": "c24ef84d-2ae4-4edc-be01-063d9917da04.png"
},
"stellvertreter": {
"vorname": "Jürgen",
diff --git a/server/data/sessions.json b/server/data/sessions.json
index 4a1df32..d2bcc0f 100644
--- a/server/data/sessions.json
+++ b/server/data/sessions.json
@@ -117,5 +117,12 @@
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzYyMzMzNDIzLCJleHAiOjE3NjI5MzgyMjN9.V-L5ethO0VFSOPT2qbsQF2zQYQZSlese1rL5sIFaHbY",
"createdAt": "2025-11-05T09:03:43.617Z",
"expiresAt": "2025-11-12T09:03:43.617Z"
+ },
+ {
+ "id": "1766060415179",
+ "userId": "1766060412277",
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE3NjYwNjA0MTIyNzciLCJlbWFpbCI6ImFkbWluQGhhcmhlaW1lcnRjLmRlIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzY2MDYwNDE1LCJleHAiOjE3NjY2NjUyMTV9.B-BzHefBmvCZ3qbJ99uN2OMIszcRPJohyj4xknJFExE",
+ "createdAt": "2025-12-18T12:20:15.179Z",
+ "expiresAt": "2025-12-25T12:20:15.179Z"
}
]
\ No newline at end of file
diff --git a/server/data/users.json b/server/data/users.json
index 5f5658c..bec3142 100644
--- a/server/data/users.json
+++ b/server/data/users.json
@@ -1 +1 @@
-vt5myp1IVj2hMck3wi+hrAym+ZAIGNkg5zeSZcHwpt8NV9ZIj3KD1bPEbzTT7LhmlgspNL/HmTYwdUYN/yoxOxZ5d3usU+/q690XcuP4j4PzMtRc+xXVlA2oZT2lszkZtw0sm9auHI7NCAIViCqfpmnAtjsJPy9Pguni/9BH5hMJtNzR1zg0wIgigqA0eYLatRyMusk+hq0Bv2qodwOH0V6kQ9NHAj6lR6Dehs/nO8R+qjgtvWgYjxPR8RMtn62s8zFki3YcXi8Zweb/I0XUTS9VV4EukyZXpEGDs7ECiN6nesYNAHSB/PhC8rqrPjUPPna2s2sZjVgfY8WueuODw5oArRGfgzDhCz/eqpTS5pjMSrGJ8AygrC7R+l5KSSsMN2hHn/AwY6PAhUtbLe3mmQ==
\ No newline at end of file
+3+uWOe4pSXnAFtrdeCqRG+HvbRIsI2HUcMkzrEBlqEmf/9rasPUIv5xhfS+3vh3BJh89fjff0N9l7C8SZbe/ABq75ffwHa1rT72fExEAQ0B/TntBBARNeACYRtx7j3OTJs0+DPiJvraXshqqVjJQjFVMRk1PdmNs3wbZQ9JkXazyne+Gvb6NJWBAeBv4s5pOe3y06GnUO2ZMsGPX3nKdumbRjFXoNzOWtzMQy9m8GYTAQGtC+dMzRTAjKuPxMLLYT1e8hMhJkhbGDOB0+VgOG2o1zGa+eD2ayoHqzUBf5/RZs09rRspXJZ7HKjvgdnkJuO2lstjQeOFtzoljGE9EC2ueRGUuOsyi0AQrbBhVTj3wWIb5V+mNxNccKv9KDs4/EPwyu8l32Ql6kepNuXofZMbJHuwwYvXIvpIj31HdJP0=
\ No newline at end of file