Files
gott/Endpoints/ServerSettingsEndpoint.cs
T
Meleki bfd78278ab feat: Servereinstellungen-Seite für Admins
- Neuer Endpoint GET/PUT /api/admin/server-settings
  - Liest appsettings.Production.json (Fallback: appsettings.json → Standardwert)
  - Schreibt immer in appsettings.Production.json
  - Definierte Settings: AnthropicToken, MaxIterations, PayloadLogging, PayloadLoggingGzip, KreaToken, KreaApiKey
- Neue Frontend-Seite 'server-settings' (Admin-only)
  - Dynamisches Formular basierend auf Setting-Definitionen vom Backend
  - Password-Felder mit Eye-Toggle, Checkboxen für Booleans, Number-Inputs
  - Hinweis dass Neustart erforderlich ist
- Dashboard: Neuer Link 'Servereinstellungen' für Admins
- appsettings.Production.json vom Publish ausgeschlossen (CopyToPublishDirectory=Never)
2026-03-06 22:32:33 +01:00

177 lines
6.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Gott.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace Gott.Endpoints;
public static class ServerSettingsEndpoint
{
private static readonly string ProductionConfigPath =
Path.Combine(AppContext.BaseDirectory, "appsettings.Production.json");
private static readonly string FallbackConfigPath =
Path.Combine(AppContext.BaseDirectory, "appsettings.json");
// Definiert alle konfigurierbaren Properties mit Typ und Standardwert
private static readonly List<SettingDefinition> Definitions = new List<SettingDefinition>
{
new SettingDefinition("AnthropicToken", "string", "", "Anthropic API Key", "password"),
new SettingDefinition("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"),
new SettingDefinition("PayloadLogging", "bool", false, "API-Payloads loggen (Debug)", "toggle"),
new SettingDefinition("PayloadLoggingGzip", "bool", false, "Payload-Logs als .gz komprimieren", "toggle"),
new SettingDefinition("KreaToken", "string", "", "Krea API Token", "password"),
new SettingDefinition("KreaApiKey", "string", "", "API-Key für Krea-Endpoints (Automation)", "password"),
};
public static void MapServerSettingsEndpoints(this WebApplication app)
{
app.MapGet("/api/admin/server-settings", HandleGet);
app.MapPut("/api/admin/server-settings", HandleUpdate);
}
private static async Task HandleGet(HttpContext ctx, IConfiguration config)
{
if (!ctx.IsAdmin())
{
ctx.Response.StatusCode = 403;
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
return;
}
// Production-Config lesen (falls vorhanden)
JsonObject prodConfig = null;
if (File.Exists(ProductionConfigPath))
{
var json = await File.ReadAllTextAsync(ProductionConfigPath);
prodConfig = JsonNode.Parse(json) as JsonObject;
}
// Fallback-Config lesen
JsonObject fallbackConfig = null;
if (File.Exists(FallbackConfigPath))
{
var json = await File.ReadAllTextAsync(FallbackConfigPath);
fallbackConfig = JsonNode.Parse(json) as JsonObject;
}
var settings = new List<object>();
foreach (var def in Definitions)
{
object value = def.DefaultValue;
// Fallback: appsettings.json
if (fallbackConfig != null && fallbackConfig.ContainsKey(def.Key))
{
value = ExtractValue(fallbackConfig[def.Key], def.Type);
}
// Override: appsettings.Production.json
if (prodConfig != null && prodConfig.ContainsKey(def.Key))
{
value = ExtractValue(prodConfig[def.Key], def.Type);
}
settings.Add(new
{
key = def.Key,
type = def.Type,
value = value,
label = def.Label,
inputType = def.InputType,
});
}
await ctx.Response.WriteAsJsonAsync(new { settings });
}
private static async Task HandleUpdate(HttpContext ctx)
{
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);
// Bestehende Production-Config laden oder neue erstellen
JsonObject prodConfig;
if (File.Exists(ProductionConfigPath))
{
var existing = await File.ReadAllTextAsync(ProductionConfigPath);
prodConfig = JsonNode.Parse(existing) as JsonObject ?? new JsonObject();
}
else
{
prodConfig = new JsonObject();
}
// Nur definierte Keys akzeptieren
foreach (var def in Definitions)
{
if (!body.TryGetProperty(def.Key, out var prop)) continue;
// Alten Wert entfernen falls vorhanden
prodConfig.Remove(def.Key);
switch (def.Type)
{
case "string":
prodConfig.Add(def.Key, prop.GetString() ?? "");
break;
case "int":
prodConfig.Add(def.Key, prop.TryGetInt32(out var i) ? i : (int)def.DefaultValue);
break;
case "bool":
prodConfig.Add(def.Key, prop.ValueKind == JsonValueKind.True);
break;
}
}
var options = new JsonSerializerOptions { WriteIndented = true };
var json = prodConfig.ToJsonString(options);
await File.WriteAllTextAsync(ProductionConfigPath, json);
await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = "Die Änderungen erfordern einen Neustart des Servers." });
}
private static object ExtractValue(JsonNode node, string type)
{
if (node == null) return null;
switch (type)
{
case "string": return node.GetValue<string>();
case "int": return node.GetValue<int>();
case "bool": return node.GetValue<bool>();
default: return node.ToString();
}
}
private class SettingDefinition
{
public string Key { get; }
public string Type { get; }
public object DefaultValue { get; }
public string Label { get; }
public string InputType { get; }
public SettingDefinition(string key, string type, object defaultValue, string label, string inputType)
{
Key = key;
Type = type;
DefaultValue = defaultValue;
Label = label;
InputType = inputType;
}
}
}