Initial commit: Modular Deploymentcenter platform

This commit is contained in:
Deploymentcenter Bot
2026-08-05 21:23:24 +02:00
commit 3a38fd4837
27 changed files with 2323 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Public folder htaccess
Options -Indexes
+8
View File
@@ -0,0 +1,8 @@
# API Unauthenticated Access
Satisfy Any
Allow from all
# Apache 2.4+ compatibility
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../src/Modules/License/Audit.php';
require_once __DIR__ . '/../../../../src/Modules/License/KeyGen.php';
require_once __DIR__ . '/../../../../src/Modules/License/RateLimiter.php';
require_once __DIR__ . '/../../../../src/Modules/License/LicenseService.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Modules\License\LicenseService;
use Deploymentcenter\Modules\License\RateLimiter;
function sendResponse(array $data, int $statusCode = 200): void {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
try {
$config = require __DIR__ . '/../../../../config/config.php';
$pdo = Db::init($config);
$limiter = new RateLimiter($pdo, 120, 60);
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if (!$limiter->check($ip)) {
sendResponse(['error' => 'Too Many Requests', 'message' => 'Rate limit exceeded.'], 429);
}
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$rawInput = file_get_contents('php://input');
$inputData = !empty($rawInput) ? (json_decode($rawInput, true) ?? []) : $_POST;
$licenseService = new LicenseService($pdo);
if (str_ends_with($uri, '/validate') && $method === 'POST') {
$res = $licenseService->validate($inputData, $ip);
sendResponse($res);
}
if (str_ends_with($uri, '/deactivate') && $method === 'POST') {
$res = $licenseService->deactivate($inputData, $ip);
sendResponse($res);
}
if (str_ends_with($uri, '/status') && $method === 'GET') {
sendResponse(['status' => 'ok', 'module' => 'LicenseLabrador', 'version' => '1.0']);
}
sendResponse(['error' => 'Not Found', 'message' => 'Endpoint not found'], 404);
} catch (Throwable $t) {
sendResponse(['error' => 'Server Error', 'message' => $t->getMessage()], 500);
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../src/Modules/UpdateService/UpdateManager.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Modules\UpdateService\UpdateManager;
function sendResponse(array $data, int $statusCode = 200): void {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
try {
$config = require __DIR__ . '/../../../../config/config.php';
$pdo = Db::init($config);
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$updateMgr = new UpdateManager($pdo);
if (str_ends_with($uri, '/check') && ($method === 'GET' || $method === 'POST')) {
$product = $_REQUEST['product'] ?? $_REQUEST['product_slug'] ?? '';
$version = $_REQUEST['version'] ?? $_REQUEST['current_version'] ?? '0.0.0';
if (empty($product)) {
sendResponse(['error' => 'Bad Request', 'message' => 'Parameter "product" is required.'], 400);
}
$latest = $updateMgr->checkUpdate($product, $version);
if ($latest) {
sendResponse([
'update_available' => true,
'latest_release' => $latest
]);
} else {
sendResponse([
'update_available' => false,
'message' => 'Application is up to date.'
]);
}
}
if (str_ends_with($uri, '/releases') && $method === 'GET') {
$product = $_GET['product'] ?? null;
$releases = $updateMgr->getReleases($product);
sendResponse(['count' => count($releases), 'releases' => $releases]);
}
sendResponse(['error' => 'Not Found', 'message' => 'Endpoint not found'], 404);
} catch (Throwable $t) {
sendResponse(['error' => 'Server Error', 'message' => $t->getMessage()], 500);
}
+122
View File
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../../../src/Core/Db.php';
require_once __DIR__ . '/../../../../src/Modules/Watchdog/MonitorRepo.php';
require_once __DIR__ . '/../../../../src/Modules/Watchdog/EventLog.php';
require_once __DIR__ . '/../../../../src/Modules/Watchdog/TokenManager.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Modules\Watchdog\MonitorRepo;
use Deploymentcenter\Modules\Watchdog\EventLog;
use Deploymentcenter\Modules\Watchdog\TokenManager;
function sendResponse(array $data, int $statusCode = 200): void {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
try {
$config = require __DIR__ . '/../../../../config/config.php';
$pdo = Db::init($config);
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$authHeader = $_SERVER['HTTP_X_WATCHDOG_KEY'] ?? $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['HTTP_X_AGENT_TOKEN'] ?? null;
if ($authHeader && str_starts_with($authHeader, 'Bearer ')) {
$authHeader = substr($authHeader, 7);
}
$sharedKey = $config['security']['shared_key'] ?? '';
$isAdminAuth = ($authHeader && hash_equals($sharedKey, $authHeader));
$tokenManager = new TokenManager($pdo);
$monitorRepo = new MonitorRepo($pdo);
$eventLog = new EventLog($pdo);
$verifyToken = function(string $source) use ($isAdminAuth, $authHeader, $tokenManager) {
if ($isAdminAuth) return true;
if (empty($authHeader)) {
sendResponse(['error' => 'Unauthorized', 'message' => 'Missing authorization token.'], 401);
}
if (!$tokenManager->validateToken($authHeader, $source)) {
sendResponse(['error' => 'Forbidden', 'message' => "Token unauthorized for source '{$source}'."], 403);
}
return true;
};
$rawInput = file_get_contents('php://input');
$inputData = !empty($rawInput) ? (json_decode($rawInput, true) ?? []) : $_POST;
// Heartbeat / Ping
if ((str_ends_with($uri, '/ping') || str_ends_with($uri, '/heartbeat')) && $method === 'POST') {
$source = trim($inputData['source'] ?? '');
$instance = trim($inputData['instance'] ?? 'default');
$type = trim($inputData['type'] ?? 'heartbeat');
$interval = (int)($inputData['interval'] ?? $inputData['expected_interval_sec'] ?? 60);
$metrics = $inputData['metrics'] ?? null;
$status = strtolower(trim($inputData['status'] ?? 'ok'));
$message = $inputData['message'] ?? $inputData['reason'] ?? null;
$groupKey = $inputData['group'] ?? $inputData['group_key'] ?? null;
$os = $inputData['os'] ?? null;
if (empty($source)) {
sendResponse(['error' => 'Bad Request', 'message' => 'Field "source" is required.'], 400);
}
$verifyToken($source);
$monitor = $monitorRepo->upsertHeartbeat($source, $instance, $type, $interval, $metrics, $status, $message, $groupKey, $os);
sendResponse([
'status' => 'success',
'message' => 'Heartbeat received',
'monitor' => [
'source' => $monitor['source'],
'instance' => $monitor['instance'],
'state' => $monitor['state'],
'last_status' => $monitor['last_status'],
'last_seen_utc' => $monitor['last_seen_utc'],
]
]);
}
// Log Event
if (str_ends_with($uri, '/event') && $method === 'POST') {
$source = trim($inputData['source'] ?? '');
$instance = trim($inputData['instance'] ?? 'default');
$kind = trim($inputData['kind'] ?? 'started');
$severity = trim($inputData['severity'] ?? 'info');
$message = $inputData['message'] ?? null;
$meta = $inputData['meta'] ?? null;
if (empty($source)) sendResponse(['error' => 'Bad Request', 'message' => 'Field "source" is required.'], 400);
$verifyToken($source);
$eventId = $eventLog->logEvent($source, $instance, $kind, null, null, $severity, $message, $meta);
sendResponse(['status' => 'success', 'event_id' => $eventId]);
}
// Status / Monitore auflisten
if (str_ends_with($uri, '/status') && $method === 'GET') {
$monitors = $monitorRepo->getAllMonitors();
sendResponse(['count' => count($monitors), 'monitors' => $monitors]);
}
// Events auflisten
if (str_ends_with($uri, '/events') && $method === 'GET') {
$limit = (int)($_GET['limit'] ?? 50);
$events = $eventLog->getRecentEvents($limit);
sendResponse(['count' => count($events), 'events' => $events]);
}
sendResponse(['error' => 'Not Found', 'message' => 'Endpoint not found'], 404);
} catch (Throwable $t) {
sendResponse(['error' => 'Server Error', 'message' => $t->getMessage()], 500);
}
+599
View File
@@ -0,0 +1,599 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Core/Db.php';
require_once __DIR__ . '/../src/Core/Auth.php';
require_once __DIR__ . '/../src/Modules/License/KeyGen.php';
require_once __DIR__ . '/../src/Modules/License/LicenseService.php';
require_once __DIR__ . '/../src/Modules/Watchdog/MonitorRepo.php';
require_once __DIR__ . '/../src/Modules/Watchdog/EventLog.php';
require_once __DIR__ . '/../src/Modules/UpdateService/UpdateManager.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Auth;
use Deploymentcenter\Modules\License\KeyGen;
use Deploymentcenter\Modules\Watchdog\MonitorRepo;
use Deploymentcenter\Modules\Watchdog\EventLog;
use Deploymentcenter\Modules\UpdateService\UpdateManager;
Auth::requireLogin();
$config = require __DIR__ . '/../config/config.php';
$pdo = Db::init($config);
// Handle POST actions (Create Product, Generate License, Revoke License, Create Release, etc.)
$msg = null;
$msgType = 'success';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Create Product
if ($action === 'create_product') {
$slug = trim($_POST['slug'] ?? '');
$name = trim($_POST['name'] ?? '');
$ttl = (int)($_POST['ttl'] ?? 168);
$notes = trim($_POST['notes'] ?? '');
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}' wurde erfolgreich erstellt.";
} catch (Throwable $e) {
$msg = "Fehler beim Erstellen des Produkts: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// Create License Key
if ($action === 'create_license') {
$productId = (int)($_POST['product_id'] ?? 0);
$customerName = trim($_POST['customer_name'] ?? '');
$customerEmail = trim($_POST['customer_email'] ?? '');
$maxActivations = (int)($_POST['max_activations'] ?? 2);
$expiresAt = !empty($_POST['expires_at']) ? $_POST['expires_at'] . ' 23:59:59' : null;
$notes = trim($_POST['notes'] ?? '');
if ($productId > 0) {
$licenseKey = KeyGen::generateKey();
try {
$stmt = $pdo->prepare('
INSERT INTO license_licenses (product_id, license_key, customer_name, customer_email, max_activations, expires_at, notes)
VALUES (:pid, :key, :cname, :cemail, :max, :exp, :notes)
');
$stmt->execute([
':pid' => $productId,
':key' => $licenseKey,
':cname' => $customerName,
':cemail' => $customerEmail,
':max' => $maxActivations,
':exp' => $expiresAt,
':notes' => $notes
]);
$msg = "Lizenzschlüssel erfolgreich generiert: <strong>{$licenseKey}</strong>";
} catch (Throwable $e) {
$msg = "Fehler bei Generierung: " . $e->getMessage();
$msgType = 'danger';
}
}
}
// 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]);
$msg = "Lizenz wurde widerrufen.";
}
}
// Add Update Release
if ($action === 'add_release') {
$productSlug = trim($_POST['product_slug'] ?? '');
$version = trim($_POST['version'] ?? '');
$url = trim($_POST['download_url'] ?? '');
$hash = trim($_POST['sha256_hash'] ?? '');
$notes = trim($_POST['release_notes'] ?? '');
$critical = isset($_POST['is_critical']);
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.";
} else {
$msg = "Fehler beim Speichern des Releases.";
$msgType = 'danger';
}
}
}
}
// Fetch stats & data
$productsCount = (int)$pdo->query('SELECT COUNT(*) FROM license_products')->fetchColumn();
$licensesCount = (int)$pdo->query('SELECT COUNT(*) FROM license_licenses')->fetchColumn();
$activationsCount = (int)$pdo->query('SELECT COUNT(*) FROM license_activations')->fetchColumn();
$monitorRepo = new MonitorRepo($pdo);
$monitors = $monitorRepo->getAllMonitors();
$monitorsUp = 0;
$monitorsWarning = 0;
$monitorsDown = 0;
foreach ($monitors as $m) {
if ($m['state'] === 'up') $monitorsUp++;
elseif ($m['state'] === 'warning') $monitorsWarning++;
else $monitorsDown++;
}
$eventLog = new EventLog($pdo);
$recentEvents = $eventLog->getRecentEvents(15);
$products = $pdo->query('SELECT * FROM license_products 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
ORDER BY l.created_at DESC
')->fetchAll();
$updateMgr = new UpdateManager($pdo);
$releases = $updateMgr->getReleases();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deploymentcenter - Central Management</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #0b0f19;
--bg-card: rgba(23, 32, 54, 0.7);
--border-card: rgba(255, 255, 255, 0.08);
--primary: #6366f1;
--primary-hover: #4f46e5;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--sidebar-width: 260px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', system-ui, sans-serif; background: var(--bg-main); color: var(--text-main); min-height: 100vh; display: flex; }
/* Sidebar */
.sidebar { width: var(--sidebar-width); background: rgba(15, 23, 42, 0.95); border-right: 1px solid var(--border-card); padding: 1.5rem 1rem; display: flex; flex-direction: column; justify-content: space-between; position: fixed; height: 100vh; }
.brand { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 2rem; padding: 0 0.5rem; }
.brand-logo { width: 36px; height: 36px; background: linear-gradient(135deg, #6366f1, #a855f7); border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.brand-logo svg { width: 20px; height: 20px; fill: none; stroke: #fff; stroke-width: 2; }
.brand-name { font-size: 1.15rem; font-weight: 700; background: linear-gradient(to right, #fff, #cbd5e1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.nav-menu { display: flex; flex-direction: column; gap: 0.35rem; list-style: none; }
.nav-link { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; color: var(--text-muted); text-decoration: none; border-radius: 10px; font-size: 0.9rem; font-weight: 500; transition: all 0.2s; cursor: pointer; }
.nav-link:hover, .nav-link.active { background: rgba(99, 102, 241, 0.15); color: #fff; }
.nav-link.active { border-left: 3px solid var(--primary); }
.nav-link svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; }
.user-panel { border-top: 1px solid var(--border-card); padding-top: 1rem; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); }
.btn-logout { color: var(--danger); text-decoration: none; font-weight: 500; }
/* Main Content */
.main-content { margin-left: var(--sidebar-width); flex: 1; padding: 2rem; max-width: 1400px; }
.top-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; }
.page-title { font-size: 1.6rem; font-weight: 700; }
/* Metric Cards */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem; margin-bottom: 2rem; }
.stat-card { background: var(--bg-card); backdrop-filter: blur(12px); border: 1px solid var(--border-card); border-radius: 16px; padding: 1.25rem; }
.stat-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); font-weight: 600; }
.stat-value { font-size: 1.8rem; font-weight: 700; margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; }
/* Tables & Cards */
.card { background: var(--bg-card); backdrop-filter: blur(12px); border: 1px solid var(--border-card); border-radius: 16px; padding: 1.5rem; margin-bottom: 2rem; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.card-title { font-size: 1.1rem; font-weight: 600; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
th { padding: 0.75rem 1rem; color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--border-card); }
td { padding: 0.875rem 1rem; border-bottom: 1px solid rgba(255, 255, 255, 0.04); }
tr:hover td { background: rgba(255, 255, 255, 0.02); }
/* Badges & Buttons */
.badge { padding: 0.25rem 0.65rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; display: inline-block; }
.badge-up { background: rgba(16, 185, 129, 0.2); color: var(--success); }
.badge-warning { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
.badge-down { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
.badge-active { background: rgba(16, 185, 129, 0.2); color: var(--success); }
.badge-revoked { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
.btn { background: var(--primary); color: #fff; border: none; border-radius: 8px; padding: 0.6rem 1rem; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: background 0.2s; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.btn:hover { background: var(--primary-hover); }
.btn-sm { padding: 0.35rem 0.65rem; font-size: 0.75rem; }
.btn-danger { background: var(--danger); }
.alert { padding: 1rem 1.25rem; border-radius: 12px; margin-bottom: 1.5rem; font-size: 0.9rem; }
.alert-success { background: rgba(16, 185, 129, 0.15); border: 1px solid rgba(16, 185, 129, 0.3); color: #a7f3d0; }
.alert-danger { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); color: #fca5a5; }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* Form elements */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.35rem; }
.form-label { font-size: 0.8rem; color: var(--text-muted); font-weight: 500; }
.form-input { background: rgba(15, 23, 42, 0.8); border: 1px solid var(--border-card); border-radius: 8px; padding: 0.6rem 0.8rem; color: #fff; font-size: 0.85rem; outline: none; }
.form-input:focus { border-color: var(--primary); }
</style>
</head>
<body>
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div>
<div class="brand">
<div class="brand-logo">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path></svg>
</div>
<div class="brand-name">Deploymentcenter</div>
</div>
<ul class="nav-menu">
<li><a class="nav-link active" onclick="switchTab('dashboard', this)">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
Übersicht
</a></li>
<li><a class="nav-link" onclick="switchTab('licenses', this)">
<svg viewBox="0 0 24 24"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
LicenseLabrador
</a></li>
<li><a class="nav-link" onclick="switchTab('watchdog', this)">
<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
WatchDog
</a></li>
<li><a class="nav-link" onclick="switchTab('updateservice', this)">
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
UpdateService
</a></li>
<li><a class="nav-link" onclick="switchTab('system', this)">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
System & DB
</a></li>
</ul>
</div>
<div class="user-panel">
<span>👤 <?= htmlspecialchars($_SESSION['dc_username'] ?? 'Admin') ?></span>
<a href="logout.php" class="btn-logout">Abmelden</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="main-content">
<?php if ($msg): ?>
<div class="alert alert-<?= $msgType ?>"><?= $msg ?></div>
<?php endif; ?>
<!-- TAB 1: DASHBOARD OVERVIEW -->
<div id="tab-dashboard" class="tab-content active">
<div class="top-header">
<h1 class="page-title">Plattform Übersicht</h1>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Produkte</div>
<div class="stat-value"><?= $productsCount ?></div>
</div>
<div class="stat-card">
<div class="stat-label">Aktive Lizenzen</div>
<div class="stat-value"><?= $licensesCount ?></div>
</div>
<div class="stat-card">
<div class="stat-label">Hardware Aktivierungen</div>
<div class="stat-value"><?= $activationsCount ?></div>
</div>
<div class="stat-card">
<div class="stat-label">Watchdog Monitore</div>
<div class="stat-value">
<span style="color:var(--success)"><?= $monitorsUp ?></span> /
<span style="color:var(--warning)"><?= $monitorsWarning ?></span> /
<span style="color:var(--danger)"><?= $monitorsDown ?></span>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">🛡️ Watchdog Monitore Status</h2>
</div>
<table>
<thead>
<tr>
<th>Source / Instanz</th>
<th>Typ</th>
<th>Status</th>
<th>Letzte Meldung</th>
<th>Zuletzt Gesehen</th>
</tr>
</thead>
<tbody>
<?php if (empty($monitors)): ?>
<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">Keine Monitore registriert.</td></tr>
<?php else: ?>
<?php foreach ($monitors as $m): ?>
<tr>
<td><strong><?= htmlspecialchars($m['source']) ?></strong> (<?= htmlspecialchars($m['instance']) ?>)</td>
<td><?= htmlspecialchars($m['type']) ?></td>
<td>
<span class="badge badge-<?= $m['state'] === 'up' ? 'up' : ($m['state'] === 'warning' ? 'warning' : 'down') ?>">
<?= strtoupper($m['state']) ?>
</span>
</td>
<td><?= htmlspecialchars($m['last_message'] ?? '-') ?></td>
<td><?= htmlspecialchars($m['last_seen_utc'] ?? 'Nie') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📜 Letzte System-Events</h2>
</div>
<table>
<thead>
<tr>
<th>Zeitpunkt (UTC)</th>
<th>Source</th>
<th>Event Kind</th>
<th>Severity</th>
<th>Nachricht</th>
</tr>
</thead>
<tbody>
<?php if (empty($recentEvents)): ?>
<tr><td colspan="5" style="text-align:center; color:var(--text-muted)">Keine Events vorhanden.</td></tr>
<?php else: ?>
<?php foreach ($recentEvents as $e): ?>
<tr>
<td><?= htmlspecialchars($e['at_utc']) ?></td>
<td><?= htmlspecialchars($e['source']) ?></td>
<td><code><?= htmlspecialchars($e['kind']) ?></code></td>
<td><span class="badge badge-<?= $e['severity'] === 'info' ? 'up' : 'down' ?>"><?= strtoupper($e['severity']) ?></span></td>
<td><?= htmlspecialchars($e['message'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- TAB 2: LICENSELABRADOR -->
<div id="tab-licenses" class="tab-content">
<div class="top-header">
<h1 class="page-title">🔑 LicenseLabrador Modul</h1>
</div>
<!-- Create Product Card -->
<div class="card">
<div class="card-header"><h2 class="card-title">Neues Produkt anlegen</h2></div>
<form method="POST">
<input type="hidden" name="action" value="create_product">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt-Slug (z. B. myapp)</label>
<input type="text" name="slug" class="form-input" required placeholder="myapp">
</div>
<div class="form-group">
<label class="form-label">Produkt Name</label>
<input type="text" name="name" class="form-input" required placeholder="My Application Deluxe">
</div>
<div class="form-group">
<label class="form-label">Cache TTL (Stunden)</label>
<input type="number" name="ttl" class="form-input" value="168">
</div>
</div>
<button type="submit" class="btn">Produkt Erstellen</button>
</form>
</div>
<!-- Create License Card -->
<div class="card">
<div class="card-header"><h2 class="card-title">Neuen Lizenzschlüssel generieren</h2></div>
<form method="POST">
<input type="hidden" name="action" value="create_license">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt</label>
<select name="product_id" class="form-input" required>
<?php foreach ($products as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Kunden Name</label>
<input type="text" name="customer_name" class="form-input" placeholder="Max Mustermann">
</div>
<div class="form-group">
<label class="form-label">Kunden E-Mail</label>
<input type="email" name="customer_email" class="form-input" placeholder="max@example.com">
</div>
<div class="form-group">
<label class="form-label">Max. Aktivierungen</label>
<input type="number" name="max_activations" class="form-input" value="2" min="1">
</div>
<div class="form-group">
<label class="form-label">Ablaufdatum (optional)</label>
<input type="date" name="expires_at" class="form-input">
</div>
</div>
<button type="submit" class="btn">Lizenz Generieren</button>
</form>
</div>
<!-- Licenses List Card -->
<div class="card">
<div class="card-header"><h2 class="card-title">Erstellte Lizenzen</h2></div>
<table>
<thead>
<tr>
<th>Produkt</th>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Aktivierungen</th>
<th>Status</th>
<th>Ablaufdatum</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php if (empty($licenses)): ?>
<tr><td colspan="7" style="text-align:center; color:var(--text-muted)">Keine Lizenzen vorhanden.</td></tr>
<?php else: ?>
<?php foreach ($licenses as $l): ?>
<tr>
<td><?= htmlspecialchars($l['product_name']) ?></td>
<td><code><?= htmlspecialchars($l['license_key']) ?></code></td>
<td><?= htmlspecialchars($l['customer_name'] ?? '-') ?></td>
<td><?= $l['active_count'] ?> / <?= $l['max_activations'] ?></td>
<td><span class="badge badge-<?= $l['status'] ?>"><?= strtoupper($l['status']) ?></span></td>
<td><?= $l['expires_at'] ? htmlspecialchars($l['expires_at']) : 'Unbefristet' ?></td>
<td>
<?php if ($l['status'] === 'active'): ?>
<form method="POST" style="display:inline">
<input type="hidden" name="action" value="revoke_license">
<input type="hidden" name="license_id" value="<?= $l['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Lizenz wirklich widerrufen?')">Widerrufen</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- TAB 3: WATCHDOG -->
<div id="tab-watchdog" class="tab-content">
<div class="top-header">
<h1 class="page-title">🛡️ Watchdog Monitoring Modul</h1>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">API Endpunkte & Integration</h2></div>
<p style="color:var(--text-muted); font-size:0.9rem; margin-bottom:1rem;">
Monitore und Agents senden Heartbeats an folgenden unauthentifizierten API-Endpunkt:
</p>
<code style="background:rgba(0,0,0,0.4); padding:0.75rem 1rem; border-radius:8px; display:block; color:#a5f3fc;">
POST https://dc.mhdf.de/api/watchdog/v1/ping
</code>
</div>
</div>
<!-- TAB 4: UPDATESERVICE -->
<div id="tab-updateservice" class="tab-content">
<div class="top-header">
<h1 class="page-title">📦 UpdateService Modul</h1>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Neues Release veröffentlichen</h2></div>
<form method="POST">
<input type="hidden" name="action" value="add_release">
<div class="form-grid">
<div class="form-group">
<label class="form-label">Produkt-Slug</label>
<input type="text" name="product_slug" class="form-input" required placeholder="myapp">
</div>
<div class="form-group">
<label class="form-label">Version (z. B. 1.2.0)</label>
<input type="text" name="version" class="form-input" required placeholder="1.2.0">
</div>
<div class="form-group">
<label class="form-label">Download URL</label>
<input type="url" name="download_url" class="form-input" required placeholder="https://cdn.example.com/myapp-v1.2.0.zip">
</div>
<div class="form-group">
<label class="form-label">SHA256 Hash (optional)</label>
<input type="text" name="sha256_hash" class="form-input" placeholder="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">
</div>
</div>
<button type="submit" class="btn">Release Veröffentlichen</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Veröffentlichte Releases</h2></div>
<table>
<thead>
<tr>
<th>Produkt</th>
<th>Version</th>
<th>Download URL</th>
<th>Release Datum</th>
</tr>
</thead>
<tbody>
<?php if (empty($releases)): ?>
<tr><td colspan="4" style="text-align:center; color:var(--text-muted)">Noch keine Releases eingetragen.</td></tr>
<?php else: ?>
<?php foreach ($releases as $r): ?>
<tr>
<td><strong><?= htmlspecialchars($r['product_slug']) ?></strong></td>
<td><code>v<?= htmlspecialchars($r['version']) ?></code></td>
<td><a href="<?= htmlspecialchars($r['download_url']) ?>" target="_blank" style="color:var(--primary);"><?= htmlspecialchars($r['download_url']) ?></a></td>
<td><?= htmlspecialchars($r['created_at']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- TAB 5: SYSTEM & DB -->
<div id="tab-system" class="tab-content">
<div class="top-header">
<h1 class="page-title">⚙️ System & Datenbank Status</h1>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Datenbank Schema & Ersteinrichtung</h2></div>
<p style="color:var(--text-muted); font-size:0.9rem; margin-bottom:1rem;">
Initialisiert alle Tabellen (<code>license_*</code>, <code>watchdog_*</code>, <code>updateservice_*</code>, <code>dc_*</code>) und richtet den Standard-Admin-User (admin) ein.
</p>
<a href="install_db.php" target="_blank" class="btn">Schema Installieren / Reparieren (install_db.php)</a>
</div>
</div>
</main>
<script>
function switchTab(tabName, el) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-link').forEach(n => n.classList.remove('active'));
document.getElementById('tab-' + tabName).classList.add('active');
el.classList.add('active');
}
</script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
header('Content-Type: application/json; charset=utf-8');
try {
$config = require __DIR__ . '/../config/config.php';
$dbCfg = $config['db'];
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', $dbCfg['host'], $dbCfg['dbname'], $dbCfg['charset']);
$pdo = new PDO($dsn, $dbCfg['username'], $dbCfg['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$sqlFile = __DIR__ . '/../sql/schema.sql';
if (!file_exists($sqlFile)) {
echo json_encode(['status' => 'error', 'message' => 'schema.sql file not found']);
exit;
}
$rawSql = file_get_contents($sqlFile);
// Remove comments
$lines = explode("\n", $rawSql);
$cleanLines = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
$cleanLines[] = $line;
}
$cleanSql = implode("\n", $cleanLines);
// Split queries by semicolon
$queries = array_filter(array_map('trim', explode(';', $cleanSql)));
$executed = 0;
foreach ($queries as $q) {
if (!empty($q)) {
$pdo->exec($q);
$executed++;
}
}
// Create / update Admin user: admin / Admin1337!
$adminUsername = 'admin';
$adminPassword = 'Admin1337!';
$passwordHash = password_hash($adminPassword, PASSWORD_ARGON2ID);
$stmt = $pdo->prepare('
INSERT INTO dc_users (username, password_hash, created_at)
VALUES (:u, :p, NOW())
ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash)
');
$stmt->execute([':u' => $adminUsername, ':p' => $passwordHash]);
// Seed default endpoints setting in dc_settings
$endpoints = json_encode([
'validate' => '/api/license/v1/validate',
'deactivate' => '/api/license/v1/deactivate',
]);
$stmtSet = $pdo->prepare('INSERT INTO dc_settings (skey, svalue) VALUES ("endpoints", :v) ON DUPLICATE KEY UPDATE svalue = VALUES(svalue)');
$stmtSet->execute([':v' => $endpoints]);
// Query created tables to verify
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
echo json_encode([
'status' => 'success',
'message' => "Successfully executed {$executed} SQL statements!",
'created_user' => 'admin',
'tables_in_db' => $tables,
'timestamp' => date('Y-m-d H:i:s')
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (Throwable $t) {
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => $t->getMessage(),
'file' => $t->getFile(),
'line' => $t->getLine()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
+249
View File
@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Core/Db.php';
require_once __DIR__ . '/../src/Core/Auth.php';
use Deploymentcenter\Core\Db;
use Deploymentcenter\Core\Auth;
Auth::startSession();
if (Auth::isLoggedIn()) {
header('Location: /index.php');
exit;
}
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
if (!empty($username) && !empty($password)) {
try {
$config = require __DIR__ . '/../config/config.php';
$pdo = Db::init($config);
if (Auth::login($pdo, $username, $password)) {
header('Location: /index.php');
exit;
} else {
$error = 'Ungültige Anmeldedaten. Bitte überprüfen Sie Benutzername und Passwort.';
}
} catch (\Throwable $e) {
$error = 'Datenbankverbindung fehlgeschlagen: ' . $e->getMessage();
}
} else {
$error = 'Bitte füllen Sie alle Felder aus.';
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmeldung - Deploymentcenter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%);
--card-bg: rgba(30, 41, 59, 0.7);
--card-border: rgba(255, 255, 255, 0.1);
--primary: #6366f1;
--primary-hover: #4f46e5;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--danger-bg: rgba(239, 68, 68, 0.15);
--danger-border: rgba(239, 68, 68, 0.3);
--danger-text: #fca5a5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: var(--bg-gradient);
color: var(--text-main);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.login-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 2.5rem;
width: 100%;
max-width: 420px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.brand-header {
text-align: center;
margin-bottom: 2rem;
}
.brand-logo {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #6366f1, #a855f7);
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.4);
}
.brand-logo svg {
width: 30px;
height: 30px;
fill: none;
stroke: #ffffff;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.brand-title {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.025em;
background: linear-gradient(to right, #ffffff, #cbd5e1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.brand-subtitle {
font-size: 0.875rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.alert-danger {
background: var(--danger-bg);
border: 1px solid var(--danger-border);
color: var(--danger-text);
padding: 0.875rem 1rem;
border-radius: 10px;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.form-control {
width: 100%;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 0.75rem 1rem;
font-size: 0.95rem;
color: var(--text-main);
outline: none;
transition: all 0.2s ease;
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
}
.btn-submit {
width: 100%;
background: linear-gradient(135deg, var(--primary), #4f46e5);
color: #ffffff;
border: none;
border-radius: 10px;
padding: 0.875rem;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.4);
margin-top: 0.5rem;
}
.btn-submit:hover {
transform: translateY(-1px);
box-shadow: 0 15px 25px -5px rgba(99, 102, 241, 0.5);
}
.btn-submit:active {
transform: translateY(0);
}
.footer-note {
text-align: center;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 2rem;
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-header">
<div class="brand-logo">
<svg viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
</div>
<h1 class="brand-title">Deploymentcenter</h1>
<p class="brand-subtitle">Bitte melden Sie sich an, um fortzufahren</p>
</div>
<?php if ($error): ?>
<div class="alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST" action="login.php">
<div class="form-group">
<label for="username" class="form-label">Benutzername</label>
<input type="text" id="username" name="username" class="form-control" required autofocus placeholder="z. B. admin">
</div>
<div class="form-group">
<label for="password" class="form-label">Passwort</label>
<input type="password" id="password" name="password" class="form-control" required placeholder="••••••••">
</div>
<button type="submit" class="btn-submit">Anmelden</button>
</form>
<div class="footer-note">
Deploymentcenter &copy; <?= date('Y') ?> &bull; Unified License & Monitoring Platform
</div>
</div>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/Core/Auth.php';
use Deploymentcenter\Core\Auth;
Auth::logout();
header('Location: /login.php');
exit;