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 { // Pfade werden beim MapEndpoints gesetzt – basierend auf ContentRootPath, // da .NET die Config-Dateien vom ContentRoot lädt (= WorkingDirectory), // nicht von AppContext.BaseDirectory (= Executable-Verzeichnis). private static string ProductionConfigPath; private static string FallbackConfigPath; // Definiert alle konfigurierbaren Properties mit Typ und Standardwert private static readonly List Definitions = new List { new SettingDefinition("Port", "int", 19000, "Server-Port (erfordert Neustart)", "number"), new SettingDefinition("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"), new SettingDefinition("MaxHistory", "int", 120, "Max History-Messages pro Request", "number"), new SettingDefinition("ReminderInterval", "int", 30, "System-Prompt Reminder alle N Messages (0 = deaktiviert)", "number"), new SettingDefinition("ExecuteTimeout", "int", 180, "Timeout für Shell-Befehle in Sekunden", "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"), new SettingDefinition("GitHubToken", "string", "", "GitHub PAT für Key-Scanner", "password"), new SettingDefinition("SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner)", "password"), new SettingDefinition("ProxyUrl", "string", "", "HTTP/SOCKS Proxy für alle Requests (z.B. socks5://127.0.0.1:1080)", "text"), new SettingDefinition("ScanIntervalHours", "int", 0, "Key-Scanner Intervall in Stunden (0 = nur manuell)", "number"), }; public static void MapServerSettingsEndpoints(this WebApplication app) { var contentRoot = app.Environment.ContentRootPath; ProductionConfigPath = Path.Combine(contentRoot, "appsettings.Production.json"); FallbackConfigPath = Path.Combine(contentRoot, "appsettings.json"); 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(); 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(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(); case "int": return node.GetValue(); case "bool": return node.GetValue(); 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; } } }