Einfügen eines centralen layouts getestet mit der serial-number.ejs und layout.ejs
This commit is contained in:
parent
642800b19a
commit
321018cee4
239
app.js
239
app.js
@ -6,6 +6,7 @@ const helmet = require("helmet");
|
|||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const expressLayouts = require("express-ejs-layouts");
|
||||||
|
|
||||||
// ✅ Verschlüsselte Config
|
// ✅ Verschlüsselte Config
|
||||||
const { configExists, saveConfig } = require("./config-manager");
|
const { configExists, saveConfig } = require("./config-manager");
|
||||||
@ -30,6 +31,39 @@ const authRoutes = require("./routes/auth.routes");
|
|||||||
|
|
||||||
const app = express();
|
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
|
SETUP HTML
|
||||||
================================ */
|
================================ */
|
||||||
@ -118,11 +152,76 @@ app.use(flashMiddleware);
|
|||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
app.use("/uploads", express.static("uploads"));
|
app.use("/uploads", express.static("uploads"));
|
||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.locals.user = req.session.user || null;
|
res.locals.user = req.session.user || null;
|
||||||
next();
|
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 ROUTES
|
||||||
================================ */
|
================================ */
|
||||||
@ -181,7 +280,10 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Sprachen Route
|
/* ===============================
|
||||||
|
Sprachen Route
|
||||||
|
================================ */
|
||||||
|
|
||||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const lang = req.session.lang || "de"; // Standard: Deutsch
|
const lang = req.session.lang || "de"; // Standard: Deutsch
|
||||||
@ -218,6 +320,141 @@ app.get("/lang/:lang", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
✅ 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)
|
DEINE LOGIK (unverändert)
|
||||||
================================ */
|
================================ */
|
||||||
|
|||||||
@ -7,16 +7,27 @@ async function postLogin(req, res) {
|
|||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await loginUser(
|
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||||
db,
|
|
||||||
username,
|
/* req.session.user = user;
|
||||||
password,
|
res.redirect("/dashboard"); */
|
||||||
LOCK_TIME_MINUTES
|
|
||||||
);
|
|
||||||
|
|
||||||
req.session.user = user;
|
req.session.user = user;
|
||||||
res.redirect("/dashboard");
|
|
||||||
|
|
||||||
|
// ✅ Direkt nach Login check:
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (!settings?.serial_number) {
|
||||||
|
return res.redirect("/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/dashboard");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.render("login", { error });
|
res.render("login", { error });
|
||||||
}
|
}
|
||||||
@ -28,5 +39,5 @@ function getLogin(req, res) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getLogin,
|
getLogin,
|
||||||
postLogin
|
postLogin,
|
||||||
};
|
};
|
||||||
|
|||||||
52
middleware/license.middleware.js
Normal file
52
middleware/license.middleware.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
const TRIAL_DAYS = 30;
|
||||||
|
|
||||||
|
async function licenseGate(req, res, next) {
|
||||||
|
// Login-Seiten immer erlauben
|
||||||
|
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||||
|
|
||||||
|
// Seriennummer-Seite immer erlauben
|
||||||
|
if (req.path.startsWith("/serial-number")) return next();
|
||||||
|
|
||||||
|
// Wenn nicht eingeloggt -> normal weiter (auth middleware macht das)
|
||||||
|
if (!req.session?.user) return next();
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
// Wenn Seriennummer vorhanden -> alles ok
|
||||||
|
if (settings?.serial_number) return next();
|
||||||
|
|
||||||
|
// Wenn keine Trial gestartet: jetzt starten
|
||||||
|
if (!settings?.trial_started_at) {
|
||||||
|
await db
|
||||||
|
.promise()
|
||||||
|
.query(
|
||||||
|
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||||
|
[settings?.id || 1],
|
||||||
|
);
|
||||||
|
return next(); // Trial läuft ab jetzt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trial prüfen
|
||||||
|
const trialStart = new Date(settings.trial_started_at);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const diffMs = now - trialStart;
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < TRIAL_DAYS) {
|
||||||
|
return next(); // Trial ist noch gültig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Trial abgelaufen -> nur noch Seriennummer Seite
|
||||||
|
return res.redirect("/serial-number");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { licenseGate };
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
@ -3045,6 +3046,11 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-ejs-layouts": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
|
||||||
|
},
|
||||||
"node_modules/express-mysql-session": {
|
"node_modules/express-mysql-session": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-ejs-layouts": "^2.5.1",
|
||||||
"express-mysql-session": "^3.0.3",
|
"express-mysql-session": "^3.0.3",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
|
|||||||
@ -78,3 +78,102 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
✅ PAGE HEADER (global)
|
||||||
|
- Höhe ca. 4cm
|
||||||
|
- Hintergrund schwarz
|
||||||
|
- Text in der Mitte
|
||||||
|
- Button + Datum/Uhrzeit rechts
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
/* ✅ Der komplette Header-Container */
|
||||||
|
.page-header {
|
||||||
|
height: 150px; /* ca. 4cm */
|
||||||
|
background: #000; /* Schwarz */
|
||||||
|
color: #fff; /* Weiße Schrift */
|
||||||
|
|
||||||
|
/* Wir nutzen Grid, damit Center wirklich immer mittig bleibt */
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
/* 3 Spalten:
|
||||||
|
1) links = leer/optional
|
||||||
|
2) mitte = Text (center)
|
||||||
|
3) rechts = Dashboard + Uhrzeit
|
||||||
|
*/
|
||||||
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
|
|
||||||
|
align-items: center; /* vertikal mittig */
|
||||||
|
padding: 0 20px; /* links/rechts Abstand */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Linke Header-Spalte (kann leer bleiben oder später Logo) */
|
||||||
|
.page-header-left {
|
||||||
|
justify-self: start; /* ganz links */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Mittlere Header-Spalte (Text zentriert) */
|
||||||
|
.page-header-center {
|
||||||
|
justify-self: center; /* wirklich zentriert in der Mitte */
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Username oben, Titel darunter */
|
||||||
|
gap: 6px; /* Abstand zwischen den Zeilen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Rechte Header-Spalte (Button + Uhrzeit rechts) */
|
||||||
|
.page-header-right {
|
||||||
|
justify-self: end; /* ganz rechts */
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Button oben, Uhrzeit unten */
|
||||||
|
align-items: flex-end; /* alles rechts ausrichten */
|
||||||
|
gap: 10px; /* Abstand Button / Uhrzeit */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Username-Zeile (z.B. Willkommen, admin) */
|
||||||
|
.page-header-username {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Titel-Zeile (z.B. Seriennummer) */
|
||||||
|
.page-header-title {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Subtitle Bereich (optional) */
|
||||||
|
.page-header-subtitle {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Uhrzeit (oben rechts unter dem Button) */
|
||||||
|
.page-header-datetime {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (weißer Rahmen) */
|
||||||
|
.page-header .btn-outline-light {
|
||||||
|
border-color: #fff !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button: keine Unterstreichung + Rahmen + rund */
|
||||||
|
.page-header a.btn {
|
||||||
|
text-decoration: none !important; /* keine Unterstreichung */
|
||||||
|
border: 2px solid #fff !important; /* Rahmen */
|
||||||
|
border-radius: 12px; /* abgerundete Ecken */
|
||||||
|
padding: 6px 12px; /* schöner Innenabstand */
|
||||||
|
display: inline-block; /* saubere Button-Form */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Dashboard Button (Hovereffekt) */
|
||||||
|
.page-header a.btn:hover {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|||||||
18
views/layout.ejs
Normal file
18
views/layout.ejs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>
|
||||||
|
<%= typeof title !== "undefined" ? title : "Privatarzt Software" %>
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<!-- ✅ Global CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<%- body %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -71,6 +71,19 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- ✅ Seriennummer (NEU) -->
|
||||||
|
<a
|
||||||
|
href="<%= hrefIfAllowed(isAdmin, '/serial-number') %>"
|
||||||
|
class="nav-item <%= active === 'serialnumber' ? 'active' : '' %> <%= lockClass(isAdmin) %>"
|
||||||
|
<%- lockClick(isAdmin) %>
|
||||||
|
title="<%= isAdmin ? '' : 'Nur Admin' %>"
|
||||||
|
>
|
||||||
|
<i class="bi bi-key"></i> Seriennummer
|
||||||
|
<% if (!isAdmin) { %>
|
||||||
|
<span style="margin-left:auto;"><i class="bi bi-lock-fill"></i></span>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<!-- ✅ Zurück zum Dashboard -->
|
<!-- ✅ Zurück zum Dashboard -->
|
||||||
|
|||||||
52
views/partials/page-header.ejs
Normal file
52
views/partials/page-header.ejs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<%
|
||||||
|
const titleText = typeof title !== "undefined" ? title : "";
|
||||||
|
const subtitleText = typeof subtitle !== "undefined" ? subtitle : "";
|
||||||
|
const showUser = typeof showUserName !== "undefined" ? showUserName : true;
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
|
||||||
|
<!-- LINKS -->
|
||||||
|
<div class="page-header-left"></div>
|
||||||
|
|
||||||
|
<!-- ✅ CENTER TEXT -->
|
||||||
|
<div class="page-header-center">
|
||||||
|
<% if (showUser && user?.username) { %>
|
||||||
|
<div class="page-header-username">
|
||||||
|
Willkommen, <%= user.username %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (titleText) { %>
|
||||||
|
<div class="page-header-title">
|
||||||
|
<%= titleText %>
|
||||||
|
<% if (subtitleText) { %>
|
||||||
|
<span class="page-header-subtitle"> - <%= subtitleText %></span>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ RIGHT -->
|
||||||
|
<div class="page-header-right">
|
||||||
|
<a href="/dashboard" class="btn btn-outline-light btn-sm">
|
||||||
|
⬅️ Dashboard
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span id="datetime" class="page-header-datetime"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function updateDateTime() {
|
||||||
|
const el = document.getElementById("datetime");
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = new Date().toLocaleString("de-DE");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDateTime();
|
||||||
|
setInterval(updateDateTime, 1000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
51
views/serial_number.ejs
Normal file
51
views/serial_number.ejs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT -->
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ✅ HEADER -->
|
||||||
|
<%- include("partials/page-header", {
|
||||||
|
user,
|
||||||
|
title: "Seriennummer",
|
||||||
|
subtitle: "Lizenz / Testphase",
|
||||||
|
showUserName: true
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<h2>🔑 Seriennummer</h2>
|
||||||
|
|
||||||
|
<% if (error) { %>
|
||||||
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (success) { %>
|
||||||
|
<div class="alert alert-success"><%= success %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (trialInfo) { %>
|
||||||
|
<div class="alert alert-warning"><%= trialInfo %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<form method="POST" action="/serial-number" style="max-width:500px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Seriennummer (AAAAA-AAAAA-AAAAA-AAAAA)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="serial_number"
|
||||||
|
value="<%= currentSerial || '' %>"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ABCDE-12345-ABCDE-12345"
|
||||||
|
maxlength="23"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="margin-top:15px;">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue
Block a user