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 fs = require("fs");
|
||||
const path = require("path");
|
||||
const expressLayouts = require("express-ejs-layouts");
|
||||
|
||||
// ✅ Verschlüsselte Config
|
||||
const { configExists, saveConfig } = require("./config-manager");
|
||||
@ -30,6 +31,39 @@ const authRoutes = require("./routes/auth.routes");
|
||||
|
||||
const app = express();
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer / Trial Konfiguration
|
||||
================================ */
|
||||
const TRIAL_DAYS = 30;
|
||||
|
||||
/* ===============================
|
||||
✅ Seriennummer Helper Funktionen
|
||||
================================ */
|
||||
function normalizeSerial(input) {
|
||||
return (input || "")
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9-]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Format: AAAAA-AAAAA-AAAAA-AAAAA
|
||||
function isValidSerialFormat(serial) {
|
||||
return /^[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$/.test(serial);
|
||||
}
|
||||
|
||||
// Modulo-3 Check (Summe aller Zeichenwerte % 3 === 0)
|
||||
function passesModulo3(serial) {
|
||||
const raw = serial.replace(/-/g, "");
|
||||
let sum = 0;
|
||||
|
||||
for (const ch of raw) {
|
||||
if (/[0-9]/.test(ch)) sum += parseInt(ch, 10);
|
||||
else sum += ch.charCodeAt(0) - 55; // A=10
|
||||
}
|
||||
|
||||
return sum % 3 === 0;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
SETUP HTML
|
||||
================================ */
|
||||
@ -118,11 +152,76 @@ app.use(flashMiddleware);
|
||||
app.use(express.static("public"));
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use(expressLayouts);
|
||||
app.set("layout", "layout"); // verwendet views/layout.ejs
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
✅ LICENSE/TRIAL GATE (NEU!)
|
||||
- wenn keine Seriennummer: 30 Tage Trial
|
||||
- danach nur noch /serial-number erreichbar
|
||||
================================ */
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
// Setup muss immer erreichbar bleiben
|
||||
if (req.path.startsWith("/setup")) return next();
|
||||
|
||||
// Login muss erreichbar bleiben
|
||||
if (req.path === "/" || req.path.startsWith("/login")) return next();
|
||||
|
||||
// Seriennummer Seite muss immer erreichbar bleiben
|
||||
if (req.path.startsWith("/serial-number")) return next();
|
||||
|
||||
// Nicht eingeloggt -> auth regelt das
|
||||
if (!req.session?.user) return next();
|
||||
|
||||
// company_settings laden
|
||||
const [rows] = await db.promise().query(
|
||||
`SELECT id, serial_number, trial_started_at
|
||||
FROM company_settings
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`,
|
||||
);
|
||||
|
||||
const settings = rows?.[0];
|
||||
|
||||
// ✅ Lizenz vorhanden -> erlaubt
|
||||
if (settings?.serial_number) return next();
|
||||
|
||||
// ✅ wenn Trial noch nicht gestartet -> starten
|
||||
if (settings?.id && !settings?.trial_started_at) {
|
||||
await db
|
||||
.promise()
|
||||
.query(
|
||||
`UPDATE company_settings SET trial_started_at = NOW() WHERE id = ?`,
|
||||
[settings.id],
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
// Wenn settings fehlen -> durchlassen (damit Setup/Settings nicht kaputt gehen)
|
||||
if (!settings?.trial_started_at) return next();
|
||||
|
||||
// ✅ Trial prüfen
|
||||
const trialStart = new Date(settings.trial_started_at);
|
||||
const now = new Date();
|
||||
|
||||
const diffDays = Math.floor((now - trialStart) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < TRIAL_DAYS) return next();
|
||||
|
||||
// ❌ Trial abgelaufen -> alles blocken außer Seriennummer
|
||||
return res.redirect("/serial-number");
|
||||
} catch (err) {
|
||||
console.error("❌ LicenseGate Fehler:", err.message);
|
||||
return next(); // im Zweifel nicht blockieren
|
||||
}
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
SETUP ROUTES
|
||||
================================ */
|
||||
@ -181,7 +280,10 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
//Sprachen Route
|
||||
/* ===============================
|
||||
Sprachen Route
|
||||
================================ */
|
||||
|
||||
// ✅ i18n Middleware (Sprache pro Benutzer über Session)
|
||||
app.use((req, res, next) => {
|
||||
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)
|
||||
================================ */
|
||||
|
||||
@ -7,16 +7,27 @@ async function postLogin(req, res) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await loginUser(
|
||||
db,
|
||||
username,
|
||||
password,
|
||||
LOCK_TIME_MINUTES
|
||||
);
|
||||
const user = await loginUser(db, username, password, LOCK_TIME_MINUTES);
|
||||
|
||||
/* req.session.user = user;
|
||||
res.redirect("/dashboard"); */
|
||||
|
||||
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) {
|
||||
res.render("login", { error });
|
||||
}
|
||||
@ -28,5 +39,5 @@ function getLogin(req, res) {
|
||||
|
||||
module.exports = {
|
||||
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",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-mysql-session": "^3.0.3",
|
||||
"express-session": "^1.18.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
@ -3045,6 +3046,11 @@
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-mysql-session": "^3.0.3",
|
||||
"express-session": "^1.18.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
|
||||
@ -78,3 +78,102 @@
|
||||
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>
|
||||
|
||||
<!-- ✅ 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>
|
||||
|
||||
<!-- ✅ 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