523 lines
15 KiB
JavaScript
523 lines
15 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");
|
|
|
|
// ✅ 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 `
|
|
<!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());
|
|
|
|
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}`);
|
|
});
|