513 lines
15 KiB
JavaScript
513 lines
15 KiB
JavaScript
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;
|