espid2sensor: User-Liste mit Passwort-Reset und Löschen im User-Tab

Ersetzt das manuelle E-Mail-Eingabefeld für den Passwort-Reset durch
eine Liste aller angemeldeten User. Pro User stehen zwei Aktionen zur
Verfügung: Passwort zurücksetzen (nutzt die bestehende
/api/resetPassword-Route) und Löschen (neue DELETE /api/users/:id-Route,
admin-only, verhindert Löschen des eigenen Accounts). Neue GET
/api/users-Route liefert Email/Rolle aller User (admin-only).
This commit is contained in:
2026-08-07 08:03:27 +00:00
parent 278f91ed3c
commit 19c6facf71
3 changed files with 104 additions and 38 deletions
+70 -29
View File
@@ -34,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
userResult.textContent = 'User erfolgreich angelegt!'; userResult.textContent = 'User erfolgreich angelegt!';
if (window.loadUsers) window.loadUsers();
} else { } else {
userResult.textContent = data.error || 'Fehler beim Anlegen.'; userResult.textContent = data.error || 'Fehler beim Anlegen.';
} }
@@ -77,34 +78,6 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
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() {
@@ -467,6 +440,73 @@ function showModal(message, showCancelButton, callback) {
}); });
} }
async function loadUsers() {
const usersTableBody = document.querySelector('#usersTable tbody');
const usersResult = document.getElementById('usersResult');
if (!usersTableBody) return;
try {
const res = await fetch('/api/users');
const users = await res.json();
usersTableBody.innerHTML = '';
users.forEach(u => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${u.email}</td>
<td>${u.role}</td>
<td>
<div class="twobuttons">
<button data-id="${u._id}" data-email="${u.email}" class="editBtn resetUserBtn" title="Passwort zurücksetzen">🔑</button>
<button data-id="${u._id}" class="deleteBtn deleteUserBtn" title="Löschen">🗑️</button>
</div>
</td>
`;
usersTableBody.appendChild(tr);
});
document.querySelectorAll('.resetUserBtn').forEach(btn => {
btn.addEventListener('click', async () => {
const email = btn.getAttribute('data-email');
const newPassword = prompt(`Neues Passwort für ${email}:`);
if (!newPassword) 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();
usersResult.textContent = data.success ? `Passwort für ${email} zurückgesetzt.` : (data.error || 'Fehler beim Zurücksetzen.');
} catch (err) {
usersResult.textContent = 'Serverfehler.';
}
});
});
document.querySelectorAll('.deleteUserBtn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-id');
showModal('User wirklich löschen?', true, async (confirmed) => {
if (!confirmed) return;
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
usersResult.textContent = 'User gelöscht.';
await loadUsers();
} else {
usersResult.textContent = data.error || 'Fehler beim Löschen.';
}
} catch (err) {
usersResult.textContent = 'Serverfehler.';
}
});
});
});
} catch (err) {
console.error(err);
}
}
saveBtn.addEventListener('click', saveEntry); saveBtn.addEventListener('click', saveEntry);
refreshBtn.addEventListener('click', loadEntries); refreshBtn.addEventListener('click', loadEntries);
cancelBtn.addEventListener('click', () => clearForm(true)); cancelBtn.addEventListener('click', () => clearForm(true));
@@ -475,9 +515,10 @@ function showModal(message, showCancelButton, callback) {
tabInput.addEventListener('click', () => showTab('input')) tabInput.addEventListener('click', () => showTab('input'))
tabList.addEventListener('click', () => showTab('list')) tabList.addEventListener('click', () => showTab('list'))
const tabUser = document.getElementById('tabUser'); const tabUser = document.getElementById('tabUser');
if (tabUser) tabUser.addEventListener('click', () => showTab('user')) if (tabUser) tabUser.addEventListener('click', () => { showTab('user'); loadUsers(); })
loadEntries(); loadEntries();
window.loadUsers = loadUsers;
}); });
window.showTab = showTab; window.showTab = showTab;
+25
View File
@@ -127,4 +127,29 @@ export function registerApiRoutes(app, requireLogin) {
res.status(500).json({ error: 'Fehler beim Zurücksetzen' }); res.status(500).json({ error: 'Fehler beim Zurücksetzen' });
} }
}); });
app.get('/api/users', requireLogin, async (req, res) => {
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
try {
const users = await usersCollection.find({}, { projection: { email: 1, role: 1 } }).sort({ email: 1 }).toArray();
res.json(users);
} catch (err) {
res.status(500).json({ error: 'Fehler beim Laden der User' });
}
});
app.delete('/api/users/:id', requireLogin, async (req, res) => {
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
const { id } = req.params;
if (id === String(req.session.userId)) {
return res.status(400).json({ error: 'Der eigene Account kann nicht gelöscht werden' });
}
try {
const result = await usersCollection.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 0) return res.status(404).json({ error: 'User nicht gefunden' });
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Fehler beim Löschen' });
}
});
} }
+9 -9
View File
@@ -103,15 +103,15 @@ html(lang="de")
button#userCancelBtn(type="button") Abbrechen button#userCancelBtn(type="button") Abbrechen
div#userResult div#userResult
div.card div.card
h2 Passwort eines Users zurücksetzen h2 Angemeldete User
form#resetPasswordForm table#usersTable
label(for="resetEmail") Email: thead
input#resetEmail(type="email" required) tr
label(for="resetNewPassword") Neues Passwort: th Email
input#resetNewPassword(type="password" required) th Rolle
.twobuttons th Aktionen
button#resetPasswordBtn(type="button") Zurücksetzen tbody
div#resetPasswordResult div#usersResult
#version Version: #{version} vom #{vdate} #version Version: #{version} vom #{vdate}
script(type="module" src="/global.js") script(type="module" src="/global.js")