diff --git a/db/mongo.js b/db/mongo.js
index 8cbfcd8..b10000c 100644
--- a/db/mongo.js
+++ b/db/mongo.js
@@ -21,7 +21,7 @@ export async function initMongo() {
const client = new MongoClient(MONGO_URL);
await client.connect();
db = client.db(DB_NAME);
- usersCollection = db.collection('users');
+ usersCollection = db.collection('user');
prop_fluxCollection = db.collection('prop_flux');
propertiesCollection = db.collection('properties')
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
diff --git a/hashpasswd b/hashpasswd
new file mode 100644
index 0000000..295d6bc
--- /dev/null
+++ b/hashpasswd
@@ -0,0 +1,3 @@
+import bcrypt from 'bcrypt';
+const hashedPassword = await bcrypt.hash('Tux4esp', 10);
+console.log(hashedPassword)
\ No newline at end of file
diff --git a/package.json b/package.json
index 28bf42d..d3fd151 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "espid2sensor",
- "version": "1.1.0",
- "date": "2025-09-01 17:00 UTC",
+ "version": "1.1.1",
+ "date": "2025-09-01 17:10 UTC",
"type": "module",
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
"main": "server.js",
diff --git a/public/global.js b/public/global.js
index fa2974a..36ff1b8 100644
--- a/public/global.js
+++ b/public/global.js
@@ -1,3 +1,46 @@
+ // Tab-Wechsel Funktion aus index.pug
+function showTab(tab) {
+ document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
+ document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
+ const tabUserContent = document.getElementById('tabUserContent');
+ if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none';
+ document.getElementById('tabInput').classList.toggle('active', tab === 'input');
+ document.getElementById('tabList').classList.toggle('active', tab === 'list');
+ const tabUser = document.getElementById('tabUser');
+ if (tabUser) tabUser.classList.toggle('active', tab === 'user');
+}
+
+// User-Tab Handling (nur für Admins)
+document.addEventListener('DOMContentLoaded', () => {
+ const userSaveBtn = document.getElementById('userSaveBtn');
+ if (userSaveBtn) {
+ userSaveBtn.addEventListener('click', async () => {
+ const username = document.getElementById('username').value.trim();
+ const password = document.getElementById('password').value.trim();
+ const role = document.getElementById('role').value;
+ const userResult = document.getElementById('userResult');
+ if (!username || !password) {
+ userResult.textContent = 'Benutzername und Passwort erforderlich.';
+ return;
+ }
+ try {
+ const res = await fetch('/api/createUser', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password, role })
+ });
+ const data = await res.json();
+ if (data.success) {
+ userResult.textContent = 'User erfolgreich angelegt!';
+ } else {
+ userResult.textContent = data.error || 'Fehler beim Anlegen.';
+ }
+ } catch (err) {
+ userResult.textContent = 'Serverfehler.';
+ }
+ });
+ }
+});
function updateSortArrows() {
const arrows = {
@@ -16,13 +59,6 @@ function updateSortArrows() {
});
}
- // Tab-Wechsel Funktion aus index.pug
-function showTab(tab) {
- document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
- document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
- document.getElementById('tabInput').classList.toggle('active', tab === 'input');
- document.getElementById('tabList').classList.toggle('active', tab === 'list');
-}
document.addEventListener('DOMContentLoaded', () => {
const saveBtn = document.getElementById('saveBtn');
@@ -39,7 +75,6 @@ document.addEventListener('DOMContentLoaded', () => {
const tabInput = document.getElementById('tabInput');
const tabList = document.getElementById('tabList');
- let editId = null;
// Modal für Fehleranzeige
function showModal(message, showCancelButton, callback) {
@@ -206,12 +241,17 @@ function showModal(message, showCancelButton, callback) {
nameInput.value = '';
descriptionInput.value = '';
addressInput.value = '';
- editId = null;
if (mitButton) {
saveBtn.textContent = 'Speichern';
}
}
+ const clearUserForm = () => {
+ document.getElementById('username').value = ''
+ document.getElementById('password').value = ''
+ document.getElementById('role').value = 'user'
+ }
+
// Globale Sortier-Variable
window.currentSort = window.currentSort || { key: null, asc: true };
@@ -239,9 +279,9 @@ function showModal(message, showCancelButton, callback) {
${item.chip.description || ''} |
${date} |
- |
`;
@@ -320,7 +360,8 @@ function showModal(message, showCancelButton, callback) {
nameInput.value = item.chip.name || '';
descriptionInput.value = item.chip.description || '';
addressInput.value = '';
- editId = id;
+ saveBtn.textContent = 'Aktualisieren';
+ showTab('input')
try {
const rt = await fetch(`api/holAdresse/${item._id}`)
const data = await rt.json();
@@ -331,8 +372,6 @@ function showModal(message, showCancelButton, callback) {
} catch (e) {
console.log("Fehler beim Adresse holen", e)
}
- saveBtn.textContent = 'Aktualisieren';
- showTab('input')
}
});
});
@@ -366,8 +405,14 @@ function showModal(message, showCancelButton, callback) {
saveBtn.addEventListener('click', saveEntry);
refreshBtn.addEventListener('click', loadEntries);
cancelBtn.addEventListener('click', () => clearForm(true));
-tabInput.addEventListener('click', () => showTab('input'))
-tabList.addEventListener('click', () => showTab('list'))
+ userCancelBtn.addEventListener('click', () => clearUserForm(true));
+
+ tabInput.addEventListener('click', () => showTab('input'))
+ tabList.addEventListener('click', () => showTab('list'))
+ const tabUser = document.getElementById('tabUser');
+ if (tabUser) tabUser.addEventListener('click', () => showTab('user'))
loadEntries();
-});
\ No newline at end of file
+});
+
+window.showTab = showTab;
\ No newline at end of file
diff --git a/public/register.js b/public/register.js
deleted file mode 100644
index 6d4a2fd..0000000
--- a/public/register.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// public/register.js
-
-document.addEventListener('DOMContentLoaded', () => {
- const emailInput = document.getElementById('email');
- const emailStatus = document.getElementById('emailStatus');
-
- let debounceTimeout;
-
- emailInput.addEventListener('input', () => {
- clearTimeout(debounceTimeout);
- const email = emailInput.value.trim();
-
- if (!email) {
- emailStatus.textContent = '';
- return;
- }
-
- // 300ms warten, um zu vermeiden, dass bei jedem Tastendruck eine Anfrage rausgeht
- debounceTimeout = setTimeout(async () => {
- try {
- const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
- const data = await res.json();
- if (data.exists) {
- emailStatus.textContent = '❌ Diese E-Mail ist schon vergeben';
- emailStatus.style.color = 'red';
- } else {
- emailStatus.textContent = '✅ E-Mail ist frei';
- emailStatus.style.color = 'green';
- }
- } catch (err) {
- console.error(err);
- emailStatus.textContent = 'Fehler bei der Prüfung';
- emailStatus.style.color = 'orange';
- }
- }, 300);
- });
-});
\ No newline at end of file
diff --git a/public/styles.css b/public/styles.css
index 40953d1..fc9273c 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -21,6 +21,11 @@
font-weight: bold;
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
}
+
+#tabUser {
+ margin-left: 50px;
+}
+
/* Modal Fehlerfenster */
.custom-modal-popup {
position: fixed;
@@ -95,6 +100,7 @@ input, button {
table {
width: 100%;
border-collapse: collapse;
+ margin-top: 30px;
}
th, td {
@@ -105,11 +111,11 @@ th, td {
/* Spaltenbreiten über colgroup steuern */
col.col-sensornumber { width: 7em; }
-col.col-espid {width: 6em}
+col.col-espid {width: 9em}
col.col-bezeichnung { width: 8em; }
-col.col-beschreibung{ width: 12em; }
+col.col-beschreibung{ width: 15em; }
col.col-date { width: 10em; }
-col.col-aktionen { width: 18em; }
+col.col-aktionen { width: 2em; }
.controls input#page,
@@ -160,12 +166,33 @@ button:hover {
background: #0056b3;
}
-.twobuttons {
- display:flex;
- width: 100%;
- justify-content: space-between;
+.editBtn, .deleteBtn {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ margin: 0 2px;
}
+.editBtn:hover {
+ background: rgba(0, 123, 255, 0.1);
+ transform: scale(1.1);
+}
+
+.deleteBtn:hover {
+ background: rgba(220, 53, 69, 0.1);
+ transform: scale(1.1);
+}
+
+.twobuttons {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ gap: 5px;
+}
p.error {
color: red;
font-weight: bold;
@@ -199,4 +226,19 @@ p.error {
#gzahl {
margin-left: 30px;
+}
+
+#role {
+ font-size: 12pt;
+ padding: 5px 0 5px 3px;
+ margin-bottom: 20px;
+}
+
+#version {
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+ font-size: 70%;
+ color: #007bff;
+ margin-top: 15px;
}
\ No newline at end of file
diff --git a/routes/api.js b/routes/api.js
index b7765e6..5d4fb8a 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -9,7 +9,7 @@ export function registerApiRoutes(app, requireLogin) {
const email = (req.query.email || '').toLowerCase().trim();
if (!email) return res.json({ exists: false });
try {
- const existingUser = await usersCollection.findOne({ email });
+ const existingUser = await usersCollection.findOne({ email:`${email}` });
res.json({ exists: !!existingUser });
} catch (err) {
console.error(err);
@@ -79,4 +79,17 @@ export function registerApiRoutes(app, requireLogin) {
await prop_fluxCollection.deleteOne({ _id: parseInt(req.params.id) });
res.json({ success: true });
});
+
+ app.post('/api/createUser', requireLogin, async (req, res) => {
+ if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
+ const { username, password, role } = req.body;
+ if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' });
+ try {
+ const hash = await bcrypt.hash(password, 10);
+ await usersCollection.insertOne({ email: username.toLowerCase(), passwordHash: hash, role: role || 'user' });
+ res.json({ success: true });
+ } catch (err) {
+ res.status(500).json({ error: 'Fehler beim Anlegen' });
+ }
+ });
}
diff --git a/routes/auth.js b/routes/auth.js
index c41e842..3bbaed2 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -3,6 +3,7 @@ import { getCollections } from '../db/mongo.js';
export function registerAuthRoutes(app) {
const { usersCollection } = getCollections();
+ const errText = 'Falsche Email oder falsches Passwort.'
app.get('/register', (req, res) => res.render('register', { error: null }));
@@ -21,11 +22,12 @@ export function registerAuthRoutes(app) {
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await usersCollection.findOne({ email: email.toLowerCase() });
- if (!user) return res.render('login', { error: 'Falsche Email oder Passwort.' });
+ if (!user) return res.render('login', { error: errText });
const match = await bcrypt.compare(password, user.passwordHash);
- if (!match) return res.render('login', { error: 'Falsche Email oder Passwort.' });
- req.session.userId = user._id;
- res.redirect('/');
+ if (!match) return res.render('login', { error: errText });
+ req.session.userId = user._id;
+ req.session.isAdmin = user.role === 'admin';
+ res.redirect('/');
});
app.get('/logout', (req, res) => {
diff --git a/server.js b/server.js
index 50887ee..d4efecf 100644
--- a/server.js
+++ b/server.js
@@ -3,6 +3,7 @@ import session from 'express-session';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
+import pkg from './package.json' with { type: "json" }
dotenv.config();
import { initMongo } from './db/mongo.js';
@@ -36,9 +37,10 @@ await initMongo();
// Login-Middleware
function requireLogin(req, res, next) {
- // if (req.session.userId) return next();
- // res.redirect('/login');
- return next();
+ if (req.session.userId) {
+ return next();
+ }
+ res.redirect('/login');
}
// Routen registrieren
@@ -47,6 +49,11 @@ registerApiRoutes(app, requireLogin);
registerAddressRoute(app, requireLogin);
// Hauptseite
-app.get('/', requireLogin, (req, res) => res.render('index'));
+app.get('/', requireLogin, (req, res) => {
+ const version = pkg.version
+ const vdate = pkg.date
+ const isAdmin = req.session && req.session.isAdmin;
+ res.render('index', { isAdmin, version, vdate });
+});
app.listen(PORT, () => console.log(`Server läuft auf http://localhost:${PORT}`));
diff --git a/views/index.pug b/views/index.pug
index 8182ec3..90e66de 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -9,11 +9,13 @@ html(lang="de")
h1 ESP-ID → Sensornummer
// Tab Navigation
div.tabs
- button.tab-btn#tabInput.active(type="button") Eingabe
- button.tab-btn#tabList(type="button") Liste
+ button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
+ button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
+ if isAdmin
+ button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
- // Eingabe-Tab
- div#tabInputContent.tab-content
+ // Eingabe-Tab
+ div#tabInputContent.tab-content
div.card
form#entryForm
label(for="sensorNumber") Sensornummer:
@@ -35,9 +37,10 @@ html(lang="de")
button#saveBtn(type="button") Speichern
button#cancelBtn(type="button") Abbrechen
div#result
+ #version Version: #{version} vom #{vdate}
- // Listen-Tab
- div#tabListContent.tab-content(style="display:none")
+ // Listen-Tab
+ div#tabListContent.tab-content(style="display:none")
div.controls
button#refreshBtn Aktualisieren
| Seite:
@@ -63,4 +66,25 @@ html(lang="de")
th(id="thDate" data-sort="date" style="cursor:pointer") Datum ↑
th Aktionen
tbody
+
+ // User-Tab (nur für Admins)
+ if isAdmin
+ div#tabUserContent.tab-content(style="display:none")
+ div.card
+ h2 Neuen User anlegen
+ form#userForm
+ label(for="username") Benutzername:
+ input#username(type="text" required)
+ label(for="password") Passwort:
+ input#password(type="password" required)
+ label(for="role") Rolle:
+ select#role
+ option(value="user") User
+ option(value="admin") Admin
+ .twobuttons
+ button#userSaveBtn(type="button") Anlegen
+ button#userCancelBtn(type="button") Abbrechen
+ div#userResult
+ #version Version: #{version} vom #{vdate}
+
script(type="module" src="/global.js")
\ No newline at end of file
diff --git a/views/login.pug b/views/login.pug
index af2e8f2..e579a2c 100644
--- a/views/login.pug
+++ b/views/login.pug
@@ -6,14 +6,16 @@ html(lang="de")
title Login
link(rel="stylesheet", href="/styles.css")
body
- h1 Login
- form(method="POST" action="/login")
- label(for="email") E-Mail:
- input#email(type="email" name="email" required)
- span#emailStatus
- label(for="password") Passwort:
- input#password(type="password" name="password" required)
- button(type="submit") Login
- if error
- p.error= error
+ h1 ESP-ID → Sensornummer
+ div.card
+ h2 Login
+ form(method="POST" action="/login")
+ label(for="email") E-Mail:
+ input#email(type="email" name="email" required)
+ span#emailStatus
+ label(for="password") Passwort:
+ input#password(type="password" name="password" required)
+ button(type="submit") Login
+ if error
+ p.error= error
script(type="module" src="/login.js")
\ No newline at end of file
diff --git a/views/register.pug b/views/register.pug
deleted file mode 100644
index 5616586..0000000
--- a/views/register.pug
+++ /dev/null
@@ -1,19 +0,0 @@
-doctype html
-html(lang="de")
- head
- meta(charset="utf-8")
- meta(name="viewport", content="width=device-width, initial-scale=1")
- title Registrieren
- link(rel="stylesheet", href="/styles.css")
- body
- h1 Registrierung
- form(method="POST" action="/register")
- label(for="email") E-Mail:
- input#email(type="email" name="email" required)
- span#emailStatus
- label(for="password") Passwort:
- input#password(type="password" name="password" required)
- button(type="submit") Registrieren
- if error
- p.error= error
- script(type="module" src="/register.js")
\ No newline at end of file