From 5ccd37b931a14d106b03b29f18ff6f0956e5a063 Mon Sep 17 00:00:00 2001 From: rxf Date: Tue, 2 Sep 2025 18:49:50 +0200 Subject: [PATCH] =?UTF-8?q?Nun=20mit=20login=20und=20Einrichten=20zus?= =?UTF-8?q?=C3=A4tzlicher=20User?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/mongo.js | 2 +- hashpasswd | 3 ++ package.json | 4 +-- public/global.js | 81 +++++++++++++++++++++++++++++++++++----------- public/register.js | 37 --------------------- public/styles.css | 56 ++++++++++++++++++++++++++++---- routes/api.js | 15 ++++++++- routes/auth.js | 10 +++--- server.js | 15 ++++++--- views/index.pug | 36 +++++++++++++++++---- views/login.pug | 22 +++++++------ views/register.pug | 19 ----------- 12 files changed, 191 insertions(+), 109 deletions(-) create mode 100644 hashpasswd delete mode 100644 public/register.js delete mode 100644 views/register.pug 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