Auslagerung verschiedener teile aus der app.js
This commit is contained in:
parent
056c087e1a
commit
7e5896bc90
190
app.js
190
app.js
@ -3,17 +3,17 @@ 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
|
||||
// ✅ DB + Session Store
|
||||
const db = require("./db");
|
||||
const { getSessionStore, resetSessionStore } = require("./config/session");
|
||||
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");
|
||||
@ -64,85 +64,48 @@ function passesModulo3(serial) {
|
||||
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(
|
||||
helmet({
|
||||
contentSecurityPolicy: false,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: "praxis.sid",
|
||||
secret: process.env.SESSION_SECRET,
|
||||
secret: process.env.SESSION_SECRET || "dev-secret",
|
||||
store: getSessionStore(),
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
// ✅ i18n Middleware 1 (setzt res.locals.t + lang)
|
||||
// ✅ i18n Middleware (SAFE)
|
||||
app.use((req, res, next) => {
|
||||
const lang = req.session.lang || "de";
|
||||
try {
|
||||
const lang = req.session.lang || "de";
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
|
||||
const filePath = path.join(__dirname, "locales", `${lang}.json`);
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
let data = {};
|
||||
if (fs.existsSync(filePath)) {
|
||||
data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
|
||||
res.locals.t = JSON.parse(raw);
|
||||
res.locals.lang = lang;
|
||||
|
||||
next();
|
||||
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");
|
||||
@ -152,20 +115,24 @@ 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"); // verwendet views/layout.ejs
|
||||
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
|
||||
- Trial startet automatisch, wenn noch NULL
|
||||
- Wenn abgelaufen:
|
||||
Admin -> /admin/serial-number
|
||||
Arzt/Member -> /serial-number
|
||||
================================ */
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
@ -189,7 +156,7 @@ app.use(async (req, res, next) => {
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
LIMIT 1`
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
@ -203,7 +170,7 @@ app.use(async (req, res, next) => {
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
[settings.id]
|
||||
);
|
||||
return next();
|
||||
}
|
||||
@ -230,57 +197,6 @@ app.use(async (req, res, 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
|
||||
================================ */
|
||||
@ -302,14 +218,6 @@ app.get("/lang/:lang", (req, res) => {
|
||||
/* ===============================
|
||||
✅ 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("/");
|
||||
@ -318,7 +226,7 @@ app.get("/serial-number", async (req, res) => {
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
LIMIT 1`
|
||||
);
|
||||
|
||||
const settings = rowsSettings?.[0];
|
||||
@ -332,7 +240,7 @@ app.get("/serial-number", async (req, res) => {
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
[settings.id]
|
||||
);
|
||||
settings.trial_started_at = new Date();
|
||||
}
|
||||
@ -371,9 +279,6 @@ app.get("/serial-number", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer eingeben
|
||||
*/
|
||||
app.get("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
@ -383,7 +288,7 @@ app.get("/admin/serial-number", async (req, res) => {
|
||||
const [rowsSettings] = await db
|
||||
.promise()
|
||||
.query(
|
||||
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`,
|
||||
`SELECT serial_number FROM company_settings ORDER BY id ASC LIMIT 1`
|
||||
);
|
||||
|
||||
const currentSerial = rowsSettings?.[0]?.serial_number || "";
|
||||
@ -402,9 +307,6 @@ app.get("/admin/serial-number", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Admin Seite: Seriennummer speichern
|
||||
*/
|
||||
app.post("/admin/serial-number", async (req, res) => {
|
||||
try {
|
||||
if (!req.session?.user) return res.redirect("/");
|
||||
@ -515,7 +417,7 @@ app.use((err, req, res, next) => {
|
||||
SERVER
|
||||
================================ */
|
||||
const PORT = process.env.PORT || 51777;
|
||||
const HOST = "127.0.0.1";
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`Server läuft auf http://${HOST}:${PORT}`);
|
||||
|
||||
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
0
backups/praxissoftware_2026-01-26_11-10-05.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
613
backups/praxissoftware_2026-01-26_11-54-40.sql
Normal file
File diff suppressed because one or more lines are too long
@ -266,8 +266,8 @@ async function showInvoiceOverview(req, res) {
|
||||
|
||||
res.render("admin/admin_invoice_overview", {
|
||||
title: "Rechnungsübersicht",
|
||||
sidebarPartial: "partials/sidebar-empty", // ✅ keine Sidebar
|
||||
active: "",
|
||||
sidebarPartial: "partials/admin-sidebar", // ✅ keine Sidebar
|
||||
active: "invoices",
|
||||
|
||||
user: req.session.user,
|
||||
lang: req.session.lang || "de",
|
||||
|
||||
@ -53,7 +53,7 @@ function listMedications(req, res, next) {
|
||||
lang: req.session.lang || "de",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 💾 UPDATE
|
||||
function updateMedication(req, res, next) {
|
||||
|
||||
47
middleware/requireSetup.js
Normal file
47
middleware/requireSetup.js
Normal file
@ -0,0 +1,47 @@
|
||||
const { configExists, loadConfig } = require("../config-manager");
|
||||
|
||||
/**
|
||||
* Leitet beim ersten Programmstart automatisch zu /setup um,
|
||||
* solange config.enc fehlt oder DB-Daten unvollständig sind.
|
||||
*/
|
||||
module.exports = function requireSetup(req, res, next) {
|
||||
// ✅ Setup immer erlauben
|
||||
if (req.path.startsWith("/setup")) return next();
|
||||
|
||||
// ✅ Static niemals blockieren
|
||||
if (req.path.startsWith("/public")) return next();
|
||||
if (req.path.startsWith("/css")) return next();
|
||||
if (req.path.startsWith("/js")) return next();
|
||||
if (req.path.startsWith("/images")) return next();
|
||||
if (req.path.startsWith("/uploads")) return next();
|
||||
if (req.path.startsWith("/favicon")) return next();
|
||||
|
||||
// ✅ Login/Logout erlauben
|
||||
if (req.path.startsWith("/login")) return next();
|
||||
if (req.path.startsWith("/logout")) return next();
|
||||
|
||||
// ✅ Wenn config.enc fehlt -> Setup erzwingen
|
||||
if (!configExists()) {
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
|
||||
// ✅ Wenn config existiert aber DB Daten fehlen -> Setup erzwingen
|
||||
let cfg = null;
|
||||
try {
|
||||
cfg = loadConfig();
|
||||
} catch (e) {
|
||||
cfg = null;
|
||||
}
|
||||
|
||||
const ok =
|
||||
cfg?.db?.host &&
|
||||
cfg?.db?.user &&
|
||||
cfg?.db?.password &&
|
||||
cfg?.db?.name;
|
||||
|
||||
if (!ok) {
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
131
package-lock.json
generated
131
package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -1648,6 +1649,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@ -1835,6 +1844,14 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@ -2062,6 +2079,15 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
|
||||
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -2514,6 +2540,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@ -4111,7 +4151,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -5299,6 +5338,12 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.25.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
|
||||
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||
@ -5380,6 +5425,44 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ssh": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||
"dependencies": {
|
||||
"is-stream": "^2.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"sb-promise-queue": "^2.1.0",
|
||||
"sb-scandir": "^3.1.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"ssh2": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ssh/node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ssh/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
@ -6036,6 +6119,25 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sb-promise-queue": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/sb-scandir": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||
"dependencies": {
|
||||
"sb-promise-queue": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
@ -6137,6 +6239,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-escape": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@ -6311,6 +6418,23 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
|
||||
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -6726,6 +6850,11 @@
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.16.0",
|
||||
"node-ssh": "^13.2.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -5,6 +5,8 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
const multer = require("multer");
|
||||
const { NodeSSH } = require("node-ssh");
|
||||
|
||||
|
||||
// ✅ Upload Ordner für Restore Dumps
|
||||
const upload = multer({ dest: path.join(__dirname, "..", "uploads_tmp") });
|
||||
@ -309,33 +311,37 @@ router.post("/database", requireAdmin, async (req, res) => {
|
||||
/* ==========================
|
||||
✅ BACKUP (NUR ADMIN)
|
||||
========================== */
|
||||
router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
// ✅ Flash Safe (funktioniert auch ohne req.flash)
|
||||
router.post("/database/backup", requireAdmin, async (req, res) => {
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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, user, password, name } = cfg.db;
|
||||
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/, "_")
|
||||
@ -343,120 +349,134 @@ router.post("/database/backup", requireAdmin, (req, res) => {
|
||||
.split(".")[0];
|
||||
|
||||
const fileName = `${name}_${stamp}.sql`;
|
||||
const filePath = path.join(backupDir, fileName);
|
||||
|
||||
// ✅ mysqldump.exe im Root
|
||||
const mysqldumpPath = path.join(__dirname, "..", "mysqldump.exe");
|
||||
// ✅ Datei wird zuerst auf DB-Server erstellt (tmp)
|
||||
const remoteTmpPath = `/tmp/${fileName}`;
|
||||
|
||||
// ✅ plugin Ordner im Root (muss existieren)
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
// ✅ Datei wird dann lokal (Programmserver) gespeichert
|
||||
const localPath = path.join(backupDir, fileName);
|
||||
|
||||
if (!fs.existsSync(mysqldumpPath)) {
|
||||
flashSafe("danger", "❌ mysqldump.exe nicht gefunden: " + mysqldumpPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pluginDir)) {
|
||||
flashSafe("danger", "❌ plugin Ordner nicht gefunden: " + pluginDir);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const cmd = `"${mysqldumpPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} > "${filePath}"`;
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ BACKUP ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Backup fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe("success", `✅ Backup erstellt: ${fileName}`);
|
||||
return res.redirect("/admin/database");
|
||||
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 ERROR:", 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, (req, res) => {
|
||||
router.post("/database/restore", requireAdmin, async (req, res) => {
|
||||
function flashSafe(type, msg) {
|
||||
if (typeof req.flash === "function") {
|
||||
req.flash(type, msg);
|
||||
return;
|
||||
}
|
||||
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, user, password, name } = cfg.db;
|
||||
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 selectedFile = req.body.backupFile;
|
||||
const localPath = path.join(backupDir, backupFile);
|
||||
|
||||
if (!selectedFile) {
|
||||
flashSafe("danger", "❌ Bitte ein Backup auswählen.");
|
||||
if (!fs.existsSync(localPath)) {
|
||||
flashSafe("danger", "❌ Datei nicht gefunden (Programmserver): " + backupFile);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const fullPath = path.join(backupDir, selectedFile);
|
||||
const sshHost = process.env.DBSERVER_HOST;
|
||||
const sshUser = process.env.DBSERVER_USER;
|
||||
const sshPort = Number(process.env.DBSERVER_PORT || 22);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
flashSafe("danger", "❌ Backup Datei nicht gefunden: " + selectedFile);
|
||||
if (!sshHost || !sshUser) {
|
||||
flashSafe("danger", "❌ SSH Config fehlt (DBSERVER_HOST / DBSERVER_USER).");
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
// ✅ mysql.exe im Root
|
||||
const mysqlPath = path.join(__dirname, "..", "mysql.exe");
|
||||
const pluginDir = path.join(__dirname, "..", "plugin");
|
||||
const remoteTmpPath = `/tmp/${backupFile}`;
|
||||
|
||||
if (!fs.existsSync(mysqlPath)) {
|
||||
flashSafe("danger", "❌ mysql.exe nicht gefunden im Root: " + mysqlPath);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
const cmd = `"${mysqlPath}" --plugin-dir="${pluginDir}" -h ${host} -u ${user} -p${password} ${name} < "${fullPath}"`;
|
||||
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error("❌ RESTORE ERROR:", error);
|
||||
console.error("STDERR:", stderr);
|
||||
|
||||
flashSafe(
|
||||
"danger",
|
||||
"❌ Restore fehlgeschlagen: " + (stderr || error.message),
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
}
|
||||
|
||||
flashSafe(
|
||||
"success",
|
||||
"✅ Restore erfolgreich abgeschlossen: " + selectedFile,
|
||||
);
|
||||
return res.redirect("/admin/database");
|
||||
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 ERROR:", err);
|
||||
console.error("❌ RESTORE SSH ERROR:", err);
|
||||
flashSafe("danger", "❌ Restore fehlgeschlagen: " + err.message);
|
||||
return res.redirect("/admin/database");
|
||||
} finally {
|
||||
try {
|
||||
ssh.dispose();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
139
routes/setup.routes.js
Normal file
139
routes/setup.routes.js
Normal file
@ -0,0 +1,139 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
// ✅ nutzt deinen bestehenden config-manager (NICHT utils/config!)
|
||||
const { configExists, saveConfig } = require("../config-manager");
|
||||
|
||||
// ✅ DB + Session Reset (wie in deiner app.js)
|
||||
const db = require("../db");
|
||||
const { resetSessionStore } = require("../config/session");
|
||||
|
||||
/**
|
||||
* Setup darf nur laufen, wenn config.enc NICHT existiert
|
||||
* (sonst könnte jeder die DB später überschreiben)
|
||||
*/
|
||||
function blockIfInstalled(req, res, next) {
|
||||
if (configExists()) {
|
||||
return res.redirect("/");
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Form anzeigen
|
||||
*/
|
||||
router.get("/", blockIfInstalled, (req, res) => {
|
||||
return res.render("setup/index", {
|
||||
title: "Erstinstallation",
|
||||
defaults: {
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
user: "",
|
||||
password: "",
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Verbindung testen (AJAX)
|
||||
*/
|
||||
router.post("/test", blockIfInstalled, async (req, res) => {
|
||||
try {
|
||||
const { host, port, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !name) {
|
||||
return res.status(400).json({
|
||||
ok: false,
|
||||
message: "Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||
});
|
||||
}
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
host,
|
||||
port: Number(port || 3306),
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
connectTimeout: 5000,
|
||||
});
|
||||
|
||||
await connection.query("SELECT 1");
|
||||
await connection.end();
|
||||
|
||||
return res.json({ ok: true, message: "✅ Verbindung erfolgreich!" });
|
||||
} catch (err) {
|
||||
return res.status(500).json({
|
||||
ok: false,
|
||||
message: "❌ Verbindung fehlgeschlagen: " + err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ✅ Setup speichern (DB Daten in config.enc)
|
||||
*/
|
||||
router.post("/", blockIfInstalled, async (req, res) => {
|
||||
try {
|
||||
const { host, port, user, password, name } = req.body;
|
||||
|
||||
if (!host || !user || !name) {
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({
|
||||
type: "danger",
|
||||
message: "❌ Bitte Host, Benutzer und Datenbankname ausfüllen.",
|
||||
});
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
|
||||
// ✅ Verbindung testen bevor speichern
|
||||
const connection = await mysql.createConnection({
|
||||
host,
|
||||
port: Number(port || 3306),
|
||||
user,
|
||||
password,
|
||||
database: name,
|
||||
connectTimeout: 5000,
|
||||
});
|
||||
|
||||
await connection.query("SELECT 1");
|
||||
await connection.end();
|
||||
|
||||
// ✅ speichern
|
||||
saveConfig({
|
||||
db: {
|
||||
host,
|
||||
port: Number(port || 3306),
|
||||
user,
|
||||
password,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ DB Pool neu starten (damit neue config sofort aktiv ist)
|
||||
if (typeof db.resetPool === "function") {
|
||||
db.resetPool();
|
||||
}
|
||||
|
||||
// ✅ Session Store neu starten
|
||||
resetSessionStore();
|
||||
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({
|
||||
type: "success",
|
||||
message: "✅ Setup abgeschlossen. Du kannst dich jetzt einloggen.",
|
||||
});
|
||||
|
||||
return res.redirect("/login");
|
||||
} catch (err) {
|
||||
req.session.flash = req.session.flash || [];
|
||||
req.session.flash.push({
|
||||
type: "danger",
|
||||
message: "❌ Setup fehlgeschlagen: " + err.message,
|
||||
});
|
||||
return res.redirect("/setup");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
8
ssh_fuer_db_Server
Normal file
8
ssh_fuer_db_Server
Normal file
@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDgIWJidh
|
||||
WoA1VkDl2ScVZmAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+
|
||||
zQcoG121zH1s0Jv2eOAuk+BS1UbfAAAAoJyvcKBc26mTti/T2iAbdEATM15fgtMs9Nlsi4
|
||||
itZSVPfQ7OdYY2QMQpaiw0whrcQIdWlxUrbcYpmwPj7DATYUP00szFN2KbdRdsarfPqHFQ
|
||||
zi8P2p9bj7/naRyLIENsfTj8JERxItd4xHvwL01keBmTty2hnprXcZHw4SkyIOIZyigTgS
|
||||
7gkivG/j9jIOn4g0p0J1eaHdFNvJScpIs19SI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
ssh_fuer_db_Server.pub
Normal file
1
ssh_fuer_db_Server.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM0AYnTTnboA6hm+zQcoG121zH1s0Jv2eOAuk+BS1Ubf cay@Cay-Workstation-VM
|
||||
52
utils/config.js
Normal file
52
utils/config.js
Normal file
@ -0,0 +1,52 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, "..", "config.enc");
|
||||
|
||||
function getKey() {
|
||||
const raw = process.env.CONFIG_KEY;
|
||||
if (!raw) {
|
||||
throw new Error("CONFIG_KEY fehlt in .env");
|
||||
}
|
||||
return crypto.createHash("sha256").update(raw).digest(); // 32 bytes
|
||||
}
|
||||
|
||||
function encrypt(obj) {
|
||||
const iv = crypto.randomBytes(12);
|
||||
const key = getKey();
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
|
||||
const data = Buffer.from(JSON.stringify(obj), "utf8");
|
||||
const enc = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
// [iv(12)] + [tag(16)] + [encData]
|
||||
return Buffer.concat([iv, tag, enc]);
|
||||
}
|
||||
|
||||
function decrypt(buf) {
|
||||
const iv = buf.subarray(0, 12);
|
||||
const tag = buf.subarray(12, 28);
|
||||
const enc = buf.subarray(28);
|
||||
|
||||
const key = getKey();
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
const data = Buffer.concat([decipher.update(enc), decipher.final()]);
|
||||
return JSON.parse(data.toString("utf8"));
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(CONFIG_PATH)) return null;
|
||||
const buf = fs.readFileSync(CONFIG_PATH);
|
||||
return decrypt(buf);
|
||||
}
|
||||
|
||||
function saveConfig(cfg) {
|
||||
const buf = encrypt(cfg);
|
||||
fs.writeFileSync(CONFIG_PATH, buf);
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, saveConfig, CONFIG_PATH };
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- ✅ Header -->
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Rechnungsübersicht",
|
||||
@ -9,6 +10,7 @@
|
||||
|
||||
<!-- FILTER: JAHR VON / BIS -->
|
||||
<div class="container-fluid mt-2">
|
||||
|
||||
<form method="get" class="row g-2 mb-4">
|
||||
<div class="col-auto">
|
||||
<input
|
||||
@ -51,21 +53,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% if (!yearly || yearly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% yearly.forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% (yearly || []).forEach(y => { %>
|
||||
<tr>
|
||||
<td><%= y.year %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(y.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -87,22 +89,22 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% if (!quarterly || quarterly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% quarterly.forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% (quarterly || []).forEach(q => { %>
|
||||
<tr>
|
||||
<td><%= q.year %></td>
|
||||
<td>Q<%= q.quarter %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(q.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -123,21 +125,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% if (!monthly || monthly.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% monthly.forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% (monthly || []).forEach(m => { %>
|
||||
<tr>
|
||||
<td><%= m.month %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(m.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -182,21 +184,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% if (!patients || patients.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-center text-muted">
|
||||
Keine Daten
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% patients.forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% (patients || []).forEach(p => { %>
|
||||
<tr>
|
||||
<td><%= p.patient %></td>
|
||||
<td class="text-end fw-semibold">
|
||||
<%= Number(p.total).toFixed(2) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -1,252 +1,263 @@
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Datenbankverwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true
|
||||
}) %>
|
||||
<div class="layout">
|
||||
|
||||
<div class="content p-4">
|
||||
<!-- ✅ SIDEBAR (wie Dashboard, aber Admin Sidebar) -->
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database", lang }) %>
|
||||
|
||||
<%- include("../partials/flash") %>
|
||||
<!-- ✅ MAIN -->
|
||||
<div class="main">
|
||||
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row g-3">
|
||||
<!-- ✅ HEADER (wie Dashboard) -->
|
||||
<%- include("../partials/page-header", {
|
||||
user,
|
||||
title: "Datenbankverwaltung",
|
||||
subtitle: "",
|
||||
showUserName: true,
|
||||
hideDashboardButton: true
|
||||
}) %>
|
||||
|
||||
<!-- ✅ Sidebar -->
|
||||
<div class="col-md-3 col-lg-2 p-0">
|
||||
<%- include("../partials/admin-sidebar", { user, active: "database" }) %>
|
||||
</div>
|
||||
<div class="content p-4">
|
||||
|
||||
<!-- ✅ Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<!-- Flash Messages -->
|
||||
<%- include("../partials/flash") %>
|
||||
|
||||
<!-- ✅ DB Konfiguration -->
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row g-3">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||
</h4>
|
||||
<!-- ✅ DB Konfiguration -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Hier kannst du die DB-Verbindung testen und speichern.
|
||||
</p>
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-sliders"></i> Datenbank Konfiguration
|
||||
</h4>
|
||||
|
||||
<!-- ✅ TEST (ohne speichern) + SPEICHERN -->
|
||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Host / IP</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.host || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<p class="text-muted mb-4">
|
||||
Hier kannst du die DB-Verbindung testen und speichern.
|
||||
</p>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
name="port"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.port || 3306 %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<!-- ✅ TEST + SPEICHERN -->
|
||||
<form method="POST" action="/admin/database/test" class="row g-3 mb-3" autocomplete="off">
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Datenbank</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.name || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Benutzer</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.user || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
value="<%= dbConfig?.password || '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex flex-wrap gap-2">
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-plug"></i> Verbindung testen
|
||||
</button>
|
||||
|
||||
<!-- ✅ Speichern + Testen -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
formaction="/admin/database"
|
||||
>
|
||||
<i class="bi bi-save"></i> Speichern
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||
<%= testResult.message %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ System Info -->
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-info-circle"></i> Systeminformationen
|
||||
</h4>
|
||||
|
||||
<% if (typeof systemInfo !== "undefined" && systemInfo?.error) { %>
|
||||
|
||||
<div class="alert alert-danger mb-0">
|
||||
❌ Fehler beim Auslesen der Datenbankinfos:
|
||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||
</div>
|
||||
|
||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">MySQL Version</div>
|
||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Host / IP</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.host) ? dbConfig.host : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Anzahl Tabellen</div>
|
||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
name="port"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.port) ? dbConfig.port : 3306 %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Datenbankgröße</div>
|
||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Datenbank</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.name) ? dbConfig.name : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Benutzer</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.user) ? dbConfig.user : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
value="<%= (typeof dbConfig !== 'undefined' && dbConfig && dbConfig.password) ? dbConfig.password : '' %>"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex flex-wrap gap-2">
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-plug"></i> Verbindung testen
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
formaction="/admin/database"
|
||||
>
|
||||
<i class="bi bi-save"></i> Speichern
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% if (typeof testResult !== "undefined" && testResult) { %>
|
||||
<div class="alert <%= testResult.ok ? 'alert-success' : 'alert-danger' %> mb-0">
|
||||
<%= testResult.message %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
|
||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||
<hr>
|
||||
|
||||
<h6 class="mb-2">Tabellenübersicht</h6>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Tabelle</th>
|
||||
<th class="text-end">Zeilen</th>
|
||||
<th class="text-end">Größe (MB)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% systemInfo.tables.forEach(t => { %>
|
||||
<tr>
|
||||
<td><%= t.name %></td>
|
||||
<td class="text-end"><%= t.row_count %></td>
|
||||
<td class="text-end"><%= t.size_mb %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="alert alert-warning mb-0">
|
||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Backup & Restore -->
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||
</h4>
|
||||
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
|
||||
<!-- ✅ Backup erstellen -->
|
||||
<form action="/admin/database/backup" method="POST">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-download"></i> Backup erstellen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- ✅ Restore auswählen -->
|
||||
<form action="/admin/database/restore" method="POST">
|
||||
<div class="input-group">
|
||||
|
||||
<select name="backupFile" class="form-select" required>
|
||||
<option value="">Backup auswählen...</option>
|
||||
|
||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||
<option value="<%= file %>"><%= file %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-upload"></i> Restore starten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||
<div class="alert alert-secondary mt-3 mb-0">
|
||||
ℹ️ Noch keine Backups vorhanden.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ System Info -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-3">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-info-circle"></i> Systeminformationen
|
||||
</h4>
|
||||
|
||||
<% if (typeof systemInfo !== "undefined" && systemInfo && systemInfo.error) { %>
|
||||
|
||||
<div class="alert alert-danger mb-0">
|
||||
❌ Fehler beim Auslesen der Datenbankinfos:
|
||||
<div class="mt-2"><code><%= systemInfo.error %></code></div>
|
||||
</div>
|
||||
|
||||
<% } else if (typeof systemInfo !== "undefined" && systemInfo) { %>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">MySQL Version</div>
|
||||
<div class="fw-bold"><%= systemInfo.version %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Anzahl Tabellen</div>
|
||||
<div class="fw-bold"><%= systemInfo.tableCount %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small">Datenbankgröße</div>
|
||||
<div class="fw-bold"><%= systemInfo.dbSizeMB %> MB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (systemInfo.tables && systemInfo.tables.length > 0) { %>
|
||||
<hr>
|
||||
|
||||
<h6 class="mb-2">Tabellenübersicht</h6>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover align-middle">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Tabelle</th>
|
||||
<th class="text-end">Zeilen</th>
|
||||
<th class="text-end">Größe (MB)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% systemInfo.tables.forEach(t => { %>
|
||||
<tr>
|
||||
<td><%= t.name %></td>
|
||||
<td class="text-end"><%= t.row_count %></td>
|
||||
<td class="text-end"><%= t.size_mb %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<div class="alert alert-warning mb-0">
|
||||
⚠️ Keine Systeminfos verfügbar (DB ist evtl. nicht konfiguriert oder Verbindung fehlgeschlagen).
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Backup & Restore -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3">
|
||||
<i class="bi bi-hdd-stack"></i> Backup & Restore
|
||||
</h4>
|
||||
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
|
||||
<!-- ✅ Backup erstellen -->
|
||||
<form action="/admin/database/backup" method="POST">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-download"></i> Backup erstellen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- ✅ Restore auswählen -->
|
||||
<form action="/admin/database/restore" method="POST">
|
||||
<div class="input-group">
|
||||
|
||||
<select name="backupFile" class="form-select" required>
|
||||
<option value="">Backup auswählen...</option>
|
||||
|
||||
<% (typeof backupFiles !== "undefined" && backupFiles ? backupFiles : []).forEach(file => { %>
|
||||
<option value="<%= file %>"><%= file %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-upload"></i> Restore starten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<% if (typeof backupFiles === "undefined" || !backupFiles || backupFiles.length === 0) { %>
|
||||
<div class="alert alert-secondary mt-3 mb-0">
|
||||
ℹ️ Noch keine Backups vorhanden.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -127,10 +127,4 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ⚠️ Inline Script wird von CSP blockiert!
|
||||
// Wenn du diese Buttons brauchst, sag Bescheid,
|
||||
// dann verlagern wir das sauber in /public/js/admin-users.js (CSP safe).
|
||||
</script>
|
||||
</div>
|
||||
84
views/setup/index.ejs
Normal file
84
views/setup/index.ejs
Normal file
@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><%= title %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background:#f5f5f5; padding:20px; }
|
||||
.card { max-width: 560px; margin: 0 auto; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.08); }
|
||||
label { display:block; margin-top: 12px; font-weight: 600; }
|
||||
input { width: 100%; padding: 10px; margin-top: 6px; border-radius: 8px; border: 1px solid #ddd; }
|
||||
.row { display:flex; gap: 12px; }
|
||||
.row > div { flex: 1; }
|
||||
button { margin-top: 16px; padding: 10px 14px; border: 0; border-radius: 10px; cursor:pointer; }
|
||||
.btn-primary { background:#2563eb; color:white; }
|
||||
.btn-secondary { background:#111827; color:white; }
|
||||
.msg { margin-top: 10px; padding:10px; border-radius: 10px; display:none; }
|
||||
.msg.ok { background:#dcfce7; color:#166534; }
|
||||
.msg.bad { background:#fee2e2; color:#991b1b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
<h2>🛠️ Erstinstallation</h2>
|
||||
<p>Bitte DB Daten eingeben. Danach wird <code>config.enc</code> gespeichert.</p>
|
||||
|
||||
<form method="POST" action="/setup">
|
||||
<label>DB Host</label>
|
||||
<input name="host" placeholder="192.168.0.86" required />
|
||||
|
||||
<label>DB Port</label>
|
||||
<input name="port" placeholder="3306" value="3306" 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 />
|
||||
<label>Passwort</label>
|
||||
<input name="password" type="password" value="<%= defaults.password %>" />
|
||||
|
||||
<button type="button" class="btn-secondary" onclick="testConnection()">🔍 Verbindung testen</button>
|
||||
<button type="submit" class="btn-primary">✅ Speichern & Setup abschließen</button>
|
||||
|
||||
<div id="msg" class="msg"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testConnection() {
|
||||
const form = document.querySelector("form");
|
||||
const data = new FormData(form);
|
||||
const body = Object.fromEntries(data.entries());
|
||||
|
||||
const msg = document.getElementById("msg");
|
||||
msg.style.display = "block";
|
||||
msg.className = "msg";
|
||||
msg.textContent = "Teste Verbindung...";
|
||||
|
||||
try {
|
||||
const res = await fetch("/setup/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
msg.textContent = json.message;
|
||||
|
||||
if (json.ok) msg.classList.add("ok");
|
||||
else msg.classList.add("bad");
|
||||
} catch (e) {
|
||||
msg.textContent = "❌ Fehler: " + e.message;
|
||||
msg.classList.add("bad");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user