Refactor file handling to prioritize internal data directories for backups and uploads; enhance error handling and logging for metadata and CSV operations.
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
This commit is contained in:
@@ -26,9 +26,12 @@ const getDataPath = (filename) => {
|
||||
}
|
||||
|
||||
// Multer-Konfiguration für PDF-Uploads
|
||||
// Store uploads in internal data directory instead of public/
|
||||
const DOCUMENTS_DIR = getDataPath('documents')
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, 'public/documents/')
|
||||
cb(null, DOCUMENTS_DIR)
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, 'satzung.pdf')
|
||||
@@ -74,8 +77,9 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.join(process.cwd(), 'public', 'documents'), { recursive: true })
|
||||
try {
|
||||
// Ensure internal documents dir exists
|
||||
await fs.mkdir(DOCUMENTS_DIR, { recursive: true })
|
||||
|
||||
// Multer-Middleware für File-Upload
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -133,8 +137,9 @@ export default defineEventHandler(async (event) => {
|
||||
configData.seiten = {}
|
||||
}
|
||||
|
||||
// Serve the uploaded statute via internal media proxy
|
||||
configData.seiten.satzung = {
|
||||
pdfUrl: '/documents/satzung.pdf',
|
||||
pdfUrl: '/api/media/documents/satzung.pdf',
|
||||
content: htmlContent
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,11 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Wichtig: In Production werden statische Dateien aus `.output/public` ausgeliefert.
|
||||
// Wenn PM2 `cwd` auf das Repo-Root setzt, ist `process.cwd()` NICHT `.output` –
|
||||
// daher schreiben wir robust in alle sinnvollen Zielorte:
|
||||
// - `.output/public/data/<file>` (damit die laufende Instanz sofort die neuen Daten liefert)
|
||||
// - `public/data/<file>` (damit der nächste Build die Daten wieder übernimmt)
|
||||
//
|
||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||
// filename is validated against allowlist above, path traversal prevented
|
||||
const cwd = process.cwd()
|
||||
// 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 {
|
||||
@@ -97,23 +93,15 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Preferred: das tatsächlich ausgelieferte Verzeichnis in Production
|
||||
// (Nuxt/Nitro serve static aus `.output/public`)
|
||||
const preferredPaths = []
|
||||
if (await pathExists(path.join(cwd, '.output/public'))) {
|
||||
preferredPaths.push(path.join(cwd, '.output/public/data', filename))
|
||||
}
|
||||
if (await pathExists(path.join(cwd, '../.output/public'))) {
|
||||
preferredPaths.push(path.join(cwd, '../.output/public/data', filename))
|
||||
}
|
||||
|
||||
// Fallbacks: Source-Public (für Persistenz bei nächstem Build) und diverse cwd-Layouts
|
||||
const fallbackPaths = [
|
||||
path.join(cwd, 'public/data', filename),
|
||||
path.join(cwd, '../public/data', filename)
|
||||
// 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)
|
||||
]
|
||||
|
||||
const uniquePaths = [...new Set([...preferredPaths, ...fallbackPaths])]
|
||||
// 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
|
||||
|
||||
@@ -17,25 +17,32 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const isVorstand = hasRole(currentUser, 'vorstand')
|
||||
|
||||
// Return users without Passwörter; Kontaktdaten nur für Vorstand
|
||||
// Nur Admin oder Vorstand duerfen vollen Benutzer-Contact und Rollen sehen.
|
||||
const canSeePrivate = hasAnyRole(currentUser, 'admin', 'vorstand')
|
||||
|
||||
const safeUsers = users.map(u => {
|
||||
const migrated = migrateUserRoles({ ...u })
|
||||
const roles = Array.isArray(migrated.roles) ? migrated.roles : (migrated.role ? [migrated.role] : ['mitglied'])
|
||||
|
||||
const email = isVorstand ? u.email : undefined
|
||||
const phone = isVorstand ? (u.phone || '') : undefined
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
email,
|
||||
name: u.name,
|
||||
roles: roles,
|
||||
role: roles[0] || 'mitglied', // Rückwärtskompatibilität
|
||||
phone,
|
||||
active: u.active,
|
||||
created: u.created,
|
||||
lastLogin: u.lastLogin
|
||||
}
|
||||
return canSeePrivate
|
||||
? {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
roles: roles,
|
||||
role: roles[0] || 'mitglied',
|
||||
phone: u.phone || '',
|
||||
active: u.active,
|
||||
created: u.created,
|
||||
lastLogin: u.lastLogin
|
||||
}
|
||||
: {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
role: roles[0] || 'mitglied',
|
||||
active: u.active,
|
||||
lastLogin: u.lastLogin
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -45,35 +45,49 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = await readGalerieMetadata()
|
||||
let metadata = []
|
||||
try {
|
||||
metadata = await readGalerieMetadata()
|
||||
if (!Array.isArray(metadata)) {
|
||||
console.warn('Galerie-Metadaten haben unerwartetes Format, verwende leere Liste')
|
||||
metadata = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Lesen der Galerie-Metadaten, liefere leeres Ergebnis:', e.message)
|
||||
metadata = []
|
||||
}
|
||||
|
||||
// Filtere Bilder basierend auf Sichtbarkeit
|
||||
const visibleImages = metadata.filter(image => {
|
||||
// Öffentliche Bilder sind für alle sichtbar
|
||||
// Defensive checks
|
||||
if (!image || typeof image !== 'object') return false
|
||||
if (image.isPublic) return true
|
||||
// Private Bilder nur für eingeloggte Mitglieder
|
||||
return isLoggedIn
|
||||
})
|
||||
|
||||
// Sortiere nach Upload-Datum (neueste zuerst)
|
||||
visibleImages.sort((a, b) => new Date(b.uploadedAt) - new Date(a.uploadedAt))
|
||||
// Sortiere nach Upload-Datum (neueste zuerst) - defensive
|
||||
visibleImages.sort((a, b) => {
|
||||
const ta = new Date(a.uploadedAt || 0).getTime()
|
||||
const tb = new Date(b.uploadedAt || 0).getTime()
|
||||
return tb - ta
|
||||
})
|
||||
|
||||
// Pagination
|
||||
const page = parseInt(getQuery(event).page) || 1
|
||||
const perPage = 10
|
||||
// Pagination (defensive defaults)
|
||||
const page = Math.max(1, parseInt(getQuery(event).page) || 1)
|
||||
const perPage = Math.max(1, parseInt(getQuery(event).perPage) || 10)
|
||||
const start = (page - 1) * perPage
|
||||
const end = start + perPage
|
||||
const paginatedImages = visibleImages.slice(start, end)
|
||||
const paginatedImages = visibleImages.slice(start, start + perPage)
|
||||
|
||||
// Konsistente Rückgabeform
|
||||
return {
|
||||
success: true,
|
||||
images: paginatedImages.map(img => ({
|
||||
id: img.id,
|
||||
title: img.title,
|
||||
description: img.description,
|
||||
isPublic: img.isPublic,
|
||||
uploadedAt: img.uploadedAt,
|
||||
previewFilename: img.previewFilename
|
||||
id: img.id || img.filename || null,
|
||||
title: img.title || '',
|
||||
description: img.description || '',
|
||||
isPublic: !!img.isPublic,
|
||||
uploadedAt: img.uploadedAt || null,
|
||||
previewFilename: img.previewFilename || null
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
|
||||
@@ -15,7 +15,10 @@ export default defineEventHandler(async (event) => {
|
||||
const cwd = process.cwd()
|
||||
const filename = 'mannschaften.csv'
|
||||
|
||||
// Prefer server/data, then .output/public/data, then public/data
|
||||
const candidates = [
|
||||
path.join(cwd, '.output/server/data', filename),
|
||||
path.join(cwd, 'server/data', filename),
|
||||
path.join(cwd, '.output/public/data', filename),
|
||||
path.join(cwd, 'public/data', filename),
|
||||
path.join(cwd, '../.output/public/data', filename),
|
||||
|
||||
@@ -143,15 +143,19 @@ export default defineEventHandler(async (event) => {
|
||||
// Sort by name
|
||||
mergedMembers.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Serverseitiger Datenschutz: Kontaktdaten nur für Vorstand
|
||||
// Serverseitiger Datenschutz: nur Vorstands-Mitglieder erhalten volle Kontaktdaten/Logindaten
|
||||
const isVorstand = hasRole(currentUser, 'vorstand')
|
||||
|
||||
// Für nicht-vorstandliche Anfragen liefern wir eine stark reduzierte, nicht-identifizierende
|
||||
// Ansicht der Mitgliederliste (nur das Nötigste für öffentliche Anzeigen)
|
||||
const safeMembers = isVorstand
|
||||
? mergedMembers
|
||||
: mergedMembers.map(m => ({
|
||||
...m,
|
||||
email: undefined,
|
||||
phone: undefined,
|
||||
address: undefined
|
||||
// Minimale, unkritische Felder
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
source: m.source,
|
||||
isMannschaftsspieler: !!m.isMannschaftsspieler
|
||||
}))
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,13 @@ import { decryptObject } from '../../utils/encryption.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Nur Vorstand oder Admin darf Mitgliedschaftsantraege lesen
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = token ? await getUserFromToken(token) : null
|
||||
if (!currentUser || !hasAnyRole(currentUser, 'admin', 'vorstand')) {
|
||||
throw createError({ statusCode: 403, statusMessage: 'Zugriff verweigert' })
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const encryptionKey = config.encryptionKey || 'local_development_encryption_key_change_in_production'
|
||||
|
||||
@@ -73,7 +80,7 @@ export default defineEventHandler(async (event) => {
|
||||
// Nach Zeitstempel sortieren (neueste zuerst)
|
||||
applications.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
|
||||
return applications
|
||||
return applications
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Mitgliedschaftsanträge:', error)
|
||||
|
||||
@@ -13,10 +13,15 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Lade Spielplandaten
|
||||
const csvPath = path.join(process.cwd(), 'public/data/spielplan.csv')
|
||||
// Lade Spielplandaten - bevorzugt aus server/data
|
||||
let csvPath = path.join(process.cwd(), 'server/data/spielplan.csv')
|
||||
try {
|
||||
await fs.access(csvPath)
|
||||
} catch {
|
||||
csvPath = path.join(process.cwd(), 'public/data/spielplan.csv')
|
||||
}
|
||||
|
||||
let csvContent
|
||||
|
||||
try {
|
||||
csvContent = await fs.readFile(csvPath, 'utf-8')
|
||||
} catch (_error) {
|
||||
|
||||
@@ -4,13 +4,20 @@ import path from 'path'
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const cwd = process.cwd()
|
||||
|
||||
// In production (.output/server), working dir is .output
|
||||
|
||||
// Prefer internal server/data, fallback to public/data
|
||||
let csvPath
|
||||
if (cwd.endsWith('.output')) {
|
||||
csvPath = path.join(cwd, '../public/data/termine.csv')
|
||||
csvPath = path.join(cwd, '../server/data/termine.csv')
|
||||
// fallback
|
||||
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) {
|
||||
csvPath = path.join(cwd, '../public/data/termine.csv')
|
||||
}
|
||||
} else {
|
||||
csvPath = path.join(cwd, 'public/data/termine.csv')
|
||||
csvPath = path.join(cwd, 'server/data/termine.csv')
|
||||
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) {
|
||||
csvPath = path.join(cwd, 'public/data/termine.csv')
|
||||
}
|
||||
}
|
||||
|
||||
const csv = await fs.readFile(csvPath, 'utf-8')
|
||||
|
||||
@@ -4,13 +4,19 @@ import path from 'path'
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const cwd = process.cwd()
|
||||
|
||||
// In production (.output/server), working dir is .output
|
||||
|
||||
// Prefer internal server/data, fallback to public/data
|
||||
let csvPath
|
||||
if (cwd.endsWith('.output')) {
|
||||
csvPath = path.join(cwd, '../public/data/vereinsmeisterschaften.csv')
|
||||
csvPath = path.join(cwd, '../server/data/vereinsmeisterschaften.csv')
|
||||
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) {
|
||||
csvPath = path.join(cwd, '../public/data/vereinsmeisterschaften.csv')
|
||||
}
|
||||
} else {
|
||||
csvPath = path.join(cwd, 'public/data/vereinsmeisterschaften.csv')
|
||||
csvPath = path.join(cwd, 'server/data/vereinsmeisterschaften.csv')
|
||||
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) {
|
||||
csvPath = path.join(cwd, 'public/data/vereinsmeisterschaften.csv')
|
||||
}
|
||||
}
|
||||
|
||||
// CSV-Datei direkt als Text zurückgeben (keine Caching-Probleme)
|
||||
|
||||
@@ -2,20 +2,16 @@ import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// Handle both dev and production paths
|
||||
// filename is always a hardcoded constant (e.g., 'termine.csv'), never user input
|
||||
// Use internal server/data directory for Termine CSV to avoid writing to public/
|
||||
const getDataPath = (filename) => {
|
||||
const cwd = process.cwd()
|
||||
|
||||
// In production (.output/server), working dir is .output
|
||||
|
||||
// Prefer server/data in both production and development
|
||||
// e.g. project-root/server/data/termine.csv or .output/server/data/termine.csv
|
||||
if (cwd.endsWith('.output')) {
|
||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||
return path.join(cwd, '../public/data', filename)
|
||||
return path.join(cwd, '../server/data', filename)
|
||||
}
|
||||
|
||||
// In development, working dir is project root
|
||||
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
||||
return path.join(cwd, 'public/data', filename)
|
||||
return path.join(cwd, 'server/data', filename)
|
||||
}
|
||||
|
||||
const TERMINE_FILE = getDataPath('termine.csv')
|
||||
|
||||
Reference in New Issue
Block a user