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"); // ✅ Reset-Funktionen (Soft-Restart) const db = require("./db"); const { getSessionStore, resetSessionStore } = require("./config/session"); // ✅ Deine Routes (unverändert) 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()); // ✅ SessionStore dynamisch (Setup: MemoryStore, normal: MySQLStore) app.use( session({ name: "praxis.sid", secret: process.env.SESSION_SECRET, store: getSessionStore(), resave: false, saveUninitialized: false, }), ); // ✅ i18n Middleware app.use((req, res, next) => { const lang = req.session.lang || "de"; // Standard DE const filePath = path.join(__dirname, "locales", `${lang}.json`); const raw = fs.readFileSync(filePath, "utf-8"); res.locals.t = JSON.parse(raw); // t = translations 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 (NEU!) - wenn keine Seriennummer: 30 Tage Trial - danach nur noch /serial-number erreichbar ================================ */ app.use(async (req, res, next) => { try { // Setup muss immer erreichbar bleiben if (req.path.startsWith("/setup")) return next(); // Login muss erreichbar bleiben if (req.path === "/" || req.path.startsWith("/login")) return next(); // Seriennummer Seite muss immer erreichbar bleiben if (req.path.startsWith("/serial-number")) return next(); // Nicht eingeloggt -> auth regelt das if (!req.session?.user) return next(); // company_settings laden const [rows] = await db.promise().query( `SELECT id, serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, ); const settings = rows?.[0]; // ✅ Lizenz vorhanden -> erlaubt if (settings?.serial_number) return next(); // ✅ wenn Trial noch nicht gestartet -> starten 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 settings fehlen -> durchlassen (damit Setup/Settings nicht kaputt gehen) if (!settings?.trial_started_at) return next(); // ✅ Trial prüfen const trialStart = new Date(settings.trial_started_at); const now = new Date(); const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24)); if (diffDays < TRIAL_DAYS) return next(); // ❌ Trial abgelaufen -> alles blocken außer Seriennummer return res.redirect("/serial-number"); } catch (err) { console.error("❌ LicenseGate Fehler:", err.message); return next(); // im Zweifel nicht blockieren } }); /* =============================== SETUP ROUTES ================================ */ // Setup-Seite app.get("/setup", (req, res) => { if (configExists()) return res.redirect("/"); return res.status(200).send(setupHtml()); }); // Setup speichern + DB testen + Soft-Restart + Login redirect 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.")); } // ✅ DB Verbindung testen const conn = await mysql.createConnection({ host, user, password, database: name, }); await conn.query("SELECT 1"); await conn.end(); // ✅ verschlüsselt speichern saveConfig({ db: { host, user, password, name }, }); // ✅ Soft-Restart (DB Pool + SessionStore neu laden) if (typeof db.resetPool === "function") { db.resetPool(); } resetSessionStore(); // ✅ automatisch zurück zur Loginseite 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(); }); /* =============================== Sprachen Route ================================ */ // ✅ i18n Middleware (Sprache pro Benutzer über Session) app.use((req, res, next) => { const lang = req.session.lang || "de"; // Standard: Deutsch let translations = {}; try { const filePath = path.join(__dirname, "locales", `${lang}.json`); translations = JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (err) { console.error("❌ i18n Fehler:", err.message); } // ✅ In EJS verfügbar machen res.locals.t = translations; res.locals.lang = lang; next(); }); 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; // ✅ WICHTIG: Session speichern bevor redirect req.session.save((err) => { if (err) console.error("❌ Session save error:", err); return res.redirect(req.get("Referrer") || "/dashboard"); }); }); /* =============================== ✅ Seriennummer Seite (NEU!) ================================ */ // ✅ GET /serial-number app.get("/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); const [rows] = await db.promise().query( `SELECT serial_number, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, ); const settings = rows?.[0]; let trialInfo = null; if (!settings?.serial_number && 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)); const rest = Math.max(0, TRIAL_DAYS - diffDays); trialInfo = `⚠️ Keine Seriennummer vorhanden. Testphase: noch ${rest} Tage.`; } return res.render("serial_number", { user: req.session.user, active: "serialnumber", currentSerial: settings?.serial_number || "", error: null, success: null, trialInfo, }); } catch (err) { console.error(err); return res.status(500).send("Interner Serverfehler"); } }); // ✅ POST /serial-number app.post("/serial-number", async (req, res) => { try { if (!req.session?.user) return res.redirect("/"); let serial = normalizeSerial(req.body.serial_number); if (!serial) { return res.render("serial_number", { user: req.session.user, active: "serialnumber", currentSerial: "", error: "Bitte Seriennummer eingeben.", success: null, trialInfo: null, }); } if (!isValidSerialFormat(serial)) { return res.render("serial_number", { user: req.session.user, active: "serialnumber", currentSerial: serial, error: "Ungültiges Format. Beispiel: ABC12-3DE45-FG678-HI901", success: null, trialInfo: null, }); } if (!passesModulo3(serial)) { return res.render("serial_number", { user: req.session.user, active: "serialnumber", currentSerial: serial, error: "Seriennummer ungültig (Modulo-3 Prüfung fehlgeschlagen).", success: null, trialInfo: null, }); } // company_settings holen const [rows] = await db .promise() .query( `SELECT id, trial_started_at FROM company_settings ORDER BY id ASC LIMIT 1`, ); if (!rows.length) { // Wenn noch kein Datensatz existiert -> anlegen await db.promise().query( `INSERT INTO company_settings (company_name, street, house_number, postal_code, city, country, default_currency, serial_number, trial_started_at) VALUES ('', '', '', '', '', 'Deutschland', 'EUR', ?, NOW())`, [serial], ); } else { const settingsId = rows[0].id; await db.promise().query( `UPDATE company_settings SET serial_number = ? WHERE id = ?`, [serial, settingsId], ); } return res.render("serial_number", { user: req.session.user, active: "serialnumber", currentSerial: serial, error: null, success: "✅ Seriennummer gespeichert!", trialInfo: null, }); } 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", { user: req.session.user, active: "serialnumber", currentSerial: req.body.serial_number || "", error: msg, success: null, trialInfo: null, }); } }); /* =============================== DEINE LOGIK (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}`); });