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>
152 lines
5.7 KiB
JavaScript
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'));
|
|
}
|