Eigenständiges token-geschütztes Verwaltungs-WebGUI (Admin WebGUI) hinzugefügt

- Unabhängige Kestrel-Instanz auf Port 5001 (vollständig getrennt vom Analyse-Webserver)
- Admin API Endpunkte ohne Swagger-Exposition
- Token-Gatekeeper mit sofortigem Login-Overlay bei unautorisiertem Zugriff
- Live-Terminal via SSE-Log-Streaming und Log-Download-Übersicht
- Steuerung von Workern, Analyse-Webserver, Wartungsaktionen und Einstellungen
- Nahtlose Integration in Avalonia Desktop-Shell und Linux Headless-Modus
- Unit-Tests für Admin-Filter, Optionen und Log-Puffer
This commit is contained in:
Richard
2026-09-07 21:09:03 +02:00
parent 301249f3b8
commit 1131d257cf
19 changed files with 3566 additions and 16 deletions
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
namespace Predictalytics.Hosting;
@@ -63,6 +64,60 @@ public sealed class PredictalyticsOptions
}
}
// ─── 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; } = "";
/// <summary>
/// Prüft, ob die Admin-Bind-Adresse öffentlich bzw. über das Netzwerk erreichbar ist.
/// </summary>
[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";
}
}
/// <summary>Kestrel-Bind-URL für das Admin-WebGUI.</summary>
[Browsable(false)]
[JsonIgnore]
public string AdminServerUrl =>
$"http://{(string.IsNullOrWhiteSpace(AdminServerHost) ? "localhost" : AdminServerHost.Trim())}:{AdminServerPort}";
/// <summary>Generiert einen sicheren, kryptografischen Zufallstoken für das Admin-WebGUI.</summary>
public static string GenerateSecureToken(int byteLength = 24) =>
Convert.ToHexString(System.Security.Cryptography.RandomNumberGenerator.GetBytes(byteLength)).ToLowerInvariant();
// ─── Egress ───
private string _egressChannelsText = "";
@@ -224,12 +279,40 @@ public sealed class PredictalyticsOptions
{
var options = JsonSerializer.Deserialize<PredictalyticsOptions>(File.ReadAllText(path))
?? new PredictalyticsOptions();
if (path == LegacySettingsFilePath) options.Save(); // Migration festschreiben
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
{
return new PredictalyticsOptions();
var fallback = new PredictalyticsOptions();
if (string.IsNullOrWhiteSpace(fallback.AdminToken))
{
fallback.AdminToken = GenerateSecureToken();
try { fallback.Save(); } catch { }
}
return fallback;
}
}
/// <summary>
/// Ermöglicht das Übersteuern von Einstellungen über appsettings.json oder Umgebungsvariablen.
/// </summary>
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();
}
}