Scaling-Bug behoben (war 'teilweise unbenutzbar'): - General Settings: toolStrip2 war NICHT gedockt (floatete), propertyGrid war Dock=Bottom mit fixer Hoehe -> jetzt toolStrip2 Dock=Top, propertyGrid Dock=Fill (Controls-Reihenfolge korrigiert). - Accounts: toolStripAccounts Dock=Top (war floatend); Grid Left + PropertyGrid Fill bleiben. - Form: tabControl1 Dock=Fill statt Anchor mit falscher Groesse; leere toolStrip1 entfernt; ueberhohe ClientSize (1523) korrigiert. Skaliert jetzt sauber mit der Fenstergroesse. Fehlende Einstellung ergaenzt: - Button 'OpenRouter-Key setzen …' (General Settings, via Designer): maskierte Eingabe, schreibt in die gitignorierte openrouter.key (genau das liest der OpenRouterClient) - KEIN Secret im Klartext in server_settings.xml. Leer = entfernen. Hinweis auf Spend-Limit. Build 0 Fehler, --smoke-ui gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
254 lines
11 KiB
C#
254 lines
11 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
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<AccountState> _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<AccountState>(_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) =====
|
||
|
||
/// <summary>Pfad der Master-Key-Datei – identisch zu Program.cs (App-Ordner, gitignored).</summary>
|
||
private static string MasterKeyFilePath => Path.Combine(AppContext.BaseDirectory, "master.key");
|
||
|
||
/// <summary>Existiert bereits ein Master-Key (Umgebungsvariable ODER Datei)?</summary>
|
||
private static bool MasterKeyExists() =>
|
||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"))
|
||
|| File.Exists(MasterKeyFilePath);
|
||
|
||
/// <summary>Button nur aktiv, solange KEIN Master-Key existiert (Überschreiben = Lockout-Gefahr).</summary>
|
||
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) =====
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>Modaler Mini-Dialog mit maskierter Eingabe. Rückgabe null = Abbruch.</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|