Files
gott/Endpoints/AdminEndpoint.cs
T
MelekiandClaude Opus 4.6 f03fbd298e feat: User-Berechtigungen für Agent-Erstellung und CLI-Nutzung
- Neue Felder CanCreateAgents und CanUseCli im UserRecord (Default: true)
- DB-Migration mit Abwärtskompatibilität (bestehende User behalten alle Rechte)
- Admin-UI: Checkboxen für beide Berechtigungen pro User
- AgentEndpoint: Prüfung auf CanCreateAgents beim Erstellen
- ChatEndpoint: CLI-Fallback nur wenn CanUseCli erlaubt
- Frontend: Neuer-Agent-Button und Agent-Page bei fehlender Berechtigung gesperrt
- Chat: Proaktive Warnung wenn kein API-Key und keine CLI-Berechtigung
- Bug Fix: is_public fehlte im Agent-INSERT (Public Agents erst nach erneutem Speichern sichtbar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:47:56 +01:00

287 lines
11 KiB
C#

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);
app.MapPost("/api/admin/users/{userId}/impersonate", HandleImpersonate);
app.MapPost("/api/admin/users/{userId}/login-link", HandleLoginLink);
}
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,
defaultPath = u.DefaultPath,
canCreateAgents = u.CanCreateAgents,
canUseCli = u.CanUseCli,
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();
var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : null;
var canCreateAgents = !body.TryGetProperty("canCreateAgents", out var cca) || cca.GetBoolean();
var canUseCli = !body.TryGetProperty("canUseCli", out var cuc) || cuc.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, defaultPath, canCreateAgents, canUseCli);
ctx.Response.StatusCode = 201;
await ctx.Response.WriteAsJsonAsync(new
{
userId = user.UserId,
username = user.Username,
displayName = user.DisplayName,
isAdmin = user.IsAdmin,
defaultPath = user.DefaultPath,
canCreateAgents = user.CanCreateAgents,
canUseCli = user.CanUseCli,
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;
var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : user.DefaultPath;
var canCreateAgents = body.TryGetProperty("canCreateAgents", out var cca) ? cca.GetBoolean() : user.CanCreateAgents;
var canUseCli = body.TryGetProperty("canUseCli", out var cuc) ? cuc.GetBoolean() : user.CanUseCli;
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, defaultPath, canCreateAgents, canUseCli);
var updated = await db.GetUser(userId);
await ctx.Response.WriteAsJsonAsync(new
{
userId = updated.UserId,
username = updated.Username,
displayName = updated.DisplayName,
isAdmin = updated.IsAdmin,
defaultPath = updated.DefaultPath,
canCreateAgents = updated.CanCreateAgents,
canUseCli = updated.CanUseCli,
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 });
}
private static async System.Threading.Tasks.Task HandleImpersonate(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;
}
// Token für den Zieluser erstellen und als Cookie setzen
var token = auth.CreateToken(user.UserId);
ctx.Response.Cookies.Append("gott_token", token, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Path = "/",
MaxAge = System.TimeSpan.FromDays(365)
});
await ctx.Response.WriteAsJsonAsync(new
{
userId = user.UserId,
username = user.Username,
displayName = user.DisplayName,
isAdmin = user.IsAdmin
});
}
private static async System.Threading.Tasks.Task HandleLoginLink(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 token = auth.CreateToken(user.UserId);
await ctx.Response.WriteAsJsonAsync(new { token });
}
}