Initial commit: ToolCTRL Landing Page
Statische Landing Page mit Lead-Formular, Impressum und Datenschutzerklärung, ausgeliefert von einem abhängigkeitsfreien Node-Server mit lokaler Lead-Erfassung. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
143748dc0d
33 files changed
+2180
No files matched your search
@@ -0,0 +1,218 @@
|
||||
/* ============================================================
|
||||
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("<h1>404</h1><p>Seite nicht gefunden.</p>");
|
||||
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);
|
||||
Reference in new issue
Block a user