/* ============================================================ ToolCTRL — Landing Page Server Abhängigkeitsfrei (nur Node-Standardbibliothek). Liefert /public und nimmt Leads unter POST /api/lead entgegen. ============================================================ */ "use strict"; const http = require("http"); const fs = require("fs"); const path = require("path"); const ROOT = __dirname; const PUBLIC_DIR = path.join(ROOT, "public"); const LEADS_DIR = path.join(ROOT, "leads"); const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson"); const LEADS_CSV = path.join(LEADS_DIR, "leads.csv"); /* Bevorzugte Ports — der erste freie wird genutzt. */ const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0]; const HOST = "127.0.0.1"; const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".ico": "image/x-icon", ".json": "application/json; charset=utf-8", ".woff2": "font/woff2" }; /* ---------- Lead-Speicher vorbereiten ---------- */ function ensureLeadStore() { if (!fs.existsSync(LEADS_DIR)) fs.mkdirSync(LEADS_DIR, { recursive: true }); if (!fs.existsSync(LEADS_CSV)) { fs.writeFileSync( LEADS_CSV, "zeitpunkt,name,firma,email,telefon,nachricht,quelle\n", "utf8" ); } } function csvCell(value) { const s = String(value == null ? "" : value); return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"'; } function saveLead(lead) { ensureLeadStore(); fs.appendFileSync(LEADS_JSON, JSON.stringify(lead) + "\n", "utf8"); const row = [ lead.zeitpunkt, lead.name, lead.firma, lead.email, lead.telefon, lead.nachricht, lead.quelle ] .map(csvCell) .join(","); fs.appendFileSync(LEADS_CSV, row + "\n", "utf8"); } /* ---------- Statische Dateien ---------- */ function serveStatic(req, res) { let urlPath = decodeURIComponent(req.url.split("?")[0]); if (urlPath === "/") urlPath = "/index.html"; /* Path-Traversal verhindern */ const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath)); if (!filePath.startsWith(PUBLIC_DIR)) { res.writeHead(403).end("Forbidden"); return; } fs.stat(filePath, (err, stat) => { if (err || !stat.isFile()) { res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); res.end("
Seite nicht gefunden.
"); return; } const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream", "Cache-Control": "no-cache" }); fs.createReadStream(filePath).pipe(res); }); } /* ---------- Lead-Endpunkt ---------- */ function handleLead(req, res) { let body = ""; let tooLarge = false; req.on("data", (chunk) => { body += chunk; if (body.length > 64 * 1024) { tooLarge = true; req.destroy(); } }); req.on("end", () => { if (tooLarge) return sendJSON(res, 413, { error: "Anfrage zu groß." }); let data; try { data = JSON.parse(body || "{}"); } catch (e) { return sendJSON(res, 400, { error: "Ungültiges Format." }); } /* Honeypot — befülltes Feld = Bot. Still mit OK quittieren. */ if (data.website && String(data.website).trim() !== "") { return sendJSON(res, 200, { ok: true }); } const name = String(data.name || "").trim(); const email = String(data.email || "").trim(); const nachricht = String(data.nachricht || "").trim(); if (!name || !email || !nachricht) { return sendJSON(res, 400, { error: "Bitte Name, E-Mail und Nachricht angeben." }); } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return sendJSON(res, 400, { error: "Bitte eine gültige E-Mail angeben." }); } const lead = { zeitpunkt: new Date().toISOString(), name: name.slice(0, 200), firma: String(data.firma || "").trim().slice(0, 200), email: email.slice(0, 200), telefon: String(data.telefon || "").trim().slice(0, 80), nachricht: nachricht.slice(0, 4000), quelle: String(data.quelle || "Landing Page").slice(0, 120) }; try { saveLead(lead); } catch (e) { console.error("[lead] Speichern fehlgeschlagen:", e.message); return sendJSON(res, 500, { error: "Speichern fehlgeschlagen. Bitte später erneut versuchen." }); } console.log( "[lead] " + lead.zeitpunkt + " — " + lead.name + (lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">" ); sendJSON(res, 200, { ok: true }); }); } function sendJSON(res, code, obj) { res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(obj)); } /* ---------- Router ---------- */ const server = http.createServer((req, res) => { const route = req.url.split("?")[0]; if (route === "/api/lead") { if (req.method !== "POST") { return sendJSON(res, 405, { error: "Methode nicht erlaubt." }); } return handleLead(req, res); } if (req.method !== "GET" && req.method !== "HEAD") { res.writeHead(405).end("Method Not Allowed"); return; } serveStatic(req, res); }); /* ---------- Freien Port suchen und starten ---------- */ function listen(candidates) { if (candidates.length === 0) { console.error("Kein freier Port gefunden."); process.exit(1); } const port = candidates[0]; server.once("error", (err) => { if (err.code === "EADDRINUSE") { console.log("Port " + port + " belegt — versuche nächsten …"); listen(candidates.slice(1)); } else { console.error("Serverfehler:", err.message); process.exit(1); } }); server.listen(port, HOST, () => { const actual = server.address().port; ensureLeadStore(); console.log(""); console.log(" ToolCTRL — Landing Page läuft"); console.log(" ----------------------------------------"); console.log(" URL: http://" + HOST + ":" + actual); console.log(" Leads: " + LEADS_CSV); console.log(" " + LEADS_JSON); console.log(" Stop: Strg + C"); console.log(""); }); } listen(PORT_CANDIDATES);