using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Windows.Forms; using PolyTrader.Core.Persistence; 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 ThreemaService? _threema; private MullvadVpnService? _vpn; 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(); 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(ThreemaService threema, MullvadVpnService vpn, TerminalLogger logger, IAccountRepository accountRepo, TradingState state) { _threema = threema; _vpn = vpn; _logger = logger; _accountRepo = accountRepo; _state = state; Reload(); LoadAccounts(); } // ===== Server-Settings ===== private void Reload() { _settings = ServerSettings.Load(SettingsPath); propertyGrid.SelectedObject = _settings; } private void Save() { try { _settings.Save(SettingsPath); _threema?.ReloadSettings(); _vpn?.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(); } // ===== 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; } } }