Merge branch 'espid2sensor-passwort'
espid2sensor: Selfservice-Passwortänderung fuer alle User und Admin-gesteuerter Passwort-Reset (kein Mail-Versand noetig). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user