501 lines
14 KiB
JavaScript
501 lines
14 KiB
JavaScript
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 `
|
|
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Praxissoftware Setup</title>
|
|
<style>
|
|
body{font-family:Arial;background:#f4f4f4;padding:30px}
|
|
.card{max-width:520px;margin:auto;background:#fff;padding:22px;border-radius:14px}
|
|
input{width:100%;padding:10px;margin:6px 0;border:1px solid #ccc;border-radius:8px}
|
|
button{padding:10px 14px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
|
.err{color:#b00020;margin:10px 0}
|
|
.hint{color:#666;font-size:13px;margin-top:12px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h2>🔧 Datenbank Einrichtung</h2>
|
|
${error ? `<div class="err">❌ ${error}</div>` : ""}
|
|
|
|
<form method="POST" action="/setup">
|
|
<label>DB Host</label>
|
|
<input name="host" placeholder="85.215.63.122" required />
|
|
|
|
<label>DB Benutzer</label>
|
|
<input name="user" placeholder="praxisuser" required />
|
|
|
|
<label>DB Passwort</label>
|
|
<input name="password" type="password" required />
|
|
|
|
<label>DB Name</label>
|
|
<input name="name" placeholder="praxissoftware" required />
|
|
|
|
<button type="submit">✅ Speichern</button>
|
|
</form>
|
|
|
|
<div class="hint">
|
|
Die Daten werden verschlüsselt gespeichert (<b>config.enc</b>).<br/>
|
|
Danach wirst du automatisch auf die Loginseite weitergeleitet.
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
/* ===============================
|
|
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}`);
|
|
});
|