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:
EZSCALE
2026-02-07 14:25:43 -06:00
parent 49fdd9e49b
commit 1e471affd0
13 changed files with 915 additions and 505 deletions

117
CHANGELOG.md Normal file
View File

@@ -0,0 +1,117 @@
# Changelog
All notable changes to the VirtFusion Direct Provisioning Module for WHMCS.
## [Unreleased]
### Added
- **Power management** — Start, restart, graceful shutdown, and force power off controls in client area
- **Server rebuild** — Reinstall with any available OS template from client area with confirmation dialog
- **Server rename** — Change server display name via client area
- **Network management** — View, add, and remove IPv4 addresses and IPv6 subnets from client area
- **VNC console** — Browser-based console access (VirtFusion v6.1.0+)
- **VNC runtime check** — VNC panel auto-hides when VNC is disabled on the server
- **Backup management** — Assign and remove backup plans via API
- **Resource modification** — In-place memory, CPU, and traffic changes (VirtFusion v6.2.0+)
- **Resources panel** — Client area panel showing current memory, CPU, storage, traffic allocation with progress bars and upgrade/downgrade link
- **UsageUpdate cron** — Automated bandwidth and disk usage sync from VirtFusion to WHMCS
- **Dry run validation** — Test server creation parameters before provisioning
- **Admin "Validate Server Config" button** — Dry run from admin services tab
- **TestConnection** — Validate API credentials from WHMCS server settings
- **ServiceSingleSignOn** — Native WHMCS SSO integration for VirtFusion panel
- **Server status badge** — Visual indicator of server state in overview
- **Traffic usage display** — Bandwidth used vs allocated
- **Checkout validation** — `ShoppingCartValidateCheckout` hook ensures OS selection before order placement
- **SSH key paste at checkout** — Users can paste a raw SSH public key during checkout; key is created via `POST /ssh_keys` during provisioning
- **Order form sliders** — Configurable option dropdowns replaced with styled range sliders for resource selection
- **Self-service billing** — Credit balance display, usage breakdown, and credit top-up from client area
- **Self-service config options** — Product config options 4-6: Self-Service Mode, Auto Top-Off Threshold, Auto Top-Off Amount
- **Auto top-off** — During WHMCS daily cron, automatically adds credit when balance falls below threshold
- **Self-service user creation** — New VirtFusion users created with self-service billing settings when enabled
- **CLAUDE.md** — Project architecture and development guidance for Claude Code
### Changed
- Enable SSL/TLS certificate verification by default (was disabled)
- Remove `error_reporting(0)` that silenced all errors
- Add input sanitization on all user parameters (type casting, regex filtering)
- Return proper HTTP status codes (401, 403, 400, 500) instead of always 200
- Add XSS protection with `htmlspecialchars()` and `encodeURIComponent()`
- Readable, unminified JavaScript with JSDoc header
- Dual panel/card CSS classes for Bootstrap 3/4/5 theme compatibility
- `changePackage()` now applies individual resource modifications from configurable options after updating the package
- `initServerBuild()` accepts optional VF user ID parameter for SSH key creation
- `ServerResource::process()` returns raw numeric resource values and `vncEnabled` boolean
- Comprehensive README rewrite with installation, configuration, troubleshooting, and API reference
### Fixed
- Add `isset()` guards before `count()` on ipv4/ipv6 arrays in ServerResource to prevent PHP 8.0+ TypeError
- Add null checks after `getWhmcsService()` and `getCP()` in all Module/ModuleFunctions methods to prevent fatal null dereference
- Fix HTTP status codes throughout admin.php (404, 400, 500, 502 instead of always 200)
- Guard ConfigureService methods against `$this->cp === false`
- Replace `exit()` with `RuntimeException` in Curl.php
- Change `catch(Exception)` to `catch(Throwable)` in hooks.php for PHP 8.0+ compatibility
- Open VNC window before AJAX call to avoid popup blocker
- Memory conversion checks key name instead of display name
### Removed
- Firewall feature (non-functional — rulesets must be created in VirtFusion admin panel)
## [0.0.18] - 2025-10-01
### Changed
- Updated GitHub Actions publish workflow
- Moved custom field SQL to `modify.sql` file
- Minor code tweaks
## [0.0.17] - 2024-01-16
### Fixed
- Fix in hooks.php (PR #2 by Prophet731)
## [0.0.16] - 2023-09-11
### Added
- GitHub issue templates
## [0.0.15] - 2023-09-10
### Fixed
- Typo fixes in module code
## [0.0.14] - 2023-09-10
### Fixed
- Fix hook event registration placement
## [0.0.13] - 2023-09-10
### Added
- Contributions from BlinkohHost
- Database-first package ID lookup with API fallback by product name
- Server build initialization on successful server creation
### Changed
- Custom fields changed to not required
- Removed linter workflow (not needed for this project)
- Code cleanup
## [0.0.9] - 2023-09-10
### Changed
- Refactored codebase to object-oriented architecture (OOP)
- Updated README with badges and documentation
## [0.0.6] - 2023-09-10
### Added
- Initial release
- Core provisioning: server create, suspend, unsuspend, terminate
- WHMCS hooks for dynamic OS template and SSH key dropdowns
- Checkout validation for OS selection
- Client area overview template with server information
- Admin services tab with server ID management
- Package change (upgrade/downgrade) support
- Configurable option mapping for dynamic resource allocation
- GitHub Actions CI/CD with semantic-release
- Security policy (SECURITY.md)
- License (GPL v3)

View File

@@ -33,15 +33,15 @@ Releases are automated via GitHub Actions using semantic-release on pushes to `m
| `VirtFusionDirect.php` | WHMCS module interface — non-namespaced functions (`VirtFusionDirect_CreateAccount()`, etc.) that delegate to library classes | | `VirtFusionDirect.php` | WHMCS module interface — non-namespaced functions (`VirtFusionDirect_CreateAccount()`, etc.) that delegate to library classes |
| `client.php` | Client-facing AJAX API — authenticated by WHMCS session + service ownership validation | | `client.php` | Client-facing AJAX API — authenticated by WHMCS session + service ownership validation |
| `admin.php` | Admin-facing AJAX API — requires WHMCS admin authentication | | `admin.php` | Admin-facing AJAX API — requires WHMCS admin authentication |
| `hooks.php` | WHMCS hooks — checkout validation (OS selection) and dynamic dropdown injection | | `hooks.php` | WHMCS hooks — checkout validation (OS selection), dynamic dropdown/slider injection, SSH key paste |
### Core Classes (in `lib/`) ### Core Classes (in `lib/`)
| Class | Role | | Class | Role |
|-------|------| |-------|------|
| `Module` | Base class with API integration, auth checks, power/firewall/network/VNC/backup/resource methods. All client/admin actions route through here. | | `Module` | Base class with API integration, auth checks, power/network/VNC/backup/resource/self-service methods. All client/admin actions route through here. |
| `ModuleFunctions` | Extends `Module`. Service lifecycle: create, suspend, unsuspend, terminate, change package, usage updates, client area rendering. | | `ModuleFunctions` | Extends `Module`. Service lifecycle: create, suspend, unsuspend, terminate, change package, usage updates, client area rendering. |
| `ConfigureService` | Extends `Module`. Order-time operations: package discovery, OS template fetching, server build initialization, SSH key retrieval. | | `ConfigureService` | Extends `Module`. Order-time operations: package discovery, OS template fetching, server build initialization, SSH key retrieval and creation. |
| `Database` | Static methods for `mod_virtfusion_direct` table operations and WHMCS DB queries. Auto-creates/migrates schema on first use. | | `Database` | Static methods for `mod_virtfusion_direct` table operations and WHMCS DB queries. Auto-creates/migrates schema on first use. |
| `Curl` | HTTP client wrapper with Bearer token auth, SSL verification, 30s timeout. Methods: `get`, `post`, `put`, `patch`, `delete`. | | `Curl` | HTTP client wrapper with Bearer token auth, SSL verification, 30s timeout. Methods: `get`, `post`, `put`, `patch`, `delete`. |
| `ServerResource` | Transforms VirtFusion API response into flat key-value format for Smarty templates. | | `ServerResource` | Transforms VirtFusion API response into flat key-value format for Smarty templates. |
@@ -50,11 +50,11 @@ Releases are automated via GitHub Actions using semantic-release on pushes to `m
### Class Hierarchy ### Class Hierarchy
`ModuleFunctions` and `ConfigureService` both extend `Module`. Most business logic lives in `Module` (888 lines) — it handles API calls, auth, validation, and all feature-specific operations (power, firewall, network, VNC, backup, resource modification). `ModuleFunctions` orchestrates the WHMCS service lifecycle (provisioning flow, suspension, termination). `ModuleFunctions` and `ConfigureService` both extend `Module`. Most business logic lives in `Module` — it handles API calls, auth, validation, and all feature-specific operations (power, network, VNC, backup, resource modification). `ModuleFunctions` orchestrates the WHMCS service lifecycle (provisioning flow, suspension, termination).
### Client-Side ### Client-Side
- **`templates/overview.tpl`** — Smarty template for client area (server info, power, firewall, network, rebuild, VNC, backups, resource modification, billing) - **`templates/overview.tpl`** — Smarty template for client area (server info, power, network, rebuild, resources, VNC, self-service billing, billing overview)
- **`templates/js/module.js`** — Vanilla JS (1000+ lines) handling AJAX calls to `client.php`, DOM updates, status badges, power actions, all management UIs - **`templates/js/module.js`** — Vanilla JS (1000+ lines) handling AJAX calls to `client.php`, DOM updates, status badges, power actions, all management UIs
- **`templates/css/module.css`** — Cross-theme styles with Bootstrap 3/4/5 dual class support (`panel card`, `panel-body card-body`) - **`templates/css/module.css`** — Cross-theme styles with Bootstrap 3/4/5 dual class support (`panel card`, `panel-body card-body`)
@@ -87,7 +87,18 @@ Custom option names can be mapped in `config/ConfigOptionMapping.php` (copy from
- **Base features:** VirtFusion v1.7.3+ - **Base features:** VirtFusion v1.7.3+
- **VNC console:** v6.1.0+ - **VNC console:** v6.1.0+
- **Resource modification:** v6.2.0+ - **Resource modification:** v6.2.0+
- Firewall endpoints use `{interface}` path parameter (primary/secondary): `/servers/{id}/firewall/{interface}` - **Self-service billing:** Requires self-service feature enabled in VirtFusion
## Product Config Options
| Option | Name | Description | Default |
|--------|------|-------------|---------|
| configoption1 | Hypervisor Group ID | VirtFusion hypervisor group for server placement | 1 |
| configoption2 | Package ID | VirtFusion package defining server resources | 1 |
| configoption3 | Default IPv4 | Number of IPv4 addresses to assign (0-10) | 1 |
| configoption4 | Self-Service Mode | 0=Disabled, 1=Hourly, 2=Resource Packs, 3=Both | 0 |
| configoption5 | Auto Top-Off Threshold | Credit balance below which auto top-off triggers | 0 |
| configoption6 | Auto Top-Off Amount | Credit amount to add on auto top-off | 100 |
## WHMCS Compatibility ## WHMCS Compatibility

View File

@@ -44,7 +44,6 @@ You also need a VirtFusion API token with the following permissions:
- Server management (create, read, update, delete, power, build) - Server management (create, read, update, delete, power, build)
- User management (create, read, reset password, authentication tokens) - User management (create, read, reset password, authentication tokens)
- Package and template read access - Package and template read access
- Firewall management (if using firewall features)
- Network management (if using IP management features) - Network management (if using IP management features)
## Features ## Features
@@ -63,9 +62,10 @@ You also need a VirtFusion API token with the following permissions:
- **Control Panel SSO** - One-click login to VirtFusion panel - **Control Panel SSO** - One-click login to VirtFusion panel
- **Server Rebuild** - Reinstall with any available OS template - **Server Rebuild** - Reinstall with any available OS template
- **Password Reset** - Reset VirtFusion panel login credentials - **Password Reset** - Reset VirtFusion panel login credentials
- **Firewall Management** - Enable/disable firewall, apply rules
- **Network Management** - View, add, and remove IPv4 addresses and IPv6 subnets - **Network Management** - View, add, and remove IPv4 addresses and IPv6 subnets
- **VNC Console** - Browser-based console access to the server - **Resources Panel** - Current memory, CPU, storage, traffic allocation with usage bars and upgrade/downgrade link
- **VNC Console** - Browser-based console access (panel auto-hides when VNC is disabled on the server)
- **Self-Service Billing** - Credit balance display, usage breakdown, and credit top-up (when enabled)
- **Bandwidth Usage** - Traffic usage display with allocation limits - **Bandwidth Usage** - Traffic usage display with allocation limits
- **Billing Overview** - Product, billing cycle, dates, and payment information - **Billing Overview** - Product, billing cycle, dates, and payment information
@@ -80,8 +80,9 @@ You also need a VirtFusion API token with the following permissions:
### Ordering Process ### Ordering Process
- Dynamic OS template dropdown populated from VirtFusion API - Dynamic OS template dropdown populated from VirtFusion API
- SSH key selection dropdown for users with saved keys - SSH key selection dropdown for users with saved keys, with option to paste a new public key
- Checkout validation ensuring OS selection before order placement - Checkout validation ensuring OS selection before order placement
- **Resource sliders** - Configurable option dropdowns are replaced with interactive range sliders
- Compatible with all WHMCS order form templates - Compatible with all WHMCS order form templates
### Usage Tracking ### Usage Tracking
@@ -96,6 +97,13 @@ You also need a VirtFusion API token with the following permissions:
### Resource Modification ### Resource Modification
- In-place modification of server resources (memory, CPU cores, traffic) - In-place modification of server resources (memory, CPU cores, traffic)
- No server rebuild required for resource changes - No server rebuild required for resource changes
- **Package change** now also applies individual resource modifications from configurable options
### Self-Service Billing
- Credit balance display and top-up from client area
- Usage breakdown reporting
- Auto top-off via WHMCS cron when credit falls below threshold
- Self-service mode configurable per product (Hourly, Resource Packs, or Both)
## Installation ## Installation
@@ -233,6 +241,9 @@ Each product has three module-specific settings:
| Config Option 1 | Hypervisor Group ID | VirtFusion hypervisor group for server placement | 1 | | Config Option 1 | Hypervisor Group ID | VirtFusion hypervisor group for server placement | 1 |
| Config Option 2 | Package ID | VirtFusion package defining server resources | 1 | | Config Option 2 | Package ID | VirtFusion package defining server resources | 1 |
| Config Option 3 | Default IPv4 | Number of IPv4 addresses to assign (0-10) | 1 | | Config Option 3 | Default IPv4 | Number of IPv4 addresses to assign (0-10) | 1 |
| Config Option 4 | Self-Service Mode | Enable VirtFusion self-service billing (0=Disabled, 1=Hourly, 2=Resource Packs, 3=Both) | 0 |
| Config Option 5 | Auto Top-Off Threshold | Credit balance below which auto top-off triggers during cron (0=disabled) | 0 |
| Config Option 6 | Auto Top-Off Amount | Credit amount to add when auto top-off triggers | 100 |
You can find your Hypervisor Group IDs and Package IDs in the VirtFusion admin panel. You can find your Hypervisor Group IDs and Package IDs in the VirtFusion admin panel.
@@ -287,13 +298,6 @@ Four power control buttons:
- **Shutdown** - Graceful ACPI shutdown - **Shutdown** - Graceful ACPI shutdown
- **Force Off** - Immediate power cut (use with caution) - **Force Off** - Immediate power cut (use with caution)
### Firewall Management
- View firewall status (enabled/disabled) with status badge
- Enable or disable the server firewall on primary/secondary interfaces
- Apply firewall rulesets by ID (rulesets are predefined in VirtFusion admin)
- Re-apply/synchronize currently assigned rulesets
- **Note**: VirtFusion uses a ruleset-based firewall system. Individual rules cannot be created or deleted via the API. Create rulesets in the VirtFusion admin panel, then apply them to servers through this module or the control panel
### Network Management ### Network Management
- View all IPv4 addresses and IPv6 subnets assigned to the server - View all IPv4 addresses and IPv6 subnets assigned to the server
- Add new IPv4 addresses (subject to pool availability) - Add new IPv4 addresses (subject to pool availability)
@@ -401,17 +405,6 @@ WHMCS automatically loads theme-specific templates when they exist. Copy the ori
| `GET` | `/media/templates/fromServerPackageSpec/{id}` | OS templates | | `GET` | `/media/templates/fromServerPackageSpec/{id}` | OS templates |
| `GET` | `/ssh_keys/user/{id}` | SSH key listing | | `GET` | `/ssh_keys/user/{id}` | SSH key listing |
### Firewall
| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/servers/{id}/firewall/{interface}` | Firewall status and rules |
| `POST` | `/servers/{id}/firewall/{interface}/enable` | Enable firewall |
| `POST` | `/servers/{id}/firewall/{interface}/disable` | Disable firewall |
| `POST` | `/servers/{id}/firewall/{interface}/rules` | Apply rulesets (body: `{"rulesets": [1,2]}`) |
`{interface}` is `primary` or `secondary`. Individual firewall rules cannot be managed via the API - use rulesets created in the VirtFusion admin panel.
### Network ### Network
| Method | Endpoint | Purpose | | Method | Endpoint | Purpose |
@@ -421,6 +414,21 @@ WHMCS automatically loads theme-specific templates when they exist. Copy the ori
| `POST` | `/servers/{id}/ipv6` | Add IPv6 subnet | | `POST` | `/servers/{id}/ipv6` | Add IPv6 subnet |
| `DELETE` | `/servers/{id}/ipv6` | Remove IPv6 subnet | | `DELETE` | `/servers/{id}/ipv6` | Remove IPv6 subnet |
### SSH Keys
| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/ssh_keys` | Create SSH key for a user (checkout key paste) |
### Self-Service Billing
| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/selfService/usage/byUserExtRelationId/{id}` | Usage data by WHMCS client ID |
| `GET` | `/selfService/report/byUserExtRelationId/{id}` | Billing report by WHMCS client ID |
| `POST` | `/selfService/credit/byUserExtRelationId/{id}` | Add credit by WHMCS client ID |
| `GET` | `/selfService/currencies` | Available self-service currencies |
### Advanced ### Advanced
| Method | Endpoint | Purpose | | Method | Endpoint | Purpose |
@@ -503,12 +511,6 @@ This data appears in the WHMCS client area and admin product details.
3. Check that VNC is enabled for the hypervisor in VirtFusion 3. Check that VNC is enabled for the hypervisor in VirtFusion
4. Popup blockers may prevent the console window from opening 4. Popup blockers may prevent the console window from opening
### Firewall Actions Failing
1. Verify the server has a network interface configured
2. Check the API token has firewall management permissions
3. Some hypervisors may not support firewall management
### UsageUpdate Not Syncing ### UsageUpdate Not Syncing
1. Verify the WHMCS cron is running: `php -q /path/to/whmcs/crons/cron.php` 1. Verify the WHMCS cron is running: `php -q /path/to/whmcs/crons/cron.php`
@@ -565,7 +567,7 @@ modules/servers/VirtFusionDirect/
hooks.php # WHMCS hooks (order form OS/SSH dropdowns, checkout validation) hooks.php # WHMCS hooks (order form OS/SSH dropdowns, checkout validation)
modify.sql # SQL for creating custom fields modify.sql # SQL for creating custom fields
lib/ lib/
Module.php # Base class: API communication, power, firewall, network, VNC, rebuild Module.php # Base class: API communication, power, network, VNC, rebuild
ModuleFunctions.php # Provisioning: create, suspend, unsuspend, terminate, change package ModuleFunctions.php # Provisioning: create, suspend, unsuspend, terminate, change package
ConfigureService.php # Order configuration: OS templates, SSH keys, server build init ConfigureService.php # Order configuration: OS templates, SSH keys, server build init
Database.php # Database operations: custom table, WHMCS table queries Database.php # Database operations: custom table, WHMCS table queries

View File

@@ -43,6 +43,27 @@ function VirtFusionDirect_ConfigOptions()
"Description" => "The default number of IPv4 addresses to assign to each server.", "Description" => "The default number of IPv4 addresses to assign to each server.",
"Default" => "1", "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) ->where('id', $service->id)
->update($update); ->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) { } catch (\Exception $e) {
// Log but continue processing other services // Log but continue processing other services
\WHMCS\Module\Server\VirtFusionDirect\Log::insert('UsageUpdate:service:' . $service->id, [], $e->getMessage()); \WHMCS\Module\Server\VirtFusionDirect\Log::insert('UsageUpdate:service:' . $service->id, [], $e->getMessage());

View File

@@ -176,133 +176,6 @@ switch ($action) {
$vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500); $vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500);
break; 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 // 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); $vf->output(['success' => false, 'errors' => 'VNC console unavailable. The server may be powered off or VNC is not supported.'], true, true, 500);
break; 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: default:
$vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400); $vf->output(['success' => false, 'errors' => 'invalid action'], true, true, 400);
} }

View File

@@ -173,6 +173,30 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
// Handle SSH keys // Handle SSH keys
if (sshInputField) { 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) { if (sshKeys.length > 0) {
var sshSelect = document.createElement('select'); var sshSelect = document.createElement('select');
sshSelect.className = 'form-control'; sshSelect.className = 'form-control';
@@ -190,20 +214,102 @@ add_hook('ClientAreaFooterOutput', 1, function ($vars) {
sshSelect.appendChild(option); 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() { 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); sshInputField.parentNode.insertBefore(sshSelect, sshInputField.nextSibling);
sshSelect.parentNode.insertBefore(sshPasteContainer, sshSelect.nextSibling);
sshInputField.style.display = 'none'; sshInputField.style.display = 'none';
} else { } else {
// No existing keys — show the paste textarea directly
sshPasteContainer.style.display = 'block';
sshInputField.parentNode.insertBefore(sshPasteContainer, sshInputField.nextSibling);
sshInputField.style.display = 'none'; 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> </script>
"; ";

View File

@@ -130,9 +130,10 @@ class ConfigureService extends Module
/** /**
* @param int $id * @param int $id
* @param array $vars * @param array $vars
* @param int|null $vfUserId VirtFusion user ID (for creating SSH keys from raw public key)
* @return bool * @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; if (!$this->cp) return false;
@@ -141,17 +142,27 @@ class ConfigureService extends Module
// Generate a random 8 character hostname // Generate a random 8 character hostname
$hostname = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8); $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 = [ $inputData = [
"operatingSystemId" => $vars['customfields']['Initial Operating System'] ?? null, "operatingSystemId" => $vars['customfields']['Initial Operating System'] ?? null,
"name" => $hostname, "name" => $hostname,
"sshKeys" => [
$vars['customfields']['Initial SSH Key'] ?? null
],
'email' => true 'email' => true
]; ];
if (empty($vars['customfields']['Initial SSH Key'] ?? null)) { if ($sshKeyId) {
unset($inputData['sshKeys']); $inputData['sshKeys'] = [$sshKeyId];
} }
$request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData)); $request->addOption(CURLOPT_POSTFIELDS, json_encode($inputData));
@@ -165,4 +176,37 @@ class ConfigureService extends Module
return ($httpCode == 200 || $httpCode == 201); 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;
}
} }

View File

@@ -319,218 +319,6 @@ class Module
return false; 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 // IP Address Management
// ========================================================================= // =========================================================================
@@ -947,6 +735,128 @@ class Module
return $curl; 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. * Decodes a response from JSON into an associative array.
* *

View File

@@ -68,13 +68,20 @@ class ModuleFunctions extends Module
$request = $this->initCurl($cp['token']); $request = $this->initCurl($cp['token']);
$request->addOption(CURLOPT_POSTFIELDS, json_encode( $userData = [
[ "name" => $user->firstname . ' ' . $user->lastname,
"name" => $user->firstname . ' ' . $user->lastname, "email" => $user->email,
"email" => $user->email, "extRelationId" => $user->id,
"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'); $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. // If the server is created successfully, we can initialize the server build.
$cs = new ConfigureService(); $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'; return 'success';
} else { } else {
@@ -197,7 +205,7 @@ class ModuleFunctions extends Module
switch ($request->getRequestInfo('http_code')) { switch ($request->getRequestInfo('http_code')) {
case 204: case 204:
return 'success'; break;
case 404: case 404:
return 'The server or package was not found in VirtFusion (HTTP 404).'; return 'The server or package was not found in VirtFusion (HTTP 404).';
case 423: case 423:
@@ -208,6 +216,33 @@ class ModuleFunctions extends Module
default: default:
return 'Update package request failed. VirtFusion API returned HTTP ' . $request->getRequestInfo('http_code'); 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.'; return 'Service not found in module database.';
} }

View File

@@ -46,6 +46,14 @@ class ServerResource
'inbound' => isset($server['settings']['resources']['networkSpeedInbound']) ? $server['settings']['resources']['networkSpeedInbound'] . ' Mbps' : '-', 'inbound' => isset($server['settings']['resources']['networkSpeedInbound']) ? $server['settings']['resources']['networkSpeedInbound'] . ' Mbps' : '-',
'outbound' => isset($server['settings']['resources']['networkSpeedOutbound']) ? $server['settings']['resources']['networkSpeedOutbound'] . ' 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)) { if (array_key_exists('network', $server)) {

View File

@@ -139,6 +139,53 @@
padding: 0.15rem 0.4rem; 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 */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.vf-power-buttons { .vf-power-buttons {

View File

@@ -40,6 +40,43 @@ function vfServerData(serviceId, systemUrl) {
statusBadge.addClass("vf-badge-awaiting"); 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(); $("#vf-server-info").show();
} else { } else {
$("#vf-server-info-error").show(); $("#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 // Network / IP Management
// ========================================================================= // =========================================================================
@@ -507,3 +473,119 @@ function vfOpenVnc(serviceId, systemUrl) {
btn.prop("disabled", false); 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);
});
}

View File

@@ -168,44 +168,6 @@
</div> </div>
</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 *} {* Network Management Panel *}
<div class="panel card panel-default mb-3"> <div class="panel card panel-default mb-3">
<div class="panel-heading card-header"> <div class="panel-heading card-header">
@@ -240,8 +202,57 @@
</div> </div>
</div> </div>
{* VNC Console Panel *} {* Resources Panel — populated by JS after server data loads *}
<div class="panel card panel-default mb-3"> <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"> <div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">VNC Console</h3> <h3 class="panel-title card-title m-0">VNC Console</h3>
</div> </div>
@@ -255,6 +266,54 @@
</div> </div>
</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'} {elseif $serviceStatus eq 'Suspended'}
<div class="panel card panel-default mb-3"> <div class="panel card panel-default mb-3">