Refactor environment configuration for local development; update SMTP settings and add JWT secret, encryption key, and debug options. Enhance Nuxt configuration for development server and runtime settings. Introduce new membership application form with validation and PDF generation functionality. Update footer and navigation components to include new membership links. Revise user and session data in JSON files.
This commit is contained in:
708
server/api/membership/generate-pdf.post.js
Normal file
708
server/api/membership/generate-pdf.post.js
Normal file
@@ -0,0 +1,708 @@
|
||||
import { createRequire } from 'module'
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { encrypt } from '../../utils/encryption.js'
|
||||
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
function generateLaTeXContent(data) {
|
||||
const heute = new Date().toLocaleDateString('de-DE')
|
||||
|
||||
// LaTeX-Inhalt mit korrekten Escapes generieren
|
||||
let latexContent = `\\documentclass[12pt,a4paper]{article}
|
||||
\\usepackage[utf8]{inputenc}
|
||||
\\usepackage[ngerman]{babel}
|
||||
\\usepackage{geometry}
|
||||
\\usepackage{enumitem}
|
||||
\\usepackage{xcolor}
|
||||
\\usepackage{helvet} % Für Sans-Serif Schriftart
|
||||
\\renewcommand{\\familydefault}{\\sfdefault} % Setzt Sans-Serif als Standard
|
||||
\\setlength{\\parindent}{0pt} % Keine Absatzeinrückung
|
||||
|
||||
\\geometry{margin=2cm}
|
||||
|
||||
\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\
|
||||
\\vspace{0.3cm}
|
||||
\\Large Beitrittserklärung}
|
||||
\\date{}
|
||||
|
||||
\\begin{document}
|
||||
\\maketitle
|
||||
|
||||
\\vspace{0.1cm} % Reduzierter Abstand zwischen Titel und Text (1/3 der ursprünglichen Größe)
|
||||
|
||||
Hiermit beantrage ich,
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Name: \\underline{${data.nachname} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Vorname: \\underline{${data.vorname} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Straße: \\underline{${data.strasse} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
PLZ/Wohnort: \\underline{${data.plz} ${data.ort} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Geburtsdatum: \\underline{${new Date(data.geburtsdatum).toLocaleDateString('de-DE')} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Telefon (privat): \\underline{${data.telefon_privat || ''} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
E-Mail: \\underline{${data.email} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Telefon (Mobil): \\underline{${data.telefon_mobil || ''} \\hspace{6cm}}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
meine Aufnahme in den Harheimer Tischtennis-Club 1954 e.V. als
|
||||
|
||||
\\vspace{0.3cm}
|
||||
|
||||
\\begin{itemize}[label={}]
|
||||
\\item[\\framebox(4,4){}] ${data.mitgliedschaftsart === 'aktiv' ? '\\framebox(4,4){X}' : '\\framebox(4,4){}'} aktives Mitglied
|
||||
\\item[\\framebox(4,4){}] ${data.mitgliedschaftsart === 'passiv' ? '\\framebox(4,4){X}' : '\\framebox(4,4){}'} passives Mitglied
|
||||
\\end{itemize}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Den derzeitigen jährlichen Mitgliedsbeitrag in Höhe von
|
||||
|
||||
\\begin{itemize}
|
||||
\\item € 120,-- (Erwachsene)
|
||||
\\item € 72,-- (Jugendliche bis zum vollendeten 18. Lebensjahr)
|
||||
\\item € 30,-- (passive Mitglieder)
|
||||
\\end{itemize}
|
||||
|
||||
bitte ich per Lastschrift jährlich von meinem Konto einzuziehen.
|
||||
Hierzu erteile ich beigefügtes SEPA-Lastschriftmandat.
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Mir ist bekannt, dass die Mitgliedschaft im Harheimer Tischtennis-Club erst nach Bestätigung durch den Vorstand Wirksamkeit erlangt. Die Beitragspflicht beginnt mit dem darauf folgenden Monat.
|
||||
|
||||
Ich erkenne die Vereinssatzung (erhältlich beim Vorstand bzw. auf der Vereinshomepage) an.
|
||||
|
||||
\\vspace{1cm}
|
||||
|
||||
Frankfurt/Main-Harheim, den \\underline{${heute} \\hspace{3cm}}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Unterschrift ${data.isVolljaehrig ? '' : '(bei Jugendlichen gesetzlicher Vertreter)'}
|
||||
|
||||
\\newpage
|
||||
|
||||
\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\
|
||||
\\Large Erteilung eines SEPA-Lastschriftmandates}
|
||||
\\date{}
|
||||
\\maketitle
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\
|
||||
Unsere Gläubiger-Identifikationsnummer: DE46ZZZ00000745362
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Hiermit ermächtige ich den Harheimer Tischtennis-Club 1954 e.V. die jährlichen Mitgliedsbeiträge von meinem untenstehenden Konto per Lastschrift einzuziehen. Zugleich weise ich mein Kreditinstitut an, die vom Harheimer Tischtennis-Club 1954 e.V. auf mein Konto gezogenen Lastschriften einzulösen.
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Hinweis: Ich kann innerhalb von acht Wochen, beginnend mit dem Belastungsdatum, die Erstattung des belasteten Betrages verlangen. Es gelten dabei die mit meinem Kreditinstitut vereinbarten Bedingungen.
|
||||
|
||||
\\vspace{1cm}
|
||||
|
||||
Kontoinhaber: \\underline{${data.kontoinhaber} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
IBAN: \\underline{${data.iban} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
BIC: \\underline{${data.bic || ''} \\hspace{6cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Kreditinstitut: \\underline{${data.bank} \\hspace{6cm}}
|
||||
|
||||
\\vspace{1cm}
|
||||
|
||||
Frankfurt/Main-Harheim, den \\underline{${heute} \\hspace{3cm}}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Unterschrift des Kontoinhabers
|
||||
|
||||
\\newpage
|
||||
|
||||
\\title{\\textbf{Harheimer Tischtennis-Club 1954 e.V.}\\\\
|
||||
\\Large Einwilligungserklärung}
|
||||
\\date{}
|
||||
\\maketitle
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
\\textbf{für die Veröffentlichung von Mitgliederdaten im Internet.}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Der Vereinsvorstand weist hiermit darauf hin, dass ausreichende technische Maßnahmen zur Gewährleistung des Datenschutzes getroffen wurden. Dennoch kann bei einer Veröffentlichung von personenbezogenen Mitgliederdaten im Internet ein umfassender Datenschutz nicht garantiert werden. Daher nimmt das Vereinsmitglied die Risiken für eine eventuelle Persönlichkeitsrechtsverletzung zur Kenntnis und ist sich bewusst, dass:
|
||||
|
||||
\\begin{itemize}
|
||||
\\item die personenbezogenen Daten auch in Staaten abrufbar sind, die keine der Bundesrepublik Deutschland vergleichbaren Datenschutzbestimmungen kennen,
|
||||
\\item die Vertraulichkeit, die Integrität (Unverletzlichkeit), die Authentizität (Echtheit) und die Verfügbarkeit der personenbezogenen Daten nicht garantiert ist.
|
||||
\\end{itemize}
|
||||
|
||||
Das Vereinsmitglied trifft die Entscheidung zur Veröffentlichung seiner Daten im Internet freiwillig und kann seine Einwilligung gegenüber dem Vereinsvorstand jederzeit widerrufen.
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
\\textbf{Erklärung:}
|
||||
|
||||
Ich bestätige das Vorstehende zur Kenntnis genommen zu haben und willige ein, dass der
|
||||
|
||||
\\textbf{Harheimer Tischtennis-Club 1954 e.V.}
|
||||
|
||||
folgende allgemeine Daten zu meiner Person:
|
||||
|
||||
Vorname: \\underline{${data.vorname} \\hspace{4cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Zuname: \\underline{${data.nachname} \\hspace{4cm}}
|
||||
|
||||
Fotografien, sonstige Daten (Mannschaft, Leistungsergebnisse, Turnierteilnahmen, Lizenzen u.ä.) bzw. spezielle Daten von Funktionsträgern:
|
||||
|
||||
Anschrift: \\underline{${data.strasse}, ${data.plz} ${data.ort} \\hspace{4cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
Telefonnummer: \\underline{${data.telefon_privat || data.telefon_mobil || ''} \\hspace{4cm}} \\\\
|
||||
\\vspace{0.3cm}
|
||||
E-Mail-Adresse: \\underline{${data.email} \\hspace{4cm}}
|
||||
|
||||
wie angegeben auf der Homepage des Vereins (www.harheimertc.de) veröffentlichen darf.
|
||||
|
||||
\\vspace{1cm}
|
||||
|
||||
Datum: \\underline{${heute} \\hspace{6cm}}
|
||||
|
||||
\\vspace{0.5cm}
|
||||
|
||||
Unterschrift ${data.isVolljaehrig ? '' : '(bei Minderjährigen Unterschrift eines Erziehungsberechtigten)'}
|
||||
|
||||
\\end{document}`
|
||||
|
||||
// Doppelte Backslashes zu einfachen Backslashes konvertieren, aber \\vspace und \\hspace beibehalten
|
||||
// Erst alle \\vspace und \\hspace temporär ersetzen
|
||||
let result = latexContent.replace(/\\\\vspace/g, 'TEMP_VSPACE')
|
||||
result = result.replace(/\\\\hspace/g, 'TEMP_HSPACE')
|
||||
|
||||
// Dann alle anderen doppelten Backslashes ersetzen
|
||||
result = result.replace(/\\\\/g, '\\')
|
||||
|
||||
// Dann die temporären Platzhalter wieder zurückersetzen
|
||||
result = result.replace(/TEMP_VSPACE/g, '\\vspace')
|
||||
result = result.replace(/TEMP_HSPACE/g, '\\hspace')
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function generateSimplePDF(data, filename, event) {
|
||||
// Fallback: HTML zu PDF mit puppeteer oder ähnlich
|
||||
// Für jetzt: Einfache Textdatei
|
||||
const textContent = `
|
||||
Harheimer Tischtennis-Club 1954 e.V.
|
||||
Beitrittserklärung
|
||||
|
||||
Antragsteller: ${data.vorname} ${data.nachname}
|
||||
Mitgliedschaftsart: ${data.mitgliedschaftsart}
|
||||
Volljährig: ${data.isVolljaehrig ? 'Ja' : 'Nein'}
|
||||
|
||||
Das ausgefüllte Formular ist als Anhang verfügbar.`
|
||||
|
||||
const textPath = path.join(process.cwd(), 'public', 'uploads', `${filename}.txt`)
|
||||
await fs.writeFile(textPath, textContent, 'utf8')
|
||||
|
||||
return `${filename}.txt`
|
||||
}
|
||||
|
||||
function getDataPath(filename) {
|
||||
// Immer den absoluten Pfad zum Projekt-Root verwenden
|
||||
// In der Entwicklung: process.cwd() ist bereits das Projekt-Root
|
||||
// In der Produktion: process.cwd() ist .output, daher ein Verzeichnis zurück
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const projectRoot = isDev ? process.cwd() : path.resolve(process.cwd(), '..')
|
||||
return path.join(projectRoot, 'server', 'data', filename)
|
||||
}
|
||||
|
||||
async function sendMembershipEmail(data, filename, event) {
|
||||
try {
|
||||
const configPath = getDataPath('config.json')
|
||||
const configData = await fs.readFile(configPath, 'utf8')
|
||||
const config = JSON.parse(configData)
|
||||
|
||||
let recipients = []
|
||||
let subject = `Neuer Mitgliedschaftsantrag - ${data.vorname} ${data.nachname}`
|
||||
|
||||
// Sammle alle verfügbaren E-Mail-Adressen
|
||||
const availableEmails = []
|
||||
|
||||
// Vorsitzender E-Mail hinzufügen (falls vorhanden)
|
||||
if (config.vorstand.vorsitzender.email && config.vorstand.vorsitzender.email.trim() !== '') {
|
||||
availableEmails.push(config.vorstand.vorsitzender.email)
|
||||
}
|
||||
|
||||
// Schriftführer E-Mail hinzufügen (falls vorhanden)
|
||||
if (config.vorstand.schriftfuehrer.email && config.vorstand.schriftfuehrer.email.trim() !== '') {
|
||||
availableEmails.push(config.vorstand.schriftfuehrer.email)
|
||||
}
|
||||
|
||||
// Fallback: Wenn keine E-Mails verfügbar sind, verwende tsschulz@tsschulz.de
|
||||
if (availableEmails.length === 0) {
|
||||
recipients = ['tsschulz@tsschulz.de']
|
||||
} else {
|
||||
recipients = availableEmails
|
||||
}
|
||||
|
||||
// In nicht-Produktionsumgebung: Alle E-Mails an tsschulz@tsschulz.de
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
recipients = ['tsschulz@tsschulz.de']
|
||||
}
|
||||
|
||||
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.`
|
||||
|
||||
// E-Mail-Versand implementieren (hier würde normalerweise nodemailer verwendet)
|
||||
console.log('E-Mail würde gesendet werden an:', recipients)
|
||||
console.log('Betreff:', subject)
|
||||
console.log('Nachricht:', message)
|
||||
|
||||
return { success: true, recipients, subject, message }
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Senden der E-Mail:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validierung der Eingabedaten
|
||||
const requiredFields = ['vorname', 'nachname', 'strasse', 'plz', 'ort', 'geburtsdatum', 'email', 'mitgliedschaftsart', 'kontoinhaber', 'iban']
|
||||
for (const field of requiredFields) {
|
||||
if (!body[field]) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `Feld '${field}' ist erforderlich`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Volljährigkeit prüfen
|
||||
const birthDate = new Date(body.geburtsdatum)
|
||||
const today = new Date()
|
||||
const age = today.getFullYear() - birthDate.getFullYear()
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth()
|
||||
const isVolljaehrig = age > 18 || (age === 18 && monthDiff >= 0)
|
||||
|
||||
const data = {
|
||||
...body,
|
||||
isVolljaehrig
|
||||
}
|
||||
|
||||
// Eindeutige Datei-ID generieren
|
||||
const timestamp = Date.now()
|
||||
const filename = `beitrittserklärung_${timestamp}`
|
||||
|
||||
// Temp-Verzeichnis erstellen
|
||||
const tempDir = path.join(process.cwd(), '.output', 'temp', 'latex')
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
|
||||
try {
|
||||
// PDF-Template-Funktion aktiv: versuche Original-PDF-Template herunterzuladen und zu befüllen
|
||||
// Versuch: Original-PDF-Template herunterladen und AcroForm-Felder befüllen
|
||||
async function fillPdfTemplate(data) {
|
||||
// Priorität: neues lokales Fillable-Template in server/templates, sonst ursprüngliches Template
|
||||
const fillablePath = path.join(process.cwd(), 'server', 'templates', 'mitgliedschaft-fillable.pdf')
|
||||
const localPath = (await fs.stat(fillablePath).then(() => fillablePath).catch(() => null)) || path.join(process.cwd(), 'server', 'templates', 'Aufnahmeantrag 2025.pdf')
|
||||
let arrayBuffer
|
||||
try {
|
||||
const localExists = await fs.stat(localPath).then(() => true).catch(() => false)
|
||||
if (localExists) {
|
||||
const buf = await fs.readFile(localPath)
|
||||
arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
|
||||
} else {
|
||||
const TEMPLATE_URL = process.env.MEMBERSHIP_TEMPLATE_URL || 'https://harheimertc.de/Aufnahmeantrag%202025.pdf'
|
||||
const res = await fetch(TEMPLATE_URL)
|
||||
if (!res.ok) throw new Error(`Template konnte nicht geladen werden: ${res.status}`)
|
||||
arrayBuffer = await res.arrayBuffer()
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('Template-Laden fehlgeschlagen: ' + e.message)
|
||||
}
|
||||
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer)
|
||||
let form
|
||||
try {
|
||||
form = pdfDoc.getForm()
|
||||
} catch (e) {
|
||||
form = null
|
||||
}
|
||||
|
||||
if (!form || form.getFields().length === 0) {
|
||||
// Keine Formularfelder vorhanden -> positional filler verwenden
|
||||
// Koordinaten (in PDF-Punkten) müssen ggf. feinjustiert werden.
|
||||
const pages = pdfDoc.getPages()
|
||||
const firstPage = pages[0]
|
||||
const { width, height } = firstPage.getSize()
|
||||
|
||||
// Schätzwerte: (x, y) in Punkten von linker unteren Ecke
|
||||
// Diese Werte müssen nach Sichtprüfung justiert werden.
|
||||
// Angepasste Koordinaten — Text beginnt links an der Linie und sitzt oberhalb der Unterstreichung
|
||||
// Linke Spalte (nachname, vorname, etc.) etwas weiter links setzen
|
||||
const leftX = 156
|
||||
// Rechte Spalte (PLZ/Ort, Telefon, E-Mail) weiter nach rechts für klare Trennung
|
||||
const rightX = 470
|
||||
// Baseline für erstes Feld (von oben aus gerechnet)
|
||||
const baseY = height - 160
|
||||
const gap = 28
|
||||
// Kleine vertikale Verschiebung, damit Texte oberhalb der Unterstreichung beginnen
|
||||
const yOffset = 9
|
||||
|
||||
const coords = {
|
||||
// Row: Name (left) / Vorname (right)
|
||||
nachname: { x: leftX, y: baseY + yOffset },
|
||||
vorname: { x: rightX, y: baseY + yOffset },
|
||||
// Row: Straße (left) / PLZ/Ort (right)
|
||||
strasse: { x: leftX, y: baseY - gap + yOffset },
|
||||
plz_ort: { x: rightX, y: baseY - gap + yOffset },
|
||||
// Row: Geburtsdatum (left) / Telefon(privat) (right)
|
||||
geburtsdatum: { x: leftX, y: baseY - gap * 2 + yOffset },
|
||||
telefon: { x: rightX, y: baseY - gap * 2 + yOffset },
|
||||
// Row: E-Mail (left) / Telefon(Mobil) (right)
|
||||
email: { x: leftX, y: baseY - gap * 3 + yOffset },
|
||||
telefon_mobil: { x: rightX, y: baseY - gap * 3 + yOffset },
|
||||
// Membership checkbox positions (approx.)
|
||||
mitglied_checkbox_aktiv: { x: leftX - 40, y: baseY - gap * 6 + yOffset },
|
||||
mitglied_checkbox_passiv: { x: leftX - 40, y: baseY - gap * 7 + yOffset },
|
||||
// Account details on subsequent page(s)
|
||||
kontoinhaber: { x: leftX, y: baseY - gap * 12 + yOffset },
|
||||
iban: { x: leftX, y: baseY - gap * 13 + yOffset },
|
||||
bic: { x: leftX, y: baseY - gap * 14 + yOffset },
|
||||
bank: { x: leftX, y: baseY - gap * 15 + yOffset }
|
||||
}
|
||||
|
||||
const drawText = (page, text, x, y, size = 11) => {
|
||||
page.drawText(text || '', {
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
font: pdfDoc.embedStandardFont ? undefined : undefined,
|
||||
// default black
|
||||
color: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// Einbettung der Standard-Schrift (Helvetica)
|
||||
const helveticaFont = await pdfDoc.embedFont(PDFDocument.PDFName ? 'Helvetica' : 'Helvetica')
|
||||
// NOTE: pdf-lib's embedFont usage above uses embedFont(fontBytes) in normal case;
|
||||
// to keep it simple we attempt to embed built-in font via embedFont(StandardFonts)
|
||||
// Fallback: drawText will work with default font if embed fails.
|
||||
|
||||
// Zeichne die Felder
|
||||
try {
|
||||
// Zeichne Name / Vorname / Adresse / Kontakt (einmal)
|
||||
firstPage.drawText(data.nachname || '', { x: coords.nachname.x, y: coords.nachname.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.vorname || '', { x: coords.vorname.x, y: coords.vorname.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.strasse || '', { x: coords.strasse.x, y: coords.strasse.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(`${data.plz || ''} ${data.ort || ''}`.trim(), { x: coords.plz_ort.x, y: coords.plz_ort.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(new Date(data.geburtsdatum).toLocaleDateString('de-DE') || '', { x: coords.geburtsdatum.x, y: coords.geburtsdatum.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.telefon_privat || data.telefon_mobil || '', { x: coords.telefon.x, y: coords.telefon.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.email || '', { x: coords.email.x, y: coords.email.y, size: 11, font: helveticaFont })
|
||||
firstPage.drawText(data.telefon_mobil || '', { x: coords.telefon_mobil.x, y: coords.telefon_mobil.y, size: 11, font: helveticaFont })
|
||||
// Kontodaten evtl. auf andere Seite: falls mehrere Seiten vorhanden, nutze last page
|
||||
const lastPage = pages[pages.length - 1]
|
||||
lastPage.drawText(data.kontoinhaber || '', { x: coords.kontoinhaber.x, y: coords.kontoinhaber.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.iban || '', { x: coords.iban.x, y: coords.iban.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.bic || '', { x: coords.bic.x, y: coords.bic.y, size: 11, font: helveticaFont })
|
||||
lastPage.drawText(data.bank || '', { x: coords.bank.x, y: coords.bank.y, size: 11, font: helveticaFont })
|
||||
// Zeichne X in die passende Mitgliedschafts-Checkbox
|
||||
try {
|
||||
if (data.mitgliedschaftsart === 'aktiv') {
|
||||
firstPage.drawText('X', { x: coords.mitglied_checkbox_aktiv.x, y: coords.mitglied_checkbox_aktiv.y, size: 12, font: helveticaFont })
|
||||
} else if (data.mitgliedschaftsart === 'passiv') {
|
||||
firstPage.drawText('X', { x: coords.mitglied_checkbox_passiv.x, y: coords.mitglied_checkbox_passiv.y, size: 12, font: helveticaFont })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Zeichnen der Checkbox:', e.message)
|
||||
}
|
||||
// Debug overlay: zeichne Marker an allen Koordinaten, wenn data.debug === true
|
||||
if (data && data.debug) {
|
||||
try {
|
||||
// Auffälliges Debug-Tag oben links
|
||||
const tagW = 100
|
||||
const tagH = 22
|
||||
firstPage.drawRectangle({ x: 40, y: height - 60, width: tagW, height: tagH, color: rgb(1, 0, 0), opacity: 0.25 })
|
||||
firstPage.drawText('DEBUG', { x: 48, y: height - 56, size: 12, color: rgb(1, 0, 0), font: helveticaFont })
|
||||
|
||||
// Markiere alle Koordinaten mit einem gefüllten roten Quadrat und Label
|
||||
const allCoords = Object.entries(coords)
|
||||
for (const [key, c] of allCoords) {
|
||||
// small filled square
|
||||
firstPage.drawRectangle({ x: c.x - 3, y: c.y - 3, width: 8, height: 8, color: rgb(1, 0, 0), opacity: 1 })
|
||||
// small label a bit to the right
|
||||
firstPage.drawText(key, { x: c.x + 8, y: c.y - 1, size: 7, color: rgb(0.6, 0, 0), font: helveticaFont })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Debug overlay fehlgeschlagen:', e.message)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim positional drawing:', e.message)
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
return Buffer.from(pdfBytes)
|
||||
}
|
||||
|
||||
// Wenn Formularfelder existieren: befülle sie per AcroForm
|
||||
const fields = form.getFields()
|
||||
if (fields && fields.length > 0) {
|
||||
try {
|
||||
const byName = {}
|
||||
for (const f of fields) byName[f.getName().toLowerCase()] = f
|
||||
const setIf = (name, value) => {
|
||||
const f = byName[name]
|
||||
if (!f) return
|
||||
try {
|
||||
if (typeof f.setText === 'function') f.setText(String(value || ''))
|
||||
else if (typeof f.check === 'function' && (value === true || String(value).toLowerCase() === 'true')) f.check()
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Setzen Feld', name, e.message)
|
||||
}
|
||||
}
|
||||
setIf('nachname', data.nachname)
|
||||
setIf('vorname', data.vorname)
|
||||
setIf('strasse', data.strasse)
|
||||
setIf('plz_ort', `${data.plz || ''} ${data.ort || ''}`.trim())
|
||||
setIf('geburtsdatum', new Date(data.geburtsdatum).toLocaleDateString('de-DE'))
|
||||
setIf('telefon', data.telefon_privat || data.telefon_mobil)
|
||||
setIf('email', data.email)
|
||||
setIf('telefon_mobil', data.telefon_mobil)
|
||||
// Checkboxes
|
||||
if (byName['mitglied_aktiv'] && data.mitgliedschaftsart === 'aktiv') byName['mitglied_aktiv'].check && byName['mitglied_aktiv'].check()
|
||||
if (byName['mitglied_passiv'] && data.mitgliedschaftsart === 'passiv') byName['mitglied_passiv'].check && byName['mitglied_passiv'].check()
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
return Buffer.from(pdfBytes)
|
||||
} catch (e) {
|
||||
console.warn('AcroForm-Füllung fehlgeschlagen, fallback auf positional:', e.message)
|
||||
}
|
||||
}
|
||||
const mapValue = (name) => {
|
||||
// einfache Heuristiken für Feldnamen
|
||||
name = name.toLowerCase()
|
||||
if (name.includes('nachname') || name.includes('zuname') || name.includes('name')) return data.nachname || ''
|
||||
if (name.includes('vorname') || name.includes('given')) return data.vorname || ''
|
||||
if (name.includes('str') || name.includes('straße') || name.includes('street')) return data.strasse || ''
|
||||
if (name.includes('plz')) return data.plz || ''
|
||||
if (name.includes('ort') || name.includes('stadt')) return data.ort || ''
|
||||
if (name.includes('geb') || name.includes('geburts')) return new Date(data.geburtsdatum).toLocaleDateString('de-DE')
|
||||
if (name.includes('telefon') || name.includes('tel')) return data.telefon_privat || data.telefon_mobil || ''
|
||||
if (name.includes('email')) return data.email || ''
|
||||
if (name.includes('kontoinhaber') || name.includes('kontoinh')) return data.kontoinhaber || ''
|
||||
if (name.includes('iban')) return data.iban || ''
|
||||
if (name.includes('bic')) return data.bic || ''
|
||||
if (name.includes('bank') || name.includes('kreditinstitut')) return data.bank || ''
|
||||
if (name.includes('mitglied') || name.includes('mitgliedschaft') || name.includes('art')) return data.mitgliedschaftsart || ''
|
||||
return ''
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
const fname = field.getName()
|
||||
const lower = fname.toLowerCase()
|
||||
try {
|
||||
// Textfelder
|
||||
if (typeof field.setText === 'function') {
|
||||
const val = mapValue(lower)
|
||||
field.setText(val)
|
||||
continue
|
||||
}
|
||||
|
||||
// Checkbox / Radio
|
||||
if (typeof field.check === 'function' || typeof field.isChecked === 'function') {
|
||||
// einfache Heuristik: bei Mitgliedschaftsart
|
||||
if (lower.includes('aktiv') || lower.includes('passiv') || lower.includes('mitglied')) {
|
||||
if (data.mitgliedschaftsart && lower.includes(data.mitgliedschaftsart)) {
|
||||
field.check && field.check()
|
||||
} else {
|
||||
if (lower.includes('aktiv') && data.mitgliedschaftsart === 'aktiv') field.check && field.check()
|
||||
if (lower.includes('passiv') && data.mitgliedschaftsart === 'passiv') field.check && field.check()
|
||||
}
|
||||
continue
|
||||
}
|
||||
const mapped = mapValue(lower)
|
||||
if (mapped === 'true' || mapped === 'ja' || mapped === 'checked') {
|
||||
field.check && field.check()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Befüllen Feld', fname, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
return Buffer.from(pdfBytes)
|
||||
}
|
||||
|
||||
let usedTemplate = false
|
||||
const uploadsDir = path.join(process.cwd(), 'public', 'uploads')
|
||||
await fs.mkdir(uploadsDir, { recursive: true })
|
||||
try {
|
||||
const filled = await fillPdfTemplate(data)
|
||||
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
|
||||
await fs.writeFile(finalPdfPath, filled)
|
||||
// Zusätzlich: Kopie ins repo-root public/uploads legen, falls Nitro cwd anders ist
|
||||
try {
|
||||
const repoRoot = path.resolve(process.cwd(), '..')
|
||||
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
||||
await fs.mkdir(repoUploads, { recursive: true })
|
||||
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
|
||||
} catch (e) {
|
||||
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
||||
}
|
||||
usedTemplate = true
|
||||
} catch (templateError) {
|
||||
// Template konnte nicht verwendet werden -> weiter zum LaTeX-Fallback
|
||||
console.warn('Template-Füllung fehlgeschlagen, fahre mit LaTeX fort:', templateError.message)
|
||||
}
|
||||
|
||||
let emailResult
|
||||
if (usedTemplate) {
|
||||
// E-Mail senden
|
||||
emailResult = await sendMembershipEmail(data, filename, event)
|
||||
// Antragsdaten verschlüsselt speichern
|
||||
const encryptionKey = process.env.ENCRYPTION_KEY || 'default-key-change-in-production'
|
||||
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
|
||||
const dataPath = path.join(uploadsDir, `${filename}.data`)
|
||||
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
||||
|
||||
// Download-Token setzen
|
||||
const downloadToken = Buffer.from(`${filename}:${Date.now()}`).toString('base64')
|
||||
setCookie(event, 'download_token', downloadToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 // 24 Stunden
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Beitrittsformular erfolgreich aus Template erzeugt und E-Mail gesendet.',
|
||||
downloadUrl: `/api/membership/download/${filename}.pdf`,
|
||||
emailSuccess: emailResult.success,
|
||||
emailMessage: emailResult.message,
|
||||
usedTemplate: true
|
||||
}
|
||||
}
|
||||
|
||||
// Falls Template nicht verwendet: weiter mit LaTeX-Fallback
|
||||
// LaTeX-Inhalt generieren
|
||||
const latexContent = generateLaTeXContent(data)
|
||||
|
||||
// LaTeX-Datei schreiben
|
||||
const texPath = path.join(tempDir, `${filename}.tex`)
|
||||
await fs.writeFile(texPath, latexContent, 'utf8')
|
||||
|
||||
// PDF mit pdflatex generieren
|
||||
const command = `cd "${tempDir}" && pdflatex -interaction=nonstopmode "${filename}.tex"`
|
||||
await execAsync(command)
|
||||
|
||||
// PDF-Datei in Uploads-Verzeichnis kopieren
|
||||
const pdfPath = path.join(tempDir, `${filename}.pdf`)
|
||||
await fs.mkdir(uploadsDir, { recursive: true })
|
||||
|
||||
const finalPdfPath = path.join(uploadsDir, `${filename}.pdf`)
|
||||
await fs.copyFile(pdfPath, finalPdfPath)
|
||||
// Kopie ins repo-root public/uploads für bessere Auffindbarkeit
|
||||
try {
|
||||
const repoRoot = path.resolve(process.cwd(), '..')
|
||||
const repoUploads = path.join(repoRoot, 'public', 'uploads')
|
||||
await fs.mkdir(repoUploads, { recursive: true })
|
||||
await fs.copyFile(finalPdfPath, path.join(repoUploads, `${filename}.pdf`))
|
||||
} catch (e) {
|
||||
console.warn('Kopie in repo public/uploads fehlgeschlagen:', e.message)
|
||||
}
|
||||
|
||||
// E-Mail senden
|
||||
emailResult = await sendMembershipEmail(data, filename, event)
|
||||
|
||||
// Antragsdaten verschlüsselt speichern
|
||||
const encryptionKey = process.env.ENCRYPTION_KEY || 'default-key-change-in-production'
|
||||
const encryptedData = encrypt(JSON.stringify(data), encryptionKey)
|
||||
const dataPath = path.join(uploadsDir, `${filename}.data`)
|
||||
await fs.writeFile(dataPath, encryptedData, 'utf8')
|
||||
|
||||
// Download-Berechtigung für den Antragsteller setzen
|
||||
const downloadToken = Buffer.from(`${filename}:${Date.now()}`).toString('base64')
|
||||
setCookie(event, 'download_token', downloadToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 // 24 Stunden
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Beitrittsformular erfolgreich erstellt und E-Mail gesendet.',
|
||||
downloadUrl: `/api/membership/download/${filename}.pdf`,
|
||||
emailSuccess: emailResult.success,
|
||||
emailMessage: emailResult.message,
|
||||
}
|
||||
|
||||
} catch (latexError) {
|
||||
console.error('LaTeX-Fehler:', latexError)
|
||||
|
||||
// Fallback: Einfache Textdatei generieren
|
||||
const fallbackFilename = await generateSimplePDF(data, filename, event)
|
||||
|
||||
// E-Mail senden (Fallback)
|
||||
const emailResult = await sendMembershipEmail(data, filename, event)
|
||||
|
||||
console.log('LaTeX nicht verfügbar, verwende Fallback-Lösung')
|
||||
console.log('E-Mail würde gesendet werden an:', emailResult.recipients || [])
|
||||
console.log('Betreff:', emailResult.subject || '')
|
||||
console.log('Nachricht:', emailResult.message || '')
|
||||
console.log('Upload-Verzeichnis:', path.join(process.cwd(), 'public', 'uploads'))
|
||||
|
||||
// Verfügbare Dateien auflisten
|
||||
const uploadsDir = path.join(process.cwd(), 'public', 'uploads')
|
||||
try {
|
||||
const files = await fs.readdir(uploadsDir)
|
||||
console.log('Verfügbare Dateien:', files)
|
||||
|
||||
// Gesuchte Datei finden
|
||||
const targetFile = files.find(file => file.startsWith(filename))
|
||||
console.log('Gesuchte Datei-ID:', filename)
|
||||
console.log('Gefundene Datei:', targetFile || 'Nicht gefunden')
|
||||
} catch (dirError) {
|
||||
console.error('Fehler beim Lesen des Upload-Verzeichnisses:', dirError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Beitrittsformular erfolgreich erstellt und E-Mail gesendet (Fallback-Lösung).',
|
||||
downloadUrl: `/api/membership/download/${fallbackFilename}`,
|
||||
emailSuccess: emailResult.success,
|
||||
emailMessage: emailResult.message,
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Generieren des PDFs:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Fehler beim Generieren des PDFs'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user