435 lines
12 KiB
JavaScript
435 lines
12 KiB
JavaScript
require("dotenv").config();
|
|
|
|
const express = require("express");
|
|
const session = require("express-session");
|
|
const helmet = require("helmet");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const expressLayouts = require("express-ejs-layouts");
|
|
|
|
// ✅ DB + Session Store
|
|
const db = require("./db");
|
|
const { getSessionStore } = require("./config/session");
|
|
|
|
// ✅ Setup Middleware + Setup Routes
|
|
const requireSetup = require("./middleware/requireSetup");
|
|
const setupRoutes = require("./routes/setup.routes");
|
|
|
|
// ✅ 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 reportRoutes = require("./routes/report.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;
|
|
}
|
|
|
|
/* ===============================
|
|
MIDDLEWARE
|
|
================================ */
|
|
app.use(express.urlencoded({ extended: true }));
|
|
app.use(express.json());
|
|
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
imgSrc: ["'self'", "data:"],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
app.use(
|
|
session({
|
|
name: "praxis.sid",
|
|
secret: process.env.SESSION_SECRET || "dev-secret",
|
|
store: getSessionStore(),
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
}),
|
|
);
|
|
|
|
// ✅ i18n Middleware (SAFE)
|
|
app.use((req, res, next) => {
|
|
try {
|
|
const lang = req.session.lang || "de";
|
|
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
|
|
|
let data = {};
|
|
if (fs.existsSync(filePath)) {
|
|
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
}
|
|
|
|
res.locals.t = data;
|
|
res.locals.lang = lang;
|
|
next();
|
|
} catch (err) {
|
|
console.error("❌ i18n Fehler:", err.message);
|
|
res.locals.t = {};
|
|
res.locals.lang = "de";
|
|
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.set("views", path.join(__dirname, "views"));
|
|
app.use(expressLayouts);
|
|
app.set("layout", "layout");
|
|
|
|
app.use((req, res, next) => {
|
|
res.locals.user = req.session.user || null;
|
|
next();
|
|
});
|
|
|
|
/* ===============================
|
|
✅ SETUP ROUTES + SETUP GATE
|
|
WICHTIG: /setup zuerst mounten, danach requireSetup
|
|
================================ */
|
|
app.use("/setup", setupRoutes);
|
|
app.use(requireSetup);
|
|
|
|
/* ===============================
|
|
✅ LICENSE/TRIAL GATE
|
|
================================ */
|
|
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();
|
|
}
|
|
});
|
|
|
|
/* ===============================
|
|
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
|
|
================================ */
|
|
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");
|
|
}
|
|
});
|
|
|
|
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");
|
|
}
|
|
});
|
|
|
|
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("/invoices", invoiceRoutes);
|
|
|
|
app.use("/reportview", reportRoutes);
|
|
|
|
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 = process.env.HOST || "0.0.0.0";
|
|
|
|
app.listen(PORT, HOST, () => {
|
|
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
|
});
|