From fe9f95584ea85c30c2af680d88b7fc8903225cae Mon Sep 17 00:00:00 2001 From: Deploymentcenter Bot Date: Wed, 5 Aug 2026 21:55:19 +0200 Subject: [PATCH] Refactor UI with purple dark theme glassmorphism, projects management, license editing, agent installer scripts, unmaskable tokens, C# test suite --- .gitignore | 3 + .../Deploymentcenter.TestClient.csproj | 11 + .../Deploymentcenter.TestClient/Program.cs | 171 ++++ public/index.php | 846 ++++++++++++------ sql/schema.sql | 20 +- src/Modules/License/LicenseService.php | 8 +- src/Modules/Watchdog/MonitorRepo.php | 5 +- src/Modules/Watchdog/TokenManager.php | 25 +- 8 files changed, 813 insertions(+), 276 deletions(-) create mode 100644 client-dotnet/Deploymentcenter.TestClient/Deploymentcenter.TestClient.csproj create mode 100644 client-dotnet/Deploymentcenter.TestClient/Program.cs diff --git a/.gitignore b/.gitignore index 1d52d58..033eff2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ scratch/ *.bak .DS_Store +bin/ +obj/ +*.user diff --git a/client-dotnet/Deploymentcenter.TestClient/Deploymentcenter.TestClient.csproj b/client-dotnet/Deploymentcenter.TestClient/Deploymentcenter.TestClient.csproj new file mode 100644 index 0000000..936787b --- /dev/null +++ b/client-dotnet/Deploymentcenter.TestClient/Deploymentcenter.TestClient.csproj @@ -0,0 +1,11 @@ + + + + Exe + net8.0 + enable + enable + Deploymentcenter Test Suite + + + diff --git a/client-dotnet/Deploymentcenter.TestClient/Program.cs b/client-dotnet/Deploymentcenter.TestClient/Program.cs new file mode 100644 index 0000000..231d75d --- /dev/null +++ b/client-dotnet/Deploymentcenter.TestClient/Program.cs @@ -0,0 +1,171 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Deploymentcenter.TestClient; + +class Program +{ + private static readonly string BaseUrl = "https://dc.mhdf.de"; + private static readonly HttpClient Client = new HttpClient(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true + }); + + static async Task Main(string[] args) + { + Console.OutputEncoding = Encoding.UTF8; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine("================================================="); + Console.WriteLine(" 🚀 Deploymentcenter Unified Client Test Suite "); + Console.WriteLine("================================================="); + Console.ResetColor(); + Console.WriteLine($" Server Base URL: {BaseUrl}\n"); + + await TestLicenseModuleAsync(); + await TestWatchdogModuleAsync(); + await TestUpdateServiceModuleAsync(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\n[✔] Alle Systemprüfungen erfolgreich abgeschlossen!"); + Console.ResetColor(); + } + + private static async Task TestLicenseModuleAsync() + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("1. Teste Module: Lizenzen (License Validation)..."); + Console.ResetColor(); + + try + { + var payload = new + { + product = "myapp", + license_key = "LLAB1-98A72-B3C4D-5E6F7-89012", + hardware_id = "HWID-TEST-CLI-CSHARP-01", + nonce = Guid.NewGuid().ToString("N"), + hostname = Environment.MachineName, + app_version = "1.0.0" + }; + + string json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await Client.PostAsync($"{BaseUrl}/api/license/v1/validate", content); + string responseBody = await response.Content.ReadAsStringAsync(); + + Console.WriteLine($" HTTP Status: {(int)response.StatusCode} {response.StatusCode}"); + Console.WriteLine($" Response Payload:\n {responseBody.Replace("\n", "\n ")}"); + + if (response.IsSuccessStatusCode && responseBody.Contains("\"status\": \"valid\"")) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(" ✔ Lizenzen Modul Test: GÜLTIG!"); + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(" ✖ Lizenzen Modul Test: Fehler oder Ungültig!"); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" ✖ Fehler beim Lizenzen-Test: {ex.Message}"); + } + Console.ResetColor(); + Console.WriteLine(); + } + + private static async Task TestWatchdogModuleAsync() + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("2. Teste Module: Watchdog (Heartbeat Ping)..."); + Console.ResetColor(); + + try + { + var payload = new + { + source = "srv-db-01", + instance = "default", + type = "heartbeat", + status = "ok", + message = "C# Client Test Suite running smoothly", + interval = 60, + group = "TestRunner", + os = Environment.OSVersion.ToString() + }; + + string json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/api/watchdog/v1/ping") + { + Content = content + }; + request.Headers.Add("X-Agent-Token", "wd_live_token_infra_01_secure"); + + HttpResponseMessage response = await Client.SendAsync(request); + string responseBody = await response.Content.ReadAsStringAsync(); + + Console.WriteLine($" HTTP Status: {(int)response.StatusCode} {response.StatusCode}"); + Console.WriteLine($" Response Payload:\n {responseBody.Replace("\n", "\n ")}"); + + if (response.IsSuccessStatusCode) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(" ✔ Watchdog Modul Test: ERFOLGREICH!"); + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(" ✖ Watchdog Modul Test: FEHLER!"); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" ✖ Fehler beim Watchdog-Test: {ex.Message}"); + } + Console.ResetColor(); + Console.WriteLine(); + } + + private static async Task TestUpdateServiceModuleAsync() + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("3. Teste Module: UpdateService (Release Check)..."); + Console.ResetColor(); + + try + { + HttpResponseMessage response = await Client.GetAsync($"{BaseUrl}/api/updateservice/v1/check?product=myapp&version=1.0.0"); + string responseBody = await response.Content.ReadAsStringAsync(); + + Console.WriteLine($" HTTP Status: {(int)response.StatusCode} {response.StatusCode}"); + Console.WriteLine($" Response Payload:\n {responseBody.Replace("\n", "\n ")}"); + + if (response.IsSuccessStatusCode) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(" ✔ UpdateService Modul Test: ERFOLGREICH!"); + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(" ✖ UpdateService Modul Test: FEHLER!"); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($" ✖ Fehler beim UpdateService-Test: {ex.Message}"); + } + Console.ResetColor(); + Console.WriteLine(); + } +} diff --git a/public/index.php b/public/index.php index 6086a9a..9cc017b 100644 --- a/public/index.php +++ b/public/index.php @@ -27,12 +27,82 @@ $pdo = Db::init($config); $msg = null; $msgType = 'success'; +// Download Handler: .lic Offline License File +if (isset($_GET['action']) && $_GET['action'] === 'download_lic') { + $licId = (int)($_GET['id'] ?? 0); + $stmt = $pdo->prepare('SELECT l.*, p.slug as product_slug FROM license_licenses l JOIN dc_projects p ON l.product_id = p.id WHERE l.id = :id'); + $stmt->execute([':id' => $licId]); + $lic = $stmt->fetch(); + + if ($lic) { + $payload = [ + 'type' => 'offline_license_file', + 'issued_at' => time(), + 'product' => $lic['product_slug'], + 'license_key' => $lic['license_key'], + 'customer' => $lic['customer_name'] ?? 'Universal', + 'valid_until' => $lic['expires_at'] ? strtotime($lic['expires_at']) : strtotime('+1 year'), + 'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true)) + ]; + + $jsonContent = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $filename = sprintf('%s_%s_offline.lic', $lic['product_slug'], substr($lic['license_key'], 0, 5)); + + header('Content-Type: application/json'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . strlen($jsonContent)); + echo $jsonContent; + exit; + } +} + +// Download Handler: Watchdog Agent Installer Script (.ps1 or .sh) +if (isset($_GET['action']) && $_GET['action'] === 'download_agent') { + $source = trim($_GET['source'] ?? 'server-node'); + $os = trim($_GET['os'] ?? 'windows'); + $token = trim($_GET['token'] ?? 'wd_live_token_default'); + + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de'; + $baseUrl = $protocol . '://' . $host; + + if ($os === 'windows') { + $script = "# Deploymentcenter Watchdog Agent Installer (Windows PowerShell)\n" + . "\$WatchdogUrl = \"{$baseUrl}/api/watchdog/v1/ping\"\n" + . "\$Token = \"{$token}\"\n" + . "\$Source = \"{$source}\"\n" + . "Write-Host \"🚀 Initialisiere Watchdog Agent für \$Source...\" -ForegroundColor Cyan\n" + . "\$body = @{ source = \$Source; status = 'ok'; message = 'Heartbeat via PowerShell Task'; interval = 60 } | ConvertTo-Json\n" + . "Invoke-RestMethod -Uri \$WatchdogUrl -Method Post -Body \$body -ContentType 'application/json' -Headers @{ 'X-Agent-Token' = \$Token }\n" + . "Write-Host \"[✔] Heartbeat erfolgreich gesendet!\" -ForegroundColor Green\n"; + + header('Content-Type: application/octet-stream'); + header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.ps1\""); + echo $script; + exit; + } else { + $script = "#!/usr/bin/env bash\n" + . "WATCHDOG_URL=\"{$baseUrl}/api/watchdog/v1/ping\"\n" + . "TOKEN=\"{$token}\"\n" + . "SOURCE=\"{$source}\"\n" + . "echo \"🚀 Initialisiere Watchdog Agent für \$SOURCE...\"\n" + . "curl -X POST \"\$WATCHDOG_URL\" -H \"Content-Type: application/json\" -H \"X-Agent-Token: \$TOKEN\" -d '{\"source\": \"'\"\$SOURCE\"'\", \"status\": \"ok\", \"message\": \"Heartbeat via Bash Cron\", \"interval\": 60}'\n" + . "echo \"[✔] Heartbeat gesendet!\"\n"; + + header('Content-Type: application/octet-stream'); + header("Content-Disposition: attachment; filename=\"watchdog-install-{$source}.sh\""); + echo $script; + exit; + } +} + // Handle POST actions if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; - // Create Product - if ($action === 'create_product') { + // Create / Edit Project + if ($action === 'save_project') { + $id = (int)($_POST['project_id'] ?? 0); $slug = trim($_POST['slug'] ?? ''); $name = trim($_POST['name'] ?? ''); $ttl = (int)($_POST['ttl'] ?? 168); @@ -40,11 +110,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($slug && $name) { try { - $stmt = $pdo->prepare('INSERT INTO license_products (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)'); - $stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]); - $msg = "Produkt '{$name}' (Slug: {$slug}) wurde erfolgreich angelegt."; + if ($id > 0) { + $stmt = $pdo->prepare('UPDATE dc_projects SET slug = :s, name = :n, default_cache_ttl_hours = :t, notes = :notes WHERE id = :id'); + $stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes, ':id' => $id]); + $msg = "Projekt '{$name}' wurde aktualisiert."; + } else { + $stmt = $pdo->prepare('INSERT INTO dc_projects (slug, name, default_cache_ttl_hours, notes) VALUES (:s, :n, :t, :notes)'); + $stmt->execute([':s' => $slug, ':n' => $name, ':t' => $ttl, ':notes' => $notes]); + $msg = "Neues Projekt '{$name}' angelegt."; + } } catch (Throwable $e) { - $msg = "Fehler beim Erstellen des Produkts: " . $e->getMessage(); + $msg = "Fehler beim Speichern des Projekts: " . $e->getMessage(); $msgType = 'danger'; } } @@ -76,11 +152,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { ':notes' => $notes ]); - // Audit log $pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.create", :d)') ->execute([':d' => json_encode(['key' => $licenseKey, 'customer' => $customerName])]); - $msg = "Lizenzschlüssel erfolgreich generiert: {$licenseKey}"; + $msg = "Lizenzschlüssel generiert: {$licenseKey}"; } catch (Throwable $e) { $msg = "Fehler bei Generierung: " . $e->getMessage(); $msgType = 'danger'; @@ -88,21 +163,51 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - // Revoke License - if ($action === 'revoke_license') { - $licId = (int)($_POST['license_id'] ?? 0); - if ($licId > 0) { - $stmt = $pdo->prepare('UPDATE license_licenses SET status = "revoked" WHERE id = :id'); - $stmt->execute([':id' => $licId]); + // Edit License + if ($action === 'edit_license') { + $id = (int)($_POST['license_id'] ?? 0); + $customerName = trim($_POST['customer_name'] ?? ''); + $customerEmail = trim($_POST['customer_email'] ?? ''); + $status = $_POST['status'] ?? 'active'; + $maxActivations = (int)($_POST['max_activations'] ?? 2); + $expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null; + $notes = trim($_POST['notes'] ?? ''); - $pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.revoke", :d)') - ->execute([':d' => json_encode(['license_id' => $licId])]); + if ($id > 0) { + $stmt = $pdo->prepare(' + UPDATE license_licenses + SET customer_name = :cname, customer_email = :cemail, status = :status, max_activations = :max_act, expires_at = :exp, notes = :notes + WHERE id = :id + '); + $stmt->execute([ + ':cname' => $customerName, + ':cemail' => $customerEmail, + ':status' => $status, + ':max_act' => $maxActivations, + ':exp' => $expiresAt, + ':notes' => $notes, + ':id' => $id + ]); - $msg = "Lizenz wurde widerrufen."; + $pdo->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES ("admin", "license.update", :d)') + ->execute([':d' => json_encode(['license_id' => $id, 'status' => $status])]); + + $msg = "Lizenzdaten wurden erfolgreich aktualisiert."; } } - // Block / Unblock Hardware Activation + // Revoke / Re-activate License + if ($action === 'toggle_license_status') { + $licId = (int)($_POST['license_id'] ?? 0); + $newStatus = $_POST['new_status'] ?? 'revoked'; + if ($licId > 0) { + $stmt = $pdo->prepare('UPDATE license_licenses SET status = :s WHERE id = :id'); + $stmt->execute([':s' => $newStatus, ':id' => $licId]); + $msg = "Lizenz-Status geändert zu: " . strtoupper($newStatus); + } + } + + // Toggle Hardware Activation Block if ($action === 'toggle_block_activation') { $actId = (int)($_POST['activation_id'] ?? 0); $block = (int)($_POST['block_state'] ?? 0); @@ -113,14 +218,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - // Extend License Expiration - if ($action === 'extend_license') { - $licId = (int)($_POST['license_id'] ?? 0); - $newExp = !empty($_POST['new_expires_at']) ? $_POST['new_expires_at'] . ' 23:59:59' : null; - if ($licId > 0) { - $stmt = $pdo->prepare('UPDATE license_licenses SET expires_at = :exp, status = "active" WHERE id = :id'); - $stmt->execute([':exp' => $newExp, ':id' => $licId]); - $msg = "Ablaufdatum der Lizenz wurde aktualisiert."; + // Add Watchdog Host Machine / Application + if ($action === 'add_watchdog_monitor') { + $source = trim($_POST['source'] ?? ''); + $type = $_POST['type'] ?? 'heartbeat'; + $group = trim($_POST['group'] ?? 'Default'); + $os = trim($_POST['os'] ?? 'Linux'); + $interval = (int)($_POST['interval'] ?? 60); + + if ($source) { + $stmt = $pdo->prepare(' + INSERT INTO watchdog_monitors (source, instance, type, state, expected_interval_sec, group_key, os, created_utc, updated_utc) + VALUES (:source, "default", :type, "stopped", :interval, :group, :os, NOW(), NOW()) + ON DUPLICATE KEY UPDATE expected_interval_sec = VALUES(expected_interval_sec), group_key = VALUES(group_key) + '); + $stmt->execute([':source' => $source, ':type' => $type, ':interval' => $interval, ':group' => $group, ':os' => $os]); + + $tokMgr = new TokenManager($pdo); + $tok = $tokMgr->createToken($source, "Token for {$source}"); + $msg = "Monitor '{$source}' angelegt. Agent Token: {$tok['raw_token']}"; } } @@ -131,7 +247,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($name) { $tokMgr = new TokenManager($pdo); $res = $tokMgr->createToken($source, $name); - $msg = "Agent-Token für '{$name}' generiert: {$res['raw_token']} (Bitte sicher aufbewahren!)"; + $msg = "Agent-Token für '{$name}' generiert: {$res['raw_token']}"; } } @@ -157,7 +273,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($productSlug && $version && $url) { $updMgr = new UpdateManager($pdo); if ($updMgr->addRelease($productSlug, $version, $notes, $url, $hash, $critical)) { - $msg = "Release v{$version} für '{$productSlug}' gespeichert."; + $msg = "Release v{$version} für Projekt '{$productSlug}' veröffentlicht."; } else { $msg = "Fehler beim Speichern des Releases."; $msgType = 'danger'; @@ -166,13 +282,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } -// Fetch all Data -$products = $pdo->query('SELECT * FROM license_products ORDER BY name ASC')->fetchAll(); +// Fetch All Data +$projects = $pdo->query('SELECT * FROM dc_projects ORDER BY name ASC')->fetchAll(); + $licenses = $pdo->query(' SELECT l.*, p.name as product_name, p.slug as product_slug, (SELECT COUNT(*) FROM license_activations a WHERE a.license_id = l.id) as active_count FROM license_licenses l - JOIN license_products p ON l.product_id = p.id + JOIN dc_projects p ON l.product_id = p.id ORDER BY l.created_at DESC ')->fetchAll(); @@ -180,7 +297,7 @@ $activations = $pdo->query(' SELECT a.*, l.license_key, p.slug as product_slug, p.name as product_name FROM license_activations a JOIN license_licenses l ON a.license_id = l.id - JOIN license_products p ON l.product_id = p.id + JOIN dc_projects p ON l.product_id = p.id ORDER BY a.last_seen DESC ')->fetchAll(); @@ -199,7 +316,8 @@ foreach ($monitors as $m) { $eventLog = new EventLog($pdo); $recentEvents = $eventLog->getRecentEvents(100); -$agentTokens = $pdo->query('SELECT * FROM watchdog_agent_tokens ORDER BY created_at_utc DESC')->fetchAll(); +$tokenManager = new TokenManager($pdo); +$agentTokens = $tokenManager->getAllTokens(); $updateMgr = new UpdateManager($pdo); $releases = $updateMgr->getReleases(); @@ -207,45 +325,46 @@ $releases = $updateMgr->getReleases(); $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'dc.mhdf.de'; $baseUrl = $protocol . '://' . $host; +$pubKeyB64 = 'Fehlt (Ed25519 Key Server Default)'; ?> - Deploymentcenter - Central Platform + Deploymentcenter - Unified Operations Center - +