MeshDD-Bot/static/js/admin.js
2026-02-16 20:45:30 +01:00

363 lines
14 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>';
document.getElementById('btnShowCreate').classList.add('d-none');
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';
});
}
// ── 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() {
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 = '';
// 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) {
// Verify (if not 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">
<i class="bi bi-check-lg"></i>
</button>`;
}
// Delete
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>`;
}
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 text-nowrap">${actions}</td>
</tr>`;
}).join('');
}
// ── 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 {
const resp = await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password, role, send_email: sendEmail })
});
if (resp.ok) {
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) {
try {
const resp = await fetch(`/api/admin/users/${id}/verify`, { method: 'POST' });
if (resp.ok) {
users = await resp.json();
renderUsers();
showToast('Benutzer verifiziert');
}
} 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();
showToast('Benutzer geloescht');
}
} catch (e) { console.error('Delete failed:', e); }
}
// ── Helpers ──────────────────────────────────────────
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'));
}