Praxissofttware/app.js

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}`);
});