// Frontend: Node.js static file server with API proxy const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const PORT = 80; const BACKEND = process.env.BACKEND_URL || 'http://publish-backend:8080'; const STATIC_DIR = '/app/dist'; const UPLOADS_DIR = '/app/uploads'; const WEBHOOK_HOST = process.env.WEBHOOK_HOST || 'host.docker.internal:5000'; const MIME_TYPES = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', '.pdf': 'application/pdf', '.md': 'text/markdown', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', '.ppt': 'application/vnd.ms-powerpoint', }; function serveStatic(req, res) { let filePath = path.join(STATIC_DIR, req.url === '/' ? '/index.html' : req.url); // Remove query string filePath = filePath.split('?')[0]; const ext = path.extname(filePath).toLowerCase(); const contentType = MIME_TYPES[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, data) => { if (err) { // SPA fallback: serve index.html if (req.url.startsWith('/api') || req.url.startsWith('/uploads')) { proxyRequest(req, res); } else { fs.readFile(path.join(STATIC_DIR, 'index.html'), (err2, data2) => { if (err2) { res.writeHead(404); res.end('Not Found'); } else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data2); } }); } } else { res.writeHead(200, { 'Content-Type': contentType }); res.end(data); } }); } function proxyRequest(req, res) { const targetUrl = BACKEND + req.url; const parsedUrl = url.parse(req.url); const options = { hostname: url.parse(BACKEND).hostname, port: url.parse(BACKEND).port || 80, path: req.url, method: req.method, headers: {} }; // Forward relevant headers ['content-type', 'authorization', 'accept', 'x-requested-with', 'host'].forEach(h => { if (req.headers[h]) options.headers[h] = req.headers[h]; }); const proxyReq = http.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on('error', (e) => { res.writeHead(502); res.end('Backend error: ' + e.message); }); if (['POST', 'PUT', 'DELETE'].includes(req.method)) { req.pipe(proxyReq); } else { proxyReq.end(); } } function proxyWebhook(req, res) { // Forward /webhook requests to the Python webhook receiver on NAS host const targetUrl = `http://${WEBHOOK_HOST}${req.url}`; const parsedUrl = url.parse(req.url); const options = { hostname: url.parse(targetUrl).hostname, port: url.parse(targetUrl).port || 80, path: req.url, method: req.method, headers: {} }; ['content-type', 'authorization', 'x-gitea-secret', 'x-gitea-event', 'user-agent', 'host'].forEach(h => { const key = h.toLowerCase(); if (req.headers[key]) options.headers[h] = req.headers[key]; }); const proxyReq = http.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on('error', (e) => { res.writeHead(502); res.end('Webhook receiver error: ' + e.message); }); if (['POST', 'PUT', 'DELETE'].includes(req.method)) { req.pipe(proxyReq); } else { proxyReq.end(); } } const server = http.createServer((req, res) => { // CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); if (req.method === 'OPTIONS') { res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.writeHead(204); res.end(); return; } if (req.url.startsWith('/webhook')) { // Proxy to webhook receiver running on NAS host (host.docker.internal:5000) proxyWebhook(req, res); } else if (req.url.startsWith('/api') || req.url.startsWith('/uploads')) { proxyRequest(req, res); } else { serveStatic(req, res); } }); server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`Backend: ${BACKEND}`); });