Enhance WHMCS integration with mTLS support and product mapping features. Added mTLS configuration options, updated API endpoints for mTLS status and fingerprint registration, and implemented product validation API. Updated database schema and documentation accordingly.
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 18s

This commit is contained in:
smueller
2026-06-30 13:17:12 +02:00
parent 4b20efe4bc
commit 316679a913
34 changed files with 1174 additions and 30 deletions

View File

@@ -11,8 +11,10 @@ if (!defined('WHMCS')) {
require_once __DIR__ . '/lib/ApiClient.php';
require_once __DIR__ . '/lib/ModuleStorage.php';
require_once __DIR__ . '/lib/EventPoller.php';
require_once __DIR__ . '/lib/ProductMappingStorage.php';
require_once __DIR__ . '/../../../lib/MtlsConfig.php';
const HEXAGAMECLOUD_ADDON_VERSION = '1.0.0';
const HEXAGAMECLOUD_ADDON_VERSION = '1.1.0';
function hexagamecloud_config(): array
{
@@ -60,6 +62,32 @@ function hexagamecloud_config(): array
'Type' => 'yesno',
'Description' => 'Run dry reconciliation during daily event poll',
],
'use_mtls' => [
'FriendlyName' => 'Use mTLS client certificate',
'Type' => 'yesno',
'Description' => 'Requires INTEGRATION_MTLS_ENABLED=true on GameCloud',
],
'mtls_cert_path' => [
'FriendlyName' => 'mTLS client certificate path',
'Type' => 'text',
'Size' => '256',
],
'mtls_key_path' => [
'FriendlyName' => 'mTLS client private key path',
'Type' => 'text',
'Size' => '256',
],
'mtls_ca_path' => [
'FriendlyName' => 'mTLS CA bundle path',
'Type' => 'text',
'Size' => '256',
],
'mtls_cert_fingerprint' => [
'FriendlyName' => 'Client certificate SHA-256 fingerprint',
'Type' => 'text',
'Size' => '128',
'Description' => 'openssl x509 -in client.crt -outform DER | openssl dgst -sha256',
],
],
];
}
@@ -98,6 +126,25 @@ function hexagamecloud_deactivate(): array
];
}
function hexagamecloud_upgrade(array $vars): void
{
if (!function_exists('hexagamecloud_getModuleVersion')) {
return;
}
$current = hexagamecloud_getModuleVersion();
if (version_compare($current, '1.1.0', '<')) {
$upgrade = file_get_contents(__DIR__ . '/sql/upgrade-1.1.0.sql');
if ($upgrade !== false) {
foreach (array_filter(array_map('trim', explode(';', $upgrade))) as $statement) {
if ($statement !== '') {
Capsule::connection()->statement($statement);
}
}
}
}
}
function hexagamecloud_output(array $vars): void
{
$action = $_GET['action'] ?? 'dashboard';
@@ -127,6 +174,21 @@ function hexagamecloud_output(array $vars): void
case 'ack':
hexagamecloud_action_ack($vars, $client, $storage, $modulelink);
break;
case 'mappings':
hexagamecloud_page_mappings($vars, $client, $modulelink);
break;
case 'save_mapping':
hexagamecloud_action_save_mapping($vars, $client, $modulelink);
break;
case 'delete_mapping':
hexagamecloud_action_delete_mapping($vars, $modulelink);
break;
case 'mtls':
hexagamecloud_page_mtls($vars, $client, $modulelink);
break;
case 'register_mtls':
hexagamecloud_action_register_mtls($vars, $client, $modulelink);
break;
default:
hexagamecloud_page_dashboard($vars, $client, $storage, $modulelink);
break;
@@ -143,7 +205,9 @@ function hexagamecloud_create_client(array $vars): HexaGameCloudAddonApiClient
throw new RuntimeException('Configure API URL, Integration ID and API Secret in addon settings.');
}
return new HexaGameCloudAddonApiClient($apiUrl, $integrationId, $apiSecret);
$mtls = HexaGameCloudMtlsConfig::fromAddonSettings($vars);
return new HexaGameCloudAddonApiClient($apiUrl, $integrationId, $apiSecret, $mtls);
}
function hexagamecloud_page_dashboard(
@@ -386,6 +450,51 @@ function hexagamecloud_expand_blocks(string $html, array $data): string
$html = str_replace('{{reconciliation.result}}', '', $html);
}
if (!empty($data['status']) && is_array($data['status'])) {
foreach ($data['status'] as $key => $value) {
$display = is_bool($value) ? ($value ? 'yes' : 'no') : (string) $value;
$html = str_replace('{{status.' . $key . '}}', htmlspecialchars($display, ENT_QUOTES, 'UTF-8'), $html);
}
}
if (!empty($data['products']) && is_array($data['products'])) {
$productOptions = '';
foreach ($data['products'] as $product) {
$productOptions .= '<option value="' . (int) $product['id'] . '">'
. htmlspecialchars((string) $product['name'], ENT_QUOTES, 'UTF-8')
. ' (#' . (int) $product['id'] . ')</option>';
}
$html = str_replace('{{mappings.product_options}}', $productOptions, $html);
}
if (!empty($data['plans']['plans']) && is_array($data['plans']['plans'])) {
$planOptions = '';
foreach ($data['plans']['plans'] as $plan) {
$planOptions .= '<option value="' . htmlspecialchars((string) $plan['slug'], ENT_QUOTES, 'UTF-8') . '">'
. htmlspecialchars((string) $plan['name'], ENT_QUOTES, 'UTF-8')
. ' (' . htmlspecialchars((string) $plan['slug'], ENT_QUOTES, 'UTF-8') . ')</option>';
}
$html = str_replace('{{mappings.plan_options}}', $planOptions, $html);
}
if (!empty($data['mappings']) && is_array($data['mappings'])) {
$rows = '';
foreach ($data['mappings'] as $mapping) {
$rows .= '<tr>';
$rows .= '<td>' . (int) $mapping['whmcs_product_id'] . '</td>';
$rows .= '<td>' . htmlspecialchars((string) $mapping['whmcs_product_name'], ENT_QUOTES, 'UTF-8') . '</td>';
$rows .= '<td>' . htmlspecialchars((string) $mapping['plan_slug'], ENT_QUOTES, 'UTF-8') . '</td>';
$rows .= '<td>' . htmlspecialchars((string) $mapping['default_software'], ENT_QUOTES, 'UTF-8') . '</td>';
$rows .= '<td>' . ((int) $mapping['is_active'] === 1 ? 'yes' : 'no') . '</td>';
$rows .= '<td><a class="btn btn-danger btn-xs" href="'
. htmlspecialchars((string) $data['modulelink'], ENT_QUOTES, 'UTF-8')
. '&action=delete_mapping&product_id=' . (int) $mapping['whmcs_product_id']
. '">Delete</a></td>';
$rows .= '</tr>';
}
$html = str_replace('{{mappings.rows}}', $rows, $html);
}
return $html;
}
@@ -394,3 +503,93 @@ function hexagamecloud_render_error(array $vars, string $message): void
echo '<div class="alert alert-danger"><strong>GameCloud addon error:</strong> ' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '</div>';
echo '<p>Configure the addon under <em>Setup → Addon Modules → HexaHost GameCloud</em>.</p>';
}
function hexagamecloud_page_mappings(array $vars, HexaGameCloudAddonApiClient $client, string $modulelink): void
{
$mappingStorage = new HexaGameCloudProductMappingStorage();
$plans = $client->listPlans();
$products = $mappingStorage->listWhmcsProducts();
$mappings = $mappingStorage->listMappings();
$validation = null;
if (!empty($_GET['validate'])) {
try {
$validation = $client->validatePlan((string) $_GET['validate']);
} catch (Throwable $exception) {
$validation = ['valid' => false, 'error' => $exception->getMessage()];
}
}
echo hexagamecloud_render_template('mappings', [
'modulelink' => $modulelink,
'plans' => $plans,
'products' => $products,
'mappings' => $mappings,
'validation' => $validation,
]);
}
function hexagamecloud_action_save_mapping(array $vars, HexaGameCloudAddonApiClient $client, string $modulelink): void
{
$mappingStorage = new HexaGameCloudProductMappingStorage();
$planSlug = trim((string) ($_POST['plan_slug'] ?? ''));
try {
$validation = $client->validatePlan($planSlug);
if (empty($validation['valid'])) {
throw new RuntimeException('Invalid GameCloud plan slug: ' . $planSlug);
}
$mappingStorage->upsertMapping([
'whmcs_product_id' => (int) ($_POST['whmcs_product_id'] ?? 0),
'whmcs_product_name' => (string) ($_POST['whmcs_product_name'] ?? ''),
'plan_slug' => $planSlug,
'default_edition' => (string) ($_POST['default_edition'] ?? 'JAVA'),
'default_software' => (string) ($_POST['default_software'] ?? 'VANILLA'),
'default_version' => (string) ($_POST['default_version'] ?? '1.21.1'),
'is_active' => !empty($_POST['is_active']),
]);
} catch (Throwable $exception) {
echo '<div class="alert alert-danger">' . htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8') . '</div>';
hexagamecloud_page_mappings($vars, $client, $modulelink);
return;
}
header('Location: ' . $modulelink . '&action=mappings');
exit;
}
function hexagamecloud_action_delete_mapping(array $vars, string $modulelink): void
{
$mappingStorage = new HexaGameCloudProductMappingStorage();
$mappingStorage->deleteMapping((int) ($_GET['product_id'] ?? 0));
header('Location: ' . $modulelink . '&action=mappings');
exit;
}
function hexagamecloud_page_mtls(array $vars, HexaGameCloudAddonApiClient $client, string $modulelink): void
{
$status = $client->mtlsStatus();
echo hexagamecloud_render_template('mtls', [
'modulelink' => $modulelink,
'status' => $status,
'fingerprint_configured' => trim((string) ($vars['mtls_cert_fingerprint'] ?? '')),
]);
}
function hexagamecloud_action_register_mtls(array $vars, HexaGameCloudAddonApiClient $client, string $modulelink): void
{
$fingerprint = trim((string) ($_POST['fingerprint'] ?? $vars['mtls_cert_fingerprint'] ?? ''));
$subject = trim((string) ($_POST['subject'] ?? ''));
try {
$client->registerMtlsFingerprint($fingerprint, $subject !== '' ? $subject : null);
} catch (Throwable $exception) {
echo '<div class="alert alert-danger">' . htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8') . '</div>';
hexagamecloud_page_mtls($vars, $client, $modulelink);
return;
}
header('Location: ' . $modulelink . '&action=mtls');
exit;
}