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 @@ + + + + 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 @@ + + + + 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 @@ +
+ +
@@ -459,6 +465,12 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" /> +
+ +
diff --git a/pages/cms/vereinsmeisterschaften.vue b/pages/cms/vereinsmeisterschaften.vue index faa408e..943c340 100644 --- a/pages/cms/vereinsmeisterschaften.vue +++ b/pages/cms/vereinsmeisterschaften.vue @@ -123,9 +123,28 @@ {{ result.platz }} -
+
+
+ +
{{ result.spieler1 }} - & {{ result.spieler2 }} + + & +
+ +
+ {{ result.spieler2 }} +
@@ -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 }}

-
- - {{ ergebnis.spieler1 }} - - / {{ ergebnis.spieler2 }} +
+
+ +
+
+ + {{ ergebnis.spieler1 }} - + + + / + + {{ ergebnis.spieler2 }} + + + / {{ ergebnis.spieler2 }} + + +
@@ -147,6 +171,37 @@
+ + +
+
+ + + + +
+

{{ lightboxImage.name }}

+
+
+
@@ -156,6 +211,7 @@ import { Trophy } from 'lucide-vue-next' const results = ref([]) const selectedYear = ref('alle') +const lightboxImage = ref(null) const loadResults = async () => { try { @@ -188,6 +244,7 @@ const loadResults = async () => { } values.push(current.trim()) + // Mindestens 6 Spalten erforderlich (die neuen Bildspalten sind optional) if (values.length < 6) return null return { @@ -196,7 +253,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) { @@ -268,6 +327,26 @@ const totalDoubles = computed(() => { return results.value.filter(r => r.kategorie === 'Doppel' && r.platz === '1').length }) +function openLightbox(filename, name) { + lightboxImage.value = { filename, name } + document.body.style.overflow = 'hidden' + setTimeout(() => { + const modal = document.querySelector('[tabindex="0"]') + if (modal) modal.focus() + }, 100) +} + +function closeLightbox() { + lightboxImage.value = null + document.body.style.overflow = '' +} + +function handleLightboxKeydown(event) { + if (event.key === 'Escape') { + closeLightbox() + } +} + onMounted(() => { loadResults() }) diff --git a/pages/vorstand.vue b/pages/vorstand.vue index 4ff3cf1..06f58d3 100644 --- a/pages/vorstand.vue +++ b/pages/vorstand.vue @@ -13,112 +13,106 @@
-
-

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