const express = require("express"); const router = express.Router(); const mysql = require("mysql2/promise"); const fs = require("fs"); const path = require("path"); const { exec } = require("child_process"); const multer = require("multer"); const { NodeSSH } = require("node-ssh"); const uploadLogo = require("../middleware/uploadLogo"); // ✅ Upload Ordner für Restore Dumps const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") }); const { listUsers, showCreateUser, postCreateUser, changeUserRole, resetUserPassword, activateUser, deactivateUser, showInvoiceOverview, updateUser, } = require("../controllers/admin.controller"); const { requireArzt, requireAdmin } = require("../middleware/auth.middleware"); // ✅ config.enc Manager const { loadConfig, saveConfig } = require("../config-manager"); // ✅ DB (für resetPool) const db = require("../db"); // ✅ Firmendaten const { getCompanySettings, saveCompanySettings } = require("../controllers/companySettings.controller"); /* ========================== ✅ VERWALTUNG (NUR ADMIN) ========================== */ router.get("/users", requireAdmin, listUsers); router.get("/create-user", requireAdmin, showCreateUser); router.post("/create-user", requireAdmin, postCreateUser); router.post("/users/change-role/:id", requireAdmin, changeUserRole); router.post("/users/reset-password/:id", requireAdmin, resetUserPassword); router.post("/users/activate/:id", requireAdmin, activateUser); router.post("/users/deactivate/:id", requireAdmin, deactivateUser); router.post("/users/update/:id", requireAdmin, updateUser); /* ========================== ✅ DATENBANKVERWALTUNG (NUR ADMIN) ========================== */ // ✅ Seite anzeigen + aktuelle DB Config aus config.enc anzeigen router.get("/database", requireAdmin, async (req, res) => { const cfg = loadConfig(); const backupDir = path.join(__dirname, "..", "backups"); let backupFiles = []; try { if (fs.existsSync(backupDir)) { backupFiles = fs .readdirSync(backupDir) .filter((f) => f.toLowerCase().endsWith(".sql")) .sort() .reverse(); // ✅ neueste zuerst } } catch (err) { console.error("❌ Backup Ordner Fehler:", err); } let systemInfo = null; try { if (cfg?.db) { const conn = await mysql.createConnection({ host: cfg.db.host, port: Number(cfg.db.port || 3306), // ✅ WICHTIG: Port nutzen user: cfg.db.user, password: cfg.db.password, database: cfg.db.name, }); // ✅ Version const [v] = await conn.query("SELECT VERSION() AS version"); // ✅ Anzahl Tabellen const [tablesCount] = await conn.query( "SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = ?", [cfg.db.name], ); // ✅ DB Größe (Bytes) const [dbSize] = await conn.query( ` SELECT IFNULL(SUM(data_length + index_length),0) AS bytes FROM information_schema.tables WHERE table_schema = ? `, [cfg.db.name], ); // ✅ Tabellen Details const [tables] = await conn.query( ` SELECT table_name AS name, table_rows AS row_count, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = ? ORDER BY (data_length + index_length) DESC `, [cfg.db.name], ); await conn.end(); systemInfo = { version: v?.[0]?.version || "unbekannt", tableCount: tablesCount?.[0]?.count || 0, dbSizeMB: Math.round(((dbSize?.[0]?.bytes || 0) / 1024 / 1024) * 100) / 100, tables, }; } } catch (err) { console.error("❌ SYSTEMINFO ERROR:", err); systemInfo = { error: err.message, }; } res.render("admin/database", { user: req.session.user, dbConfig: cfg?.db || null, testResult: null, backupFiles, systemInfo, }); }); // ✅ Nur testen (ohne speichern) router.post("/database/test", requireAdmin, async (req, res) => { const backupDir = path.join(__dirname, "..", "backups"); function getBackupFiles() { try { if (fs.existsSync(backupDir)) { return fs .readdirSync(backupDir) .filter((f) => f.toLowerCase().endsWith(".sql")) .sort() .reverse(); } } catch (err) { console.error("❌ Backup Ordner Fehler:", err); } return []; } try { const { host, port, user, password, name } = req.body; if (!host || !port || !user || !password || !name) { const cfg = loadConfig(); return res.render("admin/database", { user: req.session.user, dbConfig: cfg?.db || null, testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." }, backupFiles: getBackupFiles(), systemInfo: null, }); } const conn = await mysql.createConnection({ host, port: Number(port), user, password, database: name, }); await conn.query("SELECT 1"); await conn.end(); return res.render("admin/database", { user: req.session.user, dbConfig: { host, port: Number(port), user, password, name }, // ✅ PORT bleibt drin! testResult: { ok: true, message: "✅ Verbindung erfolgreich!" }, backupFiles: getBackupFiles(), systemInfo: null, }); } catch (err) { console.error("❌ DB TEST ERROR:", err); return res.render("admin/database", { user: req.session.user, dbConfig: req.body, testResult: { ok: false, message: "❌ Verbindung fehlgeschlagen: " + err.message, }, backupFiles: getBackupFiles(), systemInfo: null, }); } }); // ✅ DB Settings speichern + Verbindung testen router.post("/database", requireAdmin, async (req, res) => { function flashSafe(type, msg) { if (typeof req.flash === "function") { req.flash(type, msg); return; } req.session.flash = req.session.flash || []; req.session.flash.push({ type, message: msg }); } const backupDir = path.join(__dirname, "..", "backups"); // ✅ backupFiles immer bereitstellen function getBackupFiles() { try { if (fs.existsSync(backupDir)) { return fs .readdirSync(backupDir) .filter((f) => f.toLowerCase().endsWith(".sql")) .sort() .reverse(); } } catch (err) { console.error("❌ Backup Ordner Fehler:", err); } return []; } try { const { host, port, user, password, name } = req.body; if (!host || !port || !user || !password || !name) { flashSafe("danger", "❌ Bitte alle Felder ausfüllen."); return res.render("admin/database", { user: req.session.user, dbConfig: req.body, testResult: { ok: false, message: "❌ Bitte alle Felder ausfüllen." }, backupFiles: getBackupFiles(), systemInfo: null, }); } // ✅ Verbindung testen const conn = await mysql.createConnection({ host, port: Number(port), user, password, database: name, }); await conn.query("SELECT 1"); await conn.end(); // ✅ Speichern inkl. Port const current = loadConfig() || {}; current.db = { host, port: Number(port), user, password, name, }; saveConfig(current); // ✅ Pool reset if (typeof db.resetPool === "function") { db.resetPool(); } flashSafe("success", "✅ DB Einstellungen gespeichert!"); // ✅ DIREKT NEU LADEN aus config.enc (damit wirklich die gespeicherten Werte drin stehen) const freshCfg = loadConfig(); return res.render("admin/database", { user: req.session.user, dbConfig: freshCfg?.db || null, testResult: { ok: true, message: "✅ Gespeichert und Verbindung getestet.", }, backupFiles: getBackupFiles(), systemInfo: null, }); } catch (err) { console.error("❌ DB UPDATE ERROR:", err); flashSafe("danger", "❌ Verbindung fehlgeschlagen: " + err.message); return res.render("admin/database", { user: req.session.user, dbConfig: req.body, testResult: { ok: false, message: "❌ Verbindung fehlgeschlagen: " + err.message, }, backupFiles: getBackupFiles(), systemInfo: null, }); } }); /* ========================== ✅ BACKUP (NUR ADMIN) ========================== */ router.post("/database/backup", requireAdmin, async (req, res) => { function flashSafe(type, msg) { if (typeof req.flash === "function") return req.flash(type, msg); req.session.flash = req.session.flash || []; req.session.flash.push({ type, message: msg }); console.log(`[FLASH-${type}]`, msg); } try { const cfg = loadConfig(); if (!cfg?.db) { flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt)."); return res.redirect("/admin/database"); } const { host, port, user, password, name } = cfg.db; // ✅ Programmserver Backup Dir const backupDir = path.join(__dirname, "..", "backups"); if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir); // ✅ SSH Ziel (DB-Server) const sshHost = process.env.DBSERVER_HOST; const sshUser = process.env.DBSERVER_USER; const sshPort = Number(process.env.DBSERVER_PORT || 22); if (!sshHost || !sshUser) { flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER)."); return res.redirect("/admin/database"); } const stamp = new Date() .toISOString() .replace(/T/, "_") .replace(/:/g, "-") .split(".")[0]; const fileName = `${name}_${stamp}.sql`; // ✅ Datei wird zuerst auf DB-Server erstellt (tmp) const remoteTmpPath = `/tmp/${fileName}`; // ✅ Datei wird dann lokal (Programmserver) gespeichert const localPath = path.join(backupDir, fileName); const ssh = new NodeSSH(); await ssh.connect({ host: sshHost, username: sshUser, port: sshPort, privateKeyPath: "/home/cay/.ssh/id_ed25519", }); // ✅ 1) Dump auf DB-Server erstellen const dumpCmd = `mysqldump -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" > "${remoteTmpPath}"`; const dumpRes = await ssh.execCommand(dumpCmd); if (dumpRes.code !== 0) { ssh.dispose(); flashSafe("danger", "❌ Backup fehlgeschlagen: " + (dumpRes.stderr || "mysqldump Fehler")); return res.redirect("/admin/database"); } // ✅ 2) Dump Datei vom DB-Server auf Programmserver kopieren await ssh.getFile(localPath, remoteTmpPath); // ✅ 3) Temp Datei auf DB-Server löschen await ssh.execCommand(`rm -f "${remoteTmpPath}"`); ssh.dispose(); flashSafe("success", `✅ Backup gespeichert (Programmserver): ${fileName}`); return res.redirect("/admin/database"); } catch (err) { console.error("❌ BACKUP SSH ERROR:", err); flashSafe("danger", "❌ Backup fehlgeschlagen: " + err.message); return res.redirect("/admin/database"); } }); /* ========================== ✅ RESTORE (NUR ADMIN) ========================== */ router.post("/database/restore", requireAdmin, async (req, res) => { function flashSafe(type, msg) { if (typeof req.flash === "function") return req.flash(type, msg); req.session.flash = req.session.flash || []; req.session.flash.push({ type, message: msg }); console.log(`[FLASH-${type}]`, msg); } const ssh = new NodeSSH(); try { const cfg = loadConfig(); if (!cfg?.db) { flashSafe("danger", "❌ Keine DB Config gefunden (config.enc fehlt)."); return res.redirect("/admin/database"); } const { host, port, user, password, name } = cfg.db; const backupFile = req.body.backupFile; if (!backupFile) { flashSafe("danger", "❌ Kein Backup ausgewählt."); return res.redirect("/admin/database"); } if (backupFile.includes("..") || backupFile.includes("/") || backupFile.includes("\\")) { flashSafe("danger", "❌ Ungültiger Dateiname."); return res.redirect("/admin/database"); } const backupDir = path.join(__dirname, "..", "backups"); const localPath = path.join(backupDir, backupFile); if (!fs.existsSync(localPath)) { flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile); return res.redirect("/admin/database"); } const sshHost = process.env.DBSERVER_HOST; const sshUser = process.env.DBSERVER_USER; const sshPort = Number(process.env.DBSERVER_PORT || 22); if (!sshHost || !sshUser) { flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER)."); return res.redirect("/admin/database"); } const remoteTmpPath = `/tmp/${backupFile}`; await ssh.connect({ host: sshHost, username: sshUser, port: sshPort, privateKeyPath: "/home/cay/.ssh/id_ed25519", }); await ssh.putFile(localPath, remoteTmpPath); const restoreCmd = `mysql -h "${host}" -P "${port || 3306}" -u "${user}" -p'${password}' "${name}" < "${remoteTmpPath}"`; const restoreRes = await ssh.execCommand(restoreCmd); await ssh.execCommand(`rm -f "${remoteTmpPath}"`); if (restoreRes.code !== 0) { flashSafe("danger", "❌ Restore fehlgeschlagen: " + (restoreRes.stderr || "mysql Fehler")); return res.redirect("/admin/database"); } flashSafe("success", `✅ Restore erfolgreich: ${backupFile}`); return res.redirect("/admin/database"); } catch (err) { console.error("❌ RESTORE SSH ERROR:", err); flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message); return res.redirect("/admin/database"); } finally { try { ssh.dispose(); } catch (e) {} } }); /* ========================== ✅ ABRECHNUNG (NUR ARZT) ========================== */ router.get("/invoices", requireAdmin, showInvoiceOverview); /* ========================== ✅ Firmendaten ========================== */ router.get( "/company-settings", requireAdmin, getCompanySettings ); router.post( "/company-settings", requireAdmin, uploadLogo.single("logo"), saveCompanySettings ); module.exports = router;