diff --git a/espid2sensor/package.json b/espid2sensor/package.json index bff4c5a..0879969 100644 --- a/espid2sensor/package.json +++ b/espid2sensor/package.json @@ -1,6 +1,6 @@ { "name": "espid2sensor", - "version": "1.3.2", + "version": "1.4.0", "date": "2026-08-06", "type": "module", "description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB", diff --git a/espid2sensor/public/global.js b/espid2sensor/public/global.js index 5f8d91a..096abf0 100644 --- a/espid2sensor/public/global.js +++ b/espid2sensor/public/global.js @@ -2,10 +2,12 @@ function showTab(tab) { document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none'; document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none'; + document.getElementById('tabProfileContent').style.display = tab === 'profile' ? '' : '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'); + document.getElementById('tabProfile').classList.toggle('active', tab === 'profile'); const tabUser = document.getElementById('tabUser'); if (tabUser) tabUser.classList.toggle('active', tab === 'user'); } @@ -40,6 +42,69 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + + const profileSaveBtn = document.getElementById('profileSaveBtn'); + if (profileSaveBtn) { + profileSaveBtn.addEventListener('click', async () => { + const currentPassword = document.getElementById('currentPassword').value; + const newPassword = document.getElementById('newPassword').value; + const newPasswordRepeat = document.getElementById('newPasswordRepeat').value; + const profileResult = document.getElementById('profileResult'); + if (!currentPassword || !newPassword) { + profileResult.textContent = 'Aktuelles und neues Passwort erforderlich.'; + return; + } + if (newPassword !== newPasswordRepeat) { + profileResult.textContent = 'Die Wiederholung stimmt nicht mit dem neuen Passwort überein.'; + return; + } + try { + const res = await fetch('/api/changePassword', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }) + }); + const data = await res.json(); + if (data.success) { + profileResult.textContent = 'Passwort erfolgreich geändert!'; + document.getElementById('profileForm').reset(); + } else { + profileResult.textContent = data.error || 'Fehler beim Ändern.'; + } + } catch (err) { + profileResult.textContent = 'Serverfehler.'; + } + }); + } + + const resetPasswordBtn = document.getElementById('resetPasswordBtn'); + if (resetPasswordBtn) { + resetPasswordBtn.addEventListener('click', async () => { + const email = document.getElementById('resetEmail').value.trim(); + const newPassword = document.getElementById('resetNewPassword').value; + const resetPasswordResult = document.getElementById('resetPasswordResult'); + if (!email || !newPassword) { + resetPasswordResult.textContent = 'Email und neues Passwort erforderlich.'; + return; + } + try { + const res = await fetch('/api/resetPassword', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, newPassword }) + }); + const data = await res.json(); + if (data.success) { + resetPasswordResult.textContent = 'Passwort erfolgreich zurückgesetzt!'; + document.getElementById('resetPasswordForm').reset(); + } else { + resetPasswordResult.textContent = data.error || 'Fehler beim Zurücksetzen.'; + } + } catch (err) { + resetPasswordResult.textContent = 'Serverfehler.'; + } + }); + } }); function updateSortArrows() { diff --git a/espid2sensor/routes/api.js b/espid2sensor/routes/api.js index 1e28a46..2f2af8c 100644 --- a/espid2sensor/routes/api.js +++ b/espid2sensor/routes/api.js @@ -92,4 +92,39 @@ export function registerApiRoutes(app, requireLogin) { res.status(500).json({ error: 'Fehler beim Anlegen' }); } }); + + app.post('/api/changePassword', requireLogin, async (req, res) => { + const { currentPassword, newPassword } = req.body; + if (!currentPassword || !newPassword) { + return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich' }); + } + try { + const user = await usersCollection.findOne({ _id: new ObjectId(req.session.userId) }); + if (!user) return res.status(404).json({ error: 'User nicht gefunden' }); + const match = await bcrypt.compare(currentPassword, user.passwordHash); + if (!match) return res.status(403).json({ error: 'Aktuelles Passwort ist falsch' }); + const hash = await bcrypt.hash(newPassword, 10); + await usersCollection.updateOne({ _id: user._id }, { $set: { passwordHash: hash } }); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: 'Fehler beim Ändern des Passworts' }); + } + }); + + app.post('/api/resetPassword', requireLogin, async (req, res) => { + if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' }); + const { email, newPassword } = req.body; + if (!email || !newPassword) return res.status(400).json({ error: 'Email und neues Passwort erforderlich' }); + try { + const hash = await bcrypt.hash(newPassword, 10); + const result = await usersCollection.updateOne( + { email: email.toLowerCase() }, + { $set: { passwordHash: hash } } + ); + if (result.matchedCount === 0) return res.status(404).json({ error: 'User nicht gefunden' }); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: 'Fehler beim Zurücksetzen' }); + } + }); } diff --git a/espid2sensor/views/index.pug b/espid2sensor/views/index.pug index 90e66de..4c76f61 100644 --- a/espid2sensor/views/index.pug +++ b/espid2sensor/views/index.pug @@ -11,6 +11,7 @@ html(lang="de") div.tabs button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste + button.tab-btn#tabProfile(type="button" onclick="showTab('profile')") Profil if isAdmin button.tab-btn#tabUser(type="button" onclick="showTab('user')") User @@ -67,6 +68,22 @@ html(lang="de") th Aktionen tbody + // Profil-Tab (für alle eingeloggten User) + div#tabProfileContent.tab-content(style="display:none") + div.card + h2 Passwort ändern + form#profileForm + label(for="currentPassword") Aktuelles Passwort: + input#currentPassword(type="password" required) + label(for="newPassword") Neues Passwort: + input#newPassword(type="password" required) + label(for="newPasswordRepeat") Neues Passwort (Wiederholung): + input#newPasswordRepeat(type="password" required) + .twobuttons + button#profileSaveBtn(type="button") Ändern + div#profileResult + #version Version: #{version} vom #{vdate} + // User-Tab (nur für Admins) if isAdmin div#tabUserContent.tab-content(style="display:none") @@ -81,10 +98,20 @@ html(lang="de") select#role option(value="user") User option(value="admin") Admin - .twobuttons + .twobuttons button#userSaveBtn(type="button") Anlegen button#userCancelBtn(type="button") Abbrechen div#userResult + div.card + h2 Passwort eines Users zurücksetzen + form#resetPasswordForm + label(for="resetEmail") Email: + input#resetEmail(type="email" required) + label(for="resetNewPassword") Neues Passwort: + input#resetNewPassword(type="password" required) + .twobuttons + button#resetPasswordBtn(type="button") Zurücksetzen + div#resetPasswordResult #version Version: #{version} vom #{vdate} script(type="module" src="/global.js") \ No newline at end of file diff --git a/espid2sensor/views/login.pug b/espid2sensor/views/login.pug index 40626a9..3e29149 100644 --- a/espid2sensor/views/login.pug +++ b/espid2sensor/views/login.pug @@ -16,6 +16,7 @@ html(lang="de") label(for="password") Passwort: input#password(type="password" name="password" required) button(type="submit") Login + p.hint Passwort vergessen? Bitte an einen Admin wenden. #version Version: #{version} vom #{vdate} if error