- Tabler 1.4.0 als Admin-Theme: Bootstrap CSS/JS in allen 6 HTML-Seiten ersetzt - style.css komplett ueberarbeitet: Inter-Font, Tabler CSS-Variablen, Schatten, verfeinerte Sidebar (Rounded Active-Links), Hover-Animation auf Info-Boxen, pulsierender Status-Dot - app.js als shared Modul: Duplikation in allen JS-Dateien eliminiert (initPage, applyTheme, escapeHtml, Sidebar-Injektion) - WebSocket Auth-Fix: Nachrichten nur noch an eingeloggte Clients (auth_clients) - Bot-Uptime + Meshtastic-Verbindungsstatus in Dashboard und Stats-API - Dark Mode Kartentiles: CartoDB Dark Matter fuer Karte + Node-Modal - 3 Charts: Kanal-Anfragen (Doughnut), Hop-Verteilung (Bar), Hardware Top 5 - Nodes-Tabelle: Suchfeld, Online-Filter, sortierbare Spalten - Nachrichten Kanalfilter: Filter-Buttons im Nachrichten-Card-Header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
308 lines
12 KiB
JavaScript
308 lines
12 KiB
JavaScript
let currentUser = null;
|
|
let users = [];
|
|
|
|
initPage({ onAuth: (user) => {
|
|
currentUser = user;
|
|
if (!user || user.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();
|
|
} });
|
|
|
|
// ── 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 ──────────────────────────────────────────
|