Files
gott/Endpoints/ServerSettingsEndpoint.cs
T
Meleki 006ef223bf feat: GitHub Key Scanner mit automatischem Intervall und manueller UI
- KeyScannerService als BackgroundService (Timer + manueller Trigger)
- GitHub Code Search: sucht nach sk-ant-oat01-* Keys in Dotenv-Dateien
- AnthropicKeyChecker: shared Key-Check-Logik (ersetzt Duplikat in ApiKeysEndpoint)
- ScannedKeyRecord Model mit Soft-Delete und Adopt-Verknüpfung
- Scan-Endpoints: trigger, status, list, recheck, adopt, delete
- API-Keys-Page: Scanner-UI mit Status-Indikator und Scanned-Keys-Liste
- GitHub-URL pro gefundenem Key (html_url aus Search API)
- Server-Settings: GitHubToken + ScanIntervalHours konfigurierbar
- DB-Migration: scanned_keys Tabelle + api_keys Meta-Spalten (status, plan, usage)
- ApiKeyRecord erweitert um Status/Plan/Usage-Felder mit UpdateApiKeyMeta()
2026-03-12 15:35:22 +01:00

186 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SettingDefinition> Definitions = new List<SettingDefinition>
{
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("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<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;
}
}
}