using System.ComponentModel; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; namespace Predictalytics.Hosting; /// /// Laufzeit-Konfiguration der Anwendung. /// /// Die -Attribute sind plattformneutral (Teil der /// Basisbibliothek). Die Avalonia-Shell nutzt sie fuer Beschriftungen und Hilfetexte /// der Einstellungsansicht. /// /// public sealed class PredictalyticsOptions { private const string FileName = "settings.json"; // ─── Webserver ─── [Category("Webserver")] [DisplayName("Port")] [Description("Der Port, über den die WebUI und API erreichbar sind.")] [DefaultValue(5000)] public int WebserverPort { get; set; } = 5000; [Category("Webserver")] [DisplayName("Bind-Adresse")] [Description("Netzwerkschnittstelle, auf der Kestrel lauscht. 'localhost' = nur lokal (Standard), " + "'0.0.0.0' = von außen erreichbar. Bei 0.0.0.0 muss die API abgesichert werden!")] [DefaultValue("localhost")] public string WebserverHost { get; set; } = "localhost"; [Category("Webserver")] [DisplayName("Database Debug")] [Description("Wenn aktiv, werden detaillierte Verbindungsinformationen im Terminal angezeigt.")] [DefaultValue(false)] public bool DbConnectionDebug { get; set; } = false; [Category("Webserver")] [DisplayName("API-Token (Steuerbefehle)")] [Description("Schützt die schreibenden Endpunkte (Jobs, Trader-Steuerung, Dev). Aufrufer müssen dann " + "den Header X-Predictalytics-Key mitschicken. Bei einer Bind-Adresse außerhalb von localhost " + "ist der Token Pflicht — ohne ihn werden die Steuerbefehle gar nicht erst bereitgestellt.")] [PasswordPropertyText(true)] public string ApiControlToken { get; set; } = ""; /// /// Ob die Bind-Adresse den Dienst ueber das Netz erreichbar macht. Bei localhost /// uebernimmt das Betriebssystem die Absicherung, sonst nicht mehr. /// [Browsable(false)] [JsonIgnore] public bool IsPubliclyBound { get { var host = (WebserverHost ?? "").Trim(); return host.Length > 0 && !host.Equals("localhost", StringComparison.OrdinalIgnoreCase) && host != "127.0.0.1" && host != "::1"; } } // ─── Verwaltungs-Webserver (Admin WebGUI) ─── [Category("Verwaltungs-Webserver")] [DisplayName("Admin Webserver aktiv")] [Description("Aktiviert das separate Verwaltungs-WebGUI zur Steuerung und Wartung.")] [DefaultValue(true)] public bool AdminServerEnabled { get; set; } = true; [Category("Verwaltungs-Webserver")] [DisplayName("Port")] [Description("Der Port, über den das Verwaltungs-WebGUI erreichbar ist.")] [DefaultValue(5001)] public int AdminServerPort { get; set; } = 5001; [Category("Verwaltungs-Webserver")] [DisplayName("Bind-Adresse")] [Description("Netzwerkschnittstelle, auf der das Verwaltungs-WebGUI lauscht. 'localhost' = nur lokal (Standard), '0.0.0.0' = netzwerkweit erreichbar.")] [DefaultValue("localhost")] public string AdminServerHost { get; set; } = "localhost"; [Category("Verwaltungs-Webserver")] [DisplayName("Admin-Token")] [Description("Schützt das gesamte Verwaltungs-WebGUI. Ohne diesen Token ist kein Zugriff möglich. Kann neu generiert werden.")] [PasswordPropertyText(true)] public string AdminToken { get; set; } = ""; /// /// Prüft, ob die Admin-Bind-Adresse öffentlich bzw. über das Netzwerk erreichbar ist. /// [Browsable(false)] [JsonIgnore] public bool IsAdminPubliclyBound { get { var host = (AdminServerHost ?? "").Trim(); return host.Length > 0 && !host.Equals("localhost", StringComparison.OrdinalIgnoreCase) && host != "127.0.0.1" && host != "::1"; } } /// Kestrel-Bind-URL für das Admin-WebGUI. [Browsable(false)] [JsonIgnore] public string AdminServerUrl => $"http://{(string.IsNullOrWhiteSpace(AdminServerHost) ? "localhost" : AdminServerHost.Trim())}:{AdminServerPort}"; /// Generiert einen sicheren, kryptografischen Zufallstoken für das Admin-WebGUI. public static string GenerateSecureToken(int byteLength = 24) => Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(byteLength)).ToLowerInvariant(); // ─── Egress ─── private string _egressChannelsText = ""; [Category("Egress (Proxy/IP)")] [DisplayName("Egress Channels")] [Description("Liste der Egress-Kanäle im Format: id|type|value (Zeilengetrennt). " + "Beispiel: prox-1|Proxy|http://user:pass@proxy:8080\nip-1|SourceIp|192.168.1.100")] public string EgressChannelsText { get => _egressChannelsText; set => _egressChannelsText = value ?? ""; } // ─── Deployment Center ─── [Category("Deployment Center")] [DisplayName("Server URL")] [Description("Basis-URL des Deployment Centers. Fest einkompiliert — ein einstellbarer Endpoint würde erlauben, die App auf einen gefälschten Lizenz- oder Update-Server zu zeigen.")] [ReadOnly(true)] [JsonIgnore] public string DcServerUrl => DcConfig.BaseUrl; [Category("Deployment Center")] [DisplayName("API Token")] [Description("Token des Deployment Centers (Authorization: Bearer). Benötigte Rechte: 'watchdog:ping' für Heartbeats, 'bugtracker:report' für das Fehler-Reporting. Ohne Token werden weder Heartbeats noch Fehler gemeldet.")] [PasswordPropertyText(true)] public string DcToken { get; set; } = ""; [Category("Deployment Center")] [DisplayName("Heartbeat aktiv")] [Description("Sendet periodische Heartbeats an den Watchdog des Deployment Centers (Dead-Man's-Switch). Benötigt ein Token.")] [DefaultValue(true)] public bool DcHeartbeatEnabled { get; set; } = true; [Category("Deployment Center")] [DisplayName("Monitor Source")] [Description("Eindeutiger Monitor-Name dieses Dienstes im Watchdog-Dashboard.")] [DefaultValue("Predictalytics")] public string DcSource { get; set; } = "Predictalytics"; [Category("Deployment Center")] [DisplayName("Monitor Instance")] [Description("Instanz-Kennung, falls mehrere Predictalytics-Instanzen laufen.")] [DefaultValue("default")] public string DcInstance { get; set; } = "default"; [Category("Deployment Center")] [DisplayName("Heartbeat-Intervall (Sekunden)")] [Description("Sende-Takt der Heartbeats. Der Evaluator stuft nach dem Doppelten auf 'warning' und nach dem Vierfachen auf 'down'.")] [DefaultValue(60)] public int DcHeartbeatIntervalSeconds { get; set; } = 60; [Category("Deployment Center")] [DisplayName("Fehler melden")] [Description("Meldet Laufzeitfehler (Error/Fatal) an den Fehler-Stream des Deployment Centers. Gleiche Fehler werden dort gruppiert und hochgezählt.")] [DefaultValue(true)] public bool DcErrorReportingEnabled { get; set; } = true; [Category("Deployment Center")] [DisplayName("Update-Prüfung beim Start")] [Description("Prüft beim Start, ob im gewählten Kanal ein neueres Release vorliegt. Installiert wird nichts automatisch.")] [DefaultValue(true)] public bool DcUpdateCheckEnabled { get; set; } = true; [Category("Deployment Center")] [DisplayName("Update-Kanal")] [Description("prod, beta oder dev.")] [DefaultValue("prod")] public string DcUpdateChannel { get; set; } = "prod"; // ─── Database ─── private string _dbServer = "localhost"; private string _dbName = ""; private string _dbUser = ""; private string _dbPassword = ""; [Category("Database")] [DisplayName("Server")] public string DbServer { get => _dbServer; set => _dbServer = string.IsNullOrWhiteSpace(value) ? _dbServer : value.Trim(); } [Category("Database")] [DisplayName("Database")] public string DbName { get => _dbName; set => _dbName = string.IsNullOrWhiteSpace(value) ? _dbName : value.Trim(); } [Category("Database")] [DisplayName("User")] public string DbUser { get => _dbUser; set => _dbUser = string.IsNullOrWhiteSpace(value) ? _dbUser : value.Trim(); } [Category("Database")] [DisplayName("Password")] [PasswordPropertyText(true)] public string DbPassword { get => _dbPassword; set => _dbPassword = string.IsNullOrWhiteSpace(value) ? _dbPassword : value.Trim(); } [Category("Database")] [DisplayName("SSL Mode")] [Description("Verschlüsselung der MySQL-Verbindung. 'None' = unverschlüsselt (bisheriges Verhalten), " + "'Preferred' = verschlüsselt wenn der Server es unterstützt. Bei entfernten Servern 'Preferred' empfohlen.")] [DefaultValue(MySqlConnector.MySqlSslMode.None)] public MySqlConnector.MySqlSslMode DbSslMode { get; set; } = MySqlConnector.MySqlSslMode.None; [Browsable(false)] [JsonIgnore] public string ConnectionString { get { var builder = new MySqlConnector.MySqlConnectionStringBuilder { Server = DbServer?.Trim(), Database = DbName?.Trim(), UserID = DbUser?.Trim(), Password = DbPassword?.Trim(), AllowPublicKeyRetrieval = true, SslMode = DbSslMode, Pooling = true, MinimumPoolSize = 0, MaximumPoolSize = 100 }; return builder.ConnectionString; } } /// Kestrel-Bind-URL aus Host und Port. [Browsable(false)] [JsonIgnore] public string WebserverUrl => $"http://{(string.IsNullOrWhiteSpace(WebserverHost) ? "localhost" : WebserverHost.Trim())}:{WebserverPort}"; // ─── Persistenz ─── /// /// Ablageort der Einstellungen: %APPDATA%\Predictalytics unter Windows, /// ~/.config/Predictalytics unter Linux. Bewusst nicht neben der Programmdatei — /// deren Verzeichnis ist unter Linux ueblicherweise nicht beschreibbar. /// public static string SettingsDirectory => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Predictalytics"); public static string SettingsFilePath => Path.Combine(SettingsDirectory, FileName); /// Alter Ablageort neben der Programmdatei (bis einschliesslich der WinForms-Fassung). private static string LegacySettingsFilePath => Path.Combine(AppContext.BaseDirectory, FileName); public static PredictalyticsOptions Load() { // Neuer Ort hat Vorrang; sonst einmalig aus dem alten uebernehmen, damit // bestehende Zugangsdaten beim Umstieg nicht verloren gehen. var path = File.Exists(SettingsFilePath) ? SettingsFilePath : File.Exists(LegacySettingsFilePath) ? LegacySettingsFilePath : null; if (path == null) { var fresh = new PredictalyticsOptions(); fresh.Save(); return fresh; } try { var options = JsonSerializer.Deserialize(File.ReadAllText(path)) ?? new PredictalyticsOptions(); var modified = path == LegacySettingsFilePath; if (string.IsNullOrWhiteSpace(options.AdminToken)) { options.AdminToken = GenerateSecureToken(); modified = true; } if (modified) options.Save(); // Migration oder Initial-Token festschreiben return options; } catch { var fallback = new PredictalyticsOptions(); if (string.IsNullOrWhiteSpace(fallback.AdminToken)) { fallback.AdminToken = GenerateSecureToken(); try { fallback.Save(); } catch { } } return fallback; } } /// /// Ermöglicht das Übersteuern von Einstellungen über appsettings.json oder Umgebungsvariablen. /// public void ApplyConfigurationOverrides(Microsoft.Extensions.Configuration.IConfiguration? configuration) { if (configuration == null) return; var section = configuration.GetSection("Admin"); if (section.Exists()) { if (bool.TryParse(section["Enabled"], out var enabled)) AdminServerEnabled = enabled; if (int.TryParse(section["Port"], out var port) && port > 0) AdminServerPort = port; if (!string.IsNullOrWhiteSpace(section["Host"])) AdminServerHost = section["Host"]!.Trim(); if (!string.IsNullOrWhiteSpace(section["Token"])) AdminToken = section["Token"]!.Trim(); } } public void Save() { Directory.CreateDirectory(SettingsDirectory); File.WriteAllText(SettingsFilePath, JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true })); } }