import fs from 'fs/promises' import path from 'path' import { getUserFromToken, hasAnyRole } from '../../utils/auth.js' export default defineEventHandler(async (event) => { try { const token = getCookie(event, 'auth_token') const currentUser = token ? await getUserFromToken(token) : null if (!currentUser) { throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' }) } if (!hasAnyRole(currentUser, 'admin', 'vorstand')) { throw createError({ statusCode: 403, statusMessage: 'Keine Berechtigung' }) } const { filename, content } = await readBody(event) if (!filename || !content) { throw createError({ statusCode: 400, statusMessage: 'Filename und Content sind erforderlich' }) } // Sicherheitsprüfung: Nur bestimmte Dateien erlauben const allowedFiles = [ 'vereinsmeisterschaften.csv', 'mannschaften.csv', 'termine.csv', 'spielplan.csv' ] if (!allowedFiles.includes(filename)) { throw createError({ statusCode: 403, statusMessage: 'Datei nicht erlaubt' }) } // Neuer Ablauf (Option B): Schreibe CSVs ausschließlich in internes Datenverzeichnis, // damit keine direkten Schreibzugriffe auf `public/` stattfinden. // Später kann ein kontrollierter Deploy-/Sync-Prozess die Daten aus `server/data/public-data` // in die öffentlich ausgelieferte `public/`-Location übernehmen. const cwd = process.cwd() const pathExists = async (p) => { try { await fs.access(p) return true } catch { return false } } const writeFileAtomicAndVerify = async (targetPath, data) => { const dataDir = path.dirname(targetPath) await fs.mkdir(dataDir, { recursive: true }) // Atomar schreiben: erst temp-Datei in *gleichem Verzeichnis*, dann rename. // So vermeiden wir: // - halb geschriebene Dateien (Reader sieht "Partial Transfer") // - Erfolgsmeldungen, obwohl die Datei effektiv kaputt ist const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}` try { await fs.writeFile(tmpPath, data, 'utf8') await fs.rename(tmpPath, targetPath) const expectedSize = Buffer.byteLength(data, 'utf8') const st = await fs.stat(targetPath) if (st.size !== expectedSize) { throw new Error(`Size mismatch after write. expected=${expectedSize} actual=${st.size}`) } // Wenn beim Build pre-komprimierte Assets erzeugt wurden, können für CSVs // noch alte `.gz`/`.br` Dateien liegen bleiben. Nach einem Update würden dann // ggf. inkonsistente Inhalte ausgeliefert (Browser meldet Partial Transfer). // Daher: nach erfolgreichem Schreiben alte Varianten entfernen. for (const ext of ['.gz', '.br']) { try { await fs.unlink(`${targetPath}${ext}`) } catch (_e3) {} } } catch (e) { // best-effort cleanup try { await fs.unlink(tmpPath) } catch (_e2) {} throw e } } // Ziel: internes Datenverzeichnis unter `server/data/public-data` (persistente, interne Quelle) const internalPaths = [ path.join(cwd, 'server/data/public-data', filename), path.join(cwd, '../server/data/public-data', filename) ] // Behalte legacy `.output` write nur als optionalen, nicht-standardisierten Pfad // (wird NICHT automatisch gefordert). Hauptsächlich schreiben wir intern. const uniquePaths = [...new Set([...internalPaths])] const writeResults = [] const writeErrors = [] let wrotePreferred = false for (const targetPath of uniquePaths) { try { await writeFileAtomicAndVerify(targetPath, content) writeResults.push(targetPath) if (preferredPaths.includes(targetPath)) wrotePreferred = true } catch (e) { writeErrors.push({ targetPath, error: e?.message || String(e) }) } } // Wenn wir ein `.output/public` gefunden haben, MUSS auch dorthin geschrieben worden sein. // Sonst melden wir nicht "Erfolg", weil die laufende Instanz dann weiterhin alte/defekte Daten ausliefert. if (preferredPaths.length > 0 && !wrotePreferred) { console.error('CSV wurde NICHT in .output/public geschrieben. Errors:', writeErrors) throw createError({ statusCode: 500, statusMessage: 'CSV konnte nicht in das ausgelieferte Verzeichnis geschrieben werden' }) } if (writeResults.length === 0) { console.error('Konnte CSV-Datei in keinen Zielpfad schreiben:', writeErrors) throw createError({ statusCode: 500, statusMessage: 'Fehler beim Speichern der Datei' }) } return { success: true, message: 'Datei erfolgreich gespeichert', writtenTo: writeResults } } catch (error) { console.error('Fehler beim Speichern der CSV-Datei:', error) if (error.statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: 'Fehler beim Speichern der Datei' }) } })