/** * Membership PDF Generation API * Clean Code: Single Responsibility, Dependency Injection */ import fs from 'fs/promises' import path from 'path' import { encrypt } from '../../utils/encryption.js' import { PDFGeneratorService } from '../../utils/pdf-generator-service.js' import nodemailer from 'nodemailer' /** * Gets the correct data path for config files * @param {string} filename - Config filename * @returns {string} Full path to config file */ function getDataPath(filename) { const isProduction = process.env.NODE_ENV === 'production' if (isProduction) { // In production, process.cwd() points to .output return path.join(process.cwd(), '..', 'server', 'data', filename) } else { // In development, process.cwd() points to project root return path.join(process.cwd(), 'server', 'data', filename) } } /** * Formats a date string to DD.MM.YYYY format with leading zeros * @param {string} dateString - Date string (YYYY-MM-DD format) * @returns {string} Formatted date string (DD.MM.YYYY) */ function formatDateWithLeadingZeros(dateString) { if (!dateString) return '' try { const date = new Date(dateString) const day = String(date.getDate()).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0') const year = date.getFullYear() return `${day}.${month}.${year}` } catch (error) { return dateString } } /** * Validates form data * @param {Object} data - Form data * @returns {Object} Validation result */ function validateFormData(data) { const required = ['vorname', 'nachname', 'strasse', 'plz', 'ort', 'geburtsdatum', 'email', 'mitgliedschaftsart'] const missing = required.filter(field => !data[field] || data[field].trim() === '') if (missing.length > 0) { return { valid: false, error: `Fehlende Pflichtfelder: ${missing.join(', ')}` } } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(data.email)) { return { valid: false, error: 'Ungültige E-Mail-Adresse' } } // Validate IBAN if provided if (data.iban && data.iban.length < 15) { return { valid: false, error: 'Ungültige IBAN' } } return { valid: true } } /** * Processes form data and adds computed fields * @param {Object} data - Raw form data * @returns {Object} Processed form data */ function processFormData(data) { const processed = { ...data } // Add computed fields processed.isVolljaehrig = new Date().getFullYear() - new Date(data.geburtsdatum).getFullYear() >= 18 processed.sign_datum = formatDateWithLeadingZeros(new Date().toISOString().split('T')[0]) processed.sepa_datum = formatDateWithLeadingZeros(new Date().toISOString().split('T')[0]) processed.page3_datum = formatDateWithLeadingZeros(new Date().toISOString().split('T')[0]) return processed } /** * Sends membership application email * @param {Object} data - Form data * @param {string} pdfPath - Path to generated PDF * @returns {Promise} Email result */ async function sendMembershipEmail(data, pdfPath) { try { // Load config const configPath = getDataPath('config.json') let config = {} try { const configData = await fs.readFile(configPath, 'utf8') config = JSON.parse(configData) } catch (error) { console.error('Could not load config.json:', error.message) } // Get recipients const isProduction = process.env.NODE_ENV === 'production' const recipients = [] if (!isProduction) { recipients.push('tsschulz@tsschulz.de') } else { if (config.vorsitzender && config.vorsitzender.email) { recipients.push(config.vorsitzender.email) } if (config.schriftfuehrer && config.schriftfuehrer.email) { recipients.push(config.schriftfuehrer.email) } if (!data.isVolljaehrig && config.trainer && config.trainer.email) { recipients.push(config.trainer.email) } if (recipients.length === 0) { recipients.push('tsschulz@tsschulz.de') } } // Create transporter const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'localhost', port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_SECURE === 'true', auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } }) // Email content const subject = `Neuer Mitgliedschaftsantrag - ${data.vorname} ${data.nachname}` const message = `Ein neuer Mitgliedschaftsantrag wurde eingereicht. Antragsteller: ${data.vorname} ${data.nachname} Mitgliedschaftsart: ${data.mitgliedschaftsart} Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'} Das ausgefüllte Formular ist als Anhang verfügbar.` // Email options const mailOptions = { from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: recipients.join(', '), subject: subject, text: message, attachments: [ { filename: path.basename(pdfPath), path: pdfPath } ] } // Send email const info = await transporter.sendMail(mailOptions) return { success: true, message: message, recipients: recipients, messageId: info.messageId } } catch (error) { console.error('Email sending failed:', error.message) return { success: false, message: `E-Mail-Versand fehlgeschlagen: ${error.message}`, error: error.message } } } /** * Main API handler */ export default defineEventHandler(async (event) => { try { const body = await readBody(event) // Validate input const validation = validateFormData(body) if (!validation.valid) { throw createError({ statusCode: 400, statusMessage: validation.error }) } // Process form data const data = processFormData(body) // Create upload directory const uploadDir = path.join(process.cwd(), 'public', 'uploads') await fs.mkdir(uploadDir, { recursive: true }) // Create temp directory for LaTeX const tempDir = path.join(process.cwd(), '.output', 'temp', 'latex') await fs.mkdir(tempDir, { recursive: true }) // Initialize PDF generator service const pdfService = new PDFGeneratorService() // Generate PDF from template let result = await pdfService.generateFromTemplate(data) if (!result.success) { throw createError({ statusCode: 500, statusMessage: 'PDF-Generierung fehlgeschlagen' }) } // Save PDF file const filePath = await pdfService.savePDF(result.pdfBuffer, result.filename, uploadDir) // Encrypt and save form data try { const config = useRuntimeConfig() const password = config.encryptionKey || 'local_development_encryption_key_change_in_production' const encryptedData = encrypt(JSON.stringify(data), password) const dataFilePath = path.join(uploadDir, result.filename.replace('.pdf', '.data')) await fs.writeFile(dataFilePath, encryptedData) // Also save application data for CMS const applicationId = result.filename.replace('.pdf', '').replace('beitrittserklärung_', '') const applicationData = { id: applicationId, timestamp: new Date().toISOString(), status: 'pending', metadata: { mitgliedschaftsart: data.mitgliedschaftsart, isVolljaehrig: data.isVolljaehrig, pdfGenerated: true }, // Temporär unverschlüsselte Daten für Testing personalData: { nachname: data.nachname, vorname: data.vorname, email: data.email, telefon_privat: data.telefon_privat, telefon_mobil: data.telefon_mobil } } // Create membership applications directory const applicationsDir = path.join(process.cwd(), 'server', 'data', 'membership-applications') await fs.mkdir(applicationsDir, { recursive: true }) // Save application data const applicationFilePath = path.join(applicationsDir, `${applicationId}.json`) await fs.writeFile(applicationFilePath, JSON.stringify(applicationData, null, 2)) } catch (encryptError) { console.error('Fehler beim Verschlüsseln der Daten:', encryptError.message) // Continue without encryption for now } // Set download token cookie const timestamp = Date.now() const downloadToken = Buffer.from(`${result.filename.replace('.pdf', '')}:${timestamp}`).toString('base64') setCookie(event, 'download_token', downloadToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60 * 24 // 24 hours }) // Send email notification let emailResult = { success: false, message: 'E-Mail konnte nicht gesendet werden' } try { emailResult = await sendMembershipEmail(data, filePath) } catch (emailError) { console.error('E-Mail-Versand fehlgeschlagen:', emailError.message) } // Return success response return { success: true, message: 'Beitrittsformular erfolgreich erstellt und E-Mail gesendet.', downloadUrl: `/api/membership/download/${result.filename}`, emailSuccess: emailResult.success, emailMessage: emailResult.message, usedTemplate: result.success } } catch (error) { console.error('PDF generation error:', error) // Return error response return { success: false, error: true, message: error.message || 'Unbekannter Fehler bei der PDF-Generierung', url: getRequestURL(event).href, statusCode: error.statusCode || 500, statusMessage: error.statusMessage || 'Internal Server Error' } } })