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.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user