312 lines
9.7 KiB
JavaScript
312 lines
9.7 KiB
JavaScript
/**
|
|
* 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<Object>} 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'
|
|
}
|
|
}
|
|
}) |