feat: Admin-Benutzerverwaltung + Dashboard-Layout-Fixes
Backend: - AdminEndpoint.cs: 5 Endpunkte (List, Create, Update, Reset PW, Delete) - Admin-only - DatabaseService: CreateUser(), UpdateUserAdmin(), DeleteUser() (kaskadiert Tokens + Shares) - Route in Program.cs registriert Frontend: - admin-page.js: User-Liste mit Edit/Delete, Inline-Formular Create/Edit - api.js: 5 Admin-API-Funktionen - index.html: Template tpl-admin, Script-Tag, Dashboard-Link 'Benutzerverwaltung' - dashboard.js: Admin-Link nur für Admins sichtbar - form.css: Styles für admin-user-list, admin-user-row, admin-checkbox-row - dashboard.css: Header-Overflow-Fix, Agents vor Cronjobs - index.html: Template-Reihenfolge Agents vor Cronjobs TODO: User-Management als erledigt markiert
This commit is contained in:
1 parent
489b5e0eb0
commit
3ef78b5eb1
10 files changed
+667
-57
No files matched your search
@@ -2,9 +2,17 @@
|
||||
padding: var(--page-padding);
|
||||
}
|
||||
|
||||
.dashboard header {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard header * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-links {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
gap: var(--box-spacing);
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
+44
-1
@@ -190,7 +190,6 @@ label.skill-item {
|
||||
|
||||
.skill-warning {
|
||||
color: var(--dangerous);
|
||||
font-size: var(--font-size-sm, 0.8em);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -364,3 +363,47 @@ label.skill-item {
|
||||
font-size: var(--font-size);
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
/* ── Admin User List ── */
|
||||
|
||||
.admin-user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--box-padding);
|
||||
}
|
||||
|
||||
.admin-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-user-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.admin-user-meta {
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.admin-user-actions {
|
||||
display: flex;
|
||||
gap: var(--badge-spacing);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-checkbox-row {
|
||||
margin: var(--badge-spacing) 0;
|
||||
}
|
||||
|
||||
.admin-checkbox-row .toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--badge-spacing);
|
||||
cursor: pointer;
|
||||
}
|
||||
+96
-54
@@ -56,6 +56,7 @@
|
||||
<h1 class="dashboard-title">// gott</h1>
|
||||
<div class="dashboard-links">
|
||||
<a id="dashboardSettingsLink" class="dashboard-link">Einstellungen</a>
|
||||
<a id="dashboardAdminLink" class="dashboard-link" hidden>Benutzerverwaltung</a>
|
||||
<a id="dashboardProfileLink" class="dashboard-link">Mein Profil</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -67,13 +68,6 @@
|
||||
</div>
|
||||
<div class="card-grid" id="sessionCards"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Cronjobs</h2>
|
||||
<button id="newCronjobBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Cronjob</button>
|
||||
</div>
|
||||
<div class="card-grid" id="cronjobCards"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Agents</h2>
|
||||
@@ -81,6 +75,13 @@
|
||||
</div>
|
||||
<div class="card-grid" id="agentCards"></div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Cronjobs</h2>
|
||||
<button id="newCronjobBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Cronjob</button>
|
||||
</div>
|
||||
<div class="card-grid" id="cronjobCards"></div>
|
||||
</section>
|
||||
<section id="sharedLinksSection" hidden>
|
||||
<div class="section-header">
|
||||
<h2>Geteilte Links</h2>
|
||||
@@ -145,53 +146,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-cronjob">
|
||||
<div class="page form-page">
|
||||
<header>
|
||||
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
|
||||
<h1 id="cronjobPageTitle">Neuer Cronjob</h1>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="form-group form-check-group">
|
||||
<label>
|
||||
<input type="checkbox" id="cronjobEnabled" checked>
|
||||
Aktiv
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobName">Name</label>
|
||||
<input type="text" id="cronjobName" placeholder="z.B. Täglicher Report">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobSession">Session</label>
|
||||
<select id="cronjobSession"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobCron">Cron-Expression</label>
|
||||
<input type="text" id="cronjobCron" placeholder="z.B. 0 9 * * *">
|
||||
<small id="cronjobCronPreview" class="form-hint"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobPrompt">Prompt</label>
|
||||
<textarea id="cronjobPrompt" rows="6" placeholder="Die Nachricht, die an den Agent gesendet wird..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobNotifySession">Benachrichtigungs-Session <small>(optional)</small></label>
|
||||
<select id="cronjobNotifySession"></select>
|
||||
<small class="form-hint">Wenn gesetzt, wird eine Zusammenfassung in diese Session geschrieben (z.B. eine mit Telegram).</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div id="cronjobStatusBox" class="form-status-box" hidden></div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="cronjobSaveBtn">Speichern</button>
|
||||
<button id="cronjobRunBtn" class="positive-btn" hidden><i class="fa fa-play"></i> Jetzt ausführen</button>
|
||||
<button id="cronjobDeleteBtn" class="danger-btn" hidden>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-agent">
|
||||
<div class="page form-page">
|
||||
<header>
|
||||
@@ -269,6 +223,53 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-cronjob">
|
||||
<div class="page form-page">
|
||||
<header>
|
||||
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
|
||||
<h1 id="cronjobPageTitle">Neuer Cronjob</h1>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="form-group form-check-group">
|
||||
<label>
|
||||
<input type="checkbox" id="cronjobEnabled" checked>
|
||||
Aktiv
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobName">Name</label>
|
||||
<input type="text" id="cronjobName" placeholder="z.B. Täglicher Report">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobSession">Session</label>
|
||||
<select id="cronjobSession"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobCron">Cron-Expression</label>
|
||||
<input type="text" id="cronjobCron" placeholder="z.B. 0 9 * * *">
|
||||
<small id="cronjobCronPreview" class="form-hint"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobPrompt">Prompt</label>
|
||||
<textarea id="cronjobPrompt" rows="6" placeholder="Die Nachricht, die an den Agent gesendet wird..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cronjobNotifySession">Benachrichtigungs-Session <small>(optional)</small></label>
|
||||
<select id="cronjobNotifySession"></select>
|
||||
<small class="form-hint">Wenn gesetzt, wird eine Zusammenfassung in diese Session geschrieben (z.B. eine mit Telegram).</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div id="cronjobStatusBox" class="form-status-box" hidden></div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="cronjobSaveBtn">Speichern</button>
|
||||
<button id="cronjobRunBtn" class="positive-btn" hidden><i class="fa fa-play"></i> Jetzt ausführen</button>
|
||||
<button id="cronjobDeleteBtn" class="danger-btn" hidden>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-chat">
|
||||
<div class="page chat-page">
|
||||
<header>
|
||||
@@ -422,6 +423,46 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-admin">
|
||||
<div class="page form-page">
|
||||
<header>
|
||||
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="form">
|
||||
<div class="section-header">
|
||||
<h2>Benutzer</h2>
|
||||
<button id="adminNewUserBtn" class="section-add-btn"><i class="fa fa-plus"></i> Neuer Benutzer</button>
|
||||
</div>
|
||||
<div id="adminUserList" class="admin-user-list"></div>
|
||||
<div id="adminMessage" class="profile-message" hidden></div>
|
||||
</div>
|
||||
|
||||
<!-- Inline-Formular zum Erstellen/Bearbeiten -->
|
||||
<div id="adminUserForm" class="form" hidden>
|
||||
<h2 id="adminFormTitle">Neuer Benutzer</h2>
|
||||
<label>Benutzername</label>
|
||||
<input type="text" id="adminUsername" placeholder="Benutzername" autocomplete="off">
|
||||
<label>Anzeigename</label>
|
||||
<input type="text" id="adminDisplayName" placeholder="Anzeigename (optional)" autocomplete="off">
|
||||
<label id="adminPasswordLabel">Passwort</label>
|
||||
<input type="password" id="adminPassword" placeholder="Passwort" autocomplete="new-password">
|
||||
<div class="admin-checkbox-row">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="adminIsAdmin">
|
||||
<span>Administrator</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="adminFormSaveBtn">Erstellen</button>
|
||||
</div>
|
||||
<div id="adminFormMessage" class="profile-message" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-shared">
|
||||
<div class="shared-page">
|
||||
<div class="shared-header">
|
||||
@@ -467,6 +508,7 @@
|
||||
<script src="js/pages/login-page.js"></script>
|
||||
<script src="js/pages/settings-page.js"></script>
|
||||
<script src="js/pages/profile-page.js"></script>
|
||||
<script src="js/pages/admin-page.js"></script>
|
||||
<script src="js/pages/shared-page.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -407,6 +407,62 @@ async function apiDeleteShare(token) {
|
||||
return fetch(`/api/shared/${token}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// ── Admin API ──
|
||||
|
||||
async function apiAdminGetUsers() {
|
||||
const resp = await fetch('/api/admin/users');
|
||||
if (!resp.ok) throw new Error('Fehler beim Laden der Benutzer');
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiAdminCreateUser(data) {
|
||||
const resp = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Fehler beim Erstellen');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiAdminUpdateUser(userId, data) {
|
||||
const resp = await fetch('/api/admin/users/' + userId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Fehler beim Aktualisieren');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiAdminResetPassword(userId, password) {
|
||||
const resp = await fetch('/api/admin/users/' + userId + '/password', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Fehler beim Zurücksetzen');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiAdminDeleteUser(userId) {
|
||||
const resp = await fetch('/api/admin/users/' + userId, { method: 'DELETE' });
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
throw new Error(err.error || 'Fehler beim Löschen');
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
// ── Backup API ──
|
||||
|
||||
async function apiBackupDb() {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// ── Admin Page (User Management) ──
|
||||
|
||||
registerPage('admin', {
|
||||
async init() {
|
||||
if (!currentUser || !currentUser.isAdmin) {
|
||||
navigateTo('dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
var users = [];
|
||||
var editingUserId = null;
|
||||
|
||||
var list = document.getElementById('adminUserList');
|
||||
var newUserBtn = document.getElementById('adminNewUserBtn');
|
||||
var userForm = document.getElementById('adminUserForm');
|
||||
var formTitle = document.getElementById('adminFormTitle');
|
||||
var usernameInput = document.getElementById('adminUsername');
|
||||
var displayNameInput = document.getElementById('adminDisplayName');
|
||||
var passwordInput = document.getElementById('adminPassword');
|
||||
var passwordLabel = document.getElementById('adminPasswordLabel');
|
||||
var isAdminCheckbox = document.getElementById('adminIsAdmin');
|
||||
var saveBtn = document.getElementById('adminFormSaveBtn');
|
||||
var formMessage = document.getElementById('adminFormMessage');
|
||||
var pageMessage = document.getElementById('adminMessage');
|
||||
|
||||
// ── Hilfsfunktionen ──
|
||||
|
||||
function showFormMessage(text, isError) {
|
||||
formMessage.textContent = text;
|
||||
formMessage.className = 'profile-message ' + (isError ? 'profile-message-error' : 'profile-message-success');
|
||||
formMessage.hidden = false;
|
||||
if (!isError) {
|
||||
setTimeout(function() { formMessage.hidden = true; }, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function showPageMessage(text, isError) {
|
||||
pageMessage.textContent = text;
|
||||
pageMessage.className = 'profile-message ' + (isError ? 'profile-message-error' : 'profile-message-success');
|
||||
pageMessage.hidden = false;
|
||||
setTimeout(function() { pageMessage.hidden = true; }, 4000);
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
list.innerHTML = '';
|
||||
|
||||
users.forEach(function(user) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'admin-user-row';
|
||||
|
||||
var info = document.createElement('div');
|
||||
info.className = 'admin-user-info';
|
||||
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'admin-user-name';
|
||||
nameSpan.textContent = user.displayName || user.username;
|
||||
|
||||
var metaSpan = document.createElement('span');
|
||||
metaSpan.className = 'admin-user-meta';
|
||||
var metaParts = ['@' + user.username];
|
||||
if (user.isAdmin) metaParts.push('Administrator');
|
||||
metaSpan.textContent = metaParts.join(' ▪ ');
|
||||
|
||||
info.appendChild(nameSpan);
|
||||
info.appendChild(metaSpan);
|
||||
|
||||
var actions = document.createElement('div');
|
||||
actions.className = 'admin-user-actions';
|
||||
|
||||
var editBtn = document.createElement('button');
|
||||
editBtn.className = 'icon-btn';
|
||||
editBtn.title = 'Bearbeiten';
|
||||
editBtn.innerHTML = '<i class="fa fa-pencil"></i>';
|
||||
editBtn.addEventListener('click', function() {
|
||||
startEdit(user);
|
||||
});
|
||||
actions.appendChild(editBtn);
|
||||
|
||||
// Sich selbst kann man nicht löschen
|
||||
if (user.userId !== currentUser.userId) {
|
||||
var deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'icon-btn';
|
||||
deleteBtn.title = 'Löschen';
|
||||
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
deleteUser(user);
|
||||
});
|
||||
actions.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
row.appendChild(info);
|
||||
row.appendChild(actions);
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
users = await apiAdminGetUsers();
|
||||
renderUsers();
|
||||
} catch (err) {
|
||||
showPageMessage(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(user) {
|
||||
editingUserId = user.userId;
|
||||
formTitle.textContent = 'Benutzer bearbeiten';
|
||||
usernameInput.value = user.username;
|
||||
usernameInput.disabled = false;
|
||||
displayNameInput.value = user.displayName || '';
|
||||
passwordInput.value = '';
|
||||
passwordLabel.textContent = 'Neues Passwort (leer = unverändert)';
|
||||
passwordInput.placeholder = 'Neues Passwort (optional)';
|
||||
passwordInput.required = false;
|
||||
isAdminCheckbox.checked = user.isAdmin;
|
||||
saveBtn.textContent = 'Speichern';
|
||||
formMessage.hidden = true;
|
||||
userForm.hidden = false;
|
||||
usernameInput.focus();
|
||||
}
|
||||
|
||||
async function deleteUser(user) {
|
||||
if (!confirm('Benutzer "' + (user.displayName || user.username) + '" wirklich löschen?\n\nAlle Tokens werden ebenfalls gelöscht.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiAdminDeleteUser(user.userId);
|
||||
await loadUsers();
|
||||
} catch (err) {
|
||||
showPageMessage(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event-Listener ──
|
||||
|
||||
// Neuer Benutzer
|
||||
newUserBtn.addEventListener('click', function() {
|
||||
editingUserId = null;
|
||||
formTitle.textContent = 'Neuer Benutzer';
|
||||
usernameInput.value = '';
|
||||
usernameInput.disabled = false;
|
||||
displayNameInput.value = '';
|
||||
passwordInput.value = '';
|
||||
passwordLabel.textContent = 'Passwort';
|
||||
passwordInput.placeholder = 'Passwort';
|
||||
passwordInput.required = true;
|
||||
isAdminCheckbox.checked = false;
|
||||
saveBtn.textContent = 'Erstellen';
|
||||
formMessage.hidden = true;
|
||||
userForm.hidden = false;
|
||||
usernameInput.focus();
|
||||
});
|
||||
|
||||
// Speichern
|
||||
saveBtn.addEventListener('click', async function() {
|
||||
formMessage.hidden = true;
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
if (editingUserId) {
|
||||
// Update
|
||||
var data = {
|
||||
username: usernameInput.value.trim(),
|
||||
displayName: displayNameInput.value.trim(),
|
||||
isAdmin: isAdminCheckbox.checked
|
||||
};
|
||||
await apiAdminUpdateUser(editingUserId, data);
|
||||
|
||||
// Passwort nur wenn ausgefüllt
|
||||
var pw = passwordInput.value;
|
||||
if (pw) {
|
||||
await apiAdminResetPassword(editingUserId, pw);
|
||||
}
|
||||
|
||||
showFormMessage('Benutzer aktualisiert', false);
|
||||
} else {
|
||||
// Create
|
||||
var username = usernameInput.value.trim();
|
||||
var password = passwordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
showFormMessage('Benutzername und Passwort erforderlich', true);
|
||||
return;
|
||||
}
|
||||
|
||||
await apiAdminCreateUser({
|
||||
username: username,
|
||||
password: password,
|
||||
displayName: displayNameInput.value.trim(),
|
||||
isAdmin: isAdminCheckbox.checked
|
||||
});
|
||||
|
||||
showFormMessage('Benutzer erstellt', false);
|
||||
}
|
||||
|
||||
userForm.hidden = true;
|
||||
await loadUsers();
|
||||
} catch (err) {
|
||||
showFormMessage(err.message, true);
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Init: User laden ──
|
||||
await loadUsers();
|
||||
},
|
||||
|
||||
destroy() {}
|
||||
});
|
||||
@@ -14,6 +14,16 @@ registerPage('dashboard', {
|
||||
navigateTo('profile');
|
||||
});
|
||||
|
||||
// Admin-Link (nur für Admins sichtbar)
|
||||
var adminLink = document.getElementById('dashboardAdminLink');
|
||||
if (currentUser && currentUser.isAdmin) {
|
||||
adminLink.hidden = false;
|
||||
adminLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
navigateTo('admin');
|
||||
});
|
||||
}
|
||||
|
||||
// Globaler Click-Handler zum Schließen aller Card-Menüs
|
||||
document.addEventListener('click', this._closeCardMenus = () => closeAllCardMenus());
|
||||
|
||||
|
||||
Reference in new issue
Block a user