From baf6c59c0da5f896b2ef2af653480b2c6238e9ef Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 18 Dec 2025 13:37:03 +0100 Subject: [PATCH] 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. --- backups/users-1766060412221/users.json | 1 + components/ImageUpload.vue | 143 +++++++++++++++++++ components/PersonCard.vue | 35 +++++ pages/cms/einstellungen.vue | 12 ++ pages/cms/vereinsmeisterschaften.vue | 116 ++++++++++----- pages/training/trainer.vue | 8 ++ pages/vereinsmeisterschaften.vue | 93 ++++++++++++- pages/vorstand.vue | 186 ++++++++++++------------- public/data/vereinsmeisterschaften.csv | 98 ++++++------- server/api/personen/[filename].get.js | 108 ++++++++++++++ server/api/personen/upload.post.js | 127 +++++++++++++++++ server/data/config.json | 6 +- server/data/sessions.json | 7 + server/data/users.json | 2 +- 14 files changed, 756 insertions(+), 186 deletions(-) create mode 100644 backups/users-1766060412221/users.json create mode 100644 components/ImageUpload.vue create mode 100644 components/PersonCard.vue create mode 100644 server/api/personen/[filename].get.js create mode 100644 server/api/personen/upload.post.js 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