Files
harheimertc/server/api/cms/satzung-upload.post.js
Torsten Schulz (local) 66a2cfc378
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 49s
Refactor PDF text extraction and update configuration handling in Satzung upload process
This commit removes the PDF text extraction logic and replaces it with a fallback mechanism that retains existing content or provides a neutral message. The configuration update now only sets the PDF path without automatically generating HTML content, improving clarity and maintaining the integrity of the existing data.
2026-02-06 10:55:41 +01:00

179 lines
5.4 KiB
JavaScript

import multer from 'multer'
import fs from 'fs/promises'
import path from 'path'
import { exec } from 'child_process'
import { promisify } from 'util'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
import { assertPdfMagicHeader } from '../../utils/upload-validation.js'
const execAsync = promisify(exec)
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant ('satzung.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
// In production (.output/server), working dir is .output
if (cwd.endsWith('.output')) {
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
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, 'server/data', filename)
}
// Multer-Konfiguration für PDF-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/documents/')
},
filename: (req, file, cb) => {
cb(null, 'satzung.pdf')
}
})
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (file.mimetype === 'application/pdf') {
cb(null, true)
} else {
cb(new Error('Nur PDF-Dateien sind erlaubt'), false)
}
},
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
}
})
export default defineEventHandler(async (event) => {
if (event.method !== 'POST') {
throw createError({
statusCode: 405,
statusMessage: 'Method Not Allowed'
})
}
let 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'
})
}
try {
await fs.mkdir(path.join(process.cwd(), 'public', 'documents'), { recursive: true })
// Multer-Middleware für File-Upload
await new Promise((resolve, reject) => {
upload.single('pdf')(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 PDF-Datei hochgeladen'
})
}
// Zusätzliche Validierung: Magic-Bytes prüfen (mimetype kann gespooft sein)
await assertPdfMagicHeader(file.path)
// Config aktualisieren: Nur PDF-Pfad setzen, HTML-Inhalt nicht automatisch neu generieren
const configPath = getDataPath('config.json')
const configData = JSON.parse(await fs.readFile(configPath, 'utf-8'))
if (!configData.seiten) {
configData.seiten = {}
}
const previousContent = configData.seiten.satzung?.content || ''
configData.seiten.satzung = {
pdfUrl: '/documents/satzung.pdf',
// Entweder bestehenden Text behalten oder einen neutralen Hinweis setzen
content: previousContent || '<p>Die gültige Satzung des Harheimer Tischtennis-Club 1954 e. V. steht als PDF zum Download bereit. Die PDF-Fassung ist rechtlich verbindlich.</p>'
}
await fs.writeFile(configPath, JSON.stringify(configData, null, 2), 'utf-8')
return {
success: true,
message: 'Satzung erfolgreich hochgeladen und verarbeitet',
pdfUrl: '/documents/satzung.pdf'
}
} catch (error) {
console.error('PDF Upload Error:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: error.message || 'Fehler beim Verarbeiten der PDF-Datei'
})
}
})
// PDF-Text zu HTML konvertieren
function convertTextToHtml(text) {
// Text bereinigen und strukturieren
let html = text
.replace(/\r\n/g, '\n') // Windows-Zeilenumbrüche normalisieren
.replace(/\r/g, '\n') // Mac-Zeilenumbrüche normalisieren
.replace(/\n\s*\n/g, '\n\n') // Mehrfache Zeilenumbrüche reduzieren
.trim()
// Überschriften erkennen und formatieren
html = html.replace(/^(Vereinssatzung|Satzung)$/gm, '<h1>$1</h1>')
html = html.replace(/^(§\s*\d+[^§\n]*)$/gm, '<h2>$1</h2>')
// Absätze erstellen
html = html.split('\n\n').map(paragraph => {
paragraph = paragraph.trim()
if (!paragraph) return ''
// Überschriften nicht als Paragraphen behandeln
if (paragraph.match(/^<h[1-6]>/) || paragraph.match(/^§\s*\d+/)) {
return paragraph
}
// Listen erkennen
if (paragraph.includes('•') || paragraph.includes('-') || paragraph.match(/^\d+\./)) {
const listItems = paragraph.split(/\n/).map(item => {
item = item.trim()
if (item.match(/^[•-]\s/) || item.match(/^\d+\.\s/)) {
return `<li>${item.replace(/^[•-]\s/, '').replace(/^\d+\.\s/, '')}</li>`
}
return `<li>${item}</li>`
}).join('')
return `<ul>${listItems}</ul>`
}
// Normale Absätze
return `<p>${paragraph.replace(/\n/g, '<br>')}</p>`
}).join('\n')
// Mehrfache Zeilenumbrüche entfernen
html = html.replace(/\n{3,}/g, '\n\n')
return html
}