Umsetzung von #567. Entscheidung Richard, 13.09.2026: Geheimnisse verwalten wir ausschliesslich im Desktop, und selbst dort erst nach zusaetzlicher Passwortabfrage. Bisher sah und aenderte jeder, der die Desktop-Oberflaeche offen vor sich hatte: - Polymarket-Konten mit ApiKey, ApiSecret, ApiPassphrase und PrivateKey - Master-Key, OpenRouter-Key, Watchdog-Token und Lizenzschluessel - Mullvad-Account und Polygon-RPC-URL mit Alchemy-Key, offen im Editor - das WebUI-Token, per "Anzeigen" lesbar Jetzt gesperrt, bis entsperrt: - Tab "Polymarket Accounts": Gesperrt stehen die Konten gar nicht im visuellen Baum. Das Raster bleibt leer, der Editor mit den Private Keys ebenfalls; nichts ist nur verdeckt. - Master-Key erzeugen, OpenRouter-, Watchdog- und Lizenzschluessel setzen. - Mullvad-Account und Polygon-RPC-URL: mit [SensitiveSetting] markiert und im Editor ausgeblendet. - WebUI-Zugang: Gesperrt steht das Token nicht einmal maskiert im Feld. Speichern ist mitgesperrt, weil es den Feldinhalt als Token schreibt; gesperrt und leer haette es das Token geloescht. - Frei bleiben Lizenzpruefung und Test-Heartbeat, weil sie keinen Schluessel zeigen. Kern im Core (PolyTrader.Core.Security), weil nur dort testbar: - SettingsPasswordStore: PBKDF2-HMAC-SHA256, 600 000 Iterationen, Salt, eigene Datei settings-password.json neben master.key, atomar geschrieben und nur fuer den Besitzer lesbar. Nicht in server_settings.xml, die seit #561 das WebUI mitschreibt. Ein vorhandenes Passwort wird nie ueberschrieben. - SettingsLock: - erstes Festlegen mit Wiederholung und mindestens 10 Zeichen - zwei freie Fehlversuche, danach 5 s bis 5 min Wartezeit, in der auch das richtige Passwort nicht geprueft wird - Sperre nach 5 Minuten ohne geschuetzte Aktion und beim Verlassen der Einstellungen - eine unlesbare Passwortdatei entsperrt nie und gilt auch nicht still als "kein Passwort" - Protokoll ohne eingegebene Werte Oberflaeche: PasswordDialog deklarativ in .axaml, Sperr-Leiste ueber den Tabs. Die PBKDF2-Pruefung dauert etwa eine halbe Sekunde und laeuft nicht im UI-Thread. Grenzen: - Die Sperre schuetzt gegen jemanden an einer entsperrten Desktop-Sitzung. Wer Dateizugriff hat, liest master.key selbst. Deshalb gibt es kein "Passwort vergessen": Wer settings-password.json loeschen kann, braucht keins. - Die Wartezeit nach Fehlversuchen beginnt nach einem Neustart von vorn. - "Datenbankeinstellungen": Eine Ansicht dafuer gibt es im Desktop nicht; die Verbindungszeichenfolge steht nur in appsettings.Local.json. Entsteht eine, gehoert sie hinter dieselbe Sperre. Tests: 20 fuer Passwortdatei und Sperre (Festlegen, Fehlversuche, Wartezeit, Leerlauf, unlesbare Datei, Aendern, Iterationszahl aus der Datei, Dateirechte) und 5 fuer die Markierung der Geheimnisfelder. Die Oberflaeche selbst wird auf der VM abgenommen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
5.2 KiB
C#
117 lines
5.2 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using PolyTrader.Core.Modularity;
|
||
using PolyTraderSharp;
|
||
using PolyTraderSharp.Services;
|
||
|
||
namespace PolyTrader.App.Avalonia.Shell
|
||
{
|
||
/// <summary>
|
||
/// Registriert die Seiten, die der Core selbst beisteuert (Start, Dashboard, Einstellungen,
|
||
/// Terminal, Jobs). Gegenstück zur entsprechenden Registrierung im WinForms-Program.cs –
|
||
/// die View-IDs sind bewusst identisch, damit Symbole und Reihenfolge unverändert greifen.
|
||
/// </summary>
|
||
public static class CoreViews
|
||
{
|
||
/// <summary>Quelle/Ziel der Modul-Aktivierung – dieselbe Datei wie im Host.</summary>
|
||
private const string ServerSettingsPath = "server_settings.xml";
|
||
|
||
public static void Register(IModuleUiHost host, IServiceProvider services)
|
||
{
|
||
// Die Startseite ist seit der Einfenster-Shell eine View wie jede andere: sie steht in
|
||
// der Seitenleiste, wird über OpenView aufgerufen und beim Start als erste angezeigt.
|
||
// Sie heißt „Dashboard", weil sie genau das ist: Kennzahlen, Konten und Live-Überblick.
|
||
host.RegisterView(new ModuleView
|
||
{
|
||
Id = AvaloniaUiHost.StartViewId,
|
||
Title = "Dashboard",
|
||
Group = "Core",
|
||
Order = 0,
|
||
CreateView = () => new Views.LauncherWindow(host, services)
|
||
});
|
||
|
||
host.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.dashboard",
|
||
// Umbenannt: „Dashboard" heißt jetzt die Startseite; diese Auswertungsansicht
|
||
// (KPIs, Diagramme, Tradehistorie) läuft unter „Analytics". Die View-ID bleibt
|
||
// bewusst unverändert – daran hängen Symbole, Einstellungen und die Gruppierung.
|
||
Title = "Analytics",
|
||
Group = "Core",
|
||
Order = 10,
|
||
CreateView = () => new Views.DashboardWindow(
|
||
host,
|
||
services.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
|
||
services.GetRequiredService<TradingState>(),
|
||
BuildModuleInfos(services),
|
||
ServerSettingsPath)
|
||
});
|
||
|
||
host.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.settings",
|
||
Title = "Server Settings",
|
||
Group = "Core",
|
||
Order = 20,
|
||
CreateView = () => new Views.SettingsWindow(
|
||
host,
|
||
services.GetRequiredService<PolyTrader.Core.Persistence.IAccountRepository>(),
|
||
services.GetRequiredService<TradingState>(),
|
||
services.GetRequiredService<TerminalLogger>(),
|
||
services.GetService<WatchdogHeartbeatService>(),
|
||
services.GetService<MullvadVpnService>(),
|
||
services.GetService<PolyTrader.Core.Configuration.DeploymentcenterOptions>(),
|
||
services.GetService<PolyTrader.Web.Configuration.WebUiOptions>(),
|
||
services.GetRequiredService<PolyTrader.Core.Security.SettingsLock>())
|
||
});
|
||
|
||
host.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.jobs",
|
||
Title = "Server Jobs",
|
||
Group = "Core",
|
||
Order = 30,
|
||
CreateView = () => new Views.JobsWindow(host, services.GetRequiredService<JobManager>())
|
||
});
|
||
|
||
host.RegisterView(new ModuleView
|
||
{
|
||
Id = "core.terminal",
|
||
Title = "Terminal / Logs",
|
||
Group = "Core",
|
||
Order = 40,
|
||
CreateView = () => new Views.TerminalWindow(host, services.GetRequiredService<TerminalLogger>())
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Baut die Aktivierungs-Übersicht für den Modul-Tab: alle bekannten Module, ob sie in dieser
|
||
/// Sitzung laufen, und ein etwaiger Grund gegen die Aktivierung (z.B. fehlender API-Key).
|
||
/// </summary>
|
||
private static IReadOnlyList<ModuleActivationInfo> BuildModuleInfos(IServiceProvider services)
|
||
{
|
||
var running = services.GetServices<IPolyTraderModule>().ToList();
|
||
var runningNames = running.Select(m => m.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
var config = services.GetRequiredService<IConfiguration>();
|
||
|
||
// Auch die NICHT geladenen Module auflisten – sonst liessen sie sich nie wieder aktivieren.
|
||
var all = services.GetRequiredService<KnownModules>().All;
|
||
|
||
return all.Select(m => new ModuleActivationInfo(
|
||
m.Name,
|
||
runningNames.Contains(m.Name),
|
||
SafeBlocker(m, config)))
|
||
.ToList();
|
||
|
||
static string? SafeBlocker(IPolyTraderModule m, IConfiguration config)
|
||
{
|
||
try { return m.GetActivationBlocker(config); }
|
||
catch (Exception ex) { return $"Prüfung fehlgeschlagen: {ex.Message}"; }
|
||
}
|
||
}
|
||
}
|
||
}
|