Initial commit: Modular Deploymentcenter platform

This commit is contained in:
Deploymentcenter Bot
2026-08-05 21:23:24 +02:00
commit 3a38fd4837
27 changed files with 2323 additions and 0 deletions
+22
View File
@@ -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
}
}
}
+21
View File
@@ -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);
}
}
+200
View File
@@ -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'
];
}
}
+59
View File
@@ -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() ?: [];
}
}
+77
View File
@@ -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() ?: [];
}
}
+100
View File
@@ -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]);
}
}
+63
View File
@@ -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;
}
}