feat: major enhancement — OS gallery, server rename, traffic chart, backups, VNC toggle, password reset, Redis caching, UX improvements
- Remove client IP removal capability (keep backend methods removed too) - Add copy-to-clipboard buttons for IP addresses with tooltip feedback - Replace OS dropdown with tile gallery (grouped, searchable, brand colors, EOL badges) in rebuild panel and checkout page - Add inline server rename with friendly name generator and RFC 1123 validation - Add traffic statistics canvas chart with responsive resize in resources panel - Add backup listing timeline in manage panel with show-all expansion - Add VNC enable/disable toggle with connection details and password copy - Add server root password reset with auto-clipboard copy (never displayed) - Add skeleton loading placeholders, action cooldowns (power 3s, rebuild 30s), progress indicator with elapsed timer - Sanitize all client-facing error messages (no raw API errors exposed) - Convert all state-mutating AJAX calls from GET to POST - Add explicit break after all output() calls in client.php - Add Redis-backed API response caching (Cache.php): OS templates 10min, traffic/backups 2min, currencies 30min, packages 10min - Add GitHub Actions workflow for weekly VirtFusion API change detection - Move cache busting step after semantic-release in publish workflow - Add endpoint doc generator script and OpenAPI baseline placeholder - Improve hostname generation entropy (bin2hex random_bytes) - Add .superpowers/ to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,12 +23,14 @@ switch ($action) {
|
||||
|
||||
if (!$client) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$data = $vf->resetUserPassword($serviceID, $client);
|
||||
|
||||
if ($data) {
|
||||
$vf->output(['success' => true, 'data' => $data->data], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500);
|
||||
@@ -43,6 +45,7 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$data = $vf->fetchServerData($serviceID);
|
||||
@@ -50,6 +53,7 @@ switch ($action) {
|
||||
if ($data) {
|
||||
(new Module())->updateWhmcsServiceParamsOnServerObject($serviceID, $data);
|
||||
$vf->output(['success' => true, 'data' => (new ServerResource())->process($data)], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve server data'], true, true, 500);
|
||||
@@ -64,12 +68,14 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$token = $vf->fetchLoginTokens($serviceID);
|
||||
|
||||
if ($token) {
|
||||
$vf->output(['success' => true, 'token_url' => $token], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to generate login token'], true, true, 500);
|
||||
@@ -84,19 +90,22 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$powerAction = isset($_GET['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_GET['powerAction']) : '';
|
||||
$powerAction = isset($_POST['powerAction']) ? preg_replace('/[^a-zA-Z]/', '', $_POST['powerAction']) : '';
|
||||
$allowedActions = ['boot', 'shutdown', 'restart', 'poweroff'];
|
||||
|
||||
if (!in_array($powerAction, $allowedActions, true)) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid power action'], true, true, 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->serverPowerAction($serviceID, $powerAction);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['action' => $powerAction, 'message' => 'Power action queued successfully']], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Power action failed. The server may be locked or unavailable.'], true, true, 500);
|
||||
@@ -111,19 +120,22 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$osId = isset($_GET['osId']) ? (int) $_GET['osId'] : 0;
|
||||
$hostname = isset($_GET['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_GET['hostname']) : null;
|
||||
$osId = isset($_POST['osId']) ? (int) $_POST['osId'] : 0;
|
||||
$hostname = isset($_POST['hostname']) ? preg_replace('/[^a-zA-Z0-9.\-]/', '', $_POST['hostname']) : null;
|
||||
|
||||
if ($osId <= 0) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid operating system ID'], true, true, 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->rebuildServer($serviceID, $osId, $hostname);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Server rebuild initiated successfully']], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Server rebuild failed. The server may be locked or unavailable.'], true, true, 500);
|
||||
@@ -138,19 +150,21 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$newName = isset($_GET['name']) ? trim($_GET['name']) : '';
|
||||
$newName = htmlspecialchars($newName, ENT_QUOTES, 'UTF-8');
|
||||
$newName = isset($_POST['name']) ? trim($_POST['name']) : '';
|
||||
|
||||
if (empty($newName) || strlen($newName) > 255) {
|
||||
if (empty($newName) || strlen($newName) > 63 || !preg_match('/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/', $newName)) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid server name'], true, true, 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->renameServer($serviceID, $newName);
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'Server renamed successfully']], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Server rename failed'], true, true, 500);
|
||||
@@ -165,44 +179,95 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$templates = $vf->fetchOsTemplates($serviceID);
|
||||
|
||||
if ($templates !== false) {
|
||||
$vf->output(['success' => true, 'data' => $templates], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to fetch OS templates'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// IP Address Management
|
||||
// Server Password Reset
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Remove an IPv4 address.
|
||||
* Reset server root password.
|
||||
*/
|
||||
case 'removeIPv4':
|
||||
case 'resetServerPassword':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$ipAddress = isset($_GET['ip']) ? trim($_GET['ip']) : '';
|
||||
if (!filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid IPv4 address'], true, true, 400);
|
||||
$result = $vf->resetServerPassword($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->removeIPv4($serviceID, $ipAddress);
|
||||
$vf->output(['success' => false, 'errors' => 'Password reset failed'], true, true, 500);
|
||||
break;
|
||||
|
||||
if ($result) {
|
||||
$vf->output(['success' => true, 'data' => ['message' => 'IPv4 address removed successfully']], true, true, 200);
|
||||
// =================================================================
|
||||
// Backup Listing
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Get server backups.
|
||||
*/
|
||||
case 'backups':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to remove IPv4 address'], true, true, 500);
|
||||
$result = $vf->getServerBackups($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve backups'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// Traffic Statistics
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Get traffic statistics for a server.
|
||||
*/
|
||||
case 'trafficStats':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->getTrafficStats($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve traffic statistics'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
@@ -218,17 +283,42 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->getVncConsole($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'VNC console unavailable. The server may be powered off or VNC is not supported.'], true, true, 500);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Toggle VNC on/off.
|
||||
*/
|
||||
case 'toggleVnc':
|
||||
|
||||
$serviceID = $vf->validateServiceID(true);
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$enabled = isset($_POST['enabled']) && $_POST['enabled'] === '1';
|
||||
$result = $vf->toggleVnc($serviceID, $enabled);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to toggle VNC'], true, true, 500);
|
||||
break;
|
||||
|
||||
// =================================================================
|
||||
// Self Service — Credit & Usage
|
||||
// =================================================================
|
||||
@@ -242,12 +332,14 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->getSelfServiceUsage($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service usage data'], true, true, 500);
|
||||
@@ -262,12 +354,14 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->getSelfServiceReport($serviceID);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Unable to retrieve self-service report'], true, true, 500);
|
||||
@@ -282,17 +376,20 @@ switch ($action) {
|
||||
|
||||
if (!$vf->validateUserOwnsService($serviceID)) {
|
||||
$vf->output(['success' => false, 'errors' => 'service <> owner mismatch'], true, true, 403);
|
||||
break;
|
||||
}
|
||||
|
||||
$tokens = isset($_GET['tokens']) ? (float) $_GET['tokens'] : 0;
|
||||
$tokens = isset($_POST['tokens']) ? (float) $_POST['tokens'] : 0;
|
||||
if ($tokens <= 0) {
|
||||
$vf->output(['success' => false, 'errors' => 'Invalid credit amount. Must be a positive number.'], true, true, 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $vf->addSelfServiceCredit($serviceID, $tokens);
|
||||
|
||||
if ($result !== false) {
|
||||
$vf->output(['success' => true, 'data' => $result], true, true, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
$vf->output(['success' => false, 'errors' => 'Failed to add credit'], true, true, 500);
|
||||
|
||||
Reference in New Issue
Block a user