MeshDD-Bot/static/js/admin.js
ppfeiffer 0d6b26f4f8 feat: v0.5.0 - Benutzerverwaltung mit Session-Authentifizierung
Rollen-basiertes Zugriffsystem (public/user/admin), Registrierung mit
E-Mail-Verifikation, bcrypt Passwort-Hashing, Admin-Benutzerverwaltung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:38:17 +01:00

152 lines
5.7 KiB
JavaScript

let currentUser = null;
let users = [];
// Auth check
fetch('/api/auth/me').then(r => r.ok ? r.json() : null).then(u => {
currentUser = u;
updateNavbar();
updateSidebar();
if (!u || u.role !== 'admin') {
document.getElementById('usersTable').innerHTML =
'<tr><td colspan="6" class="text-center text-danger py-3">Zugriff verweigert</td></tr>';
return;
}
loadUsers();
});
function updateNavbar() {
if (currentUser) {
document.getElementById('userName').textContent = currentUser.name;
document.getElementById('userMenu').classList.remove('d-none');
document.getElementById('loginBtn').classList.add('d-none');
} else {
document.getElementById('userMenu').classList.add('d-none');
document.getElementById('loginBtn').classList.remove('d-none');
}
}
function updateSidebar() {
const isAdmin = currentUser && currentUser.role === 'admin';
document.querySelectorAll('.sidebar-admin').forEach(el => {
el.style.display = isAdmin ? '' : 'none';
});
}
async function loadUsers() {
try {
const resp = await fetch('/api/admin/users');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
users = await resp.json();
renderUsers();
} catch (e) {
document.getElementById('usersTable').innerHTML =
'<tr><td colspan="6" class="text-center text-danger py-3">Fehler beim Laden</td></tr>';
}
}
function renderUsers() {
const tbody = document.getElementById('usersTable');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-body-secondary py-3">Keine Benutzer</td></tr>';
return;
}
tbody.innerHTML = users.map(user => {
const roleBadge = user.role === 'admin'
? '<span class="badge bg-danger">Admin</span>'
: '<span class="badge bg-secondary">User</span>';
const verifiedIcon = user.is_verified
? '<i class="bi bi-check-circle-fill text-success"></i>'
: '<i class="bi bi-x-circle text-danger"></i>';
const created = user.created_at ? new Date(user.created_at * 1000).toLocaleDateString('de-DE') : '-';
const isSelf = currentUser && currentUser.id === user.id;
let actions = '';
if (!isSelf) {
const newRole = user.role === 'admin' ? 'user' : 'admin';
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) {
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>
</button>`;
}
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>
</button>`;
} else {
actions = '<small class="text-body-secondary">Du</small>';
}
return `<tr>
<td class="fw-semibold">${escapeHtml(user.name)}</td>
<td>${escapeHtml(user.email)}</td>
<td class="text-center">${roleBadge}</td>
<td class="text-center">${verifiedIcon}</td>
<td class="text-end text-body-secondary">${created}</td>
<td class="text-end">${actions}</td>
</tr>`;
}).join('');
}
async function changeRole(id, role) {
try {
const resp = await fetch(`/api/admin/users/${id}/role`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role })
});
if (resp.ok) { users = await resp.json(); renderUsers(); }
} catch (e) { console.error('Role change failed:', e); }
}
async function verifyUser(id) {
try {
const resp = await fetch(`/api/admin/users/${id}/verify`, { method: 'POST' });
if (resp.ok) { users = await resp.json(); renderUsers(); }
} catch (e) { console.error('Verify failed:', e); }
}
async function deleteUser(id, name) {
if (!confirm(`Benutzer "${name}" wirklich loeschen?`)) return;
try {
const resp = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
if (resp.ok) { users = await resp.json(); renderUsers(); }
} catch (e) { console.error('Delete failed:', e); }
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Theme toggle
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
function applyTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
themeIcon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
localStorage.setItem('theme', theme);
}
applyTheme(localStorage.getItem('theme') || 'dark');
themeToggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-bs-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sidebar toggle (mobile)
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open'));
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}