require("dotenv").config(); const express = require("express"); const session = require("express-session"); const helmet = require("helmet"); const mysql = require("mysql2/promise"); const fs = require("fs"); const path = require("path"); const expressLayouts = require("express-ejs-layouts"); // ✅ Verschlüsselte Config const { configExists, saveConfig } = require("./config-manager"); // ✅ DB + Session Reset const db = require("./db"); const { getSessionStore, resetSessionStore } = require("./config/session"); // ✅ Routes (deine) const adminRoutes = require("./routes/admin.routes"); const dashboardRoutes = require("./routes/dashboard.routes"); const patientRoutes = require("./routes/patient.routes"); const medicationRoutes = require("./routes/medications.routes"); const patientMedicationRoutes = require("./routes/patientMedication.routes"); const waitingRoomRoutes = require("./routes/waitingRoom.routes"); const serviceRoutes = require("./routes/service.routes"); const patientServiceRoutes = require("./routes/patientService.routes"); const invoiceRoutes = require("./routes/invoice.routes"); const patientFileRoutes = require("./routes/patientFile.routes"); const companySettingsRoutes = require("./routes/companySettings.routes"); const authRoutes = require("./routes/auth.routes"); const app = express(); /* =============================== ✅ Seriennummer / Trial Konfiguration ================================ */ const TRIAL_DAYS = 30; /* =============================== ✅ Seriennummer Helper Funktionen ================================ */ function normalizeSerial(input) { return (input || "") .toUpperCase() .replace(/[^A-Z0-9-]/g, "") .trim(); } // Format: AAAAA-AAAAA-AAAAA-AAAAA function isValidSerialFormat(serial) { return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial); } // Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0) function passesModulo3(serial) { const raw = serial.replace(/-/g, ""); let sum = 0; for (const ch of raw) { if (/[0-9]/.test(ch)) sum += parseInt(ch, 10); else sum += ch.charCodeAt(0) - 55; // A=10 } return sum % 3 === 0; } /* =============================== SETUP HTML ================================ */ function setupHtml(error = "") { return ` Praxissoftware Setup

🔧 Datenbank Einrichtung

${error ? `
❌ ${error}
` : ""}
Die Daten werden verschlüsselt gespeichert (config.enc).
Danach wirst du automatisch auf die Loginseite weitergeleitet.
`; } /* =============================== MIDDLEWARE ================================ */ app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(helmet()); app.use( session({ name: "praxis.sid", secret: process.env.SESSION_SECRET, store: getSessionStore(), resave: false, saveUninitialized: false, }), ); // ✅ i18n Middleware 1 (setzt res.locals.t + lang) app.use((req, res, next) => { const lang = req.session.lang || "de"; const filePath = path.join(__dirname, "locales", `${lang}.json`); const raw = fs.readFileSync(filePath, "utf-8"); res.locals.t = JSON.parse(raw); res.locals.lang = lang; next(); }); const flashMiddleware = require("./middleware/flash.middleware"); app.use(flashMiddleware); app.use(express.static("public")); app.use("/uploads", express.static("uploads")); app.set("view engine", "ejs"); app.use(expressLayouts); app.set("layout", "layout"); // verwendet views/layout.ejs app.use((req, res, next) => { res.locals.user = req.session.user || null; next(); }); /* =============================== ✅ LICENSE/TRIAL GATE - Trial startet automatisch, wenn noch NULL - Wenn abgelaufen: Admin -> /admin/serial-number Arzt/Member -> /serial-number ================================ */ app.use(async (req, res, next) => { try { // Setup muss erreichbar bleiben if (req.path.startsWith("/setup")) return next(); // Login muss erreichbar bleiben if (req.path === "/" || req.path.startsWith("/login")) return next(); // Serial Seiten müssen erreichbar bleiben if (req.path.startsWith("/serial-number")) return next(); if (req.path.startsWith("/admin/serial-number")) return next(); // Sprache ändern erlauben if (req.path.startsWith("/lang/")) return next(); // Nicht eingeloggt -> auth regelt das if (!req.session?.user) return next(); const [rowsSettings] = await db.promise().query( `SELECT id, serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, ); const settings = rowsSettings?.[0]; // ✅ Seriennummer vorhanden -> alles OK if (settings?.serial_number) return next(); // ✅ Trial Start setzen wenn leer if (settings?.id && !settings?.trial_started_at) { await db .promise() .query( `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, [settings.id], ); return next(); } // Wenn noch immer kein trial start: nicht blockieren if (!settings?.trial_started_at) return next(); const trialStart = new Date(settings.trial_started_at); const now = new Date(); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); // ✅ Trial läuft noch if (diffDays < TRIAL_DAYS) return next(); // ❌ Trial abgelaufen if (req.session.user.role === "admin") { return res.redirect("/admin/serial-number"); } return res.redirect("/serial-number"); } catch (err) { console.error("❌ LicenseGate Fehler:", err.message); return next(); } }); /* =============================== SETUP ROUTES ================================ */ app.get("/setup", (req, res) => { if (configExists()) return res.redirect("/"); return res.status(200).send(setupHtml()); }); app.post("/setup", async (req, res) => { try { const { host, user, password, name } = req.body; if (!host || !user || !password || !name) { return res.status(400).send(setupHtml("Bitte alle Felder ausfüllen.")); } const conn = await mysql.createConnection({ host, user, password, database: name, }); await conn.query("SELECT 1"); await conn.end(); saveConfig({ db: { host, user, password, name }, }); if (typeof db.resetPool === "function") { db.resetPool(); } resetSessionStore(); return res.redirect("/"); } catch (err) { return res .status(500) .send(setupHtml("DB Verbindung fehlgeschlagen: " + err.message)); } }); // Wenn keine config.enc → alles außer /setup auf Setup umleiten app.use((req, res, next) => { if (!configExists() && req.path !== "/setup") { return res.redirect("/setup"); } next(); }); /* =============================== Sprache ändern ================================ */ app.get("/lang/:lang", (req, res) => { const newLang = req.params.lang; if (!["de", "es"].includes(newLang)) { return res.redirect(req.get("Referrer") || "/dashboard"); } req.session.lang = newLang; req.session.save((err) => { if (err) console.error("❌ Session save error:", err); return res.redirect(req.get("Referrer") || "/dashboard"); }); }); /* =============================== ✅ SERIAL PAGES ================================ */ /** * ✅ /serial-number * - Trial aktiv: zeigt Resttage + Button Dashboard * - Trial abgelaufen: * Admin -> redirect /admin/serial-number * Arzt/Member -> trial_expired.ejs */ app.get("/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); const [rowsSettings] = await db.promise().query( `SELECT id, serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, ); const settings = rowsSettings?.[0]; // ✅ Seriennummer da -> ab ins Dashboard if (settings?.serial_number) return res.redirect("/dashboard"); // ✅ Trial Start setzen wenn leer if (settings?.id && !settings?.trial_started_at) { await db .promise() .query( `UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`, [settings.id], ); settings.trial_started_at = new Date(); } // ✅ Resttage berechnen let daysLeft = TRIAL_DAYS; if (settings?.trial_started_at) { const trialStart = new Date(settings.trial_started_at); const now = new Date(); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); daysLeft = Math.max(0, TRIAL_DAYS - diffDays); } // ❌ Trial abgelaufen if (daysLeft <= 0) { if (req.session.user.role === "admin") { return res.redirect("/admin/serial-number"); } return res.render("trial_expired", { user: req.session.user, lang: req.session.lang || "de", }); } // ✅ Trial aktiv return res.render("serial_number_info", { user: req.session.user, lang: req.session.lang || "de", daysLeft, }); } catch (err) { console.error(err); return res.status(500).send("Interner Serverfehler"); } }); /** * ✅ Admin Seite: Seriennummer eingeben */ app.get("/admin/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); if (req.session.user.role !== "admin") return res.status(403).send("Forbidden"); const [rowsSettings] = await db .promise() .query( `SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`, ); const currentSerial = rowsSettings?.[0]?.serial_number || ""; return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial, error: null, success: null, }); } catch (err) { console.error(err); return res.status(500).send("Interner Serverfehler"); } }); /** * ✅ Admin Seite: Seriennummer speichern */ app.post("/admin/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); if (req.session.user.role !== "admin") return res.status(403).send("Forbidden"); let serial = normalizeSerial(req.body.serial_number); if (!serial) { return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial: "", error: "Bitte Seriennummer eingeben.", success: null, }); } if (!isValidSerialFormat(serial)) { return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901", success: null, }); } if (!passesModulo3(serial)) { return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, error: "Modulo-3 Prüfung fehlgeschlagen. Seriennummer ungültig.", success: null, }); } await db .promise() .query(`UPDATE company_settings SET serial_number = ? WHERE id = 1`, [ serial, ]); return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial: serial, error: null, success: "✅ Seriennummer gespeichert!", }); } catch (err) { console.error(err); let msg = "Fehler beim Speichern."; if (err.code === "ER_DUP_ENTRY") msg = "Diese Seriennummer ist bereits vergeben."; return res.render("serial_number_admin", { user: req.session.user, lang: req.session.lang || "de", active: "serialnumber", currentSerial: req.body.serial_number || "", error: msg, success: null, }); } }); /* =============================== DEINE ROUTES (unverändert) ================================ */ app.use(companySettingsRoutes); app.use("/", authRoutes); app.use("/dashboard", dashboardRoutes); app.use("/admin", adminRoutes); app.use("/patients", patientRoutes); app.use("/patients", patientMedicationRoutes); app.use("/patients", patientServiceRoutes); app.use("/medications", medicationRoutes); console.log("🧪 /medications Router mounted"); app.use("/services", serviceRoutes); app.use("/", patientFileRoutes); app.use("/", waitingRoomRoutes); app.use("/", invoiceRoutes); app.get("/logout", (req, res) => { req.session.destroy(() => res.redirect("/")); }); /* =============================== ERROR HANDLING ================================ */ app.use((err, req, res, next) => { console.error(err); res.status(500).send("Interner Serverfehler"); }); /* =============================== SERVER ================================ */ const PORT = process.env.PORT || 51777; const HOST = "127.0.0.1"; app.listen(PORT, HOST, () => { console.log(`Server läuft auf http://${HOST}:${PORT}`); });