espid2sensor: Passwort ändern + Admin-Passwort-Reset

- POST /api/changePassword: jeder eingeloggte User kann sein eigenes
  Passwort ändern (erfordert aktuelles Passwort), neuer Profil-Tab
- POST /api/resetPassword: Admin kann das Passwort eines Users per
  Email zuruecksetzen, neue Karte im bestehenden User-Tab
- Login-Seite: Hinweis fuer "Passwort vergessen" (Kontakt zum Admin,
  da keine Mail-Infrastruktur im Projekt existiert)
- Bugfix beim Umsetzen: express-session's MemoryStore serialisiert
  Sessions per JSON, wodurch die ObjectId aus req.session.userId zu
  einem String wird - vor der Mongo-Query per ObjectId(...) casten

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 10:07:52 +00:00
parent 47d27148ca
commit cd4d7ba25d
5 changed files with 130 additions and 2 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "espid2sensor", "name": "espid2sensor",
"version": "1.3.2", "version": "1.4.0",
"date": "2026-08-06", "date": "2026-08-06",
"type": "module", "type": "module",
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB", "description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
+65
View File
@@ -2,10 +2,12 @@
function showTab(tab) { function showTab(tab) {
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none'; document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none'; document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
document.getElementById('tabProfileContent').style.display = tab === 'profile' ? '' : 'none';
const tabUserContent = document.getElementById('tabUserContent'); const tabUserContent = document.getElementById('tabUserContent');
if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none'; if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none';
document.getElementById('tabInput').classList.toggle('active', tab === 'input'); document.getElementById('tabInput').classList.toggle('active', tab === 'input');
document.getElementById('tabList').classList.toggle('active', tab === 'list'); document.getElementById('tabList').classList.toggle('active', tab === 'list');
document.getElementById('tabProfile').classList.toggle('active', tab === 'profile');
const tabUser = document.getElementById('tabUser'); const tabUser = document.getElementById('tabUser');
if (tabUser) tabUser.classList.toggle('active', tab === 'user'); 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() { function updateSortArrows() {
+35
View File
@@ -92,4 +92,39 @@ export function registerApiRoutes(app, requireLogin) {
res.status(500).json({ error: 'Fehler beim Anlegen' }); 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' });
}
});
} }
+27
View File
@@ -11,6 +11,7 @@ html(lang="de")
div.tabs div.tabs
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
button.tab-btn#tabProfile(type="button" onclick="showTab('profile')") Profil
if isAdmin if isAdmin
button.tab-btn#tabUser(type="button" onclick="showTab('user')") User button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
@@ -67,6 +68,22 @@ html(lang="de")
th Aktionen th Aktionen
tbody 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) // User-Tab (nur für Admins)
if isAdmin if isAdmin
div#tabUserContent.tab-content(style="display:none") div#tabUserContent.tab-content(style="display:none")
@@ -85,6 +102,16 @@ html(lang="de")
button#userSaveBtn(type="button") Anlegen button#userSaveBtn(type="button") Anlegen
button#userCancelBtn(type="button") Abbrechen button#userCancelBtn(type="button") Abbrechen
div#userResult 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} #version Version: #{version} vom #{vdate}
script(type="module" src="/global.js") script(type="module" src="/global.js")
+1
View File
@@ -16,6 +16,7 @@ html(lang="de")
label(for="password") Passwort: label(for="password") Passwort:
input#password(type="password" name="password" required) input#password(type="password" name="password" required)
button(type="submit") Login button(type="submit") Login
p.hint Passwort vergessen? Bitte an einen Admin wenden.
#version Version: #{version} vom #{vdate} #version Version: #{version} vom #{vdate}
if error if error