using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using System.Windows.Forms; using PolyTrader.Core.Persistence; using PolyTrader.Core.Security; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTraderSharp.Ui.Views { /// /// Core-Settings-Fenster: allgemeine Server-Einstellungen (PropertyGrid) und die /// allgemeine Verwaltung der Polymarket-Accounts (Wallets, API-Keys …). /// Die copytrading-spezifischen Detail-Limits werden NICHT hier, sondern im /// Copytrading-Modul-View bearbeitet (AccountState ist bewusst general-only). /// public partial class SettingsView : Form { private const string SettingsPath = "server_settings.xml"; private ServerSettings _settings = new(); private MullvadVpnService? _vpn; private WatchdogHeartbeatService? _watchdog; private TerminalLogger? _logger; private IAccountRepository? _accountRepo; private TradingState? _state; private BindingList _accounts = new(); public SettingsView() { InitializeComponent(); btn_save.Click += (_, _) => Save(); btn_loadsettings.Click += (_, _) => Reload(); btnGenMasterKey.Click += (_, _) => GenerateMasterKey(); btnSetOpenRouterKey.Click += (_, _) => SetOpenRouterKey(); btnSetWatchdogToken.Click += (_, _) => SetWatchdogToken(); btnTestWatchdog.Click += async (_, _) => await TestWatchdogAsync(); btnSetLicenseKey.Click += (_, _) => SetLicenseKey(); UpdateMasterKeyButtonState(); btnAccNew.Click += (_, _) => AddAccount(); btnAccDelete.Click += (_, _) => DeleteAccount(); dgvAccounts.SelectionChanged += (_, _) => { pgAccount.SelectedObject = dgvAccounts.CurrentRow?.DataBoundItem as AccountState; }; pgAccount.PropertyValueChanged += (_, _) => { if (pgAccount.SelectedObject is AccountState acc) SaveAccount(acc); }; } public void Initialize(MullvadVpnService vpn, TerminalLogger logger, IAccountRepository accountRepo, TradingState state, WatchdogHeartbeatService? watchdog = null) { _vpn = vpn; _logger = logger; _accountRepo = accountRepo; _state = state; _watchdog = watchdog; Reload(); LoadAccounts(); } // ===== Server-Settings ===== private void Reload() { _settings = ServerSettings.Load(SettingsPath); propertyGrid.SelectedObject = _settings; } private void Save() { try { _settings.Save(SettingsPath); _vpn?.ReloadSettings(); _watchdog?.ReloadSettings(); _logger?.Info("Server-Einstellungen gespeichert und Services neu geladen."); MessageBox.Show("Server-Einstellungen gespeichert.", "Erfolg", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Fehler beim Speichern: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ===== Polymarket Accounts (allgemeine Einstellungen) ===== private void LoadAccounts() { if (_state == null) return; _accounts = new BindingList(_state.Accounts.Values.OrderBy(a => a.AccountId).ToList()); dgvAccounts.DataSource = _accounts; pgAccount.SelectedObject = dgvAccounts.CurrentRow?.DataBoundItem as AccountState; } private void AddAccount() { if (_state == null || _accountRepo == null) return; int newId = _state.Accounts.Count > 0 ? _state.Accounts.Keys.Max() + 1 : 1; var acc = new AccountState { AccountId = newId, Name = "Neuer Account" }; _state.Accounts[acc.AccountId] = acc; _accountRepo.Upsert(acc); _accounts.Add(acc); dgvAccounts.CurrentCell = dgvAccounts.Rows[dgvAccounts.Rows.Count - 1].Cells[0]; } private void DeleteAccount() { if (_state == null || _accountRepo == null) return; if (dgvAccounts.CurrentRow?.DataBoundItem is not AccountState acc) return; if (MessageBox.Show($"Account '{acc.Name}' (ID {acc.AccountId}) wirklich löschen?", "Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; _state.Accounts.TryRemove(acc.AccountId, out _); _accountRepo.Delete(acc.AccountId); _accounts.Remove(acc); } private void SaveAccount(AccountState acc) { _accountRepo?.Upsert(acc); _state?.Accounts.AddOrUpdate(acc.AccountId, acc, (_, _) => acc); dgvAccounts.Refresh(); } // ===== Watchdog ===== /// /// Setzt/entfernt den Watchdog-Agent-Token über eine maskierte Eingabe – damit das Secret /// nicht offen im PropertyGrid steht und die server_settings.xml nicht per Hand bearbeitet /// werden muss. Ist ein Master-Key gesetzt, wird der Token verschlüsselt abgelegt. /// Wird sofort gespeichert, damit der Test-Button und der laufende Dienst ihn direkt nutzen. /// private void SetWatchdogToken() { bool exists = !string.IsNullOrWhiteSpace(_settings.WatchdogToken); string? token = PromptForSecret("Watchdog Agent-Token", "Agent-Token aus dem Watchdog-Admin eingeben (Header X-Watchdog-Key).\n" + "Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" + (exists ? $"Aktuell: {_settings.WatchdogTokenStatus}." : "Aktuell ist KEIN Token gesetzt.")); if (token == null) return; // Abbruch try { if (string.IsNullOrWhiteSpace(token)) { _settings.WatchdogToken = ""; _settings.Save(SettingsPath); _watchdog?.ReloadSettings(); propertyGrid.Refresh(); _logger?.Info("Watchdog-Agent-Token entfernt."); MessageBox.Show("Watchdog-Token entfernt.", "Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // Protect() gibt ohne Master-Key den Klartext unverändert zurück – kein stiller // Sicherheitsverlust, der Nutzer wird darauf hingewiesen. _settings.WatchdogToken = SecretProtection.Protect(token.Trim()); _settings.Save(SettingsPath); _watchdog?.ReloadSettings(); propertyGrid.Refresh(); bool encrypted = SecretProtection.IsEncrypted(_settings.WatchdogToken); _logger?.Info($"Watchdog-Agent-Token gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")})."); MessageBox.Show( "Watchdog-Token gespeichert und sofort aktiv.\n\n" + (encrypted ? "Der Token liegt mit dem Master-Key verschlüsselt in server_settings.xml." : "Hinweis: Es ist kein Master-Key gesetzt – der Token liegt im Klartext in der " + "(gitignorierten) server_settings.xml. Mit „Master-Key erzeugen\" kannst du das ändern."), "Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Fehler beim Speichern des Watchdog-Tokens: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// Sendet einen Heartbeat mit den GESPEICHERTEN Einstellungen und meldet das Ergebnis. /// Ungespeicherte Änderungen im PropertyGrid wirken bewusst nicht – sonst würde ein /// erfolgreicher Test eine Konfiguration bestätigen, die so nicht auf der Platte liegt. /// private async Task TestWatchdogAsync() { if (_watchdog == null) { MessageBox.Show("Der Watchdog-Dienst ist nicht verfügbar.", "Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } btnTestWatchdog.Enabled = false; try { var result = await _watchdog.SendHeartbeatAsync( "ok", "Test-Heartbeat aus dem PolyTrader-Settings-Fenster"); if (result.Success) { _logger?.Info($"✅ Watchdog-Test-Heartbeat erfolgreich gesendet ({result.Detail})."); MessageBox.Show( $"Test-Heartbeat erfolgreich gesendet ({result.Detail}).\n\n" + "Der Monitor sollte im Watchdog-Dashboard jetzt auf 'up' stehen.", "Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { _logger?.Warning($"⚠️ Watchdog-Test-Heartbeat fehlgeschlagen: {result.Detail}"); MessageBox.Show( $"Test-Heartbeat fehlgeschlagen:\n\n{result.Detail}\n\n" + "Bitte URL, Agent-Token und Source prüfen und die Einstellungen vorher speichern.", "Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Error); } } finally { btnTestWatchdog.Enabled = true; } } // ===== Lizenz (LicenseLabrador) ===== /// /// Öffnet denselben Lizenzdialog wie der Programmstart (Hardware-ID + Server-Validierung), /// nur im Verwalten-Modus. Ein erfolgreich validierter Schlüssel wird – bei gesetztem /// Master-Key verschlüsselt – in server_settings.xml gespeichert und gilt ab dem nächsten Start. /// private void SetLicenseKey() { string current = string.Empty; try { current = SecretProtection.Unprotect(_settings.LicenseKey); } catch { /* nicht lesbar → als leer behandeln */ } LicenseLabrador.Client.LicenseClient client; try { client = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig()); } catch (Exception ex) { MessageBox.Show($"Lizenzprüfung nicht verfügbar: {ex.Message}", "Lizenz", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } using var dlg = new LicenseDialog(client, current, initialResult: null, startupContext: false); dlg.ShowDialog(this); if (dlg.ValidatedResult == null || !dlg.ValidatedResult.IsUsable || string.IsNullOrWhiteSpace(dlg.ValidatedKey)) return; // abgebrochen oder nicht validiert – nichts speichern try { _settings.LicenseKey = SecretProtection.Protect(dlg.ValidatedKey!.Trim()); _settings.Save(SettingsPath); propertyGrid.Refresh(); bool encrypted = SecretProtection.IsEncrypted(_settings.LicenseKey); _logger?.Info($"Lizenz validiert und gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")})."); MessageBox.Show( "Lizenz validiert und gespeichert.\n\n" + (encrypted ? "Der Schlüssel liegt mit dem Master-Key verschlüsselt in server_settings.xml." : "Hinweis: Es ist kein Master-Key gesetzt – der Schlüssel liegt im Klartext in der " + "(gitignorierten) server_settings.xml."), "Lizenz", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Fehler beim Speichern des Lizenzschlüssels: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // ===== Master-Key (at-rest-Verschlüsselung, F1) ===== /// Pfad der Master-Key-Datei – identisch zu Program.cs (App-Ordner, gitignored). private static string MasterKeyFilePath => Path.Combine(AppContext.BaseDirectory, "master.key"); /// Existiert bereits ein Master-Key (Umgebungsvariable ODER Datei)? private static bool MasterKeyExists() => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY")) || File.Exists(MasterKeyFilePath); /// Button nur aktiv, solange KEIN Master-Key existiert (Überschreiben = Lockout-Gefahr). private void UpdateMasterKeyButtonState() => btnGenMasterKey.Enabled = !MasterKeyExists(); private void GenerateMasterKey() { // Sicherheitsnetz gegen Race/Doppelklick: einen bestehenden Key NIEMALS überschreiben. if (MasterKeyExists()) { MessageBox.Show( "Es existiert bereits ein Master-Key – Erzeugung abgebrochen. Ein Überschreiben würde den " + "Zugriff auf bereits verschlüsselte Wallet-Keys unwiederbringlich zerstören.", "Master-Key vorhanden", MessageBoxButtons.OK, MessageBoxIcon.Warning); UpdateMasterKeyButtonState(); return; } try { byte[] keyBytes = RandomNumberGenerator.GetBytes(32); // 256-Bit-Schlüssel File.WriteAllText(MasterKeyFilePath, Convert.ToBase64String(keyBytes)); _logger?.Info("🔐 Master-Key erzeugt und in master.key gespeichert. At-rest-Verschlüsselung wird beim nächsten Start aktiv."); MessageBox.Show( "Ein zufälliger 32-Byte-Master-Key wurde erzeugt und in der Datei 'master.key' (App-Ordner, gitignored) gespeichert.\n\n" + "WICHTIG:\n" + "• Sichere diese Datei SOFORT separat und sicher (z. B. Passwort-Manager / Offline-Backup).\n" + "• Master-Key-Verlust = KEIN Zugriff mehr auf die verschlüsselten Wallet-Keys!\n" + "• Die Verschlüsselung der Account-Credentials wird beim nächsten Programmstart aktiv.", "Master-Key erzeugt", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Fehler beim Erzeugen des Master-Keys: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } UpdateMasterKeyButtonState(); // nach Erzeugung deaktivieren } // ===== OpenRouter-API-Key (Supervisor) ===== /// /// Setzt/entfernt den OpenRouter-API-Key des Supervisors. Ablage in der gitignorierten Datei /// openrouter.key (genau das liest der OpenRouterClient) – kein Secret im Klartext in /// server_settings.xml. Der Nutzer gibt den Key selbst maskiert ein. /// private void SetOpenRouterKey() { string keyFile = Path.Combine(AppContext.BaseDirectory, "openrouter.key"); bool exists = File.Exists(keyFile) || !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_OPENROUTER_KEY")); string? key = PromptForSecret("OpenRouter-API-Key", "OpenRouter-API-Key für den Supervisor eingeben.\n" + "Wird in der gitignorierten Datei 'openrouter.key' gespeichert (leer = entfernen).\n" + (exists ? "Aktuell ist bereits ein Key hinterlegt." : "Aktuell ist KEIN Key gesetzt.")); if (key == null) return; // Abbruch try { if (string.IsNullOrWhiteSpace(key)) { if (File.Exists(keyFile)) File.Delete(keyFile); _logger?.Info("OpenRouter-Key aus openrouter.key entfernt."); MessageBox.Show("OpenRouter-Key entfernt.", "OpenRouter", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { File.WriteAllText(keyFile, key.Trim()); _logger?.Info("OpenRouter-Key gespeichert (openrouter.key)."); MessageBox.Show("OpenRouter-Key gespeichert. Wirkt beim nächsten Programmstart bzw. der nächsten Supervisor-Anfrage.\n\n" + "Tipp: bei OpenRouter ein Spend-Limit für diesen Key setzen (separater Key für den Supervisor empfohlen).", "OpenRouter", MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (Exception ex) { MessageBox.Show($"Fehler beim Speichern des OpenRouter-Keys: {ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// Modaler Mini-Dialog mit maskierter Eingabe. Rückgabe null = Abbruch. private string? PromptForSecret(string title, string prompt) { using var form = new Form { Text = title, Width = 480, Height = 210, FormBorderStyle = FormBorderStyle.FixedDialog, StartPosition = FormStartPosition.CenterParent, MinimizeBox = false, MaximizeBox = false }; var lbl = new Label { Text = prompt, Left = 12, Top = 12, Width = 445, Height = 70, AutoSize = false }; var tb = new TextBox { Left = 12, Top = 92, Width = 445, UseSystemPasswordChar = true }; var ok = new Button { Text = "OK", DialogResult = DialogResult.OK, Left = 296, Top = 128, Width = 75 }; var cancel = new Button { Text = "Abbrechen", DialogResult = DialogResult.Cancel, Left = 380, Top = 128, Width = 77 }; form.Controls.AddRange(new Control[] { lbl, tb, ok, cancel }); form.AcceptButton = ok; form.CancelButton = cancel; return form.ShowDialog(this) == DialogResult.OK ? tb.Text : null; } } }