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
-
+