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
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user