Merge pull request #144 from GameServerPanel/copilot/auto-run-billing-provisioning
This commit is contained in:
commit
81317e1485
8 changed files with 152 additions and 26 deletions
|
|
@ -1,6 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 2026-05-09
|
||||
- **Billing provisioning auto-run + idempotency hardening:** Updated paid capture, free checkout, PayPal webhook renewals, and admin add-home registration to always hand activated orders to `billing_invoke_provision()` in trusted internal context. `modules/billing/create_servers.php` now enforces 6-character alphanumeric default passwords, defaults new homes to `ftp_status=1`, adds stricter service/node/port/mod validation logging, retries existing `home_id` installs only when incomplete, and skips already-installed homes to prevent duplicate provisioning on refresh/retry paths.
|
||||
- **Game Monitor IP:PORT fallback reliability:** `modules/gamemanager/server_monitor.php` now resolves missing display/connect endpoints from `home_ip_ports` + `remote_server_ips` for each home so newly provisioned servers consistently show IP:PORT even when initial joined row data is incomplete.
|
||||
- **Billing docs + auto-provision/install reliability:** Updated Game Monitor and Support documentation links to always open `https://gameservers.world/docs/` (game-specific path when available) in a new tab, and hardened billing provisioning so existing `home_id` orders can retry install automatically via the shared gamemanager update trigger (no manual Update Server click required). Added structured provisioning logs (`modules/billing/logs/provisioning.log`) and enriched panel log context with order/invoice/user/home/home_cfg/mod/ip/port/mechanism/result/error fields; PayPal webhook renewals now auto-provision when an order still has no `home_id`.
|
||||
- **Payment success provisioning visibility:** Expanded `modules/billing/payment_success.php` to show per-order provisioning state (install started/pending/failed) using live order/home/IP-port/mod consistency checks, so users/admins can immediately see provisioning outcomes instead of silent failures.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@
|
|||
- Add an automated billing provisioning integration test fixture that verifies arrange_ports exact/fallback allocation, duplicate-port protection, and home_id linkage after paid/free checkout.
|
||||
- Add a billing UI badge/filter that distinguishes "pending install" vs "installed" states directly in customer/server order views.
|
||||
- Add an admin billing orders "provisioning details" drawer that reads `modules/billing/logs/provisioning.log` and shows the latest mechanism/result/error per order without leaving the panel.
|
||||
- Add an automated end-to-end check that verifies `create_servers.php` skips already-installed homes while still retrying existing-home orders with missing executable/IP-port/mod prerequisites.
|
||||
|
|
|
|||
|
|
@ -322,11 +322,10 @@ foreach ($invoices as $inv) {
|
|||
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
|
||||
}
|
||||
$ordersCreated++;
|
||||
// Queue for provisioning only if not yet provisioned (home_id still '0' / empty).
|
||||
if ($currentHomeId <= 0) {
|
||||
if (!in_array($orderId, $newOrderIds, true)) {
|
||||
$newOrderIds[] = $orderId;
|
||||
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId]);
|
||||
}
|
||||
cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId, 'home_id' => $currentHomeId]);
|
||||
}
|
||||
} else {
|
||||
// No billing_orders row yet — create one now so the provisioner can run.
|
||||
|
|
@ -353,7 +352,9 @@ foreach ($invoices as $inv) {
|
|||
// Link invoice → order so retried captures are idempotent.
|
||||
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
||||
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
|
||||
$newOrderIds[] = $newOrderId;
|
||||
if (!in_array($newOrderId, $newOrderIds, true)) {
|
||||
$newOrderIds[] = $newOrderId;
|
||||
}
|
||||
$ordersCreated++;
|
||||
cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -180,7 +180,8 @@ foreach ($invoices as $inv) {
|
|||
]);
|
||||
if ($currentHomeId > 0) {
|
||||
$repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]);
|
||||
} else {
|
||||
}
|
||||
if (!in_array($orderId, $newOrderIds, true)) {
|
||||
$newOrderIds[] = $orderId;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -206,7 +207,9 @@ foreach ($invoices as $inv) {
|
|||
if ($newOrderId > 0) {
|
||||
$repo->updateInvoiceOrderId($invoiceId, $newOrderId);
|
||||
$repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]);
|
||||
$newOrderIds[] = $newOrderId;
|
||||
if (!in_array($newOrderId, $newOrderIds, true)) {
|
||||
$newOrderIds[] = $newOrderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,47 @@ if (!defined('BILLING_NICE_DEFAULT')) {
|
|||
}
|
||||
|
||||
if (!function_exists('billing_generate_provision_password')) {
|
||||
function billing_generate_provision_password(int $bytes = 12)
|
||||
function billing_generate_provision_password()
|
||||
{
|
||||
$length = 6;
|
||||
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$alphabetLen = strlen($alphabet);
|
||||
$password = '';
|
||||
try {
|
||||
return substr(bin2hex(random_bytes($bytes)), 0, $bytes * 2);
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$password .= $alphabet[random_int(0, $alphabetLen - 1)];
|
||||
}
|
||||
return $password;
|
||||
} catch (Throwable $e) {
|
||||
return substr(hash('sha256', uniqid('gsp-provision', true) . microtime(true)), 0, $bytes * 2);
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$password .= $alphabet[mt_rand(0, $alphabetLen - 1)];
|
||||
}
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_is_valid_provision_password')) {
|
||||
function billing_is_valid_provision_password($value): bool
|
||||
{
|
||||
return is_string($value) && preg_match('/^[A-Za-z0-9]{6}$/', $value) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_should_regenerate_provision_password')) {
|
||||
function billing_should_regenerate_provision_password($value): bool
|
||||
{
|
||||
return !billing_is_valid_provision_password($value) || strcasecmp((string)$value, 'ChangeMe') === 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_agent_offline_reason')) {
|
||||
function billing_agent_offline_reason(int $remote_server_id, array $home_info): string
|
||||
{
|
||||
return "Agent is offline for remote server #{$remote_server_id} (" . ($home_info['agent_ip'] ?? 'unknown') . ":" . ($home_info['agent_port'] ?? 'unknown') . ").";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('billing_invoke_provision')) {
|
||||
function billing_invoke_provision(array $options = array())
|
||||
{
|
||||
|
|
@ -63,6 +94,8 @@ if (!function_exists('billing_allocate_home_port')) {
|
|||
return array('ok' => false, 'error' => "No IP addresses are configured for remote server #{$remote_server_id}.");
|
||||
}
|
||||
|
||||
$ips_with_no_range = array();
|
||||
$ips_exhausted = array();
|
||||
foreach ($ipIds as $ipId) {
|
||||
$ranges = $db->resultQuery(
|
||||
"SELECT start_port, end_port, port_increment
|
||||
|
|
@ -81,6 +114,7 @@ if (!function_exists('billing_allocate_home_port')) {
|
|||
);
|
||||
}
|
||||
if (empty($ranges)) {
|
||||
$ips_with_no_range[] = $ipId;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -133,9 +167,13 @@ if (!function_exists('billing_allocate_home_port')) {
|
|||
}
|
||||
}
|
||||
}
|
||||
$ips_exhausted[] = $ipId;
|
||||
}
|
||||
|
||||
return array('ok' => false, 'error' => "No available port in arrange_ports for remote server #{$remote_server_id} and home_cfg_id #{$home_cfg_id}.");
|
||||
if (!empty($ips_with_no_range) && count($ips_with_no_range) === count($ipIds)) {
|
||||
return array('ok' => false, 'error' => "No port range found for home_cfg_id #{$home_cfg_id} on ip_id(s) [" . implode(',', $ips_with_no_range) . "] for remote server #{$remote_server_id}.");
|
||||
}
|
||||
return array('ok' => false, 'error' => "No available port in arrange_ports for remote server #{$remote_server_id}, home_cfg_id #{$home_cfg_id}, ip_id(s) [" . implode(',', !empty($ips_exhausted) ? $ips_exhausted : $ipIds) . "].");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,11 +186,13 @@ if (!function_exists('billing_resolve_mod_cfg_id')) {
|
|||
}
|
||||
|
||||
$first = null;
|
||||
$available_mod_cfg_ids = array();
|
||||
foreach ((array)$mods as $mod) {
|
||||
$modCfgId = intval($mod['mod_cfg_id'] ?? 0);
|
||||
if ($modCfgId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$available_mod_cfg_ids[] = $modCfgId;
|
||||
if ($first === null) {
|
||||
$first = $modCfgId;
|
||||
}
|
||||
|
|
@ -165,7 +205,7 @@ if (!function_exists('billing_resolve_mod_cfg_id')) {
|
|||
return array('ok' => true, 'mod_cfg_id' => $first);
|
||||
}
|
||||
|
||||
return array('ok' => false, 'error' => "No usable mod_cfg_id found for home_cfg_id #{$home_cfg_id}.");
|
||||
return array('ok' => false, 'error' => "No usable mod_cfg_id found for home_cfg_id #{$home_cfg_id}. Available mod_cfg_id values: [" . implode(',', $available_mod_cfg_ids) . "].");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,17 +322,17 @@ function exec_ogp_module()
|
|||
$home_name = $order['home_name'];
|
||||
$remote_control_password = $order['remote_control_password'];
|
||||
$ftp_password = $order['ftp_password'];
|
||||
if ($remote_control_password === '' || strcasecmp((string)$remote_control_password, 'ChangeMe') === 0) {
|
||||
if (billing_should_regenerate_provision_password($remote_control_password)) {
|
||||
$remote_control_password = billing_generate_provision_password();
|
||||
}
|
||||
if ($ftp_password === '' || strcasecmp((string)$ftp_password, 'ChangeMe') === 0) {
|
||||
if (billing_should_regenerate_provision_password($ftp_password)) {
|
||||
$ftp_password = billing_generate_provision_password();
|
||||
}
|
||||
$ip = $order['ip'];
|
||||
$max_players = $order['max_players'];
|
||||
$user_id = $order['user_id'];
|
||||
$extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE;
|
||||
$alreadyProvisioned = !$extended && intval($order['home_id'] ?? 0) > 0;
|
||||
$already_provisioned = !$extended && intval($order['home_id'] ?? 0) > 0;
|
||||
$provision_invoice_id = 0;
|
||||
$selected_ip_id = 0;
|
||||
$selected_port = 0;
|
||||
|
|
@ -302,6 +342,7 @@ function exec_ogp_module()
|
|||
$install_result = 'pending';
|
||||
$install_message = '';
|
||||
$install_attempted = false;
|
||||
$needs_existing_home_retry = false;
|
||||
$home_info = array();
|
||||
$invoiceRow = $db->resultQuery(
|
||||
"SELECT invoice_id
|
||||
|
|
@ -330,6 +371,14 @@ function exec_ogp_module()
|
|||
$install_method = $service[0]['install_method'];
|
||||
$manual_url = $service[0]['manual_url'];
|
||||
$access_rights = $service[0]['access_rights'];
|
||||
if (intval($home_cfg_id) <= 0) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Invalid home_cfg_id '{$home_cfg_id}' for service_id {$service_id}.";
|
||||
}
|
||||
if (!$order_failed && intval($remote_server_id) <= 0) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Invalid remote server selection '{$remote_server_id}' on order #{$order_id} for service_id {$service_id}.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -337,7 +386,7 @@ function exec_ogp_module()
|
|||
$order_failure_reason = "Service ID {$service_id} not found.";
|
||||
}
|
||||
|
||||
if(!$order_failed && $alreadyProvisioned)
|
||||
if(!$order_failed && $already_provisioned)
|
||||
{
|
||||
$home_id = intval($order['home_id']);
|
||||
$home_info = $db->getGameHome($home_id);
|
||||
|
|
@ -351,6 +400,29 @@ function exec_ogp_module()
|
|||
$selected_ip_id = intval($existingIpPort['ip_id']);
|
||||
$selected_port = intval($existingIpPort['port']);
|
||||
}
|
||||
$has_ip_port = !empty($existingIpPort['ok']);
|
||||
$has_mods = !empty($home_info['mods']) && is_array($home_info['mods']);
|
||||
if (!$order_failed && (!$has_ip_port || !$has_mods)) {
|
||||
$needs_existing_home_retry = true;
|
||||
$install_message = "Existing home #{$home_id} requires provisioning completion (ip_port=" . ($has_ip_port ? 'yes' : 'no') . ", mods=" . ($has_mods ? 'yes' : 'no') . ").";
|
||||
}
|
||||
if (!$order_failed && !$needs_existing_home_retry) {
|
||||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']);
|
||||
if ($server_xml && !empty((string)$server_xml->server_exec_name)) {
|
||||
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
|
||||
if ($remote->status_chk() === 1) {
|
||||
$exec_path = clean_path($home_info['home_path'] . "/" . (string)$server_xml->exe_location . "/" . (string)$server_xml->server_exec_name);
|
||||
if ($remote->rfile_exists($exec_path) !== 1) {
|
||||
$needs_existing_home_retry = true;
|
||||
$install_message = "Existing home #{$home_id} missing expected executable '" . basename($exec_path) . "'; retrying install.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$order_failed && !$needs_existing_home_retry) {
|
||||
$install_result = 'completed';
|
||||
$install_message = $install_message !== '' ? $install_message : "Order #{$order_id} already provisioned and installed; no action required.";
|
||||
}
|
||||
}
|
||||
elseif(!$order_failed && $extended)
|
||||
{
|
||||
|
|
@ -408,12 +480,22 @@ function exec_ogp_module()
|
|||
//Add Game home to database
|
||||
//HARD CODE TO /home/gameserver/
|
||||
$rserver = $db->getRemoteServer($remote_server_id);
|
||||
if (empty($rserver)) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Remote server #{$remote_server_id} not found for order #{$order_id} (service_id {$service_id}).";
|
||||
}
|
||||
$game_path = "/home/gameserver/";
|
||||
$home_id = $db->addGameHome( $remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password);
|
||||
if (!$home_id || intval($home_id) <= 0) {
|
||||
if (!$order_failed) {
|
||||
$home_id = $db->addGameHome($remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password);
|
||||
}
|
||||
if (!$order_failed && (!$home_id || intval($home_id) <= 0)) {
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Could not create server_homes row for order #{$order_id}.";
|
||||
}
|
||||
if (!$order_failed) {
|
||||
// Billing storefront defaults FTP to enabled for newly provisioned homes so panel/account flows stay consistent after checkout.
|
||||
$db->changeFtpStatus('enabled', intval($home_id));
|
||||
}
|
||||
|
||||
// Add IP:Port pair with arrange_ports exact home_cfg_id preference and home_cfg_id=0 fallback.
|
||||
if (!$order_failed) {
|
||||
|
|
@ -514,8 +596,11 @@ function exec_ogp_module()
|
|||
$install_result = 'pending';
|
||||
}
|
||||
if (empty($autoInstall['ok'])) {
|
||||
if (stripos((string)($autoInstall['message'] ?? ''), 'Agent is offline') !== false) {
|
||||
$order_failure_reason = billing_agent_offline_reason(intval($remote_server_id), (array)$home_info);
|
||||
}
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.');
|
||||
$order_failure_reason = $order_failure_reason !== '' ? $order_failure_reason : ("Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.'));
|
||||
$install_result = 'failed';
|
||||
$install_message = $order_failure_reason;
|
||||
}
|
||||
|
|
@ -555,7 +640,7 @@ function exec_ogp_module()
|
|||
}
|
||||
|
||||
// Retry install for orders that already have home_id but never triggered installation.
|
||||
if (!$order_failed && !$extended && !$install_attempted && intval($home_id) > 0) {
|
||||
if (!$order_failed && !$extended && !$install_attempted && intval($home_id) > 0 && (!$already_provisioned || $needs_existing_home_retry)) {
|
||||
if ($selected_ip_id <= 0 || $selected_port <= 0) {
|
||||
$existingIpPort = billing_get_home_ip_port($db, $db_prefix, intval($home_id));
|
||||
if (!empty($existingIpPort['ok'])) {
|
||||
|
|
@ -628,8 +713,11 @@ function exec_ogp_module()
|
|||
$install_result = 'pending';
|
||||
}
|
||||
if (empty($autoInstall['ok'])) {
|
||||
if (stripos((string)($autoInstall['message'] ?? ''), 'Agent is offline') !== false) {
|
||||
$order_failure_reason = billing_agent_offline_reason(intval($remote_server_id), (array)$home_info);
|
||||
}
|
||||
$order_failed = true;
|
||||
$order_failure_reason = "Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.');
|
||||
$order_failure_reason = $order_failure_reason !== '' ? $order_failure_reason : ("Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.'));
|
||||
$install_result = 'failed';
|
||||
$install_message = $order_failure_reason;
|
||||
}
|
||||
|
|
@ -639,7 +727,7 @@ function exec_ogp_module()
|
|||
// Status values: Active (provisioned & current), Invoiced (renewal invoice open),
|
||||
// Expired (past due and awaiting deletion)
|
||||
// end_date / next_invoice_date: when the next renewal invoice should be generated
|
||||
if ($alreadyProvisioned)
|
||||
if ($already_provisioned)
|
||||
{
|
||||
$existing_end = strtotime((string)($order['end_date'] ?? ''));
|
||||
if ($existing_end === false || $existing_end <= 0) {
|
||||
|
|
|
|||
|
|
@ -558,10 +558,8 @@ function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $bil
|
|||
$last_order_id = $order_id;
|
||||
$existing_home_id = intval($row['home_id'] ?? 0);
|
||||
wh_log('info', 'order_renewed', ['order_id' => $order_id, 'new_end' => $new_end, 'home_id' => $existing_home_id]);
|
||||
if ($existing_home_id <= 0) {
|
||||
$dir = ($billing_dir !== '') ? $billing_dir : dirname(__DIR__);
|
||||
wh_try_provision($dir, $order_id, $user_id);
|
||||
}
|
||||
$dir = ($billing_dir !== '') ? $billing_dir : dirname(__DIR__);
|
||||
wh_try_provision($dir, $order_id, $user_id);
|
||||
}
|
||||
} else {
|
||||
// New order: create billing_orders row
|
||||
|
|
|
|||
|
|
@ -446,6 +446,29 @@ echo "<table id='servermonitor' class='tablesorter' data-sortlist='[[0,0],[3,1]]
|
|||
$btns = get_monitor_buttons($server_home, $server_xml);
|
||||
|
||||
//End
|
||||
if (empty($server_home['ip']) || empty($server_home['port'])) {
|
||||
$home_ip_ports = $db->getHomeIpPorts(intval($server_home['home_id']));
|
||||
if (!empty($home_ip_ports) && is_array($home_ip_ports)) {
|
||||
// Preference order: exact mod-specific mapping, generic force_mod_id=0 mapping,
|
||||
// then first available mapping so newly provisioned homes always show an endpoint.
|
||||
$fallback_ip_port = null;
|
||||
foreach ((array)$home_ip_ports as $ip_port_row) {
|
||||
if (intval($ip_port_row['force_mod_id'] ?? 0) === intval($server_home['mod_id'] ?? 0)) {
|
||||
$fallback_ip_port = $ip_port_row;
|
||||
break;
|
||||
}
|
||||
if ($fallback_ip_port === null && intval($ip_port_row['force_mod_id'] ?? 0) === 0) {
|
||||
$fallback_ip_port = $ip_port_row;
|
||||
}
|
||||
}
|
||||
if ($fallback_ip_port === null) {
|
||||
$fallback_ip_port = $home_ip_ports[0];
|
||||
}
|
||||
$server_home['ip'] = $fallback_ip_port['ip'] ?? $server_home['ip'];
|
||||
$server_home['port'] = $fallback_ip_port['port'] ?? $server_home['port'];
|
||||
$server_home['ip_id'] = $fallback_ip_port['ip_id'] ?? ($server_home['ip_id'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$remote = new OGPRemoteLibrary($server_home['agent_ip'], $server_home['agent_port'], $server_home['encryption_key'], $server_home['timeout']);
|
||||
$host_stat = $remote->status_chk();
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ function exec_ogp_module()
|
|||
// Record the server in the billing tables so it participates in the
|
||||
// normal lifecycle (renewals, expiration, admin dashboard).
|
||||
require_once('billing_integration.php');
|
||||
admin_register_server_in_billing(
|
||||
$billing_order_id = admin_register_server_in_billing(
|
||||
$db,
|
||||
$web_user_id,
|
||||
$home_cfg_id,
|
||||
|
|
@ -168,6 +168,16 @@ function exec_ogp_module()
|
|||
$control_password,
|
||||
$ftppassword
|
||||
);
|
||||
if ($billing_order_id !== FALSE && intval($billing_order_id) > 0) {
|
||||
require_once(__DIR__ . '/../billing/create_servers.php');
|
||||
billing_invoke_provision(array(
|
||||
'order_ids' => array(intval($billing_order_id)),
|
||||
'user_id' => intval($web_user_id),
|
||||
// Trusted internal panel flow; allows admin-context provisioning
|
||||
// without requiring a customer session/login redirect trigger.
|
||||
'is_admin' => true
|
||||
));
|
||||
}
|
||||
|
||||
$view->refresh("?m=user_games&p=edit&home_id=$new_home_id", 0);
|
||||
}else{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue