feat: v0.5.4 - Admin-Benutzerverwaltung mit CRUD, Passwort-Reset und Info-Mail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-16 20:45:30 +01:00
parent ee361acf33
commit b1fa21504b
5 changed files with 472 additions and 23 deletions

View file

@ -1,5 +1,18 @@
# Changelog # Changelog
## [0.5.4] - 2026-02-16
### Added
- Admin: Benutzer direkt anlegen mit Passwort und Rollenwahl
- Admin: Benutzer bearbeiten (Name, E-Mail, Rolle) per Modal
- Admin: Passwort zuruecksetzen mit optionalem E-Mail-Versand
- Admin: Info-Mail mit Zugangsdaten an Benutzer senden
- Passwort-Generator (crypto.getRandomValues) in Admin-UI
- Verifikationslink wird immer im Log ausgegeben (nicht nur ohne SMTP)
### Changed
- Admin-Seite komplett ueberarbeitet mit Modals fuer alle Aktionen
- E-Mail-Funktionen um `send_user_info_email` erweitert
## [0.5.3] - 2026-02-16 ## [0.5.3] - 2026-02-16
### Changed ### Changed
- Zugangsdaten (AUTH_SECRET_KEY, SMTP-*) aus config.yaml in .env-Datei ausgelagert - Zugangsdaten (AUTH_SECRET_KEY, SMTP-*) aus config.yaml in .env-Datei ausgelagert

View file

@ -1,4 +1,4 @@
version: "0.5.3" version: "0.5.4"
bot: bot:
name: "MeshDD-Bot" name: "MeshDD-Bot"

View file

@ -126,6 +126,7 @@ async def _send_email(db, recipient: str, subject: str, html_body: str):
async def send_verification_email(db, email: str, token: str): async def send_verification_email(db, email: str, token: str):
app_url = config.env("SMTP_APP_URL", "http://localhost:8080") app_url = config.env("SMTP_APP_URL", "http://localhost:8080")
verify_url = f"{app_url}/auth/verify?token={token}" verify_url = f"{app_url}/auth/verify?token={token}"
logger.info("Verification link for %s: %s", email, verify_url)
subject = "MeshDD-Bot - E-Mail verifizieren" subject = "MeshDD-Bot - E-Mail verifizieren"
html_body = f"""<html><body> html_body = f"""<html><body>
<h2>MeshDD-Bot Registrierung</h2> <h2>MeshDD-Bot Registrierung</h2>
@ -133,16 +134,14 @@ async def send_verification_email(db, email: str, token: str):
<p><a href="{verify_url}">{verify_url}</a></p> <p><a href="{verify_url}">{verify_url}</a></p>
<p>Der Link ist 24 Stunden gueltig.</p> <p>Der Link ist 24 Stunden gueltig.</p>
</body></html>""" </body></html>"""
if not config.env("SMTP_HOST"):
logger.info("SMTP not configured - verification link: %s", verify_url)
await _send_email(db, email, subject, html_body) await _send_email(db, email, subject, html_body)
return verify_url
async def send_reset_email(db, email: str, token: str): async def send_reset_email(db, email: str, token: str):
app_url = config.env("SMTP_APP_URL", "http://localhost:8080") app_url = config.env("SMTP_APP_URL", "http://localhost:8080")
reset_url = f"{app_url}/auth/reset-password?token={token}" reset_url = f"{app_url}/auth/reset-password?token={token}"
logger.info("Reset link for %s: %s", email, reset_url)
subject = "MeshDD-Bot - Passwort zuruecksetzen" subject = "MeshDD-Bot - Passwort zuruecksetzen"
html_body = f"""<html><body> html_body = f"""<html><body>
<h2>Passwort zuruecksetzen</h2> <h2>Passwort zuruecksetzen</h2>
@ -150,10 +149,24 @@ async def send_reset_email(db, email: str, token: str):
<p><a href="{reset_url}">{reset_url}</a></p> <p><a href="{reset_url}">{reset_url}</a></p>
<p>Der Link ist 24 Stunden gueltig.</p> <p>Der Link ist 24 Stunden gueltig.</p>
</body></html>""" </body></html>"""
await _send_email(db, email, subject, html_body)
return reset_url
if not config.env("SMTP_HOST"):
logger.info("SMTP not configured - reset link: %s", reset_url)
async def send_user_info_email(db, email: str, name: str, password: str = None):
app_url = config.env("SMTP_APP_URL", "http://localhost:8080")
login_url = f"{app_url}/login"
subject = "MeshDD-Bot - Deine Zugangsdaten"
pw_line = f"<p><strong>Passwort:</strong> {password}</p>" if password else "<p>Dein Passwort wurde nicht geaendert.</p>"
html_body = f"""<html><body>
<h2>MeshDD-Bot Zugangsdaten</h2>
<p>Hallo {name},</p>
<p>hier sind deine Zugangsdaten fuer MeshDD-Bot:</p>
<p><strong>E-Mail:</strong> {email}</p>
{pw_line}
<p>Anmelden unter: <a href="{login_url}">{login_url}</a></p>
<p>Bitte aendere dein Passwort nach dem ersten Login.</p>
</body></html>"""
await _send_email(db, email, subject, html_body) await _send_email(db, email, subject, html_body)
@ -338,6 +351,90 @@ def setup_auth_routes(app: web.Application, db):
users = await db.get_all_users() users = await db.get_all_users()
return web.json_response(users) return web.json_response(users)
async def admin_user_create(request: web.Request) -> web.Response:
require_admin_api(request)
data = await request.json()
name = data.get("name", "").strip()
email = data.get("email", "").strip().lower()
password = data.get("password", "")
role = data.get("role", "user")
if not name or not email or not password:
return web.json_response({"error": "Name, E-Mail und Passwort erforderlich"}, status=400)
if len(password) < 8:
return web.json_response({"error": "Passwort muss mindestens 8 Zeichen lang sein"}, status=400)
if role not in ("user", "admin"):
return web.json_response({"error": "Ungueltige Rolle"}, status=400)
existing = await db.get_user_by_email(email)
if existing:
return web.json_response({"error": "E-Mail bereits registriert"}, status=409)
hashed = hash_password(password)
await db.create_user(email=email, password=hashed, name=name, role=role, is_verified=1)
# Send info email if requested
if data.get("send_email"):
await send_user_info_email(db, email, name, password)
users = await db.get_all_users()
return web.json_response(users)
async def admin_user_update(request: web.Request) -> web.Response:
require_admin_api(request)
user_id = int(request.match_info["id"])
data = await request.json()
updates = {}
if "name" in data and data["name"].strip():
updates["name"] = data["name"].strip()
if "email" in data and data["email"].strip():
new_email = data["email"].strip().lower()
existing = await db.get_user_by_email(new_email)
if existing and existing["id"] != user_id:
return web.json_response({"error": "E-Mail bereits vergeben"}, status=409)
updates["email"] = new_email
if "role" in data and data["role"] in ("user", "admin"):
updates["role"] = data["role"]
if not updates:
return web.json_response({"error": "Keine Aenderungen"}, status=400)
await db.update_user(user_id, **updates)
users = await db.get_all_users()
return web.json_response(users)
async def admin_user_reset_password(request: web.Request) -> web.Response:
require_admin_api(request)
user_id = int(request.match_info["id"])
data = await request.json()
password = data.get("password", "")
if not password or len(password) < 8:
return web.json_response({"error": "Passwort muss mindestens 8 Zeichen lang sein"}, status=400)
hashed = hash_password(password)
await db.update_user(user_id, password=hashed)
# Send info email if requested
if data.get("send_email"):
user = await db.get_user_by_id(user_id)
if user:
await send_user_info_email(db, user["email"], user["name"], password)
users = await db.get_all_users()
return web.json_response(users)
async def admin_user_send_info(request: web.Request) -> web.Response:
require_admin_api(request)
user_id = int(request.match_info["id"])
user = await db.get_user_by_id(user_id)
if not user:
return web.json_response({"error": "Benutzer nicht gefunden"}, status=404)
await send_user_info_email(db, user["email"], user["name"])
return web.json_response({"ok": True, "message": f"Info-Mail an {user['email']} gesendet"})
async def admin_user_delete(request: web.Request) -> web.Response: async def admin_user_delete(request: web.Request) -> web.Response:
require_admin_api(request) require_admin_api(request)
user_id = int(request.match_info["id"]) user_id = int(request.match_info["id"])
@ -360,6 +457,10 @@ def setup_auth_routes(app: web.Application, db):
app.router.add_get("/api/auth/me", me_get) app.router.add_get("/api/auth/me", me_get)
app.router.add_get("/api/admin/users", admin_users_get) app.router.add_get("/api/admin/users", admin_users_get)
app.router.add_post("/api/admin/users", admin_user_create)
app.router.add_put("/api/admin/users/{id}", admin_user_update)
app.router.add_post("/api/admin/users/{id}/role", admin_user_role) app.router.add_post("/api/admin/users/{id}/role", admin_user_role)
app.router.add_post("/api/admin/users/{id}/verify", admin_user_verify) app.router.add_post("/api/admin/users/{id}/verify", admin_user_verify)
app.router.add_post("/api/admin/users/{id}/reset-password", admin_user_reset_password)
app.router.add_post("/api/admin/users/{id}/send-info", admin_user_send_info)
app.router.add_delete("/api/admin/users/{id}", admin_user_delete) app.router.add_delete("/api/admin/users/{id}", admin_user_delete)

View file

@ -58,7 +58,15 @@
<!-- Content --> <!-- Content -->
<main class="content-wrapper"> <main class="content-wrapper">
<h6 class="mb-2"><i class="bi bi-people me-1 text-info"></i>Benutzerverwaltung</h6> <div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="mb-0"><i class="bi bi-people me-1 text-info"></i>Benutzerverwaltung</h6>
<button class="btn btn-sm btn-info" id="btnShowCreate">
<i class="bi bi-person-plus me-1"></i>Neuer Benutzer
</button>
</div>
<!-- Toast for notifications -->
<div id="adminToast" class="alert alert-success py-1 small d-none mb-2"></div>
<div class="card card-outline card-info"> <div class="card card-outline card-info">
<div class="card-body p-0 table-responsive"> <div class="card-body p-0 table-responsive">
@ -81,6 +89,122 @@
</div> </div>
</main> </main>
<!-- Create User Modal -->
<div class="modal fade" id="createModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="bi bi-person-plus me-1"></i>Neuer Benutzer</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="createAlert" class="alert py-1 small d-none"></div>
<div class="mb-2">
<label class="form-label small mb-1">Name</label>
<input type="text" class="form-control form-control-sm" id="createName" required>
</div>
<div class="mb-2">
<label class="form-label small mb-1">E-Mail</label>
<input type="email" class="form-control form-control-sm" id="createEmail" required>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Passwort (min. 8 Zeichen)</label>
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm" id="createPassword" minlength="8" required>
<button class="btn btn-outline-secondary" type="button" id="btnGeneratePassword" title="Generieren">
<i class="bi bi-shuffle"></i>
</button>
</div>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Rolle</label>
<select class="form-select form-select-sm" id="createRole">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="createSendEmail" checked>
<label class="form-check-label small" for="createSendEmail">Zugangsdaten per E-Mail senden</label>
</div>
</div>
<div class="modal-footer py-1">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-sm btn-info" id="btnCreateUser">Erstellen</button>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="bi bi-pencil me-1"></i>Benutzer bearbeiten</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="editAlert" class="alert py-1 small d-none"></div>
<input type="hidden" id="editUserId">
<div class="mb-2">
<label class="form-label small mb-1">Name</label>
<input type="text" class="form-control form-control-sm" id="editName" required>
</div>
<div class="mb-2">
<label class="form-label small mb-1">E-Mail</label>
<input type="email" class="form-control form-control-sm" id="editEmail" required>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Rolle</label>
<select class="form-select form-select-sm" id="editRole">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div class="modal-footer py-1">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-sm btn-info" id="btnSaveEdit">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPwModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header py-2">
<h6 class="modal-title"><i class="bi bi-key me-1"></i>Passwort zuruecksetzen</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="resetPwAlert" class="alert py-1 small d-none"></div>
<input type="hidden" id="resetPwUserId">
<p class="small mb-2">Passwort fuer <strong id="resetPwUserName"></strong> zuruecksetzen:</p>
<div class="mb-2">
<label class="form-label small mb-1">Neues Passwort (min. 8 Zeichen)</label>
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm" id="resetPwPassword" minlength="8" required>
<button class="btn btn-outline-secondary" type="button" id="btnGenerateResetPw" title="Generieren">
<i class="bi bi-shuffle"></i>
</button>
</div>
</div>
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="resetPwSendEmail" checked>
<label class="form-check-label small" for="resetPwSendEmail">Neues Passwort per E-Mail senden</label>
</div>
</div>
<div class="modal-footer py-1">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-sm btn-warning" id="btnResetPassword">Zuruecksetzen</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/admin.js"></script> <script src="/static/js/admin.js"></script>
</body> </body>

View file

@ -9,6 +9,7 @@ fetch('/api/auth/me').then(r => r.ok ? r.json() : null).then(u => {
if (!u || u.role !== 'admin') { if (!u || u.role !== 'admin') {
document.getElementById('usersTable').innerHTML = document.getElementById('usersTable').innerHTML =
'<tr><td colspan="6" class="text-center text-danger py-3">Zugriff verweigert</td></tr>'; '<tr><td colspan="6" class="text-center text-danger py-3">Zugriff verweigert</td></tr>';
document.getElementById('btnShowCreate').classList.add('d-none');
return; return;
} }
loadUsers(); loadUsers();
@ -32,6 +33,38 @@ function updateSidebar() {
}); });
} }
// ── Toast ────────────────────────────────────────────
function showToast(message, type = 'success') {
const el = document.getElementById('adminToast');
el.className = `alert alert-${type} py-1 small mb-2`;
el.textContent = message;
setTimeout(() => el.classList.add('d-none'), 4000);
}
function showModalAlert(id, message, type = 'danger') {
const el = document.getElementById(id);
el.className = `alert alert-${type} py-1 small`;
el.textContent = message;
}
function hideModalAlert(id) {
document.getElementById(id).classList.add('d-none');
}
// ── Password generator ───────────────────────────────
function generatePassword(length = 12) {
const chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%';
let pw = '';
const arr = new Uint8Array(length);
crypto.getRandomValues(arr);
for (let i = 0; i < length; i++) pw += chars[arr[i] % chars.length];
return pw;
}
// ── Load & Render ────────────────────────────────────
async function loadUsers() { async function loadUsers() {
try { try {
const resp = await fetch('/api/admin/users'); const resp = await fetch('/api/admin/users');
@ -61,22 +94,30 @@ function renderUsers() {
const isSelf = currentUser && currentUser.id === user.id; const isSelf = currentUser && currentUser.id === user.id;
let actions = ''; let actions = '';
// Edit button (always)
actions += `<button class="btn btn-outline-info btn-sm py-0 px-1 me-1" onclick="openEditModal(${user.id})" title="Bearbeiten">
<i class="bi bi-pencil"></i>
</button>`;
// Reset password
actions += `<button class="btn btn-outline-warning btn-sm py-0 px-1 me-1" onclick="openResetPwModal(${user.id})" title="Passwort zuruecksetzen">
<i class="bi bi-key"></i>
</button>`;
// Send info email
actions += `<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1" onclick="sendInfoEmail(${user.id})" title="Info-Mail senden">
<i class="bi bi-envelope"></i>
</button>`;
if (!isSelf) { if (!isSelf) {
const newRole = user.role === 'admin' ? 'user' : 'admin'; // Verify (if not verified)
const roleLabel = user.role === 'admin' ? 'User' : 'Admin';
actions += `<button class="btn btn-outline-warning btn-sm py-0 px-1 me-1" onclick="changeRole(${user.id},'${newRole}')" title="Zu ${roleLabel} machen">
<i class="bi bi-arrow-repeat"></i>
</button>`;
if (!user.is_verified) { if (!user.is_verified) {
actions += `<button class="btn btn-outline-success btn-sm py-0 px-1 me-1" onclick="verifyUser(${user.id})" title="Verifizieren"> actions += `<button class="btn btn-outline-success btn-sm py-0 px-1 me-1" onclick="verifyUser(${user.id})" title="Verifizieren">
<i class="bi bi-check-lg"></i> <i class="bi bi-check-lg"></i>
</button>`; </button>`;
} }
// Delete
actions += `<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteUser(${user.id},'${escapeHtml(user.name)}')" title="Loeschen"> actions += `<button class="btn btn-outline-danger btn-sm py-0 px-1" onclick="deleteUser(${user.id},'${escapeHtml(user.name)}')" title="Loeschen">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button>`; </button>`;
} else {
actions = '<small class="text-body-secondary">Du</small>';
} }
return `<tr> return `<tr>
@ -85,26 +126,190 @@ function renderUsers() {
<td class="text-center">${roleBadge}</td> <td class="text-center">${roleBadge}</td>
<td class="text-center">${verifiedIcon}</td> <td class="text-center">${verifiedIcon}</td>
<td class="text-end text-body-secondary">${created}</td> <td class="text-end text-body-secondary">${created}</td>
<td class="text-end">${actions}</td> <td class="text-end text-nowrap">${actions}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
} }
async function changeRole(id, role) { // ── Create User ──────────────────────────────────────
const createModal = new bootstrap.Modal(document.getElementById('createModal'));
document.getElementById('btnShowCreate').addEventListener('click', () => {
document.getElementById('createName').value = '';
document.getElementById('createEmail').value = '';
document.getElementById('createPassword').value = generatePassword();
document.getElementById('createRole').value = 'user';
document.getElementById('createSendEmail').checked = true;
hideModalAlert('createAlert');
createModal.show();
});
document.getElementById('btnGeneratePassword').addEventListener('click', () => {
document.getElementById('createPassword').value = generatePassword();
});
document.getElementById('btnCreateUser').addEventListener('click', async () => {
const name = document.getElementById('createName').value.trim();
const email = document.getElementById('createEmail').value.trim();
const password = document.getElementById('createPassword').value;
const role = document.getElementById('createRole').value;
const sendEmail = document.getElementById('createSendEmail').checked;
if (!name || !email || !password) {
showModalAlert('createAlert', 'Alle Felder ausfuellen');
return;
}
if (password.length < 8) {
showModalAlert('createAlert', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
try { try {
const resp = await fetch(`/api/admin/users/${id}/role`, { const resp = await fetch('/api/admin/users', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }) body: JSON.stringify({ name, email, password, role, send_email: sendEmail })
}); });
if (resp.ok) { users = await resp.json(); renderUsers(); } if (resp.ok) {
} catch (e) { console.error('Role change failed:', e); } users = await resp.json();
renderUsers();
createModal.hide();
showToast(`Benutzer "${name}" erstellt`);
} else {
const data = await resp.json();
showModalAlert('createAlert', data.error || 'Fehler beim Erstellen');
}
} catch (e) {
showModalAlert('createAlert', 'Verbindungsfehler');
}
});
// ── Edit User ────────────────────────────────────────
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
function openEditModal(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
document.getElementById('editUserId').value = userId;
document.getElementById('editName').value = user.name;
document.getElementById('editEmail').value = user.email;
document.getElementById('editRole').value = user.role;
hideModalAlert('editAlert');
editModal.show();
} }
document.getElementById('btnSaveEdit').addEventListener('click', async () => {
const userId = document.getElementById('editUserId').value;
const name = document.getElementById('editName').value.trim();
const email = document.getElementById('editEmail').value.trim();
const role = document.getElementById('editRole').value;
if (!name || !email) {
showModalAlert('editAlert', 'Name und E-Mail erforderlich');
return;
}
try {
const resp = await fetch(`/api/admin/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, role })
});
if (resp.ok) {
users = await resp.json();
renderUsers();
editModal.hide();
showToast('Benutzer aktualisiert');
} else {
const data = await resp.json();
showModalAlert('editAlert', data.error || 'Fehler beim Speichern');
}
} catch (e) {
showModalAlert('editAlert', 'Verbindungsfehler');
}
});
// ── Reset Password ───────────────────────────────────
const resetPwModal = new bootstrap.Modal(document.getElementById('resetPwModal'));
function openResetPwModal(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
document.getElementById('resetPwUserId').value = userId;
document.getElementById('resetPwUserName').textContent = user.name;
document.getElementById('resetPwPassword').value = generatePassword();
document.getElementById('resetPwSendEmail').checked = true;
hideModalAlert('resetPwAlert');
resetPwModal.show();
}
document.getElementById('btnGenerateResetPw').addEventListener('click', () => {
document.getElementById('resetPwPassword').value = generatePassword();
});
document.getElementById('btnResetPassword').addEventListener('click', async () => {
const userId = document.getElementById('resetPwUserId').value;
const password = document.getElementById('resetPwPassword').value;
const sendEmail = document.getElementById('resetPwSendEmail').checked;
if (!password || password.length < 8) {
showModalAlert('resetPwAlert', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
try {
const resp = await fetch(`/api/admin/users/${userId}/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password, send_email: sendEmail })
});
if (resp.ok) {
users = await resp.json();
renderUsers();
resetPwModal.hide();
showToast('Passwort zurueckgesetzt');
} else {
const data = await resp.json();
showModalAlert('resetPwAlert', data.error || 'Fehler');
}
} catch (e) {
showModalAlert('resetPwAlert', 'Verbindungsfehler');
}
});
// ── Send Info Email ──────────────────────────────────
async function sendInfoEmail(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
if (!confirm(`Info-Mail an "${user.name}" (${user.email}) senden?`)) return;
try {
const resp = await fetch(`/api/admin/users/${userId}/send-info`, { method: 'POST' });
const data = await resp.json();
if (resp.ok) {
showToast(data.message || 'Info-Mail gesendet');
} else {
showToast(data.error || 'Fehler beim Senden', 'danger');
}
} catch (e) {
showToast('Verbindungsfehler', 'danger');
}
}
// ── Verify / Delete ──────────────────────────────────
async function verifyUser(id) { async function verifyUser(id) {
try { try {
const resp = await fetch(`/api/admin/users/${id}/verify`, { method: 'POST' }); const resp = await fetch(`/api/admin/users/${id}/verify`, { method: 'POST' });
if (resp.ok) { users = await resp.json(); renderUsers(); } if (resp.ok) {
users = await resp.json();
renderUsers();
showToast('Benutzer verifiziert');
}
} catch (e) { console.error('Verify failed:', e); } } catch (e) { console.error('Verify failed:', e); }
} }
@ -112,10 +317,16 @@ async function deleteUser(id, name) {
if (!confirm(`Benutzer "${name}" wirklich loeschen?`)) return; if (!confirm(`Benutzer "${name}" wirklich loeschen?`)) return;
try { try {
const resp = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' }); const resp = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
if (resp.ok) { users = await resp.json(); renderUsers(); } if (resp.ok) {
users = await resp.json();
renderUsers();
showToast('Benutzer geloescht');
}
} catch (e) { console.error('Delete failed:', e); } } catch (e) { console.error('Delete failed:', e); }
} }
// ── Helpers ──────────────────────────────────────────
function escapeHtml(str) { function escapeHtml(str) {
if (!str) return ''; if (!str) return '';
const div = document.createElement('div'); const div = document.createElement('div');