Initial commit: Modular Deploymentcenter platform
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
.deploy_cache.json
|
||||||
|
scratch/
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Module API routing
|
||||||
|
RewriteRule ^api/license/v1(?:/(.*))?$ public/api/license/v1/index.php [L,QSA]
|
||||||
|
RewriteRule ^api/watchdog/v1(?:/(.*))?$ public/api/watchdog/v1/index.php [L,QSA]
|
||||||
|
RewriteRule ^api/updateservice/v1(?:/(.*))?$ public/api/updateservice/v1/index.php [L,QSA]
|
||||||
|
|
||||||
|
# Fallback for static assets in /api/
|
||||||
|
RewriteRule ^api/(.*)$ public/api/$1 [L,QSA]
|
||||||
|
|
||||||
|
# Route /install_db.php
|
||||||
|
RewriteRule ^install_db\.php$ public/install_db.php [L,QSA]
|
||||||
|
|
||||||
|
# Route /login.php & /logout.php
|
||||||
|
RewriteRule ^login\.php$ public/login.php [L,QSA]
|
||||||
|
RewriteRule ^logout\.php$ public/logout.php [L,QSA]
|
||||||
|
|
||||||
|
# Route root requests to public/index.php
|
||||||
|
RewriteRule ^$ public/index.php [L]
|
||||||
|
RewriteRule ^index\.php$ public/index.php [L]
|
||||||
|
|
||||||
|
# Block direct access to sensitive folders
|
||||||
|
RewriteRule ^(config|src|sql|scripts)/ - [F,L]
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
url dc.mhdf.de htaccess: user: deploy pw: deploy02763!
|
||||||
|
MySQL Connectionstring: mysql -D bergisnu_db0 -u bergisnu_0 -p'r4[V?:)C~+Sh' -h lznk.your-database.de
|
||||||
|
FTP Zugangsdaten: server: www531.your-server.de user: bergisnu_4 pw: o2#M*NN^5EsT
|
||||||
|
|
||||||
|
|
||||||
|
Git http://192.168.178.10:8418/Richard/Deploymentcenter.git Token: eb42957585f9c6d41b79cee07b9a5ca8dbaf0179
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deploy:$apr1$c815$WbSj8VpE1zP2Y.0Z7G3h/1
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
// Deploymentcenter Configuration
|
||||||
|
|
||||||
|
return [
|
||||||
|
'app' => [
|
||||||
|
'name' => 'Deploymentcenter',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'url' => 'https://dc.mhdf.de',
|
||||||
|
'timezone' => 'Europe/Berlin',
|
||||||
|
],
|
||||||
|
'db' => [
|
||||||
|
'host' => 'lznk.your-database.de',
|
||||||
|
'dbname' => 'bergisnu_db0',
|
||||||
|
'username' => 'bergisnu_0',
|
||||||
|
'password' => 'r4[V?:)C~+Sh',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
'security' => [
|
||||||
|
'shared_key' => 'DC_MASTER_SECURE_TOKEN_2026_x98f',
|
||||||
|
'session_name' => 'DC_SESSION_ID',
|
||||||
|
]
|
||||||
|
];
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Public folder htaccess
|
||||||
|
Options -Indexes
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 © <?= date('Y') ?> • Unified License & Monitoring Platform
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import ftplib
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
PROJECT_DIR = SCRIPT_DIR.parent
|
||||||
|
CONFIG_FILE = SCRIPT_DIR / 'deploy_config.json'
|
||||||
|
CACHE_FILE = SCRIPT_DIR / '.deploy_cache.json'
|
||||||
|
|
||||||
|
IGNORE_PATTERNS = {
|
||||||
|
'.git',
|
||||||
|
'.gitignore',
|
||||||
|
'scripts',
|
||||||
|
'.deploy_cache.json',
|
||||||
|
'Serverdaten.txt',
|
||||||
|
'Serverdaten.txt.bak'
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
if not CONFIG_FILE.exists():
|
||||||
|
print(f"Error: Config file {CONFIG_FILE} does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def load_cache():
|
||||||
|
if CACHE_FILE.exists():
|
||||||
|
try:
|
||||||
|
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_cache(cache):
|
||||||
|
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cache, f, indent=2)
|
||||||
|
|
||||||
|
def compute_hash(filepath):
|
||||||
|
h = hashlib.md5()
|
||||||
|
with open(filepath, 'rb') as f:
|
||||||
|
while chunk := f.read(8192):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def ensure_remote_dir(ftp, remote_path):
|
||||||
|
dirs = [d for d in remote_path.strip('/').split('/') if d]
|
||||||
|
current = ''
|
||||||
|
for d in dirs:
|
||||||
|
current += '/' + d
|
||||||
|
try:
|
||||||
|
ftp.cwd(current)
|
||||||
|
except ftplib.error_perm:
|
||||||
|
try:
|
||||||
|
ftp.mkd(current)
|
||||||
|
print(f"Created remote directory: {current}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not create directory {current}: {e}")
|
||||||
|
|
||||||
|
def should_ignore(rel_path):
|
||||||
|
parts = Path(rel_path).parts
|
||||||
|
if not parts:
|
||||||
|
return False
|
||||||
|
if parts[0] in IGNORE_PATTERNS:
|
||||||
|
return True
|
||||||
|
for part in parts:
|
||||||
|
if part == '.htaccess':
|
||||||
|
continue
|
||||||
|
if part.startswith('.'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def deploy():
|
||||||
|
config = load_config()
|
||||||
|
cache = load_cache()
|
||||||
|
new_cache = dict(cache)
|
||||||
|
|
||||||
|
print(f"Connecting to FTP {config['host']}...")
|
||||||
|
try:
|
||||||
|
if config.get('secure', False):
|
||||||
|
ftp = ftplib.FTP_TLS()
|
||||||
|
ftp.connect(config['host'], config.get('port', 21))
|
||||||
|
ftp.login(config['user'], config['pass'])
|
||||||
|
ftp.prot_p()
|
||||||
|
else:
|
||||||
|
ftp = ftplib.FTP()
|
||||||
|
ftp.connect(config['host'], config.get('port', 21))
|
||||||
|
ftp.login(config['user'], config['pass'])
|
||||||
|
print("Logged in successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FTP Connection failed: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
files_to_upload = []
|
||||||
|
for root, dirs, files in os.walk(PROJECT_DIR):
|
||||||
|
rel_dir = os.path.relpath(root, PROJECT_DIR)
|
||||||
|
if rel_dir == '.':
|
||||||
|
rel_dir = ''
|
||||||
|
|
||||||
|
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(rel_dir, d))]
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
rel_file = os.path.normpath(os.path.join(rel_dir, f)).replace('\\', '/')
|
||||||
|
if should_ignore(rel_file):
|
||||||
|
continue
|
||||||
|
|
||||||
|
full_path = os.path.join(root, f)
|
||||||
|
file_hash = compute_hash(full_path)
|
||||||
|
|
||||||
|
if cache.get(rel_file) != file_hash:
|
||||||
|
files_to_upload.append((rel_file, full_path, file_hash))
|
||||||
|
|
||||||
|
if not files_to_upload:
|
||||||
|
print("No changed files to upload. Remote is up to date!")
|
||||||
|
ftp.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(files_to_upload)} file(s) to upload:")
|
||||||
|
for rel_file, full_path, file_hash in files_to_upload:
|
||||||
|
print(f" -> {rel_file}")
|
||||||
|
|
||||||
|
for rel_file, full_path, file_hash in files_to_upload:
|
||||||
|
remote_file_path = f"/{rel_file}"
|
||||||
|
remote_dir = os.path.dirname(remote_file_path).replace('\\', '/')
|
||||||
|
if remote_dir and remote_dir != '/':
|
||||||
|
ensure_remote_dir(ftp, remote_dir)
|
||||||
|
|
||||||
|
ftp.cwd('/')
|
||||||
|
print(f"Uploading {rel_file} ...")
|
||||||
|
with open(full_path, 'rb') as f:
|
||||||
|
ftp.storbinary(f"STOR {remote_file_path}", f)
|
||||||
|
new_cache[rel_file] = file_hash
|
||||||
|
|
||||||
|
save_cache(new_cache)
|
||||||
|
ftp.quit()
|
||||||
|
print("Deployment completed successfully!")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
deploy()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"host": "www531.your-server.de",
|
||||||
|
"port": 21,
|
||||||
|
"user": "bergisnu_4",
|
||||||
|
"pass": "o2#M*NN^5EsT",
|
||||||
|
"secure": true
|
||||||
|
}
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
-- Deploymentcenter Unified Database Schema
|
||||||
|
-- UTF-8 (utf8mb4) & InnoDB
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- Core Platform Tables
|
||||||
|
DROP TABLE IF EXISTS dc_users;
|
||||||
|
DROP TABLE IF EXISTS dc_settings;
|
||||||
|
|
||||||
|
-- LicenseLabrador Module Tables
|
||||||
|
DROP TABLE IF EXISTS license_api_rate_limit;
|
||||||
|
DROP TABLE IF EXISTS license_audit_log;
|
||||||
|
DROP TABLE IF EXISTS license_activations;
|
||||||
|
DROP TABLE IF EXISTS license_licenses;
|
||||||
|
DROP TABLE IF EXISTS license_products;
|
||||||
|
|
||||||
|
-- Watchdog Module Tables
|
||||||
|
DROP TABLE IF EXISTS watchdog_proxmox_targets;
|
||||||
|
DROP TABLE IF EXISTS watchdog_agent_tokens;
|
||||||
|
DROP TABLE IF EXISTS watchdog_cron_jobs;
|
||||||
|
DROP TABLE IF EXISTS watchdog_event_log;
|
||||||
|
DROP TABLE IF EXISTS watchdog_monitors;
|
||||||
|
|
||||||
|
-- UpdateService Module Tables
|
||||||
|
DROP TABLE IF EXISTS updateservice_releases;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- 1. Core Platform Tables
|
||||||
|
CREATE TABLE dc_users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
totp_secret VARCHAR(64) NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE dc_settings (
|
||||||
|
skey VARCHAR(64) PRIMARY KEY,
|
||||||
|
svalue TEXT NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 2. LicenseLabrador Module Tables
|
||||||
|
CREATE TABLE license_products (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
slug VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(190) NOT NULL,
|
||||||
|
notes TEXT NULL,
|
||||||
|
default_cache_ttl_hours INT NOT NULL DEFAULT 168,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE license_licenses (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
license_key CHAR(29) NOT NULL UNIQUE,
|
||||||
|
customer_name VARCHAR(190) NULL,
|
||||||
|
customer_email VARCHAR(190) NULL,
|
||||||
|
status ENUM('active','revoked','suspended') NOT NULL DEFAULT 'active',
|
||||||
|
expires_at DATETIME NULL,
|
||||||
|
max_activations INT NOT NULL DEFAULT 2,
|
||||||
|
notes TEXT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES license_products(id) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE license_activations (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
license_id INT NOT NULL,
|
||||||
|
hardware_id VARCHAR(128) NOT NULL,
|
||||||
|
hostname VARCHAR(190) NULL,
|
||||||
|
app_version VARCHAR(64) NULL,
|
||||||
|
first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_blocked TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uq_lic_hw (license_id, hardware_id),
|
||||||
|
FOREIGN KEY (license_id) REFERENCES license_licenses(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE license_audit_log (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
actor VARCHAR(64) NOT NULL,
|
||||||
|
action VARCHAR(64) NOT NULL,
|
||||||
|
details TEXT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE license_api_rate_limit (
|
||||||
|
ip VARBINARY(16) NOT NULL,
|
||||||
|
window_start DATETIME NOT NULL,
|
||||||
|
request_count INT NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (ip, window_start)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 3. Watchdog Module Tables
|
||||||
|
CREATE TABLE watchdog_monitors (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
source VARCHAR(100) NOT NULL,
|
||||||
|
instance VARCHAR(100) NOT NULL DEFAULT 'default',
|
||||||
|
type ENUM('heartbeat','host','hypervisor_node','guest') NOT NULL DEFAULT 'heartbeat',
|
||||||
|
state ENUM('up','warning','down','error','stopped','maintenance') NOT NULL DEFAULT 'up',
|
||||||
|
expected_interval_sec INT NOT NULL DEFAULT 60,
|
||||||
|
last_seen_utc DATETIME NULL,
|
||||||
|
last_status ENUM('ok','warning','error') NULL,
|
||||||
|
last_message TEXT NULL,
|
||||||
|
metrics_json JSON NULL,
|
||||||
|
metric_state_reason VARCHAR(255) NULL,
|
||||||
|
group_key VARCHAR(150) NULL,
|
||||||
|
parent_source VARCHAR(100) NULL,
|
||||||
|
notes TEXT NULL,
|
||||||
|
url VARCHAR(255) NULL,
|
||||||
|
icon VARCHAR(255) NULL,
|
||||||
|
is_muted TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
expect_running TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
os VARCHAR(50) NULL,
|
||||||
|
first_contact_deadline_utc DATETIME NULL,
|
||||||
|
ack_until_utc DATETIME NULL,
|
||||||
|
acknowledged_by VARCHAR(100) NULL,
|
||||||
|
warning_notified_utc DATETIME NULL,
|
||||||
|
suppress_until_utc DATETIME NULL,
|
||||||
|
last_started_utc DATETIME NULL,
|
||||||
|
last_stopped_utc DATETIME NULL,
|
||||||
|
last_crash_utc DATETIME NULL,
|
||||||
|
created_utc DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_utc DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_monitor (source, instance)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE watchdog_event_log (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
source VARCHAR(100) NOT NULL,
|
||||||
|
instance VARCHAR(100) NOT NULL DEFAULT 'default',
|
||||||
|
kind ENUM('started','stopped_graceful','crash_suspected','hard_error',
|
||||||
|
'recovered','warning_raised','warning_cleared',
|
||||||
|
'maintenance_start','maintenance_end','watchdog_started') NOT NULL,
|
||||||
|
from_state VARCHAR(20) NULL,
|
||||||
|
to_state VARCHAR(20) NULL,
|
||||||
|
severity ENUM('info','warning','alarm') NOT NULL DEFAULT 'info',
|
||||||
|
at_utc DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
message TEXT NULL,
|
||||||
|
meta_json JSON NULL,
|
||||||
|
notified TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
notified_at_utc DATETIME NULL,
|
||||||
|
channel VARCHAR(40) NULL,
|
||||||
|
KEY ix_evt (source, instance, at_utc),
|
||||||
|
KEY ix_evt_time (at_utc)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE watchdog_cron_jobs (
|
||||||
|
name VARCHAR(50) PRIMARY KEY,
|
||||||
|
interval_sec INT NOT NULL,
|
||||||
|
last_run_utc DATETIME NULL,
|
||||||
|
running TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
lock_until_utc DATETIME NULL,
|
||||||
|
last_status VARCHAR(20) NULL,
|
||||||
|
last_duration_ms INT NULL,
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE watchdog_agent_tokens (
|
||||||
|
token_id VARCHAR(64) PRIMARY KEY,
|
||||||
|
token_hash VARCHAR(128) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
monitor_source VARCHAR(100) NULL,
|
||||||
|
monitor_instance VARCHAR(100) NULL,
|
||||||
|
created_at_utc DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at_utc DATETIME NULL,
|
||||||
|
revoked TINYINT(1) NOT NULL DEFAULT 0
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE watchdog_proxmox_targets (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
url VARCHAR(255) NOT NULL,
|
||||||
|
token_id VARCHAR(150) NOT NULL,
|
||||||
|
token_secret TEXT NOT NULL,
|
||||||
|
cert_fingerprint VARCHAR(100) NULL,
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
last_poll_utc DATETIME NULL,
|
||||||
|
last_status VARCHAR(50) NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 4. UpdateService Module Tables
|
||||||
|
CREATE TABLE updateservice_releases (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_slug VARCHAR(64) NOT NULL,
|
||||||
|
version VARCHAR(32) NOT NULL,
|
||||||
|
release_notes TEXT NULL,
|
||||||
|
download_url VARCHAR(255) NOT NULL,
|
||||||
|
sha256_hash VARCHAR(64) NULL,
|
||||||
|
is_critical TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_prod_ver (product_slug, version)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Seed default cron jobs for Watchdog
|
||||||
|
INSERT INTO watchdog_cron_jobs (name, interval_sec, enabled) VALUES
|
||||||
|
('evaluator', 60, 1),
|
||||||
|
('proxmox_poll', 60, 1),
|
||||||
|
('proxmox_smart', 600, 1),
|
||||||
|
('warning_digest', 43200, 1),
|
||||||
|
('self_ping', 60, 1),
|
||||||
|
('eventlog_cleanup', 86400, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE interval_sec = VALUES(interval_sec);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Core;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
{
|
||||||
|
public static function startSession(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
$config = require __DIR__ . '/../../config/config.php';
|
||||||
|
session_name($config['security']['session_name'] ?? 'DC_SESSION_ID');
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
self::startSession();
|
||||||
|
return !empty($_SESSION['dc_user_id']) && !empty($_SESSION['dc_username']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function requireLogin(): void
|
||||||
|
{
|
||||||
|
if (!self::isLoggedIn()) {
|
||||||
|
header('Location: /login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function login(PDO $db, string $username, string $password): bool
|
||||||
|
{
|
||||||
|
self::startSession();
|
||||||
|
$stmt = $db->prepare('SELECT id, username, password_hash FROM dc_users WHERE username = :u');
|
||||||
|
$stmt->execute([':u' => $username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
$_SESSION['dc_user_id'] = $user['id'];
|
||||||
|
$_SESSION['dc_username'] = $user['username'];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function logout(): void
|
||||||
|
{
|
||||||
|
self::startSession();
|
||||||
|
$_SESSION = [];
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000,
|
||||||
|
$params["path"], $params["domain"],
|
||||||
|
$params["secure"], $params["httponly"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Core;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
|
||||||
|
class Db
|
||||||
|
{
|
||||||
|
private static ?PDO $instance = null;
|
||||||
|
|
||||||
|
public static function init(array $config): PDO
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
$dbCfg = $config['db'];
|
||||||
|
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', $dbCfg['host'], $dbCfg['dbname'], $dbCfg['charset']);
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
self::$instance = new PDO($dsn, $dbCfg['username'], $dbCfg['password'], $options);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new \Exception('Database connection failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance(): PDO
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
$config = require __DIR__ . '/../../config/config.php';
|
||||||
|
return self::init($config);
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\License;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class Audit
|
||||||
|
{
|
||||||
|
public static function log(PDO $db, string $actor, string $action, ?array $details = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare('INSERT INTO license_audit_log (actor, action, details) VALUES (:actor, :action, :details)');
|
||||||
|
$stmt->execute([
|
||||||
|
':actor' => $actor,
|
||||||
|
':action' => $action,
|
||||||
|
':details' => $details !== null ? json_encode($details, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Ignore audit log failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\License;
|
||||||
|
|
||||||
|
class KeyGen
|
||||||
|
{
|
||||||
|
private const CHARSET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
public static function generateKey(): string
|
||||||
|
{
|
||||||
|
$groups = [];
|
||||||
|
for ($i = 0; $i < 5; $i++) {
|
||||||
|
$group = '';
|
||||||
|
for ($j = 0; $j < 5; $j++) {
|
||||||
|
$group .= self::CHARSET[random_int(0, strlen(self::CHARSET) - 1)];
|
||||||
|
}
|
||||||
|
$groups[] = $group;
|
||||||
|
}
|
||||||
|
return implode('-', $groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\License;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class LicenseService
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate(array $requestData, string $clientIp): array
|
||||||
|
{
|
||||||
|
$productSlug = trim($requestData['product'] ?? '');
|
||||||
|
$licenseKey = trim($requestData['license_key'] ?? '');
|
||||||
|
$hardwareId = trim($requestData['hardware_id'] ?? '');
|
||||||
|
$nonce = trim($requestData['nonce'] ?? '');
|
||||||
|
$hostname = trim($requestData['hostname'] ?? '');
|
||||||
|
$appVersion = trim($requestData['app_version'] ?? '');
|
||||||
|
|
||||||
|
$issuedAt = time();
|
||||||
|
$endpoints = $this->getEndpoints();
|
||||||
|
$cacheTtlHours = 168;
|
||||||
|
|
||||||
|
$makePayload = function(string $status, ?array $extra = []) use ($issuedAt, $nonce, $productSlug, $licenseKey, $hardwareId, $endpoints, &$cacheTtlHours) {
|
||||||
|
$base = [
|
||||||
|
'type' => 'validation_result',
|
||||||
|
'issued_at' => $issuedAt,
|
||||||
|
'nonce' => $nonce,
|
||||||
|
'product' => $productSlug,
|
||||||
|
'license_key' => $licenseKey,
|
||||||
|
'hardware_id' => $hardwareId,
|
||||||
|
'status' => $status,
|
||||||
|
'expires_at' => null,
|
||||||
|
'cache_ttl_hours' => $cacheTtlHours,
|
||||||
|
'endpoints' => $endpoints,
|
||||||
|
'message' => null
|
||||||
|
];
|
||||||
|
return array_merge($base, $extra);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Fetch Product
|
||||||
|
$stmt = $this->db->prepare('SELECT id, default_cache_ttl_hours FROM license_products WHERE slug = :slug');
|
||||||
|
$stmt->execute([':slug' => $productSlug]);
|
||||||
|
$product = $stmt->fetch();
|
||||||
|
if (!$product) {
|
||||||
|
Audit::log($this->db, 'api', 'api.validate.unknown_product', [
|
||||||
|
'product' => $productSlug,
|
||||||
|
'key_prefix' => substr($licenseKey, 0, 5),
|
||||||
|
'ip' => $clientIp
|
||||||
|
]);
|
||||||
|
return $makePayload('not_found', ['message' => 'Product not found']);
|
||||||
|
}
|
||||||
|
$cacheTtlHours = (int)$product['default_cache_ttl_hours'];
|
||||||
|
|
||||||
|
// 2. Fetch License
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM license_licenses WHERE product_id = :pid AND license_key = :key');
|
||||||
|
$stmt->execute([':pid' => $product['id'], ':key' => $licenseKey]);
|
||||||
|
$license = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$license) {
|
||||||
|
Audit::log($this->db, 'api', 'api.validate.failed_key', [
|
||||||
|
'product' => $productSlug,
|
||||||
|
'key_prefix' => substr($licenseKey, 0, 5),
|
||||||
|
'ip' => $clientIp
|
||||||
|
]);
|
||||||
|
return $makePayload('not_found', ['message' => 'Invalid license key']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expiresAtUnix = $license['expires_at'] ? strtotime($license['expires_at']) : null;
|
||||||
|
|
||||||
|
// 3. License status check
|
||||||
|
if ($license['status'] === 'revoked') {
|
||||||
|
return $makePayload('revoked', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'License has been revoked'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license['status'] === 'suspended') {
|
||||||
|
return $makePayload('suspended', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'License is temporarily suspended'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expiresAtUnix !== null && $expiresAtUnix < $issuedAt) {
|
||||||
|
return $makePayload('expired', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'License has expired'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Activation management
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM license_activations WHERE license_id = :lic_id AND hardware_id = :hw_id');
|
||||||
|
$stmt->execute([':lic_id' => $license['id'], ':hw_id' => $hardwareId]);
|
||||||
|
$activation = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($activation) {
|
||||||
|
if ((int)$activation['is_blocked'] === 1) {
|
||||||
|
return $makePayload('revoked', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'This hardware activation is blocked'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$upd = $this->db->prepare('UPDATE license_activations SET last_seen = NOW(), hostname = :host, app_version = :ver WHERE id = :id');
|
||||||
|
$upd->execute([':host' => $hostname, ':ver' => $appVersion, ':id' => $activation['id']]);
|
||||||
|
} else {
|
||||||
|
$cntStmt = $this->db->prepare('SELECT COUNT(*) FROM license_activations WHERE license_id = :lic_id AND is_blocked = 0');
|
||||||
|
$cntStmt->execute([':lic_id' => $license['id']]);
|
||||||
|
$activeCount = (int)$cntStmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($activeCount >= (int)$license['max_activations']) {
|
||||||
|
return $makePayload('activation_limit', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'Maximum activations reached for this license'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ins = $this->db->prepare('INSERT INTO license_activations (license_id, hardware_id, hostname, app_version) VALUES (:lic_id, :hw_id, :host, :ver)');
|
||||||
|
$ins->execute([
|
||||||
|
':lic_id' => $license['id'],
|
||||||
|
':hw_id' => $hardwareId,
|
||||||
|
':host' => $hostname,
|
||||||
|
':ver' => $appVersion
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Audit::log($this->db, 'api', 'api.validate.success', [
|
||||||
|
'license_id' => $license['id'],
|
||||||
|
'hardware_id' => $hardwareId,
|
||||||
|
'ip' => $clientIp
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $makePayload('valid', [
|
||||||
|
'expires_at' => $expiresAtUnix,
|
||||||
|
'message' => 'License is valid'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deactivate(array $requestData, string $clientIp): array
|
||||||
|
{
|
||||||
|
$productSlug = trim($requestData['product'] ?? '');
|
||||||
|
$licenseKey = trim($requestData['license_key'] ?? '');
|
||||||
|
$hardwareId = trim($requestData['hardware_id'] ?? '');
|
||||||
|
$nonce = trim($requestData['nonce'] ?? '');
|
||||||
|
|
||||||
|
$issuedAt = time();
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT a.id, a.license_id
|
||||||
|
FROM license_activations a
|
||||||
|
JOIN license_licenses l ON a.license_id = l.id
|
||||||
|
JOIN license_products p ON l.product_id = p.id
|
||||||
|
WHERE p.slug = :slug AND l.license_key = :key AND a.hardware_id = :hw_id
|
||||||
|
');
|
||||||
|
$stmt->execute([':slug' => $productSlug, ':key' => $licenseKey, ':hw_id' => $hardwareId]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
$del = $this->db->prepare('DELETE FROM license_activations WHERE id = :id');
|
||||||
|
$del->execute([':id' => $row['id']]);
|
||||||
|
|
||||||
|
Audit::log($this->db, 'api', 'api.deactivate.success', [
|
||||||
|
'license_id' => $row['license_id'],
|
||||||
|
'hardware_id' => $hardwareId,
|
||||||
|
'ip' => $clientIp
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'deactivation_result',
|
||||||
|
'issued_at' => $issuedAt,
|
||||||
|
'nonce' => $nonce,
|
||||||
|
'status' => 'ok',
|
||||||
|
'message' => 'Activation deactivated successfully'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEndpoints(): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT svalue FROM dc_settings WHERE skey = 'endpoints'");
|
||||||
|
$stmt->execute();
|
||||||
|
$val = $stmt->fetchColumn();
|
||||||
|
if ($val) {
|
||||||
|
$decoded = json_decode($val, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'validate' => '/api/license/v1/validate',
|
||||||
|
'deactivate' => '/api/license/v1/deactivate'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\License;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class RateLimiter
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
private int $limit;
|
||||||
|
private int $windowSeconds;
|
||||||
|
|
||||||
|
public function __construct(PDO $db, int $limit = 60, int $windowSeconds = 60)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
$this->limit = $limit;
|
||||||
|
$this->windowSeconds = $windowSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check(string $ip): bool
|
||||||
|
{
|
||||||
|
$packedIp = inet_pton($ip);
|
||||||
|
if ($packedIp === false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$windowStart = date('Y-m-d H:i:s', $now - ($now % $this->windowSeconds));
|
||||||
|
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->prepare('SELECT request_count FROM license_api_rate_limit WHERE ip = :ip AND window_start = :ws FOR UPDATE');
|
||||||
|
$stmt->execute([':ip' => $packedIp, ':ws' => $windowStart]);
|
||||||
|
$count = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($count === false) {
|
||||||
|
$ins = $this->db->prepare('INSERT INTO license_api_rate_limit (ip, window_start, request_count) VALUES (:ip, :ws, 1)');
|
||||||
|
$ins->execute([':ip' => $packedIp, ':ws' => $windowStart]);
|
||||||
|
$this->db->commit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)$count >= $this->limit) {
|
||||||
|
$this->db->commit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $this->db->prepare('UPDATE license_api_rate_limit SET request_count = request_count + 1 WHERE ip = :ip AND window_start = :ws');
|
||||||
|
$upd->execute([':ip' => $packedIp, ':ws' => $windowStart]);
|
||||||
|
$this->db->commit();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->db->rollBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\UpdateService;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class UpdateManager
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkUpdate(string $productSlug, string $currentVersion): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
SELECT * FROM updateservice_releases
|
||||||
|
WHERE product_slug = :slug AND version > :ver
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
');
|
||||||
|
$stmt->execute([':slug' => $productSlug, ':ver' => $currentVersion]);
|
||||||
|
$latest = $stmt->fetch();
|
||||||
|
|
||||||
|
return $latest ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRelease(
|
||||||
|
string $productSlug,
|
||||||
|
string $version,
|
||||||
|
?string $releaseNotes,
|
||||||
|
string $downloadUrl,
|
||||||
|
?string $sha256Hash,
|
||||||
|
bool $isCritical = false
|
||||||
|
): bool {
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO updateservice_releases (
|
||||||
|
product_slug, version, release_notes, download_url, sha256_hash, is_critical
|
||||||
|
) VALUES (
|
||||||
|
:slug, :version, :notes, :url, :hash, :critical
|
||||||
|
) ON DUPLICATE KEY UPDATE
|
||||||
|
release_notes = VALUES(release_notes),
|
||||||
|
download_url = VALUES(download_url),
|
||||||
|
sha256_hash = VALUES(sha256_hash),
|
||||||
|
is_critical = VALUES(is_critical)
|
||||||
|
');
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
':slug' => $productSlug,
|
||||||
|
':version' => $version,
|
||||||
|
':notes' => $releaseNotes,
|
||||||
|
':url' => $downloadUrl,
|
||||||
|
':hash' => $sha256Hash,
|
||||||
|
':critical' => $isCritical ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReleases(?string $productSlug = null): array
|
||||||
|
{
|
||||||
|
if ($productSlug) {
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM updateservice_releases WHERE product_slug = :slug ORDER BY created_at DESC');
|
||||||
|
$stmt->execute([':slug' => $productSlug]);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->query('SELECT * FROM updateservice_releases ORDER BY created_at DESC');
|
||||||
|
}
|
||||||
|
return $stmt->fetchAll() ?: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\Watchdog;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class EventLog
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logEvent(
|
||||||
|
string $source,
|
||||||
|
string $instance,
|
||||||
|
string $kind,
|
||||||
|
?string $fromState = null,
|
||||||
|
?string $toState = null,
|
||||||
|
string $severity = 'info',
|
||||||
|
?string $message = null,
|
||||||
|
$meta = null
|
||||||
|
): int {
|
||||||
|
$metaJson = is_array($meta) || is_object($meta) ? json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
$nowUtc = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO watchdog_event_log (
|
||||||
|
source, instance, kind, from_state, to_state, severity, at_utc, message, meta_json
|
||||||
|
) VALUES (
|
||||||
|
:source, :instance, :kind, :from_state, :to_state, :severity, :now, :message, :meta
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':source' => $source,
|
||||||
|
':instance' => $instance,
|
||||||
|
':kind' => $kind,
|
||||||
|
':from_state' => $fromState,
|
||||||
|
':to_state' => $toState,
|
||||||
|
':severity' => $severity,
|
||||||
|
':now' => $nowUtc,
|
||||||
|
':message' => $message,
|
||||||
|
':meta' => $metaJson,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)$this->db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecentEvents(int $limit = 50, ?string $source = null, ?string $instance = null): array
|
||||||
|
{
|
||||||
|
$sql = 'SELECT * FROM watchdog_event_log';
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($source !== null) {
|
||||||
|
$where[] = 'source = :source';
|
||||||
|
$params[':source'] = $source;
|
||||||
|
}
|
||||||
|
if ($instance !== null) {
|
||||||
|
$where[] = 'instance = :instance';
|
||||||
|
$params[':instance'] = $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($where)) {
|
||||||
|
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' ORDER BY at_utc DESC LIMIT ' . (int)$limit;
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return $stmt->fetchAll() ?: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\Watchdog;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class MonitorRepo
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllMonitors(): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->query('SELECT * FROM watchdog_monitors ORDER BY group_key ASC, source ASC');
|
||||||
|
return $stmt->fetchAll() ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonitor(string $source, string $instance = 'default'): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM watchdog_monitors WHERE source = :s AND instance = :i');
|
||||||
|
$stmt->execute([':s' => $source, ':i' => $instance]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upsertHeartbeat(
|
||||||
|
string $source,
|
||||||
|
string $instance,
|
||||||
|
string $type,
|
||||||
|
int $intervalSec,
|
||||||
|
$metrics,
|
||||||
|
string $status,
|
||||||
|
?string $message,
|
||||||
|
?string $groupKey = null,
|
||||||
|
?string $os = null
|
||||||
|
): array {
|
||||||
|
$nowUtc = date('Y-m-d H:i:s');
|
||||||
|
$metricsJson = is_array($metrics) || is_object($metrics) ? json_encode($metrics, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
$state = ($status === 'ok') ? 'up' : (($status === 'warning') ? 'warning' : 'down');
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO watchdog_monitors (
|
||||||
|
source, instance, type, state, expected_interval_sec, last_seen_utc,
|
||||||
|
last_status, last_message, metrics_json, group_key, os, created_utc, updated_utc
|
||||||
|
) VALUES (
|
||||||
|
:source, :instance, :type, :state, :interval, :now,
|
||||||
|
:last_status, :message, :metrics, :group_key, :os, :now, :now
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
state = VALUES(state),
|
||||||
|
expected_interval_sec = VALUES(expected_interval_sec),
|
||||||
|
last_seen_utc = VALUES(last_seen_utc),
|
||||||
|
last_status = VALUES(last_status),
|
||||||
|
last_message = VALUES(last_message),
|
||||||
|
metrics_json = VALUES(metrics_json),
|
||||||
|
group_key = COALESCE(VALUES(group_key), group_key),
|
||||||
|
os = COALESCE(VALUES(os), os),
|
||||||
|
updated_utc = VALUES(updated_utc)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':source' => $source,
|
||||||
|
':instance' => $instance,
|
||||||
|
':type' => $type,
|
||||||
|
':state' => $state,
|
||||||
|
':interval' => $intervalSec,
|
||||||
|
':now' => $nowUtc,
|
||||||
|
':last_status' => $status,
|
||||||
|
':message' => $message,
|
||||||
|
':metrics' => $metricsJson,
|
||||||
|
':group_key' => $groupKey,
|
||||||
|
':os' => $os,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->getMonitor($source, $instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateState(int $id, string $state, ?string $reason = null): bool
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare('UPDATE watchdog_monitors SET state = :state, metric_state_reason = :reason, updated_utc = NOW() WHERE id = :id');
|
||||||
|
return $stmt->execute([':state' => $state, ':reason' => $reason, ':id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMaintenance(string $source, string $instance, ?string $untilUtc): bool
|
||||||
|
{
|
||||||
|
$state = $untilUtc ? 'maintenance' : 'up';
|
||||||
|
$stmt = $this->db->prepare('UPDATE watchdog_monitors SET state = :state, suppress_until_utc = :until, updated_utc = NOW() WHERE source = :s AND instance = :i');
|
||||||
|
return $stmt->execute([':state' => $state, ':until' => $untilUtc, ':s' => $source, ':i' => $instance]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteMonitor(string $source, string $instance = 'default'): bool
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare('DELETE FROM watchdog_monitors WHERE source = :s AND instance = :i');
|
||||||
|
return $stmt->execute([':s' => $source, ':i' => $instance]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Deploymentcenter\Modules\Watchdog;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class TokenManager
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createToken(string $source, string $name, string $notes = ''): array
|
||||||
|
{
|
||||||
|
$tokenId = 'tok_' . bin2hex(random_bytes(8));
|
||||||
|
$rawToken = 'wd_' . bin2hex(random_bytes(24));
|
||||||
|
$tokenHash = hash('sha256', $rawToken);
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare('
|
||||||
|
INSERT INTO watchdog_agent_tokens (
|
||||||
|
token_id, token_hash, name, monitor_source, created_at_utc
|
||||||
|
) VALUES (
|
||||||
|
:id, :hash, :name, :source, NOW()
|
||||||
|
)
|
||||||
|
');
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $tokenId,
|
||||||
|
':hash' => $tokenHash,
|
||||||
|
':name' => $name,
|
||||||
|
':source' => $source,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token_id' => $tokenId,
|
||||||
|
'raw_token' => $rawToken,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateToken(string $rawToken, string $targetSource): bool
|
||||||
|
{
|
||||||
|
$hash = hash('sha256', $rawToken);
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM watchdog_agent_tokens WHERE token_hash = :hash AND revoked = 0');
|
||||||
|
$stmt->execute([':hash' => $hash]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($row['monitor_source']) && $row['monitor_source'] !== $targetSource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $this->db->prepare('UPDATE watchdog_agent_tokens SET last_used_at_utc = NOW() WHERE token_id = :id');
|
||||||
|
$upd->execute([':id' => $row['token_id']]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user