feat: add VNC check, SSH key paste, resources panel, sliders, and self-service billing
- VNC panel auto-hides when VNC is disabled on the server - SSH key paste textarea at checkout with API key creation during provisioning - Resources panel with current allocation, traffic progress bar, and upgrade link - changePackage() now applies individual resource modifications from configurable options - Order form configurable option dropdowns replaced with styled range sliders - Self-service billing: credit balance, usage breakdown, credit top-up from client area - Self-service config options (mode, auto top-off threshold/amount) on products - Auto top-off via WHMCS cron when credit falls below threshold - CHANGELOG.md covering all versions from 0.0.6 to present Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,9 +130,10 @@ class ConfigureService extends Module
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array $vars
|
||||
* @param int|null $vfUserId VirtFusion user ID (for creating SSH keys from raw public key)
|
||||
* @return bool
|
||||
*/
|
||||
public function initServerBuild(int $id, array $vars): bool
|
||||
public function initServerBuild(int $id, array $vars, ?int $vfUserId = null): bool
|
||||
{
|
||||
if (!$this->cp) return false;
|
||||
|
||||
@@ -141,17 +142,27 @@ class ConfigureService extends Module
|
||||
// Generate a random 8 character hostname
|
||||
$hostname = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8);
|
||||
|
||||
$sshKeyValue = $vars['customfields']['Initial SSH Key'] ?? null;
|
||||
$sshKeyId = null;
|
||||
|
||||
if (!empty($sshKeyValue)) {
|
||||
if (is_numeric($sshKeyValue)) {
|
||||
// Existing SSH key ID
|
||||
$sshKeyId = (int) $sshKeyValue;
|
||||
} elseif (preg_match('/^ssh-/', $sshKeyValue) && $vfUserId) {
|
||||
// Raw public key — create it via API
|
||||
$sshKeyId = $this->createUserSshKey($vfUserId, $sshKeyValue);
|
||||
}
|
||||
}
|
||||
|
||||
$inputData = [
|
||||
"operatingSystemId" => $vars['customfields']['Initial Operating System'] ?? null,
|
||||
"name" => $hostname,
|
||||
"sshKeys" => [
|
||||
$vars['customfields']['Initial SSH Key'] ?? null
|
||||
],
|
||||
'email' => true
|
||||
];
|
||||
|
||||
if (empty($vars['customfields']['Initial SSH Key'] ?? null)) {
|
||||
unset($inputData['sshKeys']);
|
||||
if ($sshKeyId) {
|
||||
$inputData['sshKeys'] = [$sshKeyId];
|
||||
}
|
||||
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData));
|
||||
@@ -165,4 +176,37 @@ class ConfigureService extends Module
|
||||
|
||||
return ($httpCode == 200 || $httpCode == 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an SSH key for a VirtFusion user from a raw public key string.
|
||||
*
|
||||
* @param int $userId VirtFusion user ID
|
||||
* @param string $publicKey Raw SSH public key (ssh-rsa ..., ssh-ed25519 ..., etc.)
|
||||
* @return int|null Created key ID or null on failure
|
||||
*/
|
||||
public function createUserSshKey(int $userId, string $publicKey): ?int
|
||||
{
|
||||
if (!$this->cp) return null;
|
||||
|
||||
$request = $this->initCurl($this->cp['token']);
|
||||
|
||||
$keyData = [
|
||||
'userId' => $userId,
|
||||
'name' => 'WHMCS-' . date('Y-m-d'),
|
||||
'publicKey' => trim($publicKey),
|
||||
];
|
||||
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode($keyData));
|
||||
$response = $request->post($this->cp['url'] . '/ssh_keys');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $response);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 201) {
|
||||
$data = json_decode($response, true);
|
||||
return $data['data']['id'] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,218 +319,6 @@ class Module
|
||||
return false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Firewall Management
|
||||
//
|
||||
// VirtFusion uses a ruleset-based firewall system. Individual rules cannot
|
||||
// be created or deleted via the API. Instead, predefined rulesets (created
|
||||
// in the VirtFusion admin panel) are applied to servers by ID.
|
||||
//
|
||||
// The {interface} parameter is "primary" or "secondary".
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get firewall status and rules for a server.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param string $interface Network interface: "primary" or "secondary"
|
||||
* @return array|false
|
||||
*/
|
||||
public function getFirewallStatus($serviceID, $interface = 'primary')
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$interface = $this->sanitizeFirewallInterface($interface);
|
||||
$service = Database::getSystemService($serviceID);
|
||||
|
||||
if ($service) {
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->get($cp['url'] . '/servers/' . (int) $service->server_id . '/firewall/' . $interface);
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
if ($request->getRequestInfo('http_code') == 200) {
|
||||
return json_decode($data, true);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable firewall on a server.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param string $interface Network interface: "primary" or "secondary"
|
||||
* @return object|false
|
||||
*/
|
||||
public function enableFirewall($serviceID, $interface = 'primary')
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$interface = $this->sanitizeFirewallInterface($interface);
|
||||
$service = Database::getSystemService($serviceID);
|
||||
|
||||
if ($service) {
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/firewall/' . $interface . '/enable');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 204) {
|
||||
return json_decode($data) ?: (object) ['success' => true];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable firewall on a server.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param string $interface Network interface: "primary" or "secondary"
|
||||
* @return object|false
|
||||
*/
|
||||
public function disableFirewall($serviceID, $interface = 'primary')
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$interface = $this->sanitizeFirewallInterface($interface);
|
||||
$service = Database::getSystemService($serviceID);
|
||||
|
||||
if ($service) {
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/firewall/' . $interface . '/disable');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 204) {
|
||||
return json_decode($data) ?: (object) ['success' => true];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply firewall rulesets to a server.
|
||||
*
|
||||
* VirtFusion uses predefined rulesets (created in admin panel).
|
||||
* Individual rules cannot be added/deleted via the API.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param array $rulesetIds Array of ruleset IDs to apply
|
||||
* @param string $interface Network interface: "primary" or "secondary"
|
||||
* @return object|false
|
||||
*/
|
||||
public function applyFirewallRulesets($serviceID, array $rulesetIds, $interface = 'primary')
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$interface = $this->sanitizeFirewallInterface($interface);
|
||||
|
||||
// Validate and sanitize ruleset IDs
|
||||
$rulesetIds = array_values(array_filter(array_map('intval', $rulesetIds), function ($id) {
|
||||
return $id > 0;
|
||||
}));
|
||||
|
||||
if (empty($rulesetIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$service = Database::getSystemService($serviceID);
|
||||
|
||||
if ($service) {
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode(['rulesets' => $rulesetIds]));
|
||||
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/firewall/' . $interface . '/rules');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 201 || $httpCode == 204) {
|
||||
return json_decode($data) ?: (object) ['success' => true];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible wrapper for applying firewall rules.
|
||||
* Syncs/applies existing ruleset assignments on the server.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param string $interface Network interface: "primary" or "secondary"
|
||||
* @return object|false
|
||||
*/
|
||||
public function applyFirewallRules($serviceID, $interface = 'primary')
|
||||
{
|
||||
// Fetch current firewall status to get assigned rulesets
|
||||
$status = $this->getFirewallStatus($serviceID, $interface);
|
||||
if ($status && isset($status['data']['rulesets'])) {
|
||||
$rulesetIds = array_column($status['data']['rulesets'], 'id');
|
||||
if (!empty($rulesetIds)) {
|
||||
return $this->applyFirewallRulesets($serviceID, $rulesetIds, $interface);
|
||||
}
|
||||
}
|
||||
|
||||
// If no rulesets found, try a direct re-apply via the enable cycle
|
||||
$serviceID = (int) $serviceID;
|
||||
$interface = $this->sanitizeFirewallInterface($interface);
|
||||
$service = Database::getSystemService($serviceID);
|
||||
|
||||
if ($service) {
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode(['rulesets' => []]));
|
||||
$data = $request->post($cp['url'] . '/servers/' . (int) $service->server_id . '/firewall/' . $interface . '/rules');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 201 || $httpCode == 204) {
|
||||
return json_decode($data) ?: (object) ['success' => true];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize firewall interface parameter.
|
||||
*
|
||||
* @param string $interface
|
||||
* @return string "primary" or "secondary"
|
||||
*/
|
||||
private function sanitizeFirewallInterface($interface)
|
||||
{
|
||||
return in_array($interface, ['primary', 'secondary'], true) ? $interface : 'primary';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IP Address Management
|
||||
// =========================================================================
|
||||
@@ -947,6 +735,128 @@ class Module
|
||||
return $curl;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Self Service — Credit & Usage
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get self-service usage data for a WHMCS client.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @return array|false
|
||||
*/
|
||||
public function getSelfServiceUsage($serviceID)
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->get($cp['url'] . '/selfService/usage/byUserExtRelationId/' . (int) $whmcsService->userid);
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
if ($request->getRequestInfo('http_code') == 200) {
|
||||
return json_decode($data, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get self-service billing report for a WHMCS client.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @return array|false
|
||||
*/
|
||||
public function getSelfServiceReport($serviceID)
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->get($cp['url'] . '/selfService/report/byUserExtRelationId/' . (int) $whmcsService->userid);
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
if ($request->getRequestInfo('http_code') == 200) {
|
||||
return json_decode($data, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add self-service credit for a WHMCS client.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @param float $tokens Amount of credit tokens to add
|
||||
* @param string $reference Reference text for the transaction
|
||||
* @return array|false
|
||||
*/
|
||||
public function addSelfServiceCredit($serviceID, $tokens, $reference = '')
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$tokens = (float) $tokens;
|
||||
|
||||
if ($tokens <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode([
|
||||
'tokens' => $tokens,
|
||||
'reference_1' => $reference ?: 'WHMCS Top-up',
|
||||
'reference_2' => 'Service #' . $serviceID,
|
||||
]));
|
||||
$data = $request->post($cp['url'] . '/selfService/credit/byUserExtRelationId/' . (int) $whmcsService->userid);
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
$httpCode = $request->getRequestInfo('http_code');
|
||||
if ($httpCode == 200 || $httpCode == 201) {
|
||||
return json_decode($data, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available self-service currencies.
|
||||
*
|
||||
* @param int $serviceID
|
||||
* @return array|false
|
||||
*/
|
||||
public function getSelfServiceCurrencies($serviceID)
|
||||
{
|
||||
$serviceID = (int) $serviceID;
|
||||
$whmcsService = Database::getWhmcsService($serviceID);
|
||||
if (!$whmcsService) return false;
|
||||
|
||||
$cp = $this->getCP($whmcsService->server);
|
||||
if (!$cp) return false;
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
$data = $request->get($cp['url'] . '/selfService/currencies');
|
||||
|
||||
Log::insert(__FUNCTION__, $request->getRequestInfo(), $data);
|
||||
|
||||
if ($request->getRequestInfo('http_code') == 200) {
|
||||
return json_decode($data, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a response from JSON into an associative array.
|
||||
*
|
||||
|
||||
@@ -68,13 +68,20 @@ class ModuleFunctions extends Module
|
||||
|
||||
$request = $this->initCurl($cp['token']);
|
||||
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode(
|
||||
[
|
||||
"name" => $user->firstname . ' ' . $user->lastname,
|
||||
"email" => $user->email,
|
||||
"extRelationId" => $user->id,
|
||||
]
|
||||
));
|
||||
$userData = [
|
||||
"name" => $user->firstname . ' ' . $user->lastname,
|
||||
"email" => $user->email,
|
||||
"extRelationId" => $user->id,
|
||||
];
|
||||
|
||||
// Enable self-service billing if configured
|
||||
$selfServiceMode = (int) ($params['configoption4'] ?? 0);
|
||||
if ($selfServiceMode > 0) {
|
||||
$userData['selfService'] = $selfServiceMode;
|
||||
$userData['selfServiceHourlyCredit'] = in_array($selfServiceMode, [1, 3]);
|
||||
}
|
||||
|
||||
$request->addOption(CURLOPT_POSTFIELDS, json_encode($userData));
|
||||
|
||||
$data = $request->post($cp['url'] . '/users');
|
||||
|
||||
@@ -153,7 +160,8 @@ class ModuleFunctions extends Module
|
||||
|
||||
// If the server is created successfully, we can initialize the server build.
|
||||
$cs = new ConfigureService();
|
||||
$cs->initServerBuild($data->data->id, $params);
|
||||
$vfUserId = isset($data->data->owner->id) ? (int) $data->data->owner->id : null;
|
||||
$cs->initServerBuild($data->data->id, $params, $vfUserId);
|
||||
|
||||
return 'success';
|
||||
} else {
|
||||
@@ -197,7 +205,7 @@ class ModuleFunctions extends Module
|
||||
switch ($request->getRequestInfo('http_code')) {
|
||||
|
||||
case 204:
|
||||
return 'success';
|
||||
break;
|
||||
case 404:
|
||||
return 'The server or package was not found in VirtFusion (HTTP 404).';
|
||||
case 423:
|
||||
@@ -208,6 +216,33 @@ class ModuleFunctions extends Module
|
||||
default:
|
||||
return 'Update package request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code');
|
||||
}
|
||||
|
||||
// Apply individual resource modifications from configurable options
|
||||
if (isset($params['configoptions']) && is_array($params['configoptions'])) {
|
||||
$configOptionDefaultNaming = [
|
||||
'memory' => 'Memory',
|
||||
'cpuCores' => 'CPU Cores',
|
||||
'traffic' => 'Bandwidth',
|
||||
];
|
||||
|
||||
$configOptionCustomNaming = [];
|
||||
if (file_exists(ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php')) {
|
||||
$configOptionCustomNaming = require ROOTDIR . '/modules/servers/VirtFusionDirect/config/ConfigOptionMapping.php';
|
||||
}
|
||||
|
||||
foreach ($configOptionDefaultNaming as $resource => $optionName) {
|
||||
$currentOption = array_key_exists($resource, $configOptionCustomNaming) ? $configOptionCustomNaming[$resource] : $optionName;
|
||||
if (isset($params['configoptions'][$currentOption]) && is_numeric($params['configoptions'][$currentOption])) {
|
||||
$value = (int) $params['configoptions'][$currentOption];
|
||||
if ($resource === 'memory' && $value < 1024) {
|
||||
$value = $value * 1024;
|
||||
}
|
||||
$this->modifyResource($params['serviceid'], $resource, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}
|
||||
return 'Service not found in module database.';
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ class ServerResource
|
||||
'inbound' => isset($server['settings']['resources']['networkSpeedInbound']) ? $server['settings']['resources']['networkSpeedInbound'] . ' Mbps' : '-',
|
||||
'outbound' => isset($server['settings']['resources']['networkSpeedOutbound']) ? $server['settings']['resources']['networkSpeedOutbound'] . ' Mbps' : '-',
|
||||
],
|
||||
'vncEnabled' => isset($server['vnc']['enabled']) ? (bool) $server['vnc']['enabled'] : false,
|
||||
'memoryRaw' => isset($server['settings']['resources']['memory']) ? (int) $server['settings']['resources']['memory'] : 0,
|
||||
'cpuRaw' => isset($server['settings']['resources']['cpuCores']) ? (int) $server['settings']['resources']['cpuCores'] : 0,
|
||||
'storageRaw' => isset($server['settings']['resources']['storage']) ? (int) $server['settings']['resources']['storage'] : 0,
|
||||
'trafficRaw' => isset($server['settings']['resources']['traffic']) ? (int) $server['settings']['resources']['traffic'] : 0,
|
||||
'trafficUsedRaw' => isset($server['usage']['traffic']['used']) ? round($server['usage']['traffic']['used'] / 1073741824, 2) : 0,
|
||||
'networkSpeedInboundRaw' => isset($server['settings']['resources']['networkSpeedInbound']) ? (int) $server['settings']['resources']['networkSpeedInbound'] : 0,
|
||||
'networkSpeedOutboundRaw' => isset($server['settings']['resources']['networkSpeedOutbound']) ? (int) $server['settings']['resources']['networkSpeedOutbound'] : 0,
|
||||
];
|
||||
|
||||
if (array_key_exists('network', $server)) {
|
||||
|
||||
Reference in New Issue
Block a user