const express = require('express'); const Docxtemplater = require('docxtemplater'); const PizZip = require('pizzip'); const fs = require('fs'); const path = require('path'); const multer = require('multer'); const cors = require('cors'); const helmet = require('helmet'); const https = require('https'); const { faker } = require('@faker-js/faker'); const app = express(); const PORT = process.env.PORT || 80; // Management-System Integration removed. Management-related features are disabled. let DataSourceManager = null, CustomTagProcessor = null; const managementConfig = null; // Middleware app.use(helmet()); app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Verzeichnisse erstellen const templateDir = path.join(__dirname, 'templates'); const outputDir = path.join(__dirname, 'documents'); [templateDir, outputDir].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Multer für File Upload const upload = multer({ dest: 'uploads/' }); // Server-URL für dynamische Generierung let serverBaseUrl = `http://localhost:${PORT}`; // Funktion für Statusmeldungen function logFileCreation(filePath, type = 'Datei', customBaseUrl = null) { const absolutePath = path.resolve(filePath); const fileSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; const timestamp = new Date().toLocaleString('de-DE'); const baseUrl = customBaseUrl || serverBaseUrl; console.log(`\n🎉 ${type} erfolgreich erstellt!`); console.log(`📁 Pfad: ${absolutePath}`); console.log(`📏 Größe: ${(fileSize / 1024).toFixed(2)} KB`); console.log(`⏰ Zeit: ${timestamp}`); console.log(`🔗 URL: ${baseUrl}/documents/${path.basename(filePath)}`); console.log('─'.repeat(60)); return { path: absolutePath, size: fileSize, timestamp: timestamp, url: `${baseUrl}/documents/${path.basename(filePath)}` }; } // Demo-Daten-Generator class DemoDataGenerator { static generateData(tags) { const data = {}; tags.forEach(tag => { switch (tag.toLowerCase()) { case 'firma': case 'company': case 'unternehmen': data[tag] = faker.company.name(); break; case 'vorname': case 'firstname': data[tag] = faker.person.firstName(); break; case 'nachname': case 'lastname': case 'name': data[tag] = faker.person.lastName(); break; case 'email': case 'mail': data[tag] = faker.internet.email(); break; case 'telefon': case 'phone': case 'tel': data[tag] = faker.phone.number(); break; case 'adresse': case 'address': case 'strasse': data[tag] = faker.location.streetAddress(); break; case 'stadt': case 'city': data[tag] = faker.location.city(); break; case 'plz': case 'zip': case 'postcode': data[tag] = faker.location.zipCode(); break; case 'land': case 'country': data[tag] = faker.location.country(); break; case 'datum': case 'date': data[tag] = faker.date.recent().toLocaleDateString('de-DE'); break; case 'betrag': case 'amount': case 'summe': case 'total': data[tag] = faker.commerce.price({ min: 100, max: 5000 }); break; case 'nummer': case 'number': case 'id': data[tag] = faker.string.alphanumeric(8).toUpperCase(); break; case 'beschreibung': case 'description': case 'text': data[tag] = faker.lorem.paragraphs(2); break; case 'website': case 'url': data[tag] = faker.internet.url(); break; case 'product': case 'produkt': data[tag] = faker.commerce.productName(); break; case 'price': case 'preis': data[tag] = faker.commerce.price(); break; default: if (tag.includes('datum') || tag.includes('date')) { data[tag] = faker.date.recent().toLocaleDateString('de-DE'); } else if (tag.includes('betrag') || tag.includes('preis') || tag.includes('price')) { data[tag] = faker.commerce.price(); } else if (tag.includes('name')) { data[tag] = faker.person.fullName(); } else { data[tag] = faker.lorem.words(2); } } }); return data; } static generateTableData(columns) { const rowCount = faker.number.int({ min: 3, max: 8 }); const tableData = []; for (let i = 0; i < rowCount; i++) { const row = {}; columns.forEach(col => { if (col.includes('name') || col.includes('bezeichnung')) { row[col] = faker.commerce.productName(); } else if (col.includes('preis') || col.includes('price') || col.includes('betrag')) { row[col] = faker.commerce.price(); } else if (col.includes('datum') || col.includes('date')) { row[col] = faker.date.recent().toLocaleDateString('de-DE'); } else if (col.includes('anzahl') || col.includes('menge') || col.includes('qty')) { row[col] = faker.number.int({ min: 1, max: 10 }); } else { row[col] = faker.lorem.words(2); } }); tableData.push(row); } return tableData; } } // Template-Verarbeitung class TemplateProcessor { // Custom Tags aus Management-System laden static loadCustomTags() { try { const configPath = path.join(__dirname, 'management', 'config.json'); if (!fs.existsSync(configPath)) { console.log('📝 Keine Management-Konfiguration gefunden'); return {}; } const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const customTags = config.customTags || []; const activeTagsData = {}; customTags .filter(tag => tag.enabled) // Nur aktive Tags .forEach(tag => { activeTagsData[tag.tagName] = tag.content || ''; console.log(`✅ Custom Tag aktiviert: ${tag.tagName} = "${tag.content}"`); }); return activeTagsData; } catch (error) { console.log('⚠️ Fehler beim Laden der Custom Tags:', error.message); return {}; } } static extractTags(content) { const simpleTags = [...content.matchAll(/\{([^#/}]+)\}/g)].map(match => match[1].trim()); const loopTags = [...content.matchAll(/\{#(\w+)\}/g)].map(match => match[1].trim()); return { simpleTags: [...new Set(simpleTags)], loopTags: [...new Set(loopTags)] }; } static async processTemplate(templatePath, outputPath, customData = null) { try { const content = fs.readFileSync(templatePath, 'binary'); const zip = new PizZip(content); const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, }); const xmlContent = zip.files['word/document.xml']?.asText() || ''; const { simpleTags, loopTags } = this.extractTags(xmlContent); console.log(`📝 Gefundene Tags: ${simpleTags.length} einfache, ${loopTags.length} Schleifen`); // Custom Tags aus Management-System laden const customTagsData = this.loadCustomTags(); console.log(`🏷️ Custom Tags geladen: ${Object.keys(customTagsData).length} aktive Tags`); // Daten zusammenstellen: Custom-Daten haben Priorität, dann Custom Tags, dann Auto-Generierung let data = {}; if (customData && typeof customData === 'object') { data = { ...customData }; console.log(`🎯 Custom-Daten verwendet: ${Object.keys(customData).length} Felder`); } // Custom Tags hinzufügen data = { ...data, ...customTagsData }; // Fehlende Tags automatisch generieren const missingTags = simpleTags.filter(tag => !(tag in data)); if (missingTags.length > 0) { const generatedData = DemoDataGenerator.generateData(missingTags); data = { ...data, ...generatedData }; console.log(`🤖 Auto-generiert: ${missingTags.length} fehlende Tags`); } // Loop-Tags verarbeiten if (loopTags.length > 0) { loopTags.forEach(loopTag => { // Prüfen ob Custom-Daten für diese Tabelle vorhanden sind if (!data[loopTag]) { const loopContent = xmlContent.match(new RegExp(`{#${loopTag}}([\\s\\S]*?){/${loopTag}}`, 'g')); if (loopContent && loopContent[0]) { const innerContent = loopContent[0].replace(`{#${loopTag}}`, '').replace(`{/${loopTag}}`, ''); const tableColumns = [...innerContent.matchAll(/\{(\w+)\}/g)].map(match => match[1]); data[loopTag] = DemoDataGenerator.generateTableData(tableColumns); console.log(`📊 Auto-generierte Tabelle "${loopTag}" mit ${data[loopTag].length} Zeilen`); } } else { console.log(`📋 Custom-Tabelle "${loopTag}" mit ${data[loopTag].length} Zeilen verwendet`); } }); } doc.render(data); const buf = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE', }); fs.writeFileSync(outputPath, buf); // Statusmeldung für erstellte Datei const fileInfo = logFileCreation(outputPath, 'DOCX-Dokument'); return { success: true, data: data, extractedTags: { simpleTags, loopTags }, fileInfo: fileInfo }; } catch (error) { console.error('❌ Template Verarbeitung fehlgeschlagen:', error); return { success: false, error: error.message }; } } } // Routes // Hauptseite app.get('/', (req, res) => { res.send(` DOCX Template Server

📄 DOCX Template Server

Automatische Befüllung von Word-Templates mit intelligenten Demo-Daten

🚀 Template hochladen und verarbeiten


Automatische Tag-Erkennung: Der Server erkennt automatisch Tags wie {name}, {firma}, {datum} und füllt sie mit passenden Demo-Daten.

📋 Unterstützte Tags

  • {firma} - Firmenname
  • {name}, {vorname} - Personendaten
  • {email}, {telefon} - Kontaktdaten
  • {datum} - Datumsangaben
  • {betrag} - Preise und Beträge

🔄 Tabellen & Schleifen

  • {#items}...{/items} - Wiederholungen
  • Automatische Tabellenbefüllung
  • Intelligente Spaltenerkennung
  • 3-8 Zeilen pro Tabelle

🔌 API-Endpunkt: Template befüllen

POST /api/process-template
Befüllt ein vorhandenes Template mit benutzerdefinierten oder auto-generierten Daten.

📋 Request Body (JSON):

{
  "templateName": "mein_template.docx",    // Erforderlich: Template aus /templates/
  "outputName": "mein_dokument.docx",     // Erforderlich: Name der Ausgabedatei
  "customData": {                         // Optional: Eigene Daten
    "firma": "Meine Firma GmbH",
    "vorname": "Max",
    "nachname": "Mustermann",
    "email": "max@example.com",
    "datum": "04.10.2025",
    "betrag": "1500.00"
  }
}
💡 Tipp: Wenn customData nicht alle Tags abdeckt, werden fehlende Tags automatisch mit Demo-Daten befüllt.

📊 API-Endpunkt: Templates mit Tabellen

POST /api/process-template
Unterstützt auch Tabellen/Schleifen mit benutzerdefinierten Daten.

🔄 Tabellen-Tags in Templates:

{#items}
Artikel: {items_name}
Preis: {items_preis} EUR
Datum: {items_datum}
{/items}

📋 Request mit Tabellendaten:

{
  "templateName": "rechnung_mit_tabelle.docx",
  "outputName": "rechnung_mit_daten.docx",
  "customData": {
    "firma": "TechSolutions GmbH",
    "vorname": "Maria",
    "nachname": "Weber",
    "email": "maria.weber@techsolutions.de",
    "datum": "04.10.2025",
    "items": [
      {
        "items_name": "Webentwicklung",
        "items_preis": "2500.00",
        "items_datum": "01.10.2025",
        "items_beschreibung": "Frontend und Backend Entwicklung"
      },
      {
        "items_name": "Design & UX",
        "items_preis": "1200.00", 
        "items_datum": "02.10.2025",
        "items_beschreibung": "User Interface Design"
      },
      {
        "items_name": "Hosting & Support",
        "items_preis": "300.00",
        "items_datum": "03.10.2025",
        "items_beschreibung": "Monatliches Hosting und Support"
      }
    ],
    "betrag": "4000.00"
  }
}

🔧 Automatische Tabellen-Generierung:

Ohne customData für Tabellen: Werden automatisch 3-8 Zeilen mit passenden Demo-Daten generiert.
Mit customData: Ihre Daten werden 1:1 verwendet.
Gemischt: Ihre Tabellendaten + Auto-Generierung für fehlende einfache Tags.

📝 Beispiel: Nur einfache Daten, Tabelle auto-generiert:

{
  "templateName": "rechnung_mit_tabelle.docx",
  "outputName": "auto_tabelle_rechnung.docx",
  "customData": {
    "firma": "Mustermann GmbH",
    "vorname": "Hans",
    "nachname": "Mustermann"
    // Tabelle "items" wird automatisch generiert mit 3-8 Zeilen
  }
}

🎯 Intelligente Spaltenerkennung:

📁 API & Downloads

Templates anzeigen Dokumente anzeigen Test Template erstellen

📊 Server Status

Port: ${PORT}

Templates: ${fs.readdirSync(templateDir).length} Dateien

Dokumente: ${fs.readdirSync(outputDir).length} Dateien

`); }); // Template Upload und Verarbeitung app.post('/upload-template', upload.single('template'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Keine Datei hochgeladen' }); } const templateName = req.file.originalname; const templatePath = path.join(templateDir, templateName); const outputName = templateName.replace('.docx', '_ausgefuellt.docx'); const outputPath = path.join(outputDir, outputName); console.log(`\n📤 Template Upload: ${templateName}`); // Template in Templates-Verzeichnis kopieren fs.copyFileSync(req.file.path, templatePath); console.log(`📁 Template gespeichert: ${templatePath}`); // Template verarbeiten console.log(`⚙️ Verarbeitung gestartet...`); const result = await TemplateProcessor.processTemplate(templatePath, outputPath); // Upload-Datei löschen fs.unlinkSync(req.file.path); if (result.success) { const protocol = req.secure ? 'https' : 'http'; const host = req.get('host') || `localhost:${PORT}`; res.json({ success: true, message: 'Template erfolgreich verarbeitet', templateName: templateName, outputName: outputName, extractedTags: result.extractedTags, generatedData: result.data, fileInfo: result.fileInfo, urls: { template: `${protocol}://${host}/templates/${templateName}`, document: `${protocol}://${host}/documents/${outputName}`, download: `${protocol}://${host}/download/${outputName}` } }); } else { console.log(`❌ Verarbeitung fehlgeschlagen: ${result.error}`); res.status(500).json({ error: result.error }); } } catch (error) { console.error('❌ Upload Fehler:', error); res.status(500).json({ error: 'Server Fehler beim Upload' }); } }); // API-Endpunkt: Template befüllen mit konfigurierbaren Namen app.post('/api/process-template', async (req, res) => { try { const { templateName, outputName, customData } = req.body; // Validierung if (!templateName) { return res.status(400).json({ error: 'Template-Name ist erforderlich' }); } if (!outputName) { return res.status(400).json({ error: 'Output-Name ist erforderlich' }); } // Template-Pfad prüfen const templatePath = path.join(templateDir, templateName); if (!fs.existsSync(templatePath)) { return res.status(404).json({ error: 'Template nicht gefunden', availableTemplates: fs.readdirSync(templateDir).filter(f => f.endsWith('.docx')) }); } // Output-Namen vorbereiten (automatisch .docx hinzufügen wenn nicht vorhanden) const finalOutputName = outputName.endsWith('.docx') ? outputName : `${outputName}.docx`; const outputPath = path.join(outputDir, finalOutputName); console.log(`\n🎯 API Template-Verarbeitung gestartet:`); console.log(`📋 Template: ${templateName}`); console.log(`📄 Output: ${finalOutputName}`); // Template verarbeiten const result = await TemplateProcessor.processTemplate(templatePath, outputPath, customData); if (result.success) { const protocol = req.secure ? 'https' : 'http'; const host = req.get('host') || `localhost:${PORT}`; res.json({ success: true, message: 'Template erfolgreich verarbeitet via API', request: { templateName: templateName, outputName: finalOutputName, customDataProvided: !!customData }, processing: { extractedTags: result.extractedTags, generatedData: result.data }, fileInfo: result.fileInfo, urls: { template: `${protocol}://${host}/templates/${templateName}`, document: `${protocol}://${host}/documents/${finalOutputName}`, download: `${protocol}://${host}/download/${finalOutputName}` } }); } else { console.log(`❌ API-Verarbeitung fehlgeschlagen: ${result.error}`); res.status(500).json({ error: result.error, templateName: templateName, outputName: finalOutputName }); } } catch (error) { console.error('❌ API-Verarbeitung Fehler:', error); res.status(500).json({ error: 'Server Fehler bei API-Verarbeitung' }); } }); // Statische Dateien servieren app.use('/templates', express.static(templateDir)); app.use('/documents', express.static(outputDir)); // Download-Route mit Statusmeldung app.get('/download/:filename', (req, res) => { const filename = req.params.filename; const filePath = path.join(outputDir, filename); if (fs.existsSync(filePath)) { console.log(`\n⬇️ Download gestartet: ${filename}`); console.log(`📁 Pfad: ${filePath}`); console.log(`👤 Client: ${req.ip}`); console.log(`⏰ Zeit: ${new Date().toLocaleString('de-DE')}`); res.download(filePath, filename, (err) => { if (err) { console.error('❌ Download Fehler:', err); } else { console.log(`✅ Download abgeschlossen: ${filename}`); } }); } else { console.log(`❌ Datei nicht gefunden: ${filename}`); res.status(404).json({ error: 'Datei nicht gefunden' }); } }); // API Endpunkte app.get('/api/templates', (req, res) => { try { const protocol = req.secure ? 'https' : 'http'; const host = req.get('host') || `localhost:${PORT}`; const files = fs.readdirSync(templateDir) .filter(file => file.endsWith('.docx')) .map(file => { const filePath = path.join(templateDir, file); const stats = fs.statSync(filePath); return { name: file, size: stats.size, created: stats.birthtime, modified: stats.mtime, url: `${protocol}://${host}/templates/${file}` }; }); res.json({ templates: files, count: files.length }); } catch (error) { res.status(500).json({ error: 'Fehler beim Auflisten der Templates' }); } }); app.get('/api/documents', (req, res) => { try { const protocol = req.secure ? 'https' : 'http'; const host = req.get('host') || `localhost:${PORT}`; const files = fs.readdirSync(outputDir) .filter(file => file.endsWith('.docx')) .map(file => { const filePath = path.join(outputDir, file); const stats = fs.statSync(filePath); return { name: file, size: stats.size, created: stats.birthtime, modified: stats.mtime, url: `${protocol}://${host}/documents/${file}`, download: `${protocol}://${host}/download/${file}` }; }); res.json({ documents: files, count: files.length }); } catch (error) { res.status(500).json({ error: 'Fehler beim Auflisten der Dokumente' }); } }); // Test Template erstellen app.get('/create-test-template', (req, res) => { try { const templateName = 'test_template.docx'; const templatePath = path.join(templateDir, templateName); // Verwende das vorhandene rechnung_template.docx wenn verfügbar const existingTemplate = path.join(__dirname, 'rechnung_template.docx'); if (fs.existsSync(existingTemplate)) { fs.copyFileSync(existingTemplate, templatePath); } else { // Fallback: Einfaches Template const PizZip = require('pizzip'); const zip = new PizZip(); zip.file('word/document.xml', ` Test Template Firma: {firma} Name: {vorname} {nachname} E-Mail: {email} Datum: {datum} Betrag: {betrag} `); zip.file('[Content_Types].xml', ` `); zip.file('_rels/.rels', ` `); const buffer = zip.generate({ type: 'nodebuffer' }); fs.writeFileSync(templatePath, buffer); } // Statusmeldung für Template-Erstellung const fileInfo = logFileCreation(templatePath, 'Test-Template'); res.json({ success: true, message: 'Test Template erfolgreich erstellt', templateName: templateName, fileInfo: fileInfo, nextStep: 'Besuchen Sie die Hauptseite und laden das erstellte Template hoch' }); } catch (error) { console.error('❌ Test Template Erstellung fehlgeschlagen:', error); res.status(500).json({ error: 'Fehler beim Erstellen des Test Templates' }); } }); // API für Management-Server Integration app.get('/api/status', (req, res) => { const uptime = process.uptime(); res.json({ success: true, status: 'running', uptime: Math.floor(uptime), uptimeString: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${Math.floor(uptime % 60)}s`, templateCount: fs.existsSync(templateDir) ? fs.readdirSync(templateDir).length : 0, documentCount: fs.existsSync(outputDir) ? fs.readdirSync(outputDir).length : 0, customTagsEnabled: managementConfig && managementConfig.customTags ? managementConfig.customTags.filter(tag => tag.enabled).length : 0, ssl: fs.existsSync(path.join(__dirname, '203_cert.pem')) && fs.existsSync(path.join(__dirname, '203_key.pem')), version: '1.0.0' }); }); app.get('/api/config', (req, res) => { res.json({ success: true, config: { templateDir: templateDir, outputDir: outputDir, ssl: fs.existsSync(path.join(__dirname, '203_cert.pem')) && fs.existsSync(path.join(__dirname, '203_key.pem')), managementIntegration: !!managementConfig, customTags: managementConfig ? managementConfig.customTags || [] : [], dataSources: managementConfig ? managementConfig.dataSources || [] : [] } }); }); app.delete('/api/templates/:filename', (req, res) => { try { const filename = req.params.filename; const filePath = path.join(templateDir, filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ success: false, message: 'Template nicht gefunden' }); } fs.unlinkSync(filePath); res.json({ success: true, message: 'Template gelöscht' }); } catch (error) { res.status(500).json({ success: false, message: 'Fehler beim Löschen: ' + error.message }); } }); app.get('/api/templates/:filename/download', (req, res) => { try { const filename = req.params.filename; const filePath = path.join(templateDir, filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ success: false, message: 'Template nicht gefunden' }); } res.download(filePath); } catch (error) { res.status(500).json({ success: false, message: 'Download-Fehler: ' + error.message }); } }); // SSL-Unterstützung if (fs.existsSync(path.join(__dirname, '203_cert.pem')) && fs.existsSync(path.join(__dirname, '203_key.pem'))) { try { const sslOptions = { key: fs.readFileSync(path.join(__dirname, '203_key.pem')), cert: fs.readFileSync(path.join(__dirname, '203_cert.pem')) }; https.createServer(sslOptions, app).listen(443, '0.0.0.0', () => { console.log('🔒 HTTPS Server läuft auf Port 443 (öffentlich)'); console.log('🌐 HTTPS Zugang: https://[Ihre-Server-IP]:443'); }); } catch (error) { console.error('❌ HTTPS Server Fehler:', error.message); } } // HTTP Server starten app.listen(PORT, () => { console.log('\n🚀 DOCX Template Server gestartet!'); console.log(`📍 HTTP Server: http://localhost:${PORT}`); console.log(`📁 Templates Verzeichnis: ${templateDir}`); console.log(`📁 Output Verzeichnis: ${outputDir}`); console.log('\n💡 Tipp: Besuchen Sie http://localhost:80 für die Web-Oberfläche'); if (fs.existsSync(path.join(__dirname, '203_cert.pem'))) { console.log('🔒 HTTPS auch verfügbar: https://localhost:443'); } console.log('\n📊 Server bereit für Template-Verarbeitung mit Statusmeldungen!'); console.log('─'.repeat(60)); });