290 lines
11 KiB
C#
290 lines
11 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Nodes;
|
||
using Microsoft.Data.Sqlite;
|
||
|
||
namespace Gott.Services;
|
||
|
||
/// <summary>
|
||
/// Zentraler Settings-Service. Liest/schreibt aus der DB-Tabelle 'server_settings'.
|
||
/// In-Memory-Cache für schnellen Zugriff. Kein Neustart bei Änderungen nötig.
|
||
/// </summary>
|
||
public class ServerSettingsService
|
||
{
|
||
private readonly string _dbPath;
|
||
private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
|
||
private readonly object _lock = new object();
|
||
|
||
// Definiert alle Settings mit Typ, Default und Beschreibung
|
||
public static readonly List<SettingDefinition> Definitions = new List<SettingDefinition>
|
||
{
|
||
new SettingDefinition( "ProxyUrl", "string", "", "HTTP/SOCKS Proxy (erfordert Neustart)", "text" ),
|
||
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( "GeminiApiKey", "string", "", "Gemini API-Key (für Gemini-Provider im Chat)", "password" ),
|
||
new SettingDefinition( "GitHubToken", "string", "", "GitHub PAT für Key-Scanner und Code-Search Tool", "password" ),
|
||
new SettingDefinition( "SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner + Tool)", "password" ),
|
||
new SettingDefinition( "ScanDailyHour", "int", "-1", "Key-Scanner Full-Scan Uhrzeit (0-23, -1 = deaktiviert)", "number" ),
|
||
new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ),
|
||
new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ),
|
||
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
|
||
};
|
||
|
||
public ServerSettingsService(string dbPath)
|
||
{
|
||
_dbPath = dbPath;
|
||
EnsureTable();
|
||
LoadCache();
|
||
}
|
||
|
||
private void EnsureTable()
|
||
{
|
||
using var conn = new SqliteConnection($"Data Source={_dbPath}");
|
||
conn.Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = @"
|
||
CREATE TABLE IF NOT EXISTS server_settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL DEFAULT '',
|
||
type TEXT NOT NULL DEFAULT 'string'
|
||
)";
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
|
||
private void LoadCache()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_cache.Clear();
|
||
using var conn = new SqliteConnection($"Data Source={_dbPath}");
|
||
conn.Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = "SELECT key, value FROM server_settings";
|
||
using var reader = cmd.ExecuteReader();
|
||
while (reader.Read())
|
||
{
|
||
_cache[reader.GetString(0)] = reader.GetString(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Entfernt veraltete Settings aus der DB die nicht mehr in Definitions existieren.
|
||
/// </summary>
|
||
private void CleanupObsoleteSettings()
|
||
{
|
||
var obsoleteKeys = new[] { "ScanIntervalHours" };
|
||
using var conn = new SqliteConnection($"Data Source={_dbPath}");
|
||
conn.Open();
|
||
foreach (var key in obsoleteKeys)
|
||
{
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = "DELETE FROM server_settings WHERE key = @key";
|
||
cmd.Parameters.AddWithValue("@key", key);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
lock (_lock)
|
||
{
|
||
foreach (var key in obsoleteKeys) _cache.Remove(key);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Migriert bestehende Werte aus appsettings.Production.json in die DB (einmalig).
|
||
/// Nur Settings die noch NICHT in der DB sind werden übernommen.
|
||
/// </summary>
|
||
public void MigrateFromAppSettings(string contentRootPath)
|
||
{
|
||
CleanupObsoleteSettings();
|
||
var prodPath = Path.Combine(contentRootPath, "appsettings.Production.json");
|
||
if (!File.Exists(prodPath)) return;
|
||
|
||
try
|
||
{
|
||
var json = File.ReadAllText(prodPath);
|
||
var obj = JsonNode.Parse(json) as JsonObject;
|
||
if (obj == null) return;
|
||
|
||
// Port bleibt in appsettings – nicht migrieren
|
||
var skipKeys = new HashSet<string> { "Port", "Logging", "AllowedHosts" };
|
||
|
||
foreach (var def in Definitions)
|
||
{
|
||
if (skipKeys.Contains(def.Key)) continue;
|
||
if (!obj.ContainsKey(def.Key)) continue;
|
||
|
||
// Nur migrieren wenn nicht schon in DB
|
||
lock (_lock)
|
||
{
|
||
if (_cache.ContainsKey(def.Key)) continue;
|
||
}
|
||
|
||
var node = obj[def.Key];
|
||
string value;
|
||
switch (def.Type)
|
||
{
|
||
case "bool":
|
||
value = (node?.GetValue<bool>() ?? false) ? "true" : "false";
|
||
break;
|
||
case "int":
|
||
value = (node?.GetValue<int>() ?? 0).ToString();
|
||
break;
|
||
default:
|
||
value = node?.GetValue<string>() ?? "";
|
||
break;
|
||
}
|
||
|
||
// Default-Werte nicht migrieren (kein Müll in DB)
|
||
if (value == def.DefaultValue) continue;
|
||
|
||
Set(def.Key, value);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Fehler bei Migration ignorieren – Settings sind halt Default
|
||
}
|
||
}
|
||
|
||
// ─── Getter ────────────────────────────────────────────────
|
||
|
||
public string Get(string key, string defaultValue = "")
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _cache.TryGetValue(key, out var val) ? val : defaultValue;
|
||
}
|
||
}
|
||
|
||
public int GetInt(string key, int defaultValue = 0)
|
||
{
|
||
var raw = Get(key, null);
|
||
return raw != null && int.TryParse(raw, out var v) ? v : defaultValue;
|
||
}
|
||
|
||
public bool GetBool(string key, bool defaultValue = false)
|
||
{
|
||
var raw = Get(key, null);
|
||
if (raw == null) return defaultValue;
|
||
return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1";
|
||
}
|
||
|
||
// ─── Setter ────────────────────────────────────────────────
|
||
|
||
public void Set(string key, string value)
|
||
{
|
||
// Typ aus Definitions holen
|
||
var def = Definitions.Find(d => d.Key == key);
|
||
var type = def?.Type ?? "string";
|
||
|
||
using var conn = new SqliteConnection($"Data Source={_dbPath}");
|
||
conn.Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = @"
|
||
INSERT INTO server_settings (key, value, type) VALUES (@key, @value, @type)
|
||
ON CONFLICT(key) DO UPDATE SET value = @value, type = @type";
|
||
cmd.Parameters.AddWithValue("@key", key);
|
||
cmd.Parameters.AddWithValue("@value", value ?? "");
|
||
cmd.Parameters.AddWithValue("@type", type);
|
||
cmd.ExecuteNonQuery();
|
||
|
||
lock (_lock)
|
||
{
|
||
_cache[key] = value ?? "";
|
||
}
|
||
}
|
||
|
||
public void SetInt(string key, int value) => Set(key, value.ToString());
|
||
public void SetBool(string key, bool value) => Set(key, value ? "true" : "false");
|
||
|
||
public string[] GetStringArray(string key, string defaultCsv = "")
|
||
{
|
||
var raw = Get(key, null);
|
||
if (raw == null)
|
||
{
|
||
var def = Definitions.Find(d => d.Key == key);
|
||
raw = def?.DefaultValue ?? defaultCsv;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
|
||
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||
}
|
||
|
||
public void SetStringArray(string key, string[] values)
|
||
{
|
||
Set(key, string.Join(",", values ?? Array.Empty<string>()));
|
||
}
|
||
|
||
// ─── Bulk-Operationen (für Endpoint) ───────────────────────
|
||
|
||
public List<object> GetAll()
|
||
{
|
||
var result = new List<object>();
|
||
foreach (var def in Definitions)
|
||
{
|
||
object value;
|
||
switch (def.Type)
|
||
{
|
||
case "int":
|
||
value = GetInt(def.Key, int.TryParse(def.DefaultValue, out var dv) ? dv : 0);
|
||
break;
|
||
case "bool":
|
||
value = GetBool(def.Key, def.DefaultValue.Equals("true", StringComparison.OrdinalIgnoreCase));
|
||
break;
|
||
case "string_array":
|
||
value = GetStringArray(def.Key, def.DefaultValue);
|
||
break;
|
||
default:
|
||
value = Get(def.Key, def.DefaultValue);
|
||
break;
|
||
}
|
||
|
||
// Zusatz-Metadaten für bestimmte Settings
|
||
object options = null;
|
||
if (def.Key == "ScannerKeyTypes")
|
||
{
|
||
options = KeyPatterns.AllTypes.Select(t => new { id = t, name = KeyPatterns.DisplayName(t) }).ToArray();
|
||
}
|
||
|
||
result.Add(new
|
||
{
|
||
key = def.Key,
|
||
type = def.Type,
|
||
value = value,
|
||
label = def.Label,
|
||
inputType = def.InputType,
|
||
options = options,
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ─── Definition ────────────────────────────────────────────
|
||
|
||
public class SettingDefinition
|
||
{
|
||
public string Key { get; }
|
||
public string Type { get; }
|
||
public string DefaultValue { get; }
|
||
public string Label { get; }
|
||
public string InputType { get; }
|
||
|
||
public SettingDefinition(string key, string type, string defaultValue, string label, string inputType)
|
||
{
|
||
Key = key;
|
||
Type = type;
|
||
DefaultValue = defaultValue;
|
||
Label = label;
|
||
InputType = inputType;
|
||
}
|
||
}
|
||
}
|