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:
@@ -43,6 +43,27 @@ function VirtFusionDirect_ConfigOptions()
|
||||
"Description" => "The default number of IPv4 addresses to assign to each server.",
|
||||
"Default" => "1",
|
||||
],
|
||||
"selfServiceMode" => [
|
||||
"FriendlyName" => "Self-Service Mode",
|
||||
"Type" => "dropdown",
|
||||
"Options" => "0|Disabled,1|Hourly,2|Resource Packs,3|Both",
|
||||
"Description" => "Enable VirtFusion self-service billing for users created by this product.",
|
||||
"Default" => "0",
|
||||
],
|
||||
"autoTopOffThreshold" => [
|
||||
"FriendlyName" => "Auto Top-Off Threshold",
|
||||
"Type" => "text",
|
||||
"Size" => "10",
|
||||
"Description" => "Credit balance below which auto top-off triggers during cron. 0 = disabled.",
|
||||
"Default" => "0",
|
||||
],
|
||||
"autoTopOffAmount" => [
|
||||
"FriendlyName" => "Auto Top-Off Amount",
|
||||
"Type" => "text",
|
||||
"Size" => "10",
|
||||
"Description" => "Credit amount to add when auto top-off triggers.",
|
||||
"Default" => "100",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -238,6 +259,32 @@ function VirtFusionDirect_UsageUpdate(array $params)
|
||||
->where('id', $service->id)
|
||||
->update($update);
|
||||
}
|
||||
|
||||
// Self-service auto top-off
|
||||
$product = \WHMCS\Database\Capsule::table('tblproducts')
|
||||
->where('id', $service->packageid)
|
||||
->first();
|
||||
|
||||
if ($product) {
|
||||
$threshold = (float) ($product->configoption5 ?? 0);
|
||||
$topOffAmount = (float) ($product->configoption6 ?? 0);
|
||||
|
||||
if ($threshold > 0 && $topOffAmount > 0) {
|
||||
$usageData = $module->getSelfServiceUsage($service->id);
|
||||
if ($usageData) {
|
||||
$usageInner = $usageData['data'] ?? $usageData;
|
||||
$credit = $usageInner['credit'] ?? $usageInner['balance'] ?? null;
|
||||
if ($credit !== null && (float) $credit < $threshold) {
|
||||
$module->addSelfServiceCredit($service->id, $topOffAmount, 'Auto top-off');
|
||||
\WHMCS\Module\Server\VirtFusionDirect\Log::insert(
|
||||
'UsageUpdate:autoTopOff',
|
||||
['serviceId' => $service->id, 'credit' => $credit, 'threshold' => $threshold],
|
||||
['amount' => $topOffAmount]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log but continue processing other services
|
||||
\WHMCS\Module\Server\VirtFusionDirect\Log::insert('UsageUpdate:service:' . $service->id, [], $e->getMessage());
|
||||
|
||||
@@ -176,133 +176,6 @@ switch ($action) {
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// Firewall Management
|
||||
//
|
||||
// VirtFusion uses a ruleset-based system. Individual rules cannot
|
||||
// be added/deleted via the API. Rulesets are created in admin panel
|
||||
// and applied to servers by ID.
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Get firewall status, rules, and assigned rulesets.
|
||||
*/
|
||||
case 'firewallStatus':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$interface = isset($_GET['interface']) ? preg_replace('/[^a-z]/', '', $_GET['interface']) : 'primary';
|
||||
$result = $vf->getFirewallStatus($serviceID, $interface);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve firewall status'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Enable firewall.
|
||||
*/
|
||||
case 'firewallEnable':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$interface = isset($_GET['interface']) ? preg_replace('/[^a-z]/', '', $_GET['interface']) : 'primary';
|
||||
$result = $vf->enableFirewall($serviceID, $interface);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Firewall enabled successfully']], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to enable firewall'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Disable firewall.
|
||||
*/
|
||||
case 'firewallDisable':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$interface = isset($_GET['interface']) ? preg_replace('/[^a-z]/', '', $_GET['interface']) : 'primary';
|
||||
$result = $vf->disableFirewall($serviceID, $interface);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Firewall disabled successfully']], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to disable firewall'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Apply/sync firewall rules (re-applies currently assigned rulesets).
|
||||
*/
|
||||
case 'firewallApplyRules':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$interface = isset($_GET['interface']) ? preg_replace('/[^a-z]/', '', $_GET['interface']) : 'primary';
|
||||
$result = $vf->applyFirewallRules($serviceID, $interface);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Firewall rules applied successfully']], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to apply firewall rules'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Apply specific firewall rulesets by ID.
|
||||
* Expects comma-separated ruleset IDs in the 'rulesets' parameter.
|
||||
*/
|
||||
case 'firewallApplyRulesets':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$rulesetsParam = isset($_GET['rulesets']) ? trim($_GET['rulesets']) : '';
|
||||
if (empty($rulesetsParam)) {
|
||||
$vf->output(['success' => false, 'errors' => 'No ruleset IDs provided'], true, true, 400);
|
||||
}
|
||||
|
||||
$rulesetIds = array_values(array_filter(array_map('intval', explode(',', $rulesetsParam)), function ($id) {
|
||||
return $id > 0;
|
||||
}));
|
||||
|
||||
if (empty($rulesetIds)) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid ruleset IDs'], true, true, 400);
|
||||
}
|
||||
|
||||
$interface = isset($_GET['interface']) ? preg_replace('/[^a-z]/', '', $_GET['interface']) : 'primary';
|
||||
$result = $vf->applyFirewallRulesets($serviceID, $rulesetIds, $interface);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Firewall rulesets applied successfully']], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to apply firewall rulesets'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// IP Address Management
|
||||
// =================================================================
|
||||
@@ -445,6 +318,75 @@ switch ($action) {
|
||||
$vf->output(['success' => false, 'errors' => 'VNC console unavailable. The server may be powered off or VNC is not supported.'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// Self Service — Credit & Usage
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Get self-service usage data.
|
||||
*/
|
||||
case 'selfServiceUsage':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$result = $vf->getSelfServiceUsage($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service usage data'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Get self-service billing report.
|
||||
*/
|
||||
case 'selfServiceReport':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$result = $vf->getSelfServiceReport($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service report'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Add self-service credit.
|
||||
*/
|
||||
case 'selfServiceAddCredit':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
}
|
||||
|
||||
$tokens = isset($_GET['tokens']) ? (float) $_GET['tokens'] : 0;
|
||||
if ($tokens <= 0) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid credit amount. Must be a positive number.'], true, true, 400);
|
||||
}
|
||||
|
||||
$result = $vf->addSelfServiceCredit($serviceID, $tokens);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to add credit'], true, true, 500);
|
||||
break;
|
||||
|
||||
default:
|
||||
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,30 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
||||
|
||||
// Handle SSH keys
|
||||
if (sshInputField) {
|
||||
// Create the paste-key textarea (hidden initially if keys exist)
|
||||
var sshPasteContainer = document.createElement('div');
|
||||
sshPasteContainer.setAttribute('id', 'vf-ssh-paste-container');
|
||||
sshPasteContainer.style.display = 'none';
|
||||
sshPasteContainer.style.marginTop = '8px';
|
||||
|
||||
var pasteLabel = document.createElement('label');
|
||||
pasteLabel.textContent = 'Paste your SSH public key:';
|
||||
pasteLabel.style.display = 'block';
|
||||
pasteLabel.style.marginBottom = '4px';
|
||||
|
||||
var pasteArea = document.createElement('textarea');
|
||||
pasteArea.className = 'form-control';
|
||||
pasteArea.setAttribute('id', 'vf-ssh-paste');
|
||||
pasteArea.setAttribute('rows', '3');
|
||||
pasteArea.setAttribute('placeholder', 'ssh-rsa AAAA... or ssh-ed25519 AAAA...');
|
||||
|
||||
pasteArea.addEventListener('input', function() {
|
||||
sshInputField.value = this.value.trim();
|
||||
});
|
||||
|
||||
sshPasteContainer.appendChild(pasteLabel);
|
||||
sshPasteContainer.appendChild(pasteArea);
|
||||
|
||||
if (sshKeys.length > 0) {
|
||||
var sshSelect = document.createElement('select');
|
||||
sshSelect.className = 'form-control';
|
||||
@@ -190,20 +214,102 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
|
||||
sshSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Add new key option
|
||||
var addNewOption = document.createElement('option');
|
||||
addNewOption.value = '__new__';
|
||||
addNewOption.text = 'Add new key...';
|
||||
sshSelect.appendChild(addNewOption);
|
||||
|
||||
sshSelect.addEventListener('change', function() {
|
||||
sshInputField.value = this.value;
|
||||
if (this.value === '__new__') {
|
||||
sshPasteContainer.style.display = 'block';
|
||||
sshInputField.value = '';
|
||||
} else {
|
||||
sshPasteContainer.style.display = 'none';
|
||||
document.getElementById('vf-ssh-paste').value = '';
|
||||
sshInputField.value = this.value;
|
||||
}
|
||||
});
|
||||
|
||||
sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
|
||||
sshSelect.parentNode.insertBefore(sshPasteContainer, sshSelect.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
} else {
|
||||
// No existing keys — show the paste textarea directly
|
||||
sshPasteContainer.style.display = 'block';
|
||||
sshInputField.parentNode.insertBefore(sshPasteContainer, sshInputField.nextSibling);
|
||||
sshInputField.style.display = 'none';
|
||||
if (sshInputLabel) sshInputLabel.style.display = 'none';
|
||||
// Also hide the parent container if it exists
|
||||
var sshContainer = sshInputField.closest('.form-group');
|
||||
if (sshContainer) sshContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Slider UI: enhance known configurable option selects with range sliders
|
||||
var sliderResourceNames = ['Memory', 'CPU Cores', 'Storage', 'Bandwidth', 'Inbound Network Speed', 'Outbound Network Speed'];
|
||||
var sliderUnits = {
|
||||
'Memory': 'MB', 'CPU Cores': 'Core(s)', 'Storage': 'GB',
|
||||
'Bandwidth': 'GB', 'Inbound Network Speed': 'Mbps', 'Outbound Network Speed': 'Mbps'
|
||||
};
|
||||
|
||||
var configSelects = document.querySelectorAll('select[name^=\"configoption[\"]');
|
||||
configSelects.forEach(function(sel) {
|
||||
// Find the label for this select
|
||||
var label = null;
|
||||
var labelEl = sel.closest('.form-group, .row');
|
||||
if (labelEl) {
|
||||
label = labelEl.querySelector('label');
|
||||
}
|
||||
if (!label) return;
|
||||
|
||||
var labelText = label.textContent.trim();
|
||||
var matchedResource = null;
|
||||
sliderResourceNames.forEach(function(name) {
|
||||
if (labelText.indexOf(name) !== -1) {
|
||||
matchedResource = name;
|
||||
}
|
||||
});
|
||||
if (!matchedResource) return;
|
||||
|
||||
var options = [];
|
||||
for (var i = 0; i < sel.options.length; i++) {
|
||||
options.push({
|
||||
value: sel.options[i].value,
|
||||
label: sel.options[i].text
|
||||
});
|
||||
}
|
||||
if (options.length < 2) return;
|
||||
|
||||
var unit = sliderUnits[matchedResource] || '';
|
||||
|
||||
// Create slider container
|
||||
var container = document.createElement('div');
|
||||
container.className = 'vf-slider-container';
|
||||
|
||||
var valueDisplay = document.createElement('div');
|
||||
valueDisplay.className = 'vf-slider-value';
|
||||
valueDisplay.textContent = options[sel.selectedIndex || 0].label + (unit ? ' ' + unit : '');
|
||||
|
||||
var slider = document.createElement('input');
|
||||
slider.type = 'range';
|
||||
slider.className = 'vf-slider form-range';
|
||||
slider.min = '0';
|
||||
slider.max = String(options.length - 1);
|
||||
slider.step = '1';
|
||||
slider.value = String(sel.selectedIndex || 0);
|
||||
|
||||
slider.addEventListener('input', function() {
|
||||
var idx = parseInt(this.value);
|
||||
sel.selectedIndex = idx;
|
||||
valueDisplay.textContent = options[idx].label + (unit ? ' ' + unit : '');
|
||||
// Trigger change event on hidden select for WHMCS pricing
|
||||
var evt = new Event('change', { bubbles: true });
|
||||
sel.dispatchEvent(evt);
|
||||
});
|
||||
|
||||
container.appendChild(valueDisplay);
|
||||
container.appendChild(slider);
|
||||
|
||||
sel.parentNode.insertBefore(container, sel.nextSibling);
|
||||
sel.style.display = 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
";
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -139,6 +139,53 @@
|
||||
padding: 0.15rem 0.4rem;
|
||||
}
|
||||
|
||||
/* Resource panel */
|
||||
.vf-resource-item .progress {
|
||||
background-color: rgba(0,0,0,0.08);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Order form slider UI */
|
||||
.vf-slider-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.vf-slider-value {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.vf-slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #ddd;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.vf-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #337ab7;
|
||||
cursor: pointer;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
.vf-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #337ab7;
|
||||
cursor: pointer;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.vf-power-buttons {
|
||||
|
||||
@@ -40,6 +40,43 @@ function vfServerData(serviceId, systemUrl) {
|
||||
statusBadge.addClass("vf-badge-awaiting");
|
||||
}
|
||||
|
||||
// Show/hide VNC panel based on API response
|
||||
if (response.data.vncEnabled) {
|
||||
$("#vf-vnc-panel").show();
|
||||
}
|
||||
|
||||
// Populate resources panel
|
||||
var d = response.data;
|
||||
$("#vf-res-memory").text(d.memory || "-");
|
||||
$("#vf-res-cpu").text(d.cpu || "-");
|
||||
$("#vf-res-storage").text(d.storage || "-");
|
||||
|
||||
var trafficUsed = d.trafficUsedRaw || 0;
|
||||
var trafficTotal = d.trafficRaw || 0;
|
||||
if (trafficTotal > 0) {
|
||||
$("#vf-res-traffic").text(trafficUsed + " / " + trafficTotal + " GB");
|
||||
var pct = Math.min(100, Math.round((trafficUsed / trafficTotal) * 100));
|
||||
$("#vf-res-traffic-bar").css("width", pct + "%");
|
||||
if (pct > 90) {
|
||||
$("#vf-res-traffic-bar").addClass("bg-danger");
|
||||
} else if (pct > 70) {
|
||||
$("#vf-res-traffic-bar").addClass("bg-warning");
|
||||
}
|
||||
} else {
|
||||
$("#vf-res-traffic").text(d.traffic || "Unlimited");
|
||||
$("#vf-res-traffic-bar").css("width", "0%");
|
||||
}
|
||||
|
||||
var speedIn = d.networkSpeedInboundRaw || 0;
|
||||
var speedOut = d.networkSpeedOutboundRaw || 0;
|
||||
if (speedIn > 0 || speedOut > 0) {
|
||||
$("#vf-res-network-speed").text(speedIn + " / " + speedOut + " Mbps");
|
||||
} else {
|
||||
$("#vf-res-network-speed").text("-");
|
||||
}
|
||||
|
||||
$("#vf-resources-panel").show();
|
||||
|
||||
$("#vf-server-info").show();
|
||||
} else {
|
||||
$("#vf-server-info-error").show();
|
||||
@@ -260,77 +297,6 @@ function impersonateServerOwner(serviceId, systemUrl) {
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Firewall Management
|
||||
// =========================================================================
|
||||
|
||||
function vfLoadFirewallStatus(serviceId, systemUrl) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=firewallStatus"
|
||||
}).done(function (response) {
|
||||
if (response.success) {
|
||||
var badge = $("#vf-firewall-badge");
|
||||
var data = response.data;
|
||||
var enabled = data && data.data && data.data.enabled;
|
||||
if (enabled) {
|
||||
badge.text("Enabled").addClass("vf-badge-active");
|
||||
} else {
|
||||
badge.text("Disabled").addClass("vf-badge-awaiting");
|
||||
}
|
||||
$("#vf-firewall-content").show();
|
||||
} else {
|
||||
$("#vf-firewall-badge").text("Unknown").addClass("vf-badge-awaiting");
|
||||
$("#vf-firewall-content").show();
|
||||
}
|
||||
}).fail(function () {
|
||||
$("#vf-firewall-badge").text("Unavailable").addClass("vf-badge-awaiting");
|
||||
$("#vf-firewall-content").show();
|
||||
}).always(function () {
|
||||
$("#vf-firewall-loader").hide();
|
||||
});
|
||||
}
|
||||
|
||||
function vfFirewallAction(serviceId, systemUrl, action) {
|
||||
var btnId = {
|
||||
firewallEnable: "#vf-firewall-enable",
|
||||
firewallDisable: "#vf-firewall-disable",
|
||||
firewallApplyRules: "#vf-firewall-apply"
|
||||
};
|
||||
var btn = $(btnId[action]);
|
||||
var spinner = btn.find(".vf-btn-spinner");
|
||||
var alertDiv = $("#vf-firewall-alert");
|
||||
|
||||
btn.prop("disabled", true);
|
||||
spinner.show();
|
||||
alertDiv.hide();
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=" + encodeURIComponent(action)
|
||||
}).done(function (response) {
|
||||
if (response.success) {
|
||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
||||
alertDiv.text(response.data.message || "Firewall action completed.");
|
||||
// Refresh status badge
|
||||
vfLoadFirewallStatus(serviceId, systemUrl);
|
||||
} else {
|
||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
||||
alertDiv.text(response.errors || "Firewall action failed.");
|
||||
}
|
||||
alertDiv.show();
|
||||
}).fail(function () {
|
||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
||||
alertDiv.text("An error occurred. Please try again.");
|
||||
alertDiv.show();
|
||||
}).always(function () {
|
||||
spinner.hide();
|
||||
btn.prop("disabled", false);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Network / IP Management
|
||||
// =========================================================================
|
||||
@@ -507,3 +473,119 @@ function vfOpenVnc(serviceId, systemUrl) {
|
||||
btn.prop("disabled", false);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Self Service — Credit & Usage
|
||||
// =========================================================================
|
||||
|
||||
function vfLoadSelfServiceUsage(serviceId, systemUrl) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceUsage"
|
||||
}).done(function (response) {
|
||||
if (response.success && response.data) {
|
||||
var data = response.data.data || response.data;
|
||||
|
||||
// Credit balance
|
||||
var balance = "-";
|
||||
if (data.credit !== undefined) {
|
||||
balance = parseFloat(data.credit).toFixed(2);
|
||||
} else if (data.balance !== undefined) {
|
||||
balance = parseFloat(data.balance).toFixed(2);
|
||||
}
|
||||
$("#vf-ss-credit-balance").text(balance);
|
||||
|
||||
// Usage breakdown
|
||||
var tbody = $("#vf-ss-usage-table");
|
||||
tbody.empty();
|
||||
|
||||
var items = data.usage || data.items || [];
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
$.each(items, function (i, item) {
|
||||
var desc = item.description || item.name || item.server || "Item";
|
||||
var cost = item.cost !== undefined ? parseFloat(item.cost).toFixed(2) : "-";
|
||||
tbody.append('<tr><td>' + $('<span>').text(desc).html() + '</td><td class="text-right">' + $('<span>').text(cost).html() + '</td></tr>');
|
||||
});
|
||||
} else {
|
||||
tbody.append('<tr><td colspan="2" class="text-muted">No usage data available</td></tr>');
|
||||
}
|
||||
|
||||
$("#vf-selfservice-content").show();
|
||||
$("#vf-selfservice-panel").show();
|
||||
}
|
||||
}).fail(function () {
|
||||
// Self-service not available — keep panel hidden
|
||||
}).always(function () {
|
||||
$("#vf-selfservice-loader").hide();
|
||||
});
|
||||
}
|
||||
|
||||
function vfLoadSelfServiceReport(serviceId, systemUrl) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceReport"
|
||||
}).done(function (response) {
|
||||
if (response.success && response.data) {
|
||||
var data = response.data.data || response.data;
|
||||
var tbody = $("#vf-ss-usage-table");
|
||||
tbody.empty();
|
||||
|
||||
var items = data.items || data.report || [];
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
$.each(items, function (i, item) {
|
||||
var desc = item.description || item.name || "Item";
|
||||
var cost = item.cost !== undefined ? parseFloat(item.cost).toFixed(2) : "-";
|
||||
tbody.append('<tr><td>' + $('<span>').text(desc).html() + '</td><td class="text-right">' + $('<span>').text(cost).html() + '</td></tr>');
|
||||
});
|
||||
} else {
|
||||
tbody.append('<tr><td colspan="2" class="text-muted">No report data available</td></tr>');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function vfAddCredit(serviceId, systemUrl) {
|
||||
var amount = $("#vf-ss-credit-amount").val();
|
||||
var alertDiv = $("#vf-selfservice-alert");
|
||||
var btn = $("#vf-ss-add-credit-btn");
|
||||
var spinner = $("#vf-ss-add-credit-spinner");
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
||||
alertDiv.text("Please enter a valid positive amount.");
|
||||
alertDiv.show();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.prop("disabled", true);
|
||||
spinner.show();
|
||||
alertDiv.hide();
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: systemUrl + "modules/servers/VirtFusionDirect/client.php?serviceID=" + encodeURIComponent(serviceId) + "&action=selfServiceAddCredit&tokens=" + encodeURIComponent(amount)
|
||||
}).done(function (response) {
|
||||
if (response.success) {
|
||||
alertDiv.removeClass("alert-danger").addClass("alert-success");
|
||||
alertDiv.text("Credit added successfully.");
|
||||
alertDiv.show();
|
||||
$("#vf-ss-credit-amount").val("");
|
||||
// Refresh usage data
|
||||
vfLoadSelfServiceUsage(serviceId, systemUrl);
|
||||
} else {
|
||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
||||
alertDiv.text(response.errors || "Failed to add credit.");
|
||||
alertDiv.show();
|
||||
}
|
||||
}).fail(function () {
|
||||
alertDiv.removeClass("alert-success").addClass("alert-danger");
|
||||
alertDiv.text("An error occurred. Please try again.");
|
||||
alertDiv.show();
|
||||
}).always(function () {
|
||||
spinner.hide();
|
||||
btn.prop("disabled", false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,44 +168,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{* Firewall Management Panel *}
|
||||
<div class="panel card panel-default mb-3">
|
||||
<div class="panel-heading card-header">
|
||||
<h3 class="panel-title card-title m-0">
|
||||
Firewall
|
||||
<span id="vf-firewall-badge" class="vf-badge" style="float: right;"></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body card-body p-4">
|
||||
<div id="vf-firewall-alert" class="alert" style="display: none;"></div>
|
||||
<div id="vf-firewall-loader" class="d-flex align-items-center justify-content-center" style="min-height: 60px;">
|
||||
<div class="spinner-border spinner-border-sm"></div>
|
||||
</div>
|
||||
<div id="vf-firewall-content" style="display: none;">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="vf-power-buttons">
|
||||
<button id="vf-firewall-enable" onclick="vfFirewallAction('{$serviceid}','{$systemURL}','firewallEnable')" type="button" class="btn btn-success vf-btn-power">
|
||||
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
|
||||
Enable
|
||||
</button>
|
||||
<button id="vf-firewall-disable" onclick="vfFirewallAction('{$serviceid}','{$systemURL}','firewallDisable')" type="button" class="btn btn-danger vf-btn-power">
|
||||
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
|
||||
Disable
|
||||
</button>
|
||||
<button id="vf-firewall-apply" onclick="vfFirewallAction('{$serviceid}','{$systemURL}','firewallApplyRules')" type="button" class="btn btn-primary vf-btn-power">
|
||||
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
|
||||
Apply Rules
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="vf-small text-muted mb-0">Manage your server firewall. Use the VirtFusion control panel for advanced rule configuration.</p>
|
||||
</div>
|
||||
<script>vfLoadFirewallStatus('{$serviceid}', '{$systemURL}');</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{* Network Management Panel *}
|
||||
<div class="panel card panel-default mb-3">
|
||||
<div class="panel-heading card-header">
|
||||
@@ -240,8 +202,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{* VNC Console Panel *}
|
||||
<div class="panel card panel-default mb-3">
|
||||
{* Resources Panel — populated by JS after server data loads *}
|
||||
<div id="vf-resources-panel" class="panel card panel-default mb-3" style="display: none;">
|
||||
<div class="panel-heading card-header">
|
||||
<h3 class="panel-title card-title m-0">Resources</h3>
|
||||
</div>
|
||||
<div class="panel-body card-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="vf-resource-item mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="vf-bold">Memory</span>
|
||||
<span id="vf-res-memory"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vf-resource-item mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="vf-bold">CPU Cores</span>
|
||||
<span id="vf-res-cpu"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vf-resource-item mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="vf-bold">Storage</span>
|
||||
<span id="vf-res-storage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="vf-resource-item mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="vf-bold">Traffic</span>
|
||||
<span id="vf-res-traffic"></span>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div id="vf-res-traffic-bar" class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vf-resource-item mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="vf-bold">Network Speed</span>
|
||||
<span id="vf-res-network-speed"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="clientarea.php?action=upgrade&id={$serviceid}" class="btn btn-outline-primary mt-2">Upgrade / Downgrade Resources</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{* VNC Console Panel — hidden by default, shown by JS if VNC is enabled *}
|
||||
<div id="vf-vnc-panel" class="panel card panel-default mb-3" style="display: none;">
|
||||
<div class="panel-heading card-header">
|
||||
<h3 class="panel-title card-title m-0">VNC Console</h3>
|
||||
</div>
|
||||
@@ -255,6 +266,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{* Self Service — Billing & Usage Panel *}
|
||||
<div id="vf-selfservice-panel" class="panel card panel-default mb-3" style="display: none;">
|
||||
<div class="panel-heading card-header">
|
||||
<h3 class="panel-title card-title m-0">Billing & Usage</h3>
|
||||
</div>
|
||||
<div class="panel-body card-body p-4">
|
||||
<div id="vf-selfservice-alert" class="alert" style="display: none;"></div>
|
||||
<div id="vf-selfservice-loader" class="d-flex align-items-center justify-content-center" style="min-height: 60px;">
|
||||
<div class="spinner-border spinner-border-sm"></div>
|
||||
</div>
|
||||
<div id="vf-selfservice-content" style="display: none;">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h5 class="vf-bold">Credit Balance</h5>
|
||||
<div class="h4 mb-3" id="vf-ss-credit-balance">-</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5 class="vf-bold">Add Credit</h5>
|
||||
<div class="input-group mb-2">
|
||||
<input type="number" id="vf-ss-credit-amount" class="form-control" placeholder="Amount" min="1" step="1">
|
||||
<div class="input-group-append">
|
||||
<button id="vf-ss-add-credit-btn" onclick="vfAddCredit('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary">
|
||||
<span id="vf-ss-add-credit-spinner" class="spinner-border spinner-border-sm" style="display:none;"></span>
|
||||
Add Credit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="vf-bold">Usage Breakdown</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="text-right">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vf-ss-usage-table">
|
||||
<tr><td colspan="2" class="text-muted">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>vfLoadSelfServiceUsage('{$serviceid}', '{$systemURL}');</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{elseif $serviceStatus eq 'Suspended'}
|
||||
|
||||
<div class="panel card panel-default mb-3">
|
||||
|
||||
Reference in New Issue
Block a user