- ✅ Node.js/Express Server mit DOCX Template-Verarbeitung - ✅ Automatische Tag-Erkennung und Demo-Daten-Generierung - ✅ Tabellen-Unterstützung mit Schleifen-Tags - ✅ REST-API /api/process-template für externe Integration - ✅ Web-Oberfläche mit vollständiger Dokumentation - ✅ SSL-Unterstützung (HTTPS Port 443 öffentlich) - ✅ Intelligente Spaltenerkennung für Tabellen - ✅ Detaillierte Statusmeldungen für alle Operationen - ✅ Flexible Custom-Daten + Auto-Generierung - ✅ Template- und Dokument-Management APIs
939 lines
39 KiB
JavaScript
939 lines
39 KiB
JavaScript
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 crypto = require('crypto');
|
||
const { faker } = require('@faker-js/faker');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 80;
|
||
|
||
// 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 });
|
||
}
|
||
});
|
||
|
||
// WebDAV-Implementierung für Word/Office-Integration mit SharePoint-Kompatibilität
|
||
const webdavMethods = ['PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK'];
|
||
|
||
// SharePoint-spezifische Namespaces und Header
|
||
const sharePointNamespaces = {
|
||
'xmlns:D': 'DAV:',
|
||
'xmlns:S': 'http://schemas.microsoft.com/sharepoint/soap/',
|
||
'xmlns:Z': 'urn:schemas-microsoft-com:',
|
||
'xmlns:O': 'urn:schemas-microsoft-com:office:office',
|
||
'xmlns:T': 'http://schemas.microsoft.com/repl/'
|
||
};
|
||
|
||
// WebDAV PROPFIND Handler mit SharePoint-Modus
|
||
app.use('/webdav', (req, res, next) => {
|
||
// Erweiterte CORS und WebDAV-Header für Office/SharePoint
|
||
res.set({
|
||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||
'MS-Author-Via': 'DAV',
|
||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||
'SPIisLatency': '0',
|
||
'Allow': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||
'Access-Control-Allow-Origin': '*',
|
||
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, Destination, If, Lock-Token, Overwrite, Timeout, X-Requested-With, SOAPAction',
|
||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||
'Pragma': 'no-cache',
|
||
'Expires': '0',
|
||
'X-SharePointHealthScore': '0',
|
||
'X-AspNet-Version': '4.0.30319',
|
||
'X-Powered-By': 'ASP.NET'
|
||
});
|
||
|
||
if (req.method === 'OPTIONS') {
|
||
return res.status(200).end();
|
||
}
|
||
|
||
if (req.method === 'PROPFIND') {
|
||
const depth = req.headers.depth || 'infinity';
|
||
const requestPath = req.path.replace('/webdav', '');
|
||
|
||
let targetDir;
|
||
if (requestPath.startsWith('/templates')) {
|
||
targetDir = path.join(templateDir, requestPath.replace('/templates', ''));
|
||
} else if (requestPath.startsWith('/documents')) {
|
||
targetDir = path.join(outputDir, requestPath.replace('/documents', ''));
|
||
} else {
|
||
targetDir = __dirname;
|
||
}
|
||
|
||
try {
|
||
let items = [];
|
||
|
||
if (fs.existsSync(targetDir)) {
|
||
const stats = fs.statSync(targetDir);
|
||
|
||
if (stats.isDirectory()) {
|
||
const files = fs.readdirSync(targetDir);
|
||
|
||
// Verzeichnis selbst
|
||
items.push({
|
||
href: req.path + (req.path.endsWith('/') ? '' : '/'),
|
||
isDirectory: true,
|
||
lastModified: stats.mtime.toUTCString(),
|
||
size: 0
|
||
});
|
||
|
||
// Dateien im Verzeichnis
|
||
files.forEach(file => {
|
||
const filePath = path.join(targetDir, file);
|
||
const fileStats = fs.statSync(filePath);
|
||
|
||
items.push({
|
||
href: req.path + (req.path.endsWith('/') ? '' : '/') + file,
|
||
isDirectory: fileStats.isDirectory(),
|
||
lastModified: fileStats.mtime.toUTCString(),
|
||
size: fileStats.size || 0,
|
||
contentType: fileStats.isDirectory() ? 'httpd/unix-directory' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||
});
|
||
});
|
||
} else {
|
||
// Einzelne Datei
|
||
items.push({
|
||
href: req.path,
|
||
isDirectory: false,
|
||
lastModified: stats.mtime.toUTCString(),
|
||
size: stats.size,
|
||
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||
});
|
||
}
|
||
}
|
||
|
||
// WebDAV XML Response mit SharePoint-spezifischen Eigenschaften
|
||
const xmlResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||
<D:multistatus xmlns:D="DAV:" xmlns:S="http://schemas.microsoft.com/sharepoint/soap/" xmlns:Z="urn:schemas-microsoft-com:" xmlns:O="urn:schemas-microsoft-com:office:office">
|
||
${items.map(item => `
|
||
<D:response>
|
||
<D:href>${item.href}</D:href>
|
||
<D:propstat>
|
||
<D:prop>
|
||
<D:displayname>${path.basename(item.href)}</D:displayname>
|
||
<D:getlastmodified>${item.lastModified}</D:getlastmodified>
|
||
<D:getcontentlength>${item.size}</D:getcontentlength>
|
||
<D:getcontenttype>${item.contentType}</D:getcontenttype>
|
||
<D:resourcetype>${item.isDirectory ? '<D:collection/>' : ''}</D:resourcetype>
|
||
<D:supportedlock>
|
||
<D:lockentry>
|
||
<D:lockscope><D:exclusive/></D:lockscope>
|
||
<D:locktype><D:write/></D:locktype>
|
||
</D:lockentry>
|
||
<D:lockentry>
|
||
<D:lockscope><D:shared/></D:lockscope>
|
||
<D:locktype><D:write/></D:locktype>
|
||
</D:lockentry>
|
||
</D:supportedlock>
|
||
<D:getetag>"${Date.now()}-${item.size}"</D:getetag>
|
||
<D:creationdate>${new Date(item.lastModified).toISOString()}</D:creationdate>
|
||
<D:iscollection>${item.isDirectory ? 'true' : 'false'}</D:iscollection>
|
||
<D:executable>F</D:executable>
|
||
<D:ishidden>F</D:ishidden>
|
||
<D:isreadonly>F</D:isreadonly>
|
||
<O:IsReadonly>F</O:IsReadonly>
|
||
<Z:Win32CreationTime>${new Date(item.lastModified).toISOString()}</Z:Win32CreationTime>
|
||
<Z:Win32LastAccessTime>${new Date().toISOString()}</Z:Win32LastAccessTime>
|
||
<Z:Win32LastModifiedTime>${new Date(item.lastModified).toISOString()}</Z:Win32LastModifiedTime>
|
||
<Z:Win32FileAttributes>00000020</Z:Win32FileAttributes>
|
||
<S:modifiedby>System Account</S:modifiedby>
|
||
<S:checkedoutby></S:checkedoutby>
|
||
<S:IsCheckedoutToLocal>0</S:IsCheckedoutToLocal>
|
||
<S:CheckoutExpires></S:CheckoutExpires>
|
||
<S:CanCheckOut>1</S:CanCheckOut>
|
||
<S:ContentTypeId>0x0101</S:ContentTypeId>
|
||
<S:DocumentWorkflowStatus>0</S:DocumentWorkflowStatus>
|
||
</D:prop>
|
||
<D:status>HTTP/1.1 200 OK</D:status>
|
||
</D:propstat>
|
||
</D:response>`).join('')}
|
||
</D:multistatus>`;
|
||
|
||
res.set('Content-Type', 'application/xml; charset=utf-8');
|
||
res.status(207).send(xmlResponse);
|
||
} catch (error) {
|
||
res.status(404).send('Not Found');
|
||
}
|
||
return;
|
||
}
|
||
|
||
next();
|
||
});
|
||
|
||
// WebDAV PUT Handler für Dateien speichern
|
||
// PUT - File Upload (für Word Speichern)
|
||
app.put('/dav/*', (req, res) => {
|
||
const filePath = path.join(__dirname, 'uploads', req.params[0]);
|
||
const dirPath = path.dirname(filePath);
|
||
|
||
console.log(`<EFBFBD> PUT Request: ${req.params[0]}`);
|
||
console.log(`📂 Speichere nach: ${filePath}`);
|
||
|
||
// Stelle sicher dass der Ordner existiert
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
|
||
const writeStream = fs.createWriteStream(filePath);
|
||
|
||
req.pipe(writeStream);
|
||
|
||
writeStream.on('finish', () => {
|
||
console.log(`✅ Datei gespeichert: ${filePath}`);
|
||
res.set({
|
||
'DAV': '1, 2, ordered-collections, versioning, extended-mkcol',
|
||
'MS-Author-Via': 'DAV',
|
||
'Server': 'Microsoft-IIS/10.0 Microsoft-HTTPAPI/2.0',
|
||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||
'Cache-Control': 'no-cache',
|
||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||
'Last-Modified': new Date().toUTCString(),
|
||
'X-SharePoint-HealthScore': '0'
|
||
});
|
||
res.status(201).send('Created');
|
||
});
|
||
|
||
writeStream.on('error', (err) => {
|
||
console.error(`❌ Fehler beim Speichern: ${err.message}`);
|
||
res.status(500).send('Internal Server Error');
|
||
});
|
||
});
|
||
|
||
app.put('/webdav/documents/:filename', (req, res) => {
|
||
const filename = req.params.filename;
|
||
const filePath = path.join(outputDir, filename);
|
||
|
||
console.log(`📝 Word speichert Dokument: ${filename}`);
|
||
|
||
const writeStream = fs.createWriteStream(filePath);
|
||
req.pipe(writeStream);
|
||
|
||
writeStream.on('finish', () => {
|
||
// Dateirechte explizit setzen
|
||
try {
|
||
fs.chmodSync(filePath, 0o666); // Lese-/Schreibrechte für alle
|
||
} catch (error) {
|
||
console.warn('Warnung: Konnte Dateiberechtigungen nicht setzen:', error.message);
|
||
}
|
||
|
||
res.set({
|
||
'DAV': '1, 2, ordered-collections, versioning',
|
||
'MS-Author-Via': 'DAV',
|
||
'Server': 'Microsoft-IIS/10.0',
|
||
'Cache-Control': 'no-cache',
|
||
'ETag': `"${Date.now()}-${req.headers['content-length'] || 0}"`,
|
||
'Last-Modified': new Date().toUTCString()
|
||
});
|
||
res.status(201).send('Created');
|
||
console.log(`✅ Dokument gespeichert: ${filename}`);
|
||
});
|
||
|
||
writeStream.on('error', (error) => {
|
||
console.error('❌ Fehler beim Speichern:', error);
|
||
res.status(500).send('Internal Server Error');
|
||
});
|
||
});
|
||
|
||
// WebDAV GET Handler für Dateien lesen
|
||
app.get('/webdav/templates/:filename', (req, res) => {
|
||
const filename = req.params.filename;
|
||
const filePath = path.join(templateDir, filename);
|
||
|
||
if (fs.existsSync(filePath)) {
|
||
const stats = fs.statSync(filePath);
|
||
res.set({
|
||
'DAV': '1, 2, ordered-collections, versioning',
|
||
'MS-Author-Via': 'DAV',
|
||
'Server': 'Microsoft-IIS/10.0',
|
||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'Content-Length': stats.size,
|
||
'Last-Modified': stats.mtime.toUTCString(),
|
||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||
'Accept-Ranges': 'bytes',
|
||
'Cache-Control': 'no-cache'
|
||
});
|
||
console.log(`📖 Word öffnet Template: ${filename}`);
|
||
res.sendFile(filePath);
|
||
} else {
|
||
res.status(404).send('Not Found');
|
||
}
|
||
});
|
||
|
||
app.get('/webdav/documents/:filename', (req, res) => {
|
||
const filename = req.params.filename;
|
||
const filePath = path.join(outputDir, filename);
|
||
|
||
if (fs.existsSync(filePath)) {
|
||
const stats = fs.statSync(filePath);
|
||
res.set({
|
||
'DAV': '1, 2, ordered-collections, versioning',
|
||
'MS-Author-Via': 'DAV',
|
||
'Server': 'Microsoft-IIS/10.0',
|
||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'Content-Length': stats.size,
|
||
'Last-Modified': stats.mtime.toUTCString(),
|
||
'ETag': `"${stats.mtime.getTime()}-${stats.size}"`,
|
||
'Accept-Ranges': 'bytes',
|
||
'Cache-Control': 'no-cache'
|
||
});
|
||
console.log(`📖 Word öffnet Dokument: ${filename}`);
|
||
res.sendFile(filePath);
|
||
} else {
|
||
res.status(404).send('Not Found');
|
||
}
|
||
});
|
||
|
||
// WebDAV LOCK/UNLOCK für Office
|
||
app.use('/webdav', (req, res, next) => {
|
||
if (req.method === 'LOCK') {
|
||
const lockToken = 'opaquelocktoken:' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||
|
||
const lockResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||
<D:prop xmlns:D="DAV:">
|
||
<D:lockdiscovery>
|
||
<D:activelock>
|
||
<D:locktype><D:write/></D:locktype>
|
||
<D:lockscope><D:exclusive/></D:lockscope>
|
||
<D:depth>0</D:depth>
|
||
<D:timeout>Second-604800</D:timeout>
|
||
<D:locktoken><D:href>${lockToken}</D:href></D:locktoken>
|
||
</D:activelock>
|
||
</D:lockdiscovery>
|
||
</D:prop>`;
|
||
|
||
res.set({
|
||
'Content-Type': 'application/xml; charset=utf-8',
|
||
'Lock-Token': `<${lockToken}>`,
|
||
'DAV': '1, 2',
|
||
'MS-Author-Via': 'DAV'
|
||
});
|
||
res.status(200).send(lockResponse);
|
||
return;
|
||
}
|
||
|
||
if (req.method === 'UNLOCK') {
|
||
res.set({
|
||
'DAV': '1, 2',
|
||
'MS-Author-Via': 'DAV'
|
||
});
|
||
res.status(204).send();
|
||
return;
|
||
}
|
||
|
||
next();
|
||
});
|
||
|
||
app.use('/webdav/documents', express.static(outputDir, {
|
||
setHeaders: (res, path) => {
|
||
res.set({
|
||
'DAV': '1, 2',
|
||
'Allow': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||
'Access-Control-Allow-Origin': '*',
|
||
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, HEAD, OPTIONS',
|
||
'Access-Control-Allow-Headers': 'Content-Type, Depth, Authorization, If-Match, If-None-Match, Overwrite, Destination',
|
||
'MS-Author-Via': 'DAV',
|
||
'Cache-Control': 'no-cache'
|
||
});
|
||
}
|
||
}));
|
||
|
||
// WebDAV PROPFIND Support für Word
|
||
app.use('/webdav/*', (req, res, next) => {
|
||
if (req.method === 'PROPFIND') {
|
||
res.set({
|
||
'Content-Type': 'application/xml; charset=utf-8',
|
||
'DAV': '1, 2',
|
||
'MS-Author-Via': 'DAV'
|
||
});
|
||
|
||
const propfindResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||
<D:multistatus xmlns:D="DAV:">
|
||
<D:response>
|
||
<D:href>${req.originalUrl}</D:href>
|
||
<D:propstat>
|
||
<D:status>HTTP/1.1 200 OK</D:status>
|
||
<D:prop>
|
||
<D:resourcetype><D:collection/></D:resourcetype>
|
||
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
||
<D:supportedlock>
|
||
<D:lockentry>
|
||
<D:lockscope><D:exclusive/></D:lockscope>
|
||
<D:locktype><D:write/></D:locktype>
|
||
</D:lockentry>
|
||
</D:supportedlock>
|
||
</D:prop>
|
||
</D:propstat>
|
||
</D:response>
|
||
</D:multistatus>`;
|
||
|
||
res.status(207).send(propfindResponse);
|
||
return;
|
||
}
|
||
next();
|
||
});
|
||
|
||
// SharePoint-spezifische Endpunkte
|
||
app.get('/_vti_inf.html', (req, res) => {
|
||
// SharePoint Info-Seite
|
||
res.set('Content-Type', 'text/html');
|
||
res.send(`<!-- FrontPage Configuration Information
|
||
FP_VER:15.0.0.0000
|
||
FP_SCHEMA_VER:15.0.0.0000
|
||
-->`);
|
||
});
|
||
|
||
app.post('/_vti_bin/lists.asmx', (req, res) => {
|
||
// SharePoint Lists Web Service
|
||
res.set({
|
||
'Content-Type': 'text/xml; charset=utf-8',
|
||
'SOAPAction': req.headers.soapaction || ''
|
||
});
|
||
|
||
const soapResponse = `<?xml version="1.0" encoding="utf-8"?>
|
||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||
<soap:Body>
|
||
<GetListCollectionResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
|
||
<GetListCollectionResult>
|
||
<Lists>
|
||
<List DocTemplateUrl="" DefaultViewUrl="/templates" ID="{${crypto.randomUUID()}}" Title="Templates" Description="Document Templates" ImageUrl="/_layouts/images/itdl.gif" Name="{${crypto.randomUUID()}}" BaseType="1" ServerTemplate="101" Created="2025-10-02T19:00:00Z" Modified="2025-10-02T19:00:00Z" LastDeleted="1900-01-01T00:00:00Z" Version="1" Direction="none" ThumbnailSize="0" WebImageWidth="0" WebImageHeight="0" Flags="0" ItemCount="3" AnonymousPermMask="0" RootFolder="/webdav/templates" ReadSecurity="1" WriteSecurity="1" Author="1" InheritedSecurity="true" AllowMultiResponses="false" AllowDeletion="true" AllowAnonymousAccess="false" EnableAttachments="true" EnableModeration="false" EnableVersioning="false" Hidden="false" MultipleDataList="false" Ordered="false" ShowUser="true" EnablePeopleSelector="false" EnableResourceSelector="false" EnableMinorVersion="false" RequireCheckout="false" />
|
||
</Lists>
|
||
</GetListCollectionResult>
|
||
</GetListCollectionResponse>
|
||
</soap:Body>
|
||
</soap:Envelope>`;
|
||
|
||
res.send(soapResponse);
|
||
});
|
||
|
||
app.get('/_vti_bin/owssvr.dll', (req, res) => {
|
||
// SharePoint OWS Service
|
||
if (req.query.Cmd === 'Display' && req.query.List) {
|
||
res.set('Content-Type', 'text/xml');
|
||
res.send(`<?xml version="1.0" encoding="utf-8"?>
|
||
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
|
||
<s:Schema id="RowsetSchema">
|
||
<s:ElementType name="row" content="eltOnly" rs:updatable="true">
|
||
<s:AttributeType name="ows_Title" rs:name="Title" rs:number="1"/>
|
||
<s:AttributeType name="ows_Modified" rs:name="Modified" rs:number="2"/>
|
||
<s:AttributeType name="ows_Editor" rs:name="Editor" rs:number="3"/>
|
||
</s:ElementType>
|
||
</s:Schema>
|
||
<rs:data>
|
||
<z:row ows_Title="Templates" ows_Modified="2025-10-02T19:00:00Z" ows_Editor="System Account"/>
|
||
</rs:data>
|
||
</xml>`);
|
||
} else {
|
||
res.status(404).send('Not Found');
|
||
}
|
||
});
|
||
|
||
// SharePoint-kompatible WebDAV-Root
|
||
app.use('/sharepoint', (req, res, next) => {
|
||
// SharePoint-spezifische Header
|
||
res.set({
|
||
'MicrosoftSharePointTeamServices': '15.0.0.4569',
|
||
'SharePointHealthScore': '0',
|
||
'SPRequestGuid': `{${crypto.randomUUID()}}`,
|
||
'X-SharePoint-HealthScore': '0'
|
||
});
|
||
|
||
// Weiterleitung zu WebDAV
|
||
const newPath = req.path.replace('/sharepoint', '/webdav');
|
||
req.url = newPath;
|
||
next();
|
||
});
|
||
|
||
console.log('SharePoint-Kompatibilitätsmodus aktiviert:');
|
||
console.log('SharePoint WebDAV: http://localhost:' + (process.env.PORT || 80) + '/sharepoint/templates/');
|
||
console.log('SharePoint Info: http://localhost:' + (process.env.PORT || 80) + '/_vti_inf.html');
|
||
|
||
// Multer für File Upload
|
||
const upload = multer({ dest: 'uploads/' });
|
||
|
||
// Demo-Daten Generator
|
||
class DemoDataGenerator {
|
||
static generateData(tags) {
|
||
const data = {};
|
||
|
||
tags.forEach(tag => {
|
||
const lowerTag = tag.toLowerCase();
|
||
|
||
if (lowerTag.includes('name') || lowerTag.includes('vorname')) {
|
||
data[tag] = faker.person.firstName();
|
||
} else if (lowerTag.includes('nachname') || lowerTag.includes('surname')) {
|
||
data[tag] = faker.person.lastName();
|
||
} else if (lowerTag.includes('email') || lowerTag.includes('mail')) {
|
||
data[tag] = faker.internet.email();
|
||
} else if (lowerTag.includes('telefon') || lowerTag.includes('phone')) {
|
||
data[tag] = faker.phone.number();
|
||
} else if (lowerTag.includes('adresse') || lowerTag.includes('address')) {
|
||
data[tag] = faker.location.streetAddress();
|
||
} else if (lowerTag.includes('stadt') || lowerTag.includes('city')) {
|
||
data[tag] = faker.location.city();
|
||
} else if (lowerTag.includes('plz') || lowerTag.includes('postal')) {
|
||
data[tag] = faker.location.zipCode();
|
||
} else if (lowerTag.includes('land') || lowerTag.includes('country')) {
|
||
data[tag] = faker.location.country();
|
||
} else if (lowerTag.includes('datum') || lowerTag.includes('date')) {
|
||
data[tag] = faker.date.recent().toLocaleDateString('de-DE');
|
||
} else if (lowerTag.includes('betrag') || lowerTag.includes('preis') || lowerTag.includes('amount')) {
|
||
data[tag] = faker.commerce.price();
|
||
} else if (lowerTag.includes('firma') || lowerTag.includes('company')) {
|
||
data[tag] = faker.company.name();
|
||
} else if (lowerTag.includes('produkt') || lowerTag.includes('product')) {
|
||
data[tag] = faker.commerce.productName();
|
||
} else if (lowerTag.includes('beschreibung') || lowerTag.includes('description')) {
|
||
data[tag] = faker.lorem.paragraph();
|
||
} else if (lowerTag.includes('nummer') || lowerTag.includes('number') || lowerTag.includes('id')) {
|
||
data[tag] = faker.string.numeric(6);
|
||
} else {
|
||
// Fallback für unbekannte Tags
|
||
data[tag] = faker.lorem.words(2);
|
||
}
|
||
});
|
||
|
||
return data;
|
||
}
|
||
|
||
static generateTableData(tableStructure) {
|
||
const rows = Math.floor(Math.random() * 5) + 2; // 2-6 Zeilen
|
||
const tableData = [];
|
||
|
||
for (let i = 0; i < rows; i++) {
|
||
const row = {};
|
||
tableStructure.forEach(column => {
|
||
if (column.includes('position')) {
|
||
row[column] = (i + 1).toString(); // Fortlaufende Positionsnummer
|
||
} else {
|
||
row[column] = this.generateData([column])[column];
|
||
}
|
||
});
|
||
tableData.push(row);
|
||
}
|
||
|
||
return tableData;
|
||
}
|
||
}
|
||
|
||
// Template Tag Extractor
|
||
class TemplateTagExtractor {
|
||
static extractTags(docxBuffer) {
|
||
try {
|
||
const zip = new PizZip(docxBuffer);
|
||
const doc = new Docxtemplater(zip, {
|
||
paragraphLoop: true,
|
||
linebreaks: true,
|
||
});
|
||
|
||
// Alle Tags aus dem Template extrahieren
|
||
const tags = new Set();
|
||
const content = zip.file('word/document.xml').asText();
|
||
|
||
// Einfache Tags: {tag}
|
||
const simpleTagRegex = /{([^{}]+)}/g;
|
||
let match;
|
||
while ((match = simpleTagRegex.exec(content)) !== null) {
|
||
const tag = match[1].trim();
|
||
if (!tag.includes('#') && !tag.includes('/')) {
|
||
tags.add(tag);
|
||
}
|
||
}
|
||
|
||
// Loop Tags für Tabellen: {#items}...{/items}
|
||
const loopTagRegex = /{#(\w+)}/g;
|
||
const loopTags = [];
|
||
while ((match = loopTagRegex.exec(content)) !== null) {
|
||
loopTags.push(match[1]);
|
||
}
|
||
|
||
return {
|
||
simpleTags: Array.from(tags),
|
||
loopTags: loopTags
|
||
};
|
||
} catch (error) {
|
||
console.error('Fehler beim Extrahieren der Tags:', error);
|
||
return { simpleTags: [], loopTags: [] };
|
||
}
|
||
}
|
||
}
|
||
|
||
// Template Processor
|
||
class TemplateProcessor {
|
||
static async processTemplate(templatePath, outputPath, customData = null) {
|
||
try {
|
||
const content = fs.readFileSync(templatePath, 'binary');
|
||
const zip = new PizZip(content);
|
||
|
||
// Tags extrahieren
|
||
const { simpleTags, loopTags } = TemplateTagExtractor.extractTags(Buffer.from(content, 'binary'));
|
||
|
||
// Daten generieren oder verwenden
|
||
let data = customData || {};
|
||
|
||
if (!customData) {
|
||
// Demo-Daten für einfache Tags generieren
|
||
data = DemoDataGenerator.generateData(simpleTags);
|
||
|
||
// Demo-Daten für Loop Tags (Tabellen) generieren
|
||
loopTags.forEach(loopTag => {
|
||
// Erweiterte Tabellen-Spalten für professionelle Rechnung
|
||
const tableColumns = [`${loopTag}_position`, `${loopTag}_name`, `${loopTag}_value`, `${loopTag}_date`];
|
||
data[loopTag] = DemoDataGenerator.generateTableData(tableColumns);
|
||
});
|
||
}
|
||
|
||
const doc = new Docxtemplater(zip, {
|
||
paragraphLoop: true,
|
||
linebreaks: true,
|
||
});
|
||
|
||
doc.render(data);
|
||
|
||
const buf = doc.getZip().generate({
|
||
type: 'nodebuffer',
|
||
compression: 'DEFLATE',
|
||
});
|
||
|
||
fs.writeFileSync(outputPath, buf);
|
||
|
||
return {
|
||
success: true,
|
||
data: data,
|
||
extractedTags: { simpleTags, loopTags }
|
||
};
|
||
} catch (error) {
|
||
console.error('Template Verarbeitung fehlgeschlagen:', error);
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// Routes
|
||
|
||
// Hauptseite
|
||
app.get('/', (req, res) => {
|
||
res.send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>DOCX Template Server</title>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||
.container { max-width: 800px; }
|
||
.section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
|
||
.button { background: #007cba; color: white; padding: 10px 20px; text-decoration: none; border-radius: 3px; display: inline-block; margin: 5px; }
|
||
.button:hover { background: #005a87; }
|
||
input[type="file"] { margin: 10px 0; }
|
||
.info { background: #f0f8ff; padding: 10px; border-left: 4px solid #007cba; margin: 10px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>DOCX Template Server</h1>
|
||
|
||
<div class="section">
|
||
<h2>📄 Template hochladen und verarbeiten</h2>
|
||
<form action="/upload-template" method="post" enctype="multipart/form-data">
|
||
<input type="file" name="template" accept=".docx" required>
|
||
<br>
|
||
<input type="submit" value="Template hochladen und verarbeiten" class="button">
|
||
</form>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📁 Office-Integration</h2>
|
||
<div class="info">
|
||
<strong>Word WebDAV Templates:</strong> <a href="/webdav/templates/" target="_blank">http://localhost:${PORT}/webdav/templates/</a><br>
|
||
<strong>Word WebDAV Dokumente:</strong> <a href="/webdav/documents/" target="_blank">http://localhost:${PORT}/webdav/documents/</a><br>
|
||
${fs.existsSync(path.join(__dirname, '203_key.pem')) && fs.existsSync(path.join(__dirname, '203_cert.pem')) ?
|
||
`<strong>Templates (HTTPS):</strong> <a href="https://localhost:443/webdav/templates/" target="_blank">https://localhost:443/webdav/templates/</a><br>
|
||
<strong>Dokumente (HTTPS):</strong> <a href="https://localhost:443/webdav/documents/" target="_blank">https://localhost:443/webdav/documents/</a>` :
|
||
'<em>HTTPS verfügbar sobald SSL-Zertifikate (203_cert.pem und 203_key.pem) im Projektverzeichnis vorhanden sind</em>'}
|
||
</div>
|
||
<p><strong>Office-Integration:</strong> Öffnen Sie in Word/Excel: Datei → Öffnen → Netzwerk hinzufügen → WebDAV → http://localhost:${PORT}/webdav/templates/</p>
|
||
<p><strong>Direkt speichern:</strong> Word speichert automatisch auf dem Server wenn über WebDAV geöffnet.</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🔧 API Endpunkte</h2>
|
||
<ul>
|
||
<li><a href="/api/templates" class="button">Templates auflisten</a></li>
|
||
<li><a href="/api/documents" class="button">Dokumente auflisten</a></li>
|
||
</ul>
|
||
<p><strong>Word-Integration:</strong> Die API liefert spezielle Links für Microsoft Word:</p>
|
||
<ul>
|
||
<li><code>wordWebdavUrl</code> - Direkt in Word öffnen (ms-word: Protocol)</li>
|
||
<li><code>wordDirectUrl</code> - Alternative Word-Integration</li>
|
||
<li><code>webdavDirect</code> - Windows UNC-Pfad für Netzlaufwerk</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>📋 Test Template</h2>
|
||
<a href="/create-test-template" class="button">Test Template erstellen</a>
|
||
<p>Erstellt ein Beispiel-Template mit verschiedenen Tag-Typen für Tests.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
});
|
||
|
||
// 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);
|
||
|
||
// Template in Templates-Verzeichnis kopieren
|
||
fs.copyFileSync(req.file.path, templatePath);
|
||
|
||
// Template verarbeiten
|
||
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({
|
||
message: 'Template erfolgreich verarbeitet',
|
||
templateName: templateName,
|
||
outputName: outputName,
|
||
extractedTags: result.extractedTags,
|
||
generatedData: result.data,
|
||
fileUrls: {
|
||
template: `${protocol}://${host}/webdav/templates/${templateName}`,
|
||
document: `${protocol}://${host}/webdav/documents/${outputName}`,
|
||
wordWebdavTemplate: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||
wordWebdavDocument: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`,
|
||
wordDirectTemplate: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(templateName)}`,
|
||
wordDirectDocument: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(outputName)}`
|
||
}
|
||
});
|
||
} else {
|
||
res.status(500).json({ error: result.error });
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload Fehler:', error);
|
||
res.status(500).json({ error: 'Server Fehler beim Upload' });
|
||
}
|
||
});
|
||
|
||
// Test Template erstellen
|
||
app.get('/create-test-template', (req, res) => {
|
||
const PizZip = require('pizzip');
|
||
|
||
// Minimales DOCX Template erstellen
|
||
const testContent = `
|
||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||
<w:body>
|
||
<w:p><w:r><w:t>Firmenname: {firma}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>Ansprechpartner: {vorname} {nachname}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>E-Mail: {email}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>Telefon: {telefon}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>Adresse: {adresse}, {plz} {stadt}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>Datum: {datum}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>Rechnungsnummer: {nummer}</w:t></w:r></w:p>
|
||
|
||
<w:p><w:r><w:t>Positionen:</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>{#items}</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>- {items_name}: {items_value} EUR ({items_date})</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>{/items}</w:t></w:r></w:p>
|
||
|
||
<w:p><w:r><w:t>Gesamtbetrag: {betrag} EUR</w:t></w:r></w:p>
|
||
|
||
<w:p><w:r><w:t>Beschreibung:</w:t></w:r></w:p>
|
||
<w:p><w:r><w:t>{beschreibung}</w:t></w:r></w:p>
|
||
</w:body>
|
||
</w:document>
|
||
`;
|
||
|
||
try {
|
||
// Minimal DOCX Struktur
|
||
const zip = new PizZip();
|
||
|
||
// document.xml
|
||
zip.file('word/document.xml', testContent);
|
||
|
||
// [Content_Types].xml
|
||
zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||
</Types>`);
|
||
|
||
// _rels/.rels
|
||
zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||
</Relationships>`);
|
||
|
||
// word/_rels/document.xml.rels
|
||
zip.file('word/_rels/document.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
</Relationships>`);
|
||
|
||
const buffer = zip.generate({ type: 'nodebuffer' });
|
||
const testTemplatePath = path.join(templateDir, 'test_template.docx');
|
||
|
||
fs.writeFileSync(testTemplatePath, buffer);
|
||
|
||
res.json({
|
||
message: 'Test Template erstellt',
|
||
templatePath: 'test_template.docx',
|
||
fileUrl: `http://localhost:${PORT}/webdav/templates/test_template.docx`,
|
||
info: 'Sie können das Template jetzt über den Datei-Server herunterladen, bearbeiten und wieder hochladen.'
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Erstellen des Test Templates:', error);
|
||
res.status(500).json({ error: 'Fehler beim Erstellen des Test Templates' });
|
||
}
|
||
});
|
||
|
||
// API Endpunkte
|
||
app.get('/api/templates', (req, res) => {
|
||
try {
|
||
const files = fs.readdirSync(templateDir).filter(file => file.endsWith('.docx'));
|
||
const protocol = req.secure ? 'https' : 'http';
|
||
const host = req.get('host') || `localhost:${PORT}`;
|
||
|
||
res.json({
|
||
templates: files.map(file => ({
|
||
name: file,
|
||
fileUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/templates/${encodeURIComponent(file)}`,
|
||
downloadUrl: `${protocol}://${host}/webdav/templates/${file}`,
|
||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\templates\\${file}`
|
||
}))
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ error: 'Fehler beim Auflisten der Templates' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/documents', (req, res) => {
|
||
try {
|
||
const files = fs.readdirSync(outputDir).filter(file => file.endsWith('.docx'));
|
||
const protocol = req.secure ? 'https' : 'http';
|
||
const host = req.get('host') || `localhost:${PORT}`;
|
||
|
||
res.json({
|
||
documents: files.map(file => ({
|
||
name: file,
|
||
fileUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||
wordWebdavUrl: `ms-word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||
wordDirectUrl: `word:ofe|u|${protocol}://${host}/webdav/documents/${encodeURIComponent(file)}`,
|
||
downloadUrl: `${protocol}://${host}/webdav/documents/${file}`,
|
||
webdavDirect: `\\\\${host.split(':')[0]}@${PORT}\\webdav\\documents\\${file}`,
|
||
createdAt: fs.statSync(path.join(outputDir, file)).mtime
|
||
}))
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ error: 'Fehler beim Auflisten der Dokumente' });
|
||
}
|
||
});
|
||
|
||
// Template mit benutzerdefinierten Daten verarbeiten
|
||
app.post('/api/process-template/:templateName', (req, res) => {
|
||
try {
|
||
const templateName = req.params.templateName;
|
||
const templatePath = path.join(templateDir, templateName);
|
||
|
||
if (!fs.existsSync(templatePath)) {
|
||
return res.status(404).json({ error: 'Template nicht gefunden' });
|
||
}
|
||
|
||
const outputName = templateName.replace('.docx', '_custom.docx');
|
||
const outputPath = path.join(outputDir, outputName);
|
||
|
||
TemplateProcessor.processTemplate(templatePath, outputPath, req.body).then(result => {
|
||
if (result.success) {
|
||
res.json({
|
||
message: 'Template mit benutzerdefinierten Daten verarbeitet',
|
||
outputName: outputName,
|
||
fileUrl: `http://localhost:${PORT}/webdav/documents/${outputName}`
|
||
});
|
||
} else {
|
||
res.status(500).json({ error: result.error });
|
||
}
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ error: 'Fehler beim Verarbeiten des Templates' });
|
||
}
|
||
});
|
||
|
||
// SSL Konfiguration
|
||
const certPath = path.join(__dirname, '203_cert.pem');
|
||
const keyPath = path.join(__dirname, '203_key.pem');
|
||
|
||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||
try {
|
||
const sslOptions = {
|
||
key: fs.readFileSync(keyPath),
|
||
cert: fs.readFileSync(certPath)
|
||
};
|
||
|
||
https.createServer(sslOptions, app).listen(443, () => {
|
||
console.log('🔒 HTTPS Server läuft auf Port 443');
|
||
console.log('🌐 HTTPS Zugang: https://localhost:443');
|
||
});
|
||
} catch (error) {
|
||
console.warn('⚠️ SSL-Zertifikate gefunden, aber Fehler beim Laden:', error.message);
|
||
console.log('ℹ️ Server läuft nur mit HTTP');
|
||
}
|
||
} else {
|
||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Server läuft nur mit HTTP');
|
||
console.log('💡 Für HTTPS: Platzieren Sie 203_cert.pem und 203_key.pem in /home/OfficeServerJS/');
|
||
}
|
||
|
||
// Server starten
|
||
app.listen(PORT, () => {
|
||
console.log(`\n🚀 DOCX Template Server gestartet!`);
|
||
console.log(`📍 HTTP Server: http://localhost:${PORT}`);
|
||
console.log(`📁 Templates: http://localhost:${PORT}/webdav/templates/`);
|
||
console.log(`📁 Documents: http://localhost:${PORT}/webdav/documents/`);
|
||
console.log(`\n💡 Tipp: Besuchen Sie http://localhost:${PORT} für die Web-Oberfläche`);
|
||
|
||
// SSL-Status anzeigen
|
||
const certPath = path.join(__dirname, '203_cert.pem');
|
||
const keyPath = path.join(__dirname, '203_key.pem');
|
||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||
console.log(`🔒 HTTPS auch verfügbar: https://localhost:443`);
|
||
}
|
||
});
|
||
|
||
module.exports = app; |