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:
Meleki committed 2026-03-05 19:19:16 +01:00
1 parent 489b5e0eb0
commit 3ef78b5eb1
10 files changed
+667 -57

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
using System.Linq;
using System.Text.Json;
using Gott.Extensions;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class AdminEndpoint
{
public static void MapAdminEndpoints(this WebApplication app)
{
app.MapGet("/api/admin/users", HandleListUsers);
app.MapPost("/api/admin/users", HandleCreateUser);
app.MapPut("/api/admin/users/{userId}", HandleUpdateUser);
app.MapPut("/api/admin/users/{userId}/password", HandleResetPassword);
app.MapDelete("/api/admin/users/{userId}", HandleDeleteUser);
}
private static async System.Threading.Tasks.Task HandleListUsers(HttpContext ctx, DatabaseService db)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
var users = await db.GetUsers();
var result = users.Select(u => new
{
userId = u.UserId,
username = u.Username,
displayName = u.DisplayName,
isAdmin = u.IsAdmin,
createdAt = u.CreatedAt,
updatedAt = u.UpdatedAt
});
await ctx.Response.WriteAsJsonAsync(result);
}
private static async System.Threading.Tasks.Task HandleCreateUser(HttpContext ctx, DatabaseService db, AuthService auth)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var username = body.TryGetProperty("username", out var u) ? u.GetString() : null;
var password = body.TryGetProperty("password", out var p) ? p.GetString() : null;
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null;
var isAdmin = body.TryGetProperty("isAdmin", out var ia) && ia.GetBoolean();
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzername und Passwort erforderlich" });
return;
}
if (password.Length < 4)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Passwort muss mindestens 4 Zeichen lang sein" });
return;
}
var existing = await db.GetUserByUsername(username);
if (existing != null)
{
ctx.Response.StatusCode = 409;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzername bereits vergeben" });
return;
}
var hash = auth.HashPassword(password);
var user = await db.CreateUser(username, hash, displayName, isAdmin);
ctx.Response.StatusCode = 201;
await ctx.Response.WriteAsJsonAsync(new
{
userId = user.UserId,
username = user.Username,
displayName = user.DisplayName,
isAdmin = user.IsAdmin,
createdAt = user.CreatedAt
});
}
private static async System.Threading.Tasks.Task HandleUpdateUser(HttpContext ctx, DatabaseService db, string userId)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
var user = await db.GetUser(userId);
if (user == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzer nicht gefunden" });
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var username = body.TryGetProperty("username", out var u) ? u.GetString() : user.Username;
var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : user.DisplayName;
var isAdmin = body.TryGetProperty("isAdmin", out var ia) ? ia.GetBoolean() : user.IsAdmin;
if (string.IsNullOrWhiteSpace(username))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzername darf nicht leer sein" });
return;
}
// Prüfen ob anderer User den Username bereits hat
var existing = await db.GetUserByUsername(username);
if (existing != null && existing.UserId != userId)
{
ctx.Response.StatusCode = 409;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzername bereits vergeben" });
return;
}
await db.UpdateUserAdmin(userId, username, displayName, isAdmin);
var updated = await db.GetUser(userId);
await ctx.Response.WriteAsJsonAsync(new
{
userId = updated.UserId,
username = updated.Username,
displayName = updated.DisplayName,
isAdmin = updated.IsAdmin,
createdAt = updated.CreatedAt,
updatedAt = updated.UpdatedAt
});
}
private static async System.Threading.Tasks.Task HandleResetPassword(HttpContext ctx, DatabaseService db, AuthService auth, string userId)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
var user = await db.GetUser(userId);
if (user == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzer nicht gefunden" });
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var newPassword = body.TryGetProperty("password", out var p) ? p.GetString() : null;
if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 4)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Passwort muss mindestens 4 Zeichen lang sein" });
return;
}
var hash = auth.HashPassword(newPassword);
await db.UpdateUserPassword(userId, hash);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
private static async System.Threading.Tasks.Task HandleDeleteUser(HttpContext ctx, DatabaseService db, string userId)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
// Sich selbst löschen verhindern
var currentUserId = ctx.GetUserId();
if (currentUserId == userId)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Du kannst dich nicht selbst löschen" });
return;
}
var user = await db.GetUser(userId);
if (user == null)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Benutzer nicht gefunden" });
return;
}
await db.DeleteUser(userId);
await ctx.Response.WriteAsJsonAsync(new { ok = true });
}
}
+1
View File
@@ -81,5 +81,6 @@ app.MapTelegramEndpoints();
app.MapCronjobEndpoints();
app.MapKreaEndpoints();
app.MapSharedEndpoints();
app.MapAdminEndpoints();
app.Run("http://0.0.0.0:19000");
+28
View File
@@ -912,6 +912,34 @@ public class DatabaseService
new { userId, settings, now = DateTime.UtcNow.ToString("o") });
}
public async Task<UserRecord> CreateUser(string username, string passwordHash, string displayName, bool isAdmin)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
var userId = Guid.NewGuid().ToString("N");
await conn.ExecuteAsync(@"
INSERT INTO users (user_id, username, password_hash, display_name, is_admin, created_at, updated_at)
VALUES (@userId, @username, @passwordHash, @displayName, @isAdmin, @now, @now)",
new { userId, username, passwordHash, displayName, isAdmin, now });
return await GetUser(userId);
}
public async Task UpdateUserAdmin(string userId, string username, string displayName, bool isAdmin)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE users SET username = @username, display_name = @displayName, is_admin = @isAdmin, updated_at = @now WHERE user_id = @userId",
new { userId, username, displayName, isAdmin, now = DateTime.UtcNow.ToString("o") });
}
public async Task DeleteUser(string userId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync("DELETE FROM tokens WHERE user_id = @userId", new { userId });
await conn.ExecuteAsync("DELETE FROM shared_messages WHERE user_id = @userId", new { userId });
await conn.ExecuteAsync("DELETE FROM users WHERE user_id = @userId", new { userId });
}
// ── Telegram (Session-basiert) ──
public async Task<SessionRecord> GetSessionByTelegramChatId(long chatId)
+1 -1
View File
@@ -46,7 +46,7 @@
## Features aus Devy übernehmen
- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..."
- [ ] 🎨 Chat-Hintergründe - Benutzerdefinierte Hintergründe pro Session
- [ ] 👤 User-Management - Admin-only: Benutzer CRUD, Login-as-User, Default-Pfade
- [x] 👤 User-Management - Admin-only: Benutzer CRUD, Login-as-User, Default-Pfade
- [x] 🖥️ Windows Desktop App - WPF + WebView2 Wrapper mit konfigurierbarer URL
- [ ] 🎨 Canvas - HTML/CSS/JS inline rendern in sandboxed iframes mit Resize und Source-View
(Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`)
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+56
View File
@@ -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() {
+212
View File
@@ -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() {}
});
+10
View File
@@ -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());