Lizenzsystem auf Deploymentcenter umgestellt

Predictalytics hing noch am abgeloesten LicenseLabrador (license.mhdf.de),
waehrend die Lizenzen inzwischen im Deploymentcenter (dc.mhdf.de) gepflegt
werden. Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client
abgesichert: Slug predictalytics, HWID v2 aus machine-guid, Server meldet valid.

Hosting/LicenseGuard.cs auf Deploymentcenter.Client umgeschrieben. Die
Aufteilung bleibt: plattformneutrale Pruefung im Hosting, interaktive
Aktivierung in den Hosts.

Gewinn fuer die Linux-Portierung:
- Hardware-ID v2 mit Quellen-Kaskade /etc/machine-id, dbus, DMI-UUID, MAC,
  Schluesseldatei; Container ueber DEPLOYMENTCENTER_HWID
- Der Rechnername steckt nicht mehr im Hash, Umbenennen verbraucht keinen
  Aktivierungsplatz
- Cache mit AES-256-GCM statt DPAPI, ohne stillen Klartext-Rueckfall unter
  Linux. Damit erledigt sich Punkt 3.2 Nr. 2 der Portierungsanalyse.
- XDG_CONFIG_HOME wird beruecksichtigt
- Keine Basic-Auth-Zugangsdaten mehr im Quelltext

Neue CLI-Schalter aus dem Integrationsleitfaden, ausgewertet vor jeder
Avalonia-Initialisierung: --license-status, --license-set-key,
--license-deactivate. Schluessel werden in der Ausgabe maskiert.

Die Lizenzfenster beider Hosts zeigen jetzt die Hardware-ID an, an die der
Server die Aktivierung bindet.

Build: 0 Fehler. Tests: 100 bestanden.
Verifiziert: --license-status meldet gueltig; die GUI startet vollstaendig
durch bis MainWindowViewModel.Start(), belegt durch die Logdateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-07 10:41:04 +02:00
co-authored by Claude Opus 5
parent 1efd23abe4
commit 65b44e984e
14 changed files with 384 additions and 140 deletions
@@ -0,0 +1,104 @@
using Predictalytics.Hosting;
namespace Predictalytics.Shell.Services;
/// <summary>
/// Lizenzverwaltung von der Kommandozeile — die im Deploymentcenter-Integrations-
/// leitfaden vorgesehenen Schalter. Gedacht fuer Server ohne Oberflaeche, wo es
/// keinen Aktivierungsdialog geben kann.
/// </summary>
internal static class LicenseCli
{
public const string StatusSwitch = "--license-status";
public const string SetKeySwitch = "--license-set-key";
public const string DeactivateSwitch = "--license-deactivate";
public static bool IsLicenseCommand(string[] args) =>
args.Length > 0 && args[0] is StatusSwitch or SetKeySwitch or DeactivateSwitch;
public static async Task<int> RunAsync(string[] args)
{
switch (args[0])
{
case StatusSwitch: return await ShowStatusAsync();
case SetKeySwitch: return await SetKeyAsync(args.Length > 1 ? args[1] : null);
case DeactivateSwitch: return await DeactivateAsync();
default: return 1;
}
}
private static async Task<int> ShowStatusAsync()
{
var hw = LicenseGuard.GetHardwareInfo();
Console.WriteLine("Predictalytics — Lizenzstatus");
Console.WriteLine($" Produkt : {LicenseGuard.ProductSlug}");
Console.WriteLine($" Hardware-ID : {hw.HardwareId}");
Console.WriteLine($" HWID-Quelle : {hw.HwidSource} (v{hw.HwidVersion}, {hw.Platform})");
Console.WriteLine($" Cache-Ablage : {LicenseGuard.StorageDirectory}");
var cachedKey = LicenseGuard.GetCachedLicenseKey();
if (cachedKey == null)
{
Console.WriteLine(" Schlüssel : keiner hinterlegt");
Console.WriteLine();
Console.WriteLine($"Aktivierung: {SetKeySwitch} <SCHLÜSSEL>");
return HeadlessRunner.ExitNoLicense;
}
Console.WriteLine($" Schlüssel : {Mask(cachedKey)}");
Console.WriteLine();
Console.Write("Prüfe am Server… ");
var result = await LicenseGuard.ValidateCachedAsync();
Console.WriteLine(result is { IsValid: true }
? $"gültig ({result.Status}{(result.IsCached ? ", aus lokalem Cache" : "")})"
: $"NICHT gültig ({result?.Status}): {result?.Message}");
return result is { IsValid: true } ? HeadlessRunner.ExitOk : HeadlessRunner.ExitNoLicense;
}
private static async Task<int> SetKeyAsync(string? key)
{
if (string.IsNullOrWhiteSpace(key))
{
Console.Error.WriteLine($"Aufruf: {SetKeySwitch} <SCHLÜSSEL>");
return HeadlessRunner.ExitFailed;
}
Console.Write($"Aktiviere {Mask(LicenseGuard.Normalize(key))} … ");
var result = await LicenseGuard.ValidateAsync(key);
if (result.IsValid)
{
Console.WriteLine($"erfolgreich ({result.Status})");
Console.WriteLine($"Hardware-ID: {result.HardwareId}");
return HeadlessRunner.ExitOk;
}
Console.Error.WriteLine($"fehlgeschlagen ({result.Status}): {result.Message}");
return HeadlessRunner.ExitNoLicense;
}
private static async Task<int> DeactivateAsync()
{
if (LicenseGuard.GetCachedLicenseKey() == null)
{
Console.Error.WriteLine("Kein hinterlegter Schlüssel — nichts zu deaktivieren.");
return HeadlessRunner.ExitFailed;
}
Console.Write("Gebe Aktivierungsplatz am Server frei… ");
var ok = await LicenseGuard.DeactivateAsync();
Console.WriteLine(ok ? "erfolgreich" : "fehlgeschlagen");
return ok ? HeadlessRunner.ExitOk : HeadlessRunner.ExitFailed;
}
/// <summary>Nur die Segmentstruktur zeigen, nicht den vollstaendigen Schluessel.</summary>
private static string Mask(string key)
{
var parts = key.Split('-');
if (parts.Length < 2) return "*****";
for (var i = 1; i < parts.Length - 1; i++) parts[i] = new string('*', parts[i].Length);
return string.Join('-', parts);
}
}