Enhance API with OIDC support, including login and callback endpoints. Update environment variables for OIDC configuration in .env.example. Add new features to the catalog service for listing software families, Minecraft versions, and deployment regions. Implement server management actions such as kill, delete, and update in the servers module. Integrate feature flags for maintenance mode in server operations. Update pnpm-lock.yaml with new dependencies and versions.
This commit is contained in:
17
integrations/whmcs/composer.json
Normal file
17
integrations/whmcs/composer.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "hexahost/whmcs-integration-dev",
|
||||
"description": "Development dependencies for HexaHost GameCloud WHMCS integration tests",
|
||||
"type": "project",
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"modules/servers/hexagamecloud/lib/"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"test:unit": "phpunit --testsuite Unit"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,31 @@
|
||||
# Server provisioning module placeholder
|
||||
# HexaHost GameCloud WHMCS Server Module
|
||||
|
||||
Implementation planned in WHMCS Phase A–B.
|
||||
Provisioning module for WHMCS that integrates with the GameCloud Integration API.
|
||||
|
||||
Target install path: `/modules/servers/hexagamecloud/`
|
||||
## Features
|
||||
|
||||
See `integrations/whmcs/README.md` for the full module layout.
|
||||
- Provision, suspend, unsuspend, terminate, renew, change package
|
||||
- Client-area SSO to the GameCloud panel
|
||||
- Custom actions: **Start**, **Stop**, **Restart**, **Backup** (Integration API `actions/*`)
|
||||
- Admin service tab with live reconcile snapshot
|
||||
- DE/EN language files
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| Hostname | GameCloud API base URL (e.g. `https://api.example.net`) |
|
||||
| Username | WHMCS integration ID |
|
||||
| Password | Integration API secret |
|
||||
|
||||
## Build package
|
||||
|
||||
```bash
|
||||
./build-package.sh
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
composer test
|
||||
```
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('WHMCS')) {
|
||||
die('This file cannot be accessed directly');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/lib/ApiClient.php';
|
||||
require_once __DIR__ . '/lib/Idempotency.php';
|
||||
require_once __DIR__ . '/lib/ServiceMetadata.php';
|
||||
require_once __DIR__ . '/../../../lib/ProductMapping.php';
|
||||
|
||||
const HEXAGAMECLOUD_SERVER_VERSION = '1.1.0';
|
||||
|
||||
function hexagamecloud_MetaData(): array
|
||||
{
|
||||
return [
|
||||
@@ -97,6 +103,36 @@ function hexagamecloud_TerminateAccount(array $params): string
|
||||
return hexagamecloud_lifecycleAction($params, 'terminate');
|
||||
}
|
||||
|
||||
function hexagamecloud_Renew(array $params): string
|
||||
{
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$payload = array_filter([
|
||||
'invoiceId' => hexagamecloud_resolveRenewInvoiceId($params),
|
||||
'periodEnd' => hexagamecloud_formatIso8601($params['nextduedate'] ?? null),
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$client->renewService(
|
||||
$serviceId,
|
||||
$payload,
|
||||
HexaGameCloudIdempotency::renewKey($serviceId, $params)
|
||||
);
|
||||
|
||||
return 'success';
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall(
|
||||
'hexagamecloud',
|
||||
__FUNCTION__,
|
||||
$params,
|
||||
$exception->getMessage(),
|
||||
$exception->getMessage(),
|
||||
['password', 'secret', 'apiSecret']
|
||||
);
|
||||
return $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_ChangePackage(array $params): string
|
||||
{
|
||||
try {
|
||||
@@ -121,6 +157,137 @@ function hexagamecloud_AdminSingleSignOn(array $params)
|
||||
return hexagamecloud_requestSso($params, 'admin');
|
||||
}
|
||||
|
||||
function hexagamecloud_ClientArea(array $params): array
|
||||
{
|
||||
hexagamecloud_loadLanguage($params);
|
||||
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$snapshot = hexagamecloud_fetchServiceSnapshot($params);
|
||||
$server = $snapshot['service']['server'] ?? [];
|
||||
$linkStatus = (string) ($snapshot['service']['status'] ?? 'PENDING');
|
||||
|
||||
return [
|
||||
'templatefile' => 'templates/clientarea',
|
||||
'vars' => [
|
||||
'LANG' => hexagamecloud_langVars(),
|
||||
'apiWarning' => $snapshot['warning'],
|
||||
'notProvisioned' => $linkStatus === 'PENDING' && ($server['id'] ?? '') === '',
|
||||
'serverName' => (string) ($server['name'] ?? ($params['domain'] ?? '')),
|
||||
'technicalStatus' => (string) ($server['status'] ?? 'UNKNOWN'),
|
||||
'joinAddress' => (string) ($server['joinHostname'] ?? '-'),
|
||||
'edition' => (string) ($server['edition'] ?? 'JAVA'),
|
||||
'softwareFamily' => (string) ($server['softwareFamily'] ?? HexaGameCloudProductMapping::resolveSoftwareFamily($params)),
|
||||
'minecraftVersion' => (string) ($server['minecraftVersion'] ?? HexaGameCloudProductMapping::resolveMinecraftVersion($params)),
|
||||
'planSlug' => HexaGameCloudProductMapping::resolvePlanSlug($params),
|
||||
'ramMb' => (int) ($server['ramMb'] ?? 0),
|
||||
'nextDueDate' => (string) ($params['nextduedate'] ?? '-'),
|
||||
'billingStatus' => $linkStatus === 'SUSPENDED'
|
||||
? hexagamecloud_lang('hexagamecloud_suspended_yes')
|
||||
: hexagamecloud_lang('hexagamecloud_suspended_no'),
|
||||
'lastSync' => $snapshot['syncedAt'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function hexagamecloud_ClientAreaCustomButtonArray(): array
|
||||
{
|
||||
return [
|
||||
hexagamecloud_lang('hexagamecloud_manage_panel') => 'ManagePanel',
|
||||
hexagamecloud_lang('hexagamecloud_action_start') => 'StartServer',
|
||||
hexagamecloud_lang('hexagamecloud_action_stop') => 'StopServer',
|
||||
hexagamecloud_lang('hexagamecloud_action_restart') => 'RestartServer',
|
||||
hexagamecloud_lang('hexagamecloud_action_backup') => 'BackupServer',
|
||||
];
|
||||
}
|
||||
|
||||
function hexagamecloud_StartServer(array $params): string
|
||||
{
|
||||
return hexagamecloud_serviceAction($params, 'start');
|
||||
}
|
||||
|
||||
function hexagamecloud_StopServer(array $params): string
|
||||
{
|
||||
return hexagamecloud_serviceAction($params, 'stop');
|
||||
}
|
||||
|
||||
function hexagamecloud_RestartServer(array $params): string
|
||||
{
|
||||
return hexagamecloud_serviceAction($params, 'restart');
|
||||
}
|
||||
|
||||
function hexagamecloud_BackupServer(array $params): string
|
||||
{
|
||||
return hexagamecloud_serviceAction($params, 'backup');
|
||||
}
|
||||
|
||||
function hexagamecloud_serviceAction(array $params, string $action): string
|
||||
{
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
if ($action === 'start') {
|
||||
$client->startService($serviceId);
|
||||
} elseif ($action === 'stop') {
|
||||
$client->stopService($serviceId);
|
||||
} elseif ($action === 'restart') {
|
||||
$client->restartService($serviceId);
|
||||
} else {
|
||||
$client->backupService($serviceId);
|
||||
}
|
||||
return 'success';
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall('hexagamecloud', $action, $params, $exception->getMessage());
|
||||
return $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_ManagePanel(array $params): array
|
||||
{
|
||||
return hexagamecloud_requestSso($params, 'service');
|
||||
}
|
||||
|
||||
function hexagamecloud_AdminServicesTabFields(array $params): array
|
||||
{
|
||||
hexagamecloud_loadLanguage($params);
|
||||
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$snapshot = hexagamecloud_fetchServiceSnapshot($params);
|
||||
$service = $snapshot['service'];
|
||||
$server = $service['server'] ?? [];
|
||||
$metadata = HexaGameCloudServiceMetadata::load($serviceId);
|
||||
$warning = $snapshot['warning'] !== null
|
||||
? '<div class="alert alert-warning">' . htmlspecialchars($snapshot['warning'], ENT_QUOTES, 'UTF-8') . '</div>'
|
||||
: '';
|
||||
|
||||
return [
|
||||
hexagamecloud_lang('hexagamecloud_admin_server_id') => $warning . htmlspecialchars((string) ($server['id'] ?? '-'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_user_id') => htmlspecialchars((string) ($server['userId'] ?? '-'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_link_status') => htmlspecialchars((string) ($service['status'] ?? 'PENDING'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_technical_status') => htmlspecialchars((string) ($server['status'] ?? 'UNKNOWN'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_node') => htmlspecialchars((string) ($server['nodeId'] ?? '-'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_host_port') => htmlspecialchars((string) ($server['hostPort'] ?? '-'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_plan') => htmlspecialchars(HexaGameCloudProductMapping::resolvePlanSlug($params), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_ram') => htmlspecialchars((string) ((int) ($server['ramMb'] ?? 0)) . ' MiB', ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_last_sync') => htmlspecialchars((string) ($snapshot['syncedAt'] ?? '-'), ENT_QUOTES, 'UTF-8'),
|
||||
hexagamecloud_lang('hexagamecloud_admin_correlation_note') => '<textarea class="form-control" rows="3" name="hexagamecloud_correlation_note">'
|
||||
. htmlspecialchars((string) ($metadata['correlation_note'] ?? ''), ENT_QUOTES, 'UTF-8')
|
||||
. '</textarea>',
|
||||
];
|
||||
}
|
||||
|
||||
function hexagamecloud_AdminServicesTabFieldsSave(array $params): void
|
||||
{
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$note = trim((string) ($params['hexagamecloud_correlation_note'] ?? ''));
|
||||
$existing = HexaGameCloudServiceMetadata::load($serviceId);
|
||||
|
||||
HexaGameCloudServiceMetadata::save($serviceId, array_merge($existing, [
|
||||
'correlation_note' => $note,
|
||||
'updated_at' => gmdate('c'),
|
||||
'updated_by_admin_id' => (string) ($params['adminid'] ?? ''),
|
||||
]));
|
||||
}
|
||||
|
||||
function hexagamecloud_TestConnection(array $params): array
|
||||
{
|
||||
try {
|
||||
@@ -233,6 +400,103 @@ function hexagamecloud_lifecycleAction(array $params, string $action, array $bod
|
||||
}
|
||||
}
|
||||
|
||||
function hexagamecloud_fetchServiceSnapshot(array $params): array
|
||||
{
|
||||
$serviceId = (string) $params['serviceid'];
|
||||
$cached = HexaGameCloudServiceMetadata::load($serviceId);
|
||||
$snapshot = [
|
||||
'service' => $cached['service'] ?? [
|
||||
'externalServiceId' => $serviceId,
|
||||
'status' => 'PENDING',
|
||||
'server' => [],
|
||||
],
|
||||
'warning' => null,
|
||||
'syncedAt' => $cached['synced_at'] ?? null,
|
||||
];
|
||||
|
||||
try {
|
||||
$client = new HexaGameCloudApiClient($params);
|
||||
$service = $client->getService($serviceId);
|
||||
$snapshot['service'] = $service;
|
||||
$snapshot['syncedAt'] = gmdate('c');
|
||||
|
||||
HexaGameCloudServiceMetadata::save($serviceId, [
|
||||
'service' => $service,
|
||||
'synced_at' => $snapshot['syncedAt'],
|
||||
'correlation_note' => $cached['correlation_note'] ?? '',
|
||||
'updated_by_admin_id' => $cached['updated_by_admin_id'] ?? '',
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
logModuleCall('hexagamecloud', 'fetchServiceSnapshot', $params, $exception->getMessage());
|
||||
$snapshot['warning'] = hexagamecloud_lang('hexagamecloud_api_unavailable');
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
function hexagamecloud_resolveRenewInvoiceId(array $params): ?string
|
||||
{
|
||||
$invoiceId = (string) ($params['invoiceid'] ?? '');
|
||||
if ($invoiceId !== '') {
|
||||
return $invoiceId;
|
||||
}
|
||||
|
||||
if (!empty($params['model']['invoiceid'])) {
|
||||
return (string) $params['model']['invoiceid'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hexagamecloud_formatIso8601(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return gmdate('c', $timestamp);
|
||||
}
|
||||
|
||||
function hexagamecloud_loadLanguage(array $params): void
|
||||
{
|
||||
static $loaded = false;
|
||||
if ($loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$language = strtolower((string) ($params['clientsdetails']['language'] ?? $params['language'] ?? 'english'));
|
||||
if (!in_array($language, ['english', 'german'], true)) {
|
||||
$language = 'english';
|
||||
}
|
||||
|
||||
$langFile = __DIR__ . '/lang/' . $language . '.php';
|
||||
if (!is_readable($langFile)) {
|
||||
$langFile = __DIR__ . '/lang/english.php';
|
||||
}
|
||||
|
||||
require $langFile;
|
||||
$loaded = true;
|
||||
}
|
||||
|
||||
function hexagamecloud_lang(string $key): string
|
||||
{
|
||||
global $_LANG;
|
||||
|
||||
return (string) ($_LANG[$key] ?? $key);
|
||||
}
|
||||
|
||||
function hexagamecloud_langVars(): array
|
||||
{
|
||||
global $_LANG;
|
||||
|
||||
return is_array($_LANG ?? null) ? $_LANG : [];
|
||||
}
|
||||
|
||||
function hexagamecloud_requestSso(array $params, string $kind)
|
||||
{
|
||||
try {
|
||||
|
||||
68
integrations/whmcs/modules/servers/hexagamecloud/hooks.php
Normal file
68
integrations/whmcs/modules/servers/hexagamecloud/hooks.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('WHMCS')) {
|
||||
die('This file cannot be accessed directly');
|
||||
}
|
||||
|
||||
if (!defined('HEXAGAMECLOUD_SERVER_VERSION')) {
|
||||
define('HEXAGAMECLOUD_SERVER_VERSION', '1.1.0');
|
||||
}
|
||||
|
||||
add_hook('AfterModuleRenew', 1, function (array $vars): void {
|
||||
if (($vars['params']['moduletype'] ?? '') !== 'hexagamecloud') {
|
||||
return;
|
||||
}
|
||||
|
||||
$serviceId = (string) ($vars['params']['serviceid'] ?? '');
|
||||
if ($serviceId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity('HexaGameCloud service renewed via WHMCS (service #' . $serviceId . ')');
|
||||
});
|
||||
|
||||
add_hook('AfterModuleCreate', 1, function (array $vars): void {
|
||||
if (($vars['params']['moduletype'] ?? '') !== 'hexagamecloud') {
|
||||
return;
|
||||
}
|
||||
|
||||
$serviceId = (string) ($vars['params']['serviceid'] ?? '');
|
||||
if ($serviceId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity('HexaGameCloud service provisioned (service #' . $serviceId . ')');
|
||||
});
|
||||
|
||||
add_hook('ClientAreaPageProductDetails', 1, function (array $vars): array {
|
||||
if (($vars['module'] ?? '') !== 'hexagamecloud') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'gamecloudModuleVersion' => HEXAGAMECLOUD_SERVER_VERSION,
|
||||
];
|
||||
});
|
||||
|
||||
add_hook('AdminAreaHeadOutput', 1, function (array $vars): string {
|
||||
if (($vars['filename'] ?? '') !== 'clientsservices') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<!-- HexaGameCloud server module ' . htmlspecialchars(HEXAGAMECLOUD_SERVER_VERSION, ENT_QUOTES, 'UTF-8') . ' -->';
|
||||
});
|
||||
|
||||
add_hook('ServiceEdit', 1, function (array $vars): void {
|
||||
if (($vars['moduletype'] ?? '') !== 'hexagamecloud') {
|
||||
return;
|
||||
}
|
||||
|
||||
$serviceId = (string) ($vars['serviceid'] ?? '');
|
||||
if ($serviceId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
logActivity('HexaGameCloud service edited in WHMCS admin (service #' . $serviceId . ')');
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
if (!defined('WHMCS')) {
|
||||
die('This file cannot be accessed directly');
|
||||
}
|
||||
|
||||
$_LANG['hexagamecloud_manage_panel'] = 'Manage in GameCloud Panel';
|
||||
$_LANG['hexagamecloud_action_start'] = 'Start Server';
|
||||
$_LANG['hexagamecloud_action_stop'] = 'Stop Server';
|
||||
$_LANG['hexagamecloud_action_restart'] = 'Restart Server';
|
||||
$_LANG['hexagamecloud_action_backup'] = 'Create Backup';
|
||||
$_LANG['hexagamecloud_server_overview'] = 'Game Server Overview';
|
||||
$_LANG['hexagamecloud_server_name'] = 'Server Name';
|
||||
$_LANG['hexagamecloud_status'] = 'Status';
|
||||
$_LANG['hexagamecloud_join_address'] = 'Join Address';
|
||||
$_LANG['hexagamecloud_edition'] = 'Edition';
|
||||
$_LANG['hexagamecloud_software'] = 'Software';
|
||||
$_LANG['hexagamecloud_version'] = 'Version';
|
||||
$_LANG['hexagamecloud_plan'] = 'Plan';
|
||||
$_LANG['hexagamecloud_ram'] = 'RAM';
|
||||
$_LANG['hexagamecloud_next_due'] = 'Next Billing Date';
|
||||
$_LANG['hexagamecloud_suspend_status'] = 'Billing Suspension';
|
||||
$_LANG['hexagamecloud_api_unavailable'] = 'GameCloud is temporarily unavailable. Cached data may be shown.';
|
||||
$_LANG['hexagamecloud_last_sync'] = 'Last successful sync';
|
||||
$_LANG['hexagamecloud_not_provisioned'] = 'This service has not been provisioned in GameCloud yet.';
|
||||
$_LANG['hexagamecloud_admin_server_id'] = 'GameCloud Server ID';
|
||||
$_LANG['hexagamecloud_admin_user_id'] = 'GameCloud User ID';
|
||||
$_LANG['hexagamecloud_admin_link_status'] = 'Link Status';
|
||||
$_LANG['hexagamecloud_admin_technical_status'] = 'Technical Status';
|
||||
$_LANG['hexagamecloud_admin_node'] = 'Node';
|
||||
$_LANG['hexagamecloud_admin_host_port'] = 'Host Port';
|
||||
$_LANG['hexagamecloud_admin_correlation_note'] = 'Admin Correlation Note';
|
||||
$_LANG['hexagamecloud_admin_last_sync'] = 'Last Sync';
|
||||
$_LANG['hexagamecloud_suspended_yes'] = 'Suspended';
|
||||
$_LANG['hexagamecloud_suspended_no'] = 'Active';
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
if (!defined('WHMCS')) {
|
||||
die('This file cannot be accessed directly');
|
||||
}
|
||||
|
||||
$_LANG['hexagamecloud_manage_panel'] = 'Im GameCloud-Panel verwalten';
|
||||
$_LANG['hexagamecloud_action_start'] = 'Server starten';
|
||||
$_LANG['hexagamecloud_action_stop'] = 'Server stoppen';
|
||||
$_LANG['hexagamecloud_action_restart'] = 'Server neu starten';
|
||||
$_LANG['hexagamecloud_action_backup'] = 'Backup erstellen';
|
||||
$_LANG['hexagamecloud_server_overview'] = 'Spieleserver-Übersicht';
|
||||
$_LANG['hexagamecloud_server_name'] = 'Servername';
|
||||
$_LANG['hexagamecloud_status'] = 'Status';
|
||||
$_LANG['hexagamecloud_join_address'] = 'Join-Adresse';
|
||||
$_LANG['hexagamecloud_edition'] = 'Edition';
|
||||
$_LANG['hexagamecloud_software'] = 'Software';
|
||||
$_LANG['hexagamecloud_version'] = 'Version';
|
||||
$_LANG['hexagamecloud_plan'] = 'Tarif';
|
||||
$_LANG['hexagamecloud_ram'] = 'Arbeitsspeicher';
|
||||
$_LANG['hexagamecloud_next_due'] = 'Nächstes Abrechnungsdatum';
|
||||
$_LANG['hexagamecloud_suspend_status'] = 'Abrechnungssperre';
|
||||
$_LANG['hexagamecloud_api_unavailable'] = 'GameCloud ist vorübergehend nicht erreichbar. Es können zwischengespeicherte Daten angezeigt werden.';
|
||||
$_LANG['hexagamecloud_last_sync'] = 'Letzte erfolgreiche Synchronisierung';
|
||||
$_LANG['hexagamecloud_not_provisioned'] = 'Dieser Dienst wurde in GameCloud noch nicht bereitgestellt.';
|
||||
$_LANG['hexagamecloud_admin_server_id'] = 'GameCloud-Server-ID';
|
||||
$_LANG['hexagamecloud_admin_user_id'] = 'GameCloud-Benutzer-ID';
|
||||
$_LANG['hexagamecloud_admin_link_status'] = 'Verknüpfungsstatus';
|
||||
$_LANG['hexagamecloud_admin_technical_status'] = 'Technischer Status';
|
||||
$_LANG['hexagamecloud_admin_node'] = 'Node';
|
||||
$_LANG['hexagamecloud_admin_host_port'] = 'Host-Port';
|
||||
$_LANG['hexagamecloud_admin_correlation_note'] = 'Admin-Korrelationsnotiz';
|
||||
$_LANG['hexagamecloud_admin_last_sync'] = 'Letzte Synchronisierung';
|
||||
$_LANG['hexagamecloud_suspended_yes'] = 'Gesperrt';
|
||||
$_LANG['hexagamecloud_suspended_no'] = 'Aktiv';
|
||||
@@ -82,6 +82,24 @@ final class HexaGameCloudApiClient
|
||||
);
|
||||
}
|
||||
|
||||
public function getService(string $externalServiceId): array
|
||||
{
|
||||
return $this->request(
|
||||
'GET',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId)
|
||||
);
|
||||
}
|
||||
|
||||
public function renewService(string $externalServiceId, array $payload, string $idempotencyKey): array
|
||||
{
|
||||
return $this->request(
|
||||
'POST',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/renew',
|
||||
$payload,
|
||||
$idempotencyKey
|
||||
);
|
||||
}
|
||||
|
||||
public function changePackage(string $externalServiceId, array $payload): array
|
||||
{
|
||||
return $this->request(
|
||||
@@ -173,6 +191,46 @@ final class HexaGameCloudApiClient
|
||||
);
|
||||
}
|
||||
|
||||
public function startService(string $externalServiceId): array
|
||||
{
|
||||
return $this->request(
|
||||
'POST',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/actions/start',
|
||||
[],
|
||||
'service:start:' . $externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
public function stopService(string $externalServiceId): array
|
||||
{
|
||||
return $this->request(
|
||||
'POST',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/actions/stop',
|
||||
[],
|
||||
'service:stop:' . $externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
public function restartService(string $externalServiceId): array
|
||||
{
|
||||
return $this->request(
|
||||
'POST',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/actions/restart',
|
||||
[],
|
||||
'service:restart:' . $externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
public function backupService(string $externalServiceId): array
|
||||
{
|
||||
return $this->request(
|
||||
'POST',
|
||||
'/api/v1/integrations/whmcs/services/' . rawurlencode($externalServiceId) . '/actions/backup',
|
||||
[],
|
||||
'service:backup:' . $externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
private function request(string $method, string $pathWithQuery, array $body = [], ?string $idempotencySuffix = null): array
|
||||
{
|
||||
$pathParts = explode('?', $pathWithQuery, 2);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class HexaGameCloudIdempotency
|
||||
{
|
||||
public static function serviceOperationKey(string $operation, string $externalServiceId, ?string $reference = null): string
|
||||
{
|
||||
$suffix = ($reference !== null && $reference !== '') ? $reference : gmdate('Y-m-d');
|
||||
|
||||
return 'service:' . $operation . ':' . $externalServiceId . ':' . $suffix;
|
||||
}
|
||||
|
||||
public static function renewKey(string $externalServiceId, array $params): string
|
||||
{
|
||||
$reference = (string) ($params['invoiceid'] ?? '');
|
||||
if ($reference === '' && !empty($params['model']['invoiceid'])) {
|
||||
$reference = (string) $params['model']['invoiceid'];
|
||||
}
|
||||
if ($reference === '' && !empty($params['renewalid'])) {
|
||||
$reference = (string) $params['renewalid'];
|
||||
}
|
||||
|
||||
return self::serviceOperationKey('renew', $externalServiceId, $reference !== '' ? $reference : null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use WHMCS\Database\Capsule;
|
||||
|
||||
final class HexaGameCloudServiceMetadata
|
||||
{
|
||||
private const OPERATION_PREFIX = 'service:admin-meta:';
|
||||
|
||||
public static function load(string $serviceId): array
|
||||
{
|
||||
try {
|
||||
$row = Capsule::table('mod_hexagamecloud_operations')
|
||||
->where('operation', self::OPERATION_PREFIX . $serviceId)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($row === null || empty($row->metadata)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $row->metadata, true);
|
||||
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static function save(string $serviceId, array $data): void
|
||||
{
|
||||
$payload = json_encode($data, JSON_THROW_ON_ERROR);
|
||||
|
||||
try {
|
||||
$existing = Capsule::table('mod_hexagamecloud_operations')
|
||||
->where('operation', self::OPERATION_PREFIX . $serviceId)
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($existing !== null) {
|
||||
Capsule::table('mod_hexagamecloud_operations')
|
||||
->where('id', $existing->id)
|
||||
->update([
|
||||
'metadata' => $payload,
|
||||
'status' => 'saved',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Capsule::table('mod_hexagamecloud_operations')->insert([
|
||||
'operation' => self::OPERATION_PREFIX . $serviceId,
|
||||
'status' => 'saved',
|
||||
'metadata' => $payload,
|
||||
'created_at' => gmdate('Y-m-d H:i:s'),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
logActivity('HexaGameCloud admin metadata save failed: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{$LANG.hexagamecloud_server_overview}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{if $apiWarning}
|
||||
<div class="alert alert-warning">{$apiWarning}</div>
|
||||
{/if}
|
||||
{if $notProvisioned}
|
||||
<p class="text-muted">{$LANG.hexagamecloud_not_provisioned}</p>
|
||||
{else}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{$LANG.hexagamecloud_server_name}</dt>
|
||||
<dd>{$serverName}</dd>
|
||||
<dt>{$LANG.hexagamecloud_status}</dt>
|
||||
<dd>{$technicalStatus}</dd>
|
||||
<dt>{$LANG.hexagamecloud_join_address}</dt>
|
||||
<dd>{$joinAddress}</dd>
|
||||
<dt>{$LANG.hexagamecloud_edition}</dt>
|
||||
<dd>{$edition}</dd>
|
||||
<dt>{$LANG.hexagamecloud_software}</dt>
|
||||
<dd>{$softwareFamily}</dd>
|
||||
<dt>{$LANG.hexagamecloud_version}</dt>
|
||||
<dd>{$minecraftVersion}</dd>
|
||||
<dt>{$LANG.hexagamecloud_plan}</dt>
|
||||
<dd>{$planSlug}</dd>
|
||||
<dt>{$LANG.hexagamecloud_ram}</dt>
|
||||
<dd>{$ramMb} MiB</dd>
|
||||
<dt>{$LANG.hexagamecloud_next_due}</dt>
|
||||
<dd>{$nextDueDate}</dd>
|
||||
<dt>{$LANG.hexagamecloud_suspend_status}</dt>
|
||||
<dd>{$billingStatus}</dd>
|
||||
</dl>
|
||||
{if $lastSync}
|
||||
<p class="text-muted"><small>{$LANG.hexagamecloud_last_sync}: {$lastSync}</small></p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
57
integrations/whmcs/packaging/build-package.sh
Executable file
57
integrations/whmcs/packaging/build-package.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PACKAGING="${ROOT}/packaging"
|
||||
MANIFEST="${PACKAGING}/manifest.json"
|
||||
DIST="${ROOT}/dist"
|
||||
STAGING="${DIST}/staging"
|
||||
VERSION="$(python3 -c "import json; print(json.load(open('${MANIFEST}'))['version'])")"
|
||||
PACKAGE_NAME="hexagamecloud-whmcs-${VERSION}"
|
||||
ZIP_PATH="${DIST}/${PACKAGE_NAME}.zip"
|
||||
CHECKSUMS_PATH="${DIST}/checksums.txt"
|
||||
|
||||
rm -rf "${STAGING}" "${ZIP_PATH}"
|
||||
mkdir -p "${STAGING}/modules/servers" "${STAGING}/modules/addons" "${STAGING}/hooks" "${STAGING}/lib" "${DIST}"
|
||||
|
||||
copy_tree() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --exclude '.gitkeep' "${source}/" "${target}/"
|
||||
else
|
||||
cp -a "${source}/." "${target}/"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_tree "${ROOT}/modules/servers/hexagamecloud" "${STAGING}/modules/servers/hexagamecloud"
|
||||
copy_tree "${ROOT}/modules/addons/hexagamecloud" "${STAGING}/modules/addons/hexagamecloud"
|
||||
copy_tree "${ROOT}/hooks" "${STAGING}/hooks"
|
||||
copy_tree "${ROOT}/lib" "${STAGING}/lib"
|
||||
|
||||
cp "${MANIFEST}" "${STAGING}/manifest.json"
|
||||
|
||||
(
|
||||
cd "${STAGING}"
|
||||
zip -r "${ZIP_PATH}" .
|
||||
)
|
||||
|
||||
SHA256="$(sha256sum "${ZIP_PATH}" | awk '{print $1}')"
|
||||
SHA512="$(sha512sum "${ZIP_PATH}" | awk '{print $1}')"
|
||||
FILE_SIZE="$(stat -c '%s' "${ZIP_PATH}")"
|
||||
BUILT_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
|
||||
cat > "${CHECKSUMS_PATH}" <<EOF
|
||||
# HexaHost GameCloud WHMCS package checksums
|
||||
# version=${VERSION}
|
||||
# built_at=${BUILT_AT}
|
||||
|
||||
SHA256 ${SHA256} ${PACKAGE_NAME}.zip
|
||||
SHA512 ${SHA512} ${PACKAGE_NAME}.zip
|
||||
SIZE ${FILE_SIZE} ${PACKAGE_NAME}.zip
|
||||
EOF
|
||||
|
||||
rm -rf "${STAGING}"
|
||||
|
||||
echo "Built ${ZIP_PATH}"
|
||||
echo "Checksums written to ${CHECKSUMS_PATH}"
|
||||
43
integrations/whmcs/packaging/manifest.json
Normal file
43
integrations/whmcs/packaging/manifest.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "hexagamecloud-whmcs",
|
||||
"displayName": "HexaHost GameCloud WHMCS Integration",
|
||||
"version": "1.1.0",
|
||||
"description": "Server provisioning and addon modules for HexaHost GameCloud billing integration.",
|
||||
"author": "HexaHost",
|
||||
"license": "Proprietary",
|
||||
"modules": {
|
||||
"server": {
|
||||
"name": "hexagamecloud",
|
||||
"path": "modules/servers/hexagamecloud",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"addon": {
|
||||
"name": "hexagamecloud",
|
||||
"path": "modules/addons/hexagamecloud",
|
||||
"version": "1.1.0"
|
||||
}
|
||||
},
|
||||
"includes": [
|
||||
"hooks/hexagamecloud.php",
|
||||
"lib/MtlsConfig.php",
|
||||
"lib/ProductMapping.php"
|
||||
],
|
||||
"compatibility": {
|
||||
"whmcs": {
|
||||
"minimum": "8.5.0",
|
||||
"tested": ["8.5.0", "8.6.0", "8.7.0", "8.8.0", "8.9.0", "8.10.0", "8.11.0"]
|
||||
},
|
||||
"php": {
|
||||
"minimum": "8.1.0",
|
||||
"maximum": "8.3.99",
|
||||
"extensions": ["curl", "json", "openssl"]
|
||||
},
|
||||
"gamecloud": {
|
||||
"minimumApiVersion": "1.1"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"zip": "dist/hexagamecloud-whmcs-{version}.zip",
|
||||
"checksums": "dist/checksums.txt"
|
||||
}
|
||||
}
|
||||
15
integrations/whmcs/phpunit.xml.dist
Normal file
15
integrations/whmcs/phpunit.xml.dist
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Contract">
|
||||
<directory>tests/Contract</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
30
integrations/whmcs/tests/Contract/README.md
Normal file
30
integrations/whmcs/tests/Contract/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Contract tests
|
||||
|
||||
Contract tests validate WHMCS module requests and responses against the GameCloud integration API (`/api/v1/integrations/whmcs/*`).
|
||||
|
||||
## Scope
|
||||
|
||||
- Request signing headers (`X-HGC-Integration-ID`, `X-HGC-Signature`, `X-HGC-Idempotency-Key`, …)
|
||||
- Canonical HMAC signature base string
|
||||
- JSON request/response shapes from `packages/contracts/src/whmcs.ts`
|
||||
- Idempotent lifecycle operations (`provision`, `suspend`, `renew`, `change-package`)
|
||||
|
||||
## Running
|
||||
|
||||
Contract tests require a reachable GameCloud API or a recorded mock server. They are not executed in CI by default.
|
||||
|
||||
```bash
|
||||
# From integrations/whmcs after installing dev dependencies:
|
||||
composer install
|
||||
./vendor/bin/phpunit --testsuite Contract
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
Sample WHMCS `$params` payloads and signed API examples will live under `tests/Fixtures/` as the contract suite grows.
|
||||
|
||||
## Related
|
||||
|
||||
- OpenAPI / Zod contracts: `packages/contracts/src/whmcs.ts`
|
||||
- Server module client: `modules/servers/hexagamecloud/lib/ApiClient.php`
|
||||
- Addon module client: `modules/addons/hexagamecloud/lib/ApiClient.php`
|
||||
53
integrations/whmcs/tests/Unit/IdempotencyTest.php
Normal file
53
integrations/whmcs/tests/Unit/IdempotencyTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/modules/servers/hexagamecloud/lib/Idempotency.php';
|
||||
|
||||
final class IdempotencyTest extends TestCase
|
||||
{
|
||||
public function testServiceOperationKeyUsesReferenceWhenProvided(): void
|
||||
{
|
||||
$key = HexaGameCloudIdempotency::serviceOperationKey('renew', '12345', 'invoice-99');
|
||||
|
||||
$this->assertSame('service:renew:12345:invoice-99', $key);
|
||||
}
|
||||
|
||||
public function testServiceOperationKeyFallsBackToDateSuffix(): void
|
||||
{
|
||||
$key = HexaGameCloudIdempotency::serviceOperationKey('suspend', '42', null);
|
||||
|
||||
$this->assertStringStartsWith('service:suspend:42:', $key);
|
||||
$this->assertMatchesRegularExpression('/^service:suspend:42:\d{4}-\d{2}-\d{2}$/', $key);
|
||||
}
|
||||
|
||||
public function testRenewKeyPrefersInvoiceId(): void
|
||||
{
|
||||
$key = HexaGameCloudIdempotency::renewKey('9001', [
|
||||
'invoiceid' => '7788',
|
||||
'renewalid' => '5555',
|
||||
]);
|
||||
|
||||
$this->assertSame('service:renew:9001:7788', $key);
|
||||
}
|
||||
|
||||
public function testRenewKeyUsesRenewalIdWhenInvoiceMissing(): void
|
||||
{
|
||||
$key = HexaGameCloudIdempotency::renewKey('9001', [
|
||||
'renewalid' => '5555',
|
||||
]);
|
||||
|
||||
$this->assertSame('service:renew:9001:5555', $key);
|
||||
}
|
||||
|
||||
public function testRenewKeyUsesModelInvoiceId(): void
|
||||
{
|
||||
$key = HexaGameCloudIdempotency::renewKey('9001', [
|
||||
'model' => ['invoiceid' => '6611'],
|
||||
]);
|
||||
|
||||
$this->assertSame('service:renew:9001:6611', $key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user