diff --git a/Panel/modules/website/README.md b/Panel/modules/website/README.md index 42e91cad..e3d33cb8 100644 --- a/Panel/modules/website/README.md +++ b/Panel/modules/website/README.md @@ -26,7 +26,9 @@ Panel/modules/website/ forgot_password.php reset_password.php account.php + my_account.php orders.php + my_orders.php invoices.php my_servers.php staff.php @@ -115,6 +117,9 @@ website sales, catalog, pricing, coupons, invoices, payments, PayPal settings, and the paid-order provisioning queue. Access currently requires a Panel admin account. +Staff links must point to `staff.php` and related `staff_*.php` website pages. +Do not use the Panel activity logger as the website staff dashboard. + ## Shared Accounts The Panel user table is the identity source for the website. Website login checks @@ -129,6 +134,10 @@ sessions. Control Panel and staff links point directly to the configured Panel U `sso.php` endpoints are compatibility redirects only. They do not create tokens or sessions. +Successful website login redirects to `my_account.php` by default, or to a saved +safe internal return path such as `cart.php?checkout=1`. `account.php` remains as +a compatibility route. + ## Ordering The active catalog route is `serverlist.php`. Order buttons point to the website diff --git a/Panel/modules/website/assets/css/site.css b/Panel/modules/website/assets/css/site.css index a2b85fdb..8a180275 100644 --- a/Panel/modules/website/assets/css/site.css +++ b/Panel/modules/website/assets/css/site.css @@ -89,9 +89,9 @@ textarea { .header-shell { display: grid; - grid-template-columns: auto 1fr auto; + grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - gap: 24px; + gap: 18px; min-height: 76px; } @@ -130,15 +130,31 @@ textarea { .primary-nav { display: flex; + flex-wrap: nowrap; + justify-content: center; + gap: 10px; + min-width: 0; +} + +.nav-group { + display: inline-flex; + align-items: center; flex-wrap: wrap; justify-content: center; - gap: 8px; + gap: 6px; + min-width: 0; +} + +.nav-group + .nav-group { + padding-left: 10px; + border-left: 1px solid var(--line); } .nav-link { - padding: 10px 12px; + padding: 9px 10px; border-radius: 6px; color: var(--muted); + white-space: nowrap; } .nav-link:hover, @@ -149,8 +165,9 @@ textarea { .header-actions { display: flex; - gap: 12px; + gap: 8px; align-items: center; + flex-wrap: nowrap; } .button { @@ -693,6 +710,25 @@ textarea { } @media (max-width: 1080px) { + .header-shell { + grid-template-columns: 1fr; + justify-items: center; + padding: 12px 0; + } + + .brand { + justify-self: center; + } + + .primary-nav { + width: 100%; + flex-wrap: wrap; + } + + .header-actions { + justify-content: center; + } + .hero-layout, .panel-preview, .footer-grid { @@ -750,10 +786,33 @@ textarea { padding-top: 12px; } + .nav-group, + .nav-group + .nav-group { + display: grid; + grid-template-columns: 1fr; + justify-items: stretch; + gap: 4px; + padding-left: 0; + padding-top: 8px; + border-left: 0; + border-top: 1px solid var(--line); + } + + .nav-group:first-child { + border-top: 0; + padding-top: 0; + } + + .nav-link { + min-height: 44px; + display: flex; + align-items: center; + } + .header-actions { grid-column: 1 / -1; display: none; - flex-wrap: wrap; + flex-direction: column; margin-top: 6px; } @@ -762,7 +821,7 @@ textarea { } .header-actions .button { - flex: 1 1 180px; + width: 100%; } .hero, diff --git a/Panel/modules/website/cart.php b/Panel/modules/website/cart.php index dd9819e2..dcec5c27 100644 --- a/Panel/modules/website/cart.php +++ b/Panel/modules/website/cart.php @@ -7,7 +7,7 @@ require_once __DIR__ . '/includes/bootstrap.php'; $message = ''; $error = ''; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { $action = (string)($_POST['action'] ?? ''); if (!website_verify_csrf()) { $error = 'Your form expired. Please try again.'; diff --git a/Panel/modules/website/doc_asset.php b/Panel/modules/website/doc_asset.php index 3d30544e..ebad8a56 100644 --- a/Panel/modules/website/doc_asset.php +++ b/Panel/modules/website/doc_asset.php @@ -18,14 +18,15 @@ if ($assetPath === null || !is_readable($assetPath)) { exit; } -$mimeType = match (strtolower(pathinfo($assetPath, PATHINFO_EXTENSION))) { +$extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION)); +$mimeTypes = [ 'png' => 'image/png', - 'jpg', 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', - default => 'application/octet-stream', -}; +]; +$mimeType = $mimeTypes[$extension] ?? 'application/octet-stream'; header('Content-Type: ' . $mimeType); header('Content-Length: ' . (string)filesize($assetPath)); readfile($assetPath); - diff --git a/Panel/modules/website/forgot_password.php b/Panel/modules/website/forgot_password.php index 1492fe73..7bbcabfc 100644 --- a/Panel/modules/website/forgot_password.php +++ b/Panel/modules/website/forgot_password.php @@ -7,7 +7,7 @@ require_once __DIR__ . '/includes/bootstrap.php'; $message = ''; $error = ''; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { if (!website_verify_csrf()) { $error = 'Your form expired. Please try again.'; } else { diff --git a/Panel/modules/website/includes/billing.php b/Panel/modules/website/includes/billing.php index fc407001..eaf0f7da 100644 --- a/Panel/modules/website/includes/billing.php +++ b/Panel/modules/website/includes/billing.php @@ -340,7 +340,16 @@ function website_fetch_remote_servers(): array return []; } $rows = []; - $result = @$db->query("SELECT `remote_server_id`, `remote_server_name`, `agent_ip`, `enabled` FROM `{$table}` ORDER BY `remote_server_name` ASC"); + $columns = website_table_columns($table); + $nameColumn = isset($columns['remote_server_name']) ? 'remote_server_name' : (isset($columns['hostname']) ? 'hostname' : ''); + $agentColumn = isset($columns['agent_ip']) ? 'agent_ip' : (isset($columns['display_public_ip']) ? 'display_public_ip' : ''); + $enabledSql = isset($columns['enabled']) ? '`enabled`' : '1 AS `enabled`'; + if ($nameColumn === '' || $agentColumn === '') { + return []; + } + $safeNameColumn = str_replace('`', '``', $nameColumn); + $safeAgentColumn = str_replace('`', '``', $agentColumn); + $result = @$db->query("SELECT `remote_server_id`, `{$safeNameColumn}` AS `remote_server_name`, `{$safeAgentColumn}` AS `agent_ip`, {$enabledSql} FROM `{$table}` ORDER BY `{$safeNameColumn}` ASC"); if ($result instanceof mysqli_result) { while ($row = $result->fetch_assoc()) { $rows[] = $row; diff --git a/Panel/modules/website/includes/bootstrap.php b/Panel/modules/website/includes/bootstrap.php index c6aaf41f..9da8fa7e 100644 --- a/Panel/modules/website/includes/bootstrap.php +++ b/Panel/modules/website/includes/bootstrap.php @@ -105,7 +105,7 @@ function website_start_session(): void $_SESSION['website_last_seen_at'] = $now; } -function website_config(?string $key = null, mixed $default = null): mixed +function website_config(?string $key = null, $default = null) { global $websiteConfig; @@ -121,7 +121,53 @@ function website_log(string $message): void error_log('[website] ' . $message); } -function website_escape(mixed $value): string +function website_error_reference(): string +{ + try { + return strtoupper(bin2hex(random_bytes(4))); + } catch (Throwable $e) { + return strtoupper(substr(md5((string)microtime(true)), 0, 8)); + } +} + +function website_render_fatal_error(string $reference): void +{ + if (!headers_sent()) { + http_response_code(500); + header('Content-Type: text/html; charset=utf-8'); + } + + echo ''; + echo ''; + echo 'Website Error - Gameservers.World'; + echo ''; + echo '
'; + echo '

Something went wrong

'; + echo '

We could not load this page. Please try again or contact support with reference '; + echo website_escape($reference) . '.

'; + echo '
'; +} + +register_shutdown_function(static function (): void { + $error = error_get_last(); + if (!is_array($error)) { + return; + } + $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR]; + if (!in_array((int)$error['type'], $fatalTypes, true)) { + return; + } + $reference = website_error_reference(); + website_log('Fatal error [' . $reference . '] ' . ($error['message'] ?? 'unknown') . ' in ' . ($error['file'] ?? 'unknown') . ':' . (string)($error['line'] ?? '0')); + if (!headers_sent()) { + while (ob_get_level() > 0) { + ob_end_clean(); + } + website_render_fatal_error($reference); + } +}); + +function website_escape($value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } @@ -151,6 +197,11 @@ function website_request_scheme(): string return 'http'; } +function website_request_method(): string +{ + return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); +} + function website_base_path(): string { static $basePath = null; @@ -563,12 +614,12 @@ function website_log_activity(string $message, int $userId = 0, string $eventTyp function website_safe_return_path(string $returnPath, string $default = 'index.php'): string { - if ($returnPath === '' || preg_match('#^[a-z][a-z0-9+.-]*://#i', $returnPath) === 1 || str_starts_with($returnPath, '//')) { + if ($returnPath === '' || preg_match('#^[a-z][a-z0-9+.-]*://#i', $returnPath) === 1 || strpos($returnPath, '//') === 0) { return $default; } $returnPath = ltrim($returnPath, '/'); - if (str_contains($returnPath, "\0") || str_starts_with($returnPath, '../') || str_contains($returnPath, '/../')) { + if (strpos($returnPath, "\0") !== false || strpos($returnPath, '../') === 0 || strpos($returnPath, '/../') !== false) { return $default; } @@ -589,7 +640,7 @@ function website_control_panel_url(string $returnPath = 'home.php?m=dashboard&p= return panel_url(website_safe_return_path($returnPath, 'home.php?m=dashboard&p=dashboard')); } -function website_order_url(int|string $serviceId): string +function website_order_url($serviceId): string { return website_url('order.php?service_id=' . rawurlencode((string)$serviceId)); } @@ -627,14 +678,21 @@ function website_fetch_service_by_id(int $serviceId): ?array } $safeServiceTable = $db->real_escape_string($serviceTable); - $safeConfigTable = $db->real_escape_string($prefix . 'config_homes'); - $stmt = $db->prepare( - "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key, ch.home_cfg_file AS cfg_file - FROM `{$safeServiceTable}` bs - LEFT JOIN `{$safeConfigTable}` ch ON ch.home_cfg_id = bs.home_cfg_id - WHERE bs.service_id = ? - LIMIT 1" - ); + $configTable = $prefix . 'config_homes'; + if (website_table_exists($configTable) && website_column_exists($serviceTable, 'home_cfg_id')) { + $safeConfigTable = $db->real_escape_string($configTable); + $sql = "SELECT bs.*, ch.game_name AS cfg_game_name, ch.game_key AS cfg_game_key, ch.home_cfg_file AS cfg_file + FROM `{$safeServiceTable}` bs + LEFT JOIN `{$safeConfigTable}` ch ON ch.home_cfg_id = bs.home_cfg_id + WHERE bs.service_id = ? + LIMIT 1"; + } else { + $sql = "SELECT bs.*, '' AS cfg_game_name, '' AS cfg_game_key, '' AS cfg_file + FROM `{$safeServiceTable}` bs + WHERE bs.service_id = ? + LIMIT 1"; + } + $stmt = $db->prepare($sql); if (!$stmt) { return null; } @@ -822,16 +880,36 @@ function website_fetch_services(int $limit = 0, bool $includeDisabled = false): $prefix = website_table_prefix(); - $sql = "SELECT bs.*, - ch.game_name AS cfg_game_name, - ch.game_key AS cfg_game_key, - ch.home_cfg_file AS cfg_file - FROM `{$prefix}billing_services` bs - LEFT JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id - WHERE " . ($includeDisabled ? '1 = 1' : "bs.enabled = 1 - AND bs.remote_server_id <> '' - AND bs.remote_server_id IS NOT NULL") . " - ORDER BY bs.service_name ASC"; + $serviceTable = $prefix . 'billing_services'; + $configTable = $prefix . 'config_homes'; + if (!website_table_exists($serviceTable)) { + return []; + } + $serviceColumns = website_table_columns($serviceTable); + $hasEnabled = isset($serviceColumns['enabled']); + $hasRemoteServerId = isset($serviceColumns['remote_server_id']); + $where = $includeDisabled ? '1 = 1' : '1 = 1'; + if (!$includeDisabled && $hasEnabled) { + $where .= ' AND bs.enabled = 1'; + } + if (!$includeDisabled && $hasRemoteServerId) { + $where .= " AND bs.remote_server_id <> '' AND bs.remote_server_id IS NOT NULL"; + } + if (website_table_exists($configTable) && isset($serviceColumns['home_cfg_id'])) { + $sql = "SELECT bs.*, + ch.game_name AS cfg_game_name, + ch.game_key AS cfg_game_key, + ch.home_cfg_file AS cfg_file + FROM `{$serviceTable}` bs + LEFT JOIN `{$configTable}` ch ON ch.home_cfg_id = bs.home_cfg_id + WHERE {$where} + ORDER BY bs.service_name ASC"; + } else { + $sql = "SELECT bs.*, '' AS cfg_game_name, '' AS cfg_game_key, '' AS cfg_file + FROM `{$serviceTable}` bs + WHERE {$where} + ORDER BY bs.service_name ASC"; + } if ($limit > 0) { $sql .= ' LIMIT ' . max(1, $limit); diff --git a/Panel/modules/website/includes/navigation.php b/Panel/modules/website/includes/navigation.php index b1756fc7..a22b9e84 100644 --- a/Panel/modules/website/includes/navigation.php +++ b/Panel/modules/website/includes/navigation.php @@ -5,27 +5,32 @@ declare(strict_types=1); $activePage = $activePage ?? ''; $currentUser = website_current_user(); $cartCount = website_cart_count(); -$navLinks = [ +$publicLinks = [ ['key' => 'home', 'label' => 'Home', 'href' => website_url('index.php')], ['key' => 'servers', 'label' => 'Game Servers', 'href' => website_url('serverlist.php')], - ['key' => 'docs', 'label' => 'Documentation', 'href' => website_url('docs.php')], ['key' => 'pricing', 'label' => 'Pricing', 'href' => website_url('pricing.php')], ['key' => 'locations', 'label' => 'Locations', 'href' => website_url('locations.php')], + ['key' => 'docs', 'label' => 'Documentation', 'href' => website_url('docs.php')], ['key' => 'support', 'label' => 'Support', 'href' => website_url('support.php')], ]; if ($currentUser) { - $navLinks[] = ['key' => 'account', 'label' => 'My Account', 'href' => website_url('account.php')]; - $navLinks[] = ['key' => 'orders', 'label' => 'My Orders', 'href' => website_url('orders.php')]; - $navLinks[] = ['key' => 'invoices', 'label' => 'My Invoices', 'href' => website_url('invoices.php')]; - $navLinks[] = ['key' => 'servers', 'label' => 'My Servers', 'href' => website_url('my_servers.php')]; - if (website_current_user_is_staff()) { - $navLinks[] = ['key' => 'staff', 'label' => 'Staff Dashboard', 'href' => website_url('staff.php')]; - } + $accountLinks = [ + ['key' => 'account', 'label' => 'My Account', 'href' => website_url('my_account.php')], + ['key' => 'orders', 'label' => 'My Orders', 'href' => website_url('my_orders.php')], + ['key' => 'servers', 'label' => 'My Servers', 'href' => website_url('my_servers.php')], + ['key' => 'cart', 'label' => 'Cart' . ($cartCount > 0 ? ' (' . $cartCount . ')' : ''), 'href' => website_cart_url()], + ['key' => 'logout', 'label' => 'Logout', 'href' => website_url('logout.php')], + ]; } else { - $navLinks[] = ['key' => 'account', 'label' => 'Login', 'href' => website_login_url()]; - $navLinks[] = ['key' => 'register', 'label' => 'Create Account', 'href' => website_register_url()]; + $accountLinks = [ + ['key' => 'account', 'label' => 'Login', 'href' => website_login_url()], + ['key' => 'register', 'label' => 'Create Account', 'href' => website_register_url()], + ['key' => 'cart', 'label' => 'Cart' . ($cartCount > 0 ? ' (' . $cartCount . ')' : ''), 'href' => website_cart_url()], + ]; } -$navLinks[] = ['key' => 'cart', 'label' => 'Cart' . ($cartCount > 0 ? ' (' . $cartCount . ')' : ''), 'href' => website_cart_url()]; +$staffLinks = ($currentUser && website_current_user_is_staff()) ? [ + ['key' => 'staff', 'label' => 'Staff Dashboard', 'href' => website_url('staff.php')], +] : []; ?> diff --git a/Panel/modules/website/login.php b/Panel/modules/website/login.php index 922a342a..2d1af852 100644 --- a/Panel/modules/website/login.php +++ b/Panel/modules/website/login.php @@ -5,7 +5,7 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; $error = ''; -$returnPath = website_safe_return_path((string)($_GET['return'] ?? $_POST['return'] ?? $_SESSION['website_login_return'] ?? 'account.php'), 'account.php'); +$returnPath = website_safe_return_path((string)($_GET['return'] ?? $_POST['return'] ?? $_SESSION['website_login_return'] ?? 'my_account.php'), 'my_account.php'); if (website_is_logged_in()) { if ($returnPath === 'panel') { @@ -16,7 +16,7 @@ if (website_is_logged_in()) { exit; } -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { if (!website_verify_csrf()) { $error = 'Your form expired. Please try again.'; } else { diff --git a/Panel/modules/website/my_account.php b/Panel/modules/website/my_account.php new file mode 100644 index 00000000..7038a2bc --- /dev/null +++ b/Panel/modules/website/my_account.php @@ -0,0 +1,4 @@ +

My Account

-

Signed in as . Use these links to manage servers, order hosting, request support, or open the GSP control panel.

+

Signed in as . Use these links for website billing, server orders, support, staff tools, and the GSP control panel.

@@ -16,10 +16,13 @@ $isStaff = website_current_user_is_staff();
-

My Servers

-

Open the GSP control panel to manage assigned game servers, files, logs, updates, backups, and server state.

+

Customer Account

+

Review orders, invoices, cart items, and servers tied to your Gameservers.World account.

@@ -39,13 +42,23 @@ $isStaff = website_current_user_is_staff();
+
+

Game Server Panel

+

Open the GSP control panel for live server controls, files, logs, updates, backups, and operational management.

+ +
diff --git a/Panel/modules/website/pages/login.php b/Panel/modules/website/pages/login.php index 8c928150..3008ba2c 100644 --- a/Panel/modules/website/pages/login.php +++ b/Panel/modules/website/pages/login.php @@ -25,6 +25,6 @@ declare(strict_types=1);

Forgot your password?

Gameservers.World and the GSP Panel use the same account credentials, but they keep separate secure sessions. You may be asked to log in separately when opening the Panel.

-

Need an account? Create one through the GSP Panel registration page.

+

Need an account? Create one for Gameservers.World.

diff --git a/Panel/modules/website/register.php b/Panel/modules/website/register.php index d368abbc..7ec8a016 100644 --- a/Panel/modules/website/register.php +++ b/Panel/modules/website/register.php @@ -7,7 +7,7 @@ require_once __DIR__ . '/includes/bootstrap.php'; $error = ''; $returnPath = website_safe_return_path((string)($_GET['return'] ?? $_POST['return'] ?? $_SESSION['website_login_return'] ?? 'account.php'), 'account.php'); -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { if (!website_verify_csrf()) { $error = 'Your form expired. Please try again.'; } else { @@ -44,7 +44,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $insertColumns[] = 'users_theme'; $values[] = 'SimpleBootstrap'; } - $columnSql = '`' . implode('`, `', array_map(static fn($column) => str_replace('`', '``', $column), $insertColumns)) . '`'; + $safeColumns = array_map(static function ($column) { + return str_replace('`', '``', $column); + }, $insertColumns); + $columnSql = '`' . implode('`, `', $safeColumns) . '`'; $placeholders = implode(', ', array_fill(0, count($values), '?')); $stmt = $db->prepare("INSERT INTO `{$usersTable}` ({$columnSql}) VALUES ({$placeholders})"); if ($stmt) { diff --git a/Panel/modules/website/reset_password.php b/Panel/modules/website/reset_password.php index 2caddb6a..3b4dcaee 100644 --- a/Panel/modules/website/reset_password.php +++ b/Panel/modules/website/reset_password.php @@ -23,7 +23,7 @@ if ($db instanceof mysqli && $tokenHash !== '' && website_table_exists(website_t } } -if ($_SERVER['REQUEST_METHOD'] === 'POST' && $valid) { +if (website_request_method() === 'POST' && $valid) { if (!website_verify_csrf()) { $error = 'Your form expired. Please try again.'; } else { diff --git a/Panel/modules/website/staff_coupons.php b/Panel/modules/website/staff_coupons.php index d42512fa..da367092 100644 --- a/Panel/modules/website/staff_coupons.php +++ b/Panel/modules/website/staff_coupons.php @@ -2,6 +2,6 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $message=''; $error=''; $table=website_table('billing_coupons'); -if($_SERVER['REQUEST_METHOD']==='POST'){ if(!website_verify_csrf())$error='Invalid CSRF token.'; elseif($db instanceof mysqli && website_table_exists($table)){ $code=strtoupper(trim((string)$_POST['code'])); $name=trim((string)$_POST['name']); $value=(float)$_POST['discount_value']; $active=!empty($_POST['is_active'])?1:0; $max=($_POST['max_uses']===''?null:(int)$_POST['max_uses']); $stmt=$db->prepare("INSERT INTO `{$table}` (`code`,`name`,`discount_type`,`discount_value`,`is_active`,`max_uses`,`created_at`) VALUES (?,?,'percent',?,?,?,NOW()) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`discount_value`=VALUES(`discount_value`),`is_active`=VALUES(`is_active`),`max_uses`=VALUES(`max_uses`)"); if($stmt){$stmt->bind_param('ssdii',$code,$name,$value,$active,$max);$stmt->execute();$stmt->close();$message='Coupon saved.';}}} +if(website_request_method()==='POST'){ if(!website_verify_csrf())$error='Invalid CSRF token.'; elseif($db instanceof mysqli && website_table_exists($table)){ $code=strtoupper(trim((string)$_POST['code'])); $name=trim((string)$_POST['name']); $value=(float)$_POST['discount_value']; $active=!empty($_POST['is_active'])?1:0; $max=($_POST['max_uses']===''?null:(int)$_POST['max_uses']); $stmt=$db->prepare("INSERT INTO `{$table}` (`code`,`name`,`discount_type`,`discount_value`,`is_active`,`max_uses`,`created_at`) VALUES (?,?,'percent',?,?,?,NOW()) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`discount_value`=VALUES(`discount_value`),`is_active`=VALUES(`is_active`),`max_uses`=VALUES(`max_uses`)"); if($stmt){$stmt->bind_param('ssdii',$code,$name,$value,$active,$max);$stmt->execute();$stmt->close();$message='Coupon saved.';}}} $coupons=[]; if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` ORDER BY `coupon_id` DESC"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$coupons[]=$row; } website_render('staff_coupons.php',['activePage'=>'staff','pageTitle'=>'Coupons - Gameservers.World','canonicalPath'=>'staff_coupons.php','message'=>$message,'error'=>$error,'coupons'=>$coupons]); diff --git a/Panel/modules/website/staff_migrations.php b/Panel/modules/website/staff_migrations.php index 0eceef5a..deff271e 100644 --- a/Panel/modules/website/staff_migrations.php +++ b/Panel/modules/website/staff_migrations.php @@ -3,7 +3,7 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $ran = false; $errors = []; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { if (website_verify_csrf()) { $errors = website_run_billing_migrations(); $ran = true; } else { $errors = ['Invalid CSRF token.']; } } diff --git a/Panel/modules/website/staff_paypal.php b/Panel/modules/website/staff_paypal.php index 8ef8bf40..74be0eca 100644 --- a/Panel/modules/website/staff_paypal.php +++ b/Panel/modules/website/staff_paypal.php @@ -3,7 +3,7 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $message=''; $error=''; -if ($_SERVER['REQUEST_METHOD']==='POST') { +if (website_request_method()==='POST') { if(!website_verify_csrf()) $error='Invalid CSRF token.'; else { website_set_setting('paypal_enabled', !empty($_POST['enabled'])?'1':'0'); diff --git a/Panel/modules/website/staff_provisioning.php b/Panel/modules/website/staff_provisioning.php index 224a67c8..bd6592f6 100644 --- a/Panel/modules/website/staff_provisioning.php +++ b/Panel/modules/website/staff_provisioning.php @@ -1,2 +1,2 @@ prepare("UPDATE `{$table}` SET `status`='paid', `provisioning_claim`=NULL, `provisioning_error`=NULL WHERE `order_id`=? AND `status` IN ('failed','paid')"); if($stmt){$stmt->bind_param('i',$orderId);$stmt->execute();$stmt->close();$message='Order queued for provisioning retry.';}} $rows=[]; if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` WHERE `status` IN ('paid','provisioning','failed','installed','active') ORDER BY `order_id` DESC LIMIT 100"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$rows[]=$row; } website_render('staff_provisioning.php',['activePage'=>'staff','pageTitle'=>'Provisioning Queue - Gameservers.World','canonicalPath'=>'staff_provisioning.php','orders'=>$rows,'message'=>$message]); +declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $message=''; $table=website_table('billing_orders'); if(website_request_method()==='POST' && website_verify_csrf() && $db instanceof mysqli && website_table_exists($table)){ $orderId=(int)$_POST['order_id']; $claim=bin2hex(random_bytes(12)); $stmt=$db->prepare("UPDATE `{$table}` SET `status`='paid', `provisioning_claim`=NULL, `provisioning_error`=NULL WHERE `order_id`=? AND `status` IN ('failed','paid')"); if($stmt){$stmt->bind_param('i',$orderId);$stmt->execute();$stmt->close();$message='Order queued for provisioning retry.';}} $rows=[]; if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` WHERE `status` IN ('paid','provisioning','failed','installed','active') ORDER BY `order_id` DESC LIMIT 100"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$rows[]=$row; } website_render('staff_provisioning.php',['activePage'=>'staff','pageTitle'=>'Provisioning Queue - Gameservers.World','canonicalPath'=>'staff_provisioning.php','orders'=>$rows,'message'=>$message]); diff --git a/Panel/modules/website/staff_services.php b/Panel/modules/website/staff_services.php index a60f534b..517925c5 100644 --- a/Panel/modules/website/staff_services.php +++ b/Panel/modules/website/staff_services.php @@ -3,7 +3,7 @@ declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db = website_db(); $message=''; $error=''; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { +if (website_request_method() === 'POST') { if (!website_verify_csrf()) { $error='Invalid CSRF token.'; } elseif ($db instanceof mysqli && isset($_POST['service']) && is_array($_POST['service'])) { $table = website_table('billing_services'); diff --git a/docs/modules/website.md b/docs/modules/website.md index 59bf1ee4..7096596e 100644 --- a/docs/modules/website.md +++ b/docs/modules/website.md @@ -47,6 +47,10 @@ Website login verifies credentials against the existing Panel password hash form `Panel/modules/website/sso.php` and `Panel/sso.php` are retained only as compatibility redirects for old links. Active navigation must not depend on them. +Successful website login redirects to `my_account.php` unless a safe internal +return path was stored, such as `cart.php?checkout=1`. `account.php` is retained +as a compatibility account entry point. + ## Ordering The current public catalog route is `serverlist.php`. Customer-facing Order buttons must use: @@ -73,7 +77,17 @@ Website footer account links are state-aware: - logged in: `My Account`, `Order a Server`, `Control Panel`, `My Servers`, `Log Out` - staff-only links appear only for Panel admin users and still enforce website staff authorization server-side -The website main navigation also includes visible `Login`, `Create Account`, and `Cart` entries when appropriate. Control Panel links point directly to the configured Panel domain. `My Servers` opens a website customer page that summarizes website orders and links to the Panel for live server controls. Staff Dashboard opens the website sales/billing staff area, not Panel activity logging. +The shared header groups navigation by purpose: + +- public: Home, Game Servers, Pricing, Locations, Documentation, Support +- account: Login/Create Account/Cart or My Account/My Orders/My Servers/Cart/Logout +- staff: Staff Dashboard, only for authorized website staff +- actions: Custom Projects and Control Panel + +Control Panel links point directly to the configured Panel domain. `My Servers` +opens a website customer page that summarizes website orders and links to the +Panel for live server controls. Staff Dashboard opens the website sales/billing +staff area, not Panel activity logging. ## Deployment @@ -100,7 +114,9 @@ Recommended: - `forgot_password.php` - `reset_password.php` - `account.php` +- `my_account.php` - `orders.php` +- `my_orders.php` - `invoices.php` - `my_servers.php` - `order.php` diff --git a/docs/modules/website_billing_rebuild.md b/docs/modules/website_billing_rebuild.md index 9ae1e647..d5a23160 100644 --- a/docs/modules/website_billing_rebuild.md +++ b/docs/modules/website_billing_rebuild.md @@ -216,3 +216,72 @@ Customer-facing account pages: - Paid order appears in provisioning queue. - Customer orders, invoices, and My Servers pages show only the authenticated user's records. - No hardcoded credentials or raw PHP errors are displayed. + +## HTTP 500 Troubleshooting + +### Login remains on `login.php` + +Check: + +- submitted field names are `login`, `password`, and `csrf_token` +- CSRF token validates +- the Panel user exists in the configured `users` table +- `users_passwd` matches the current Panel-compatible hash +- account role is not `banned` +- no output is sent before the redirect +- successful login redirects to `my_account.php` unless a safe internal return path is stored + +The website also provides `account.php` and `my_account.php` as compatible account routes. + +### `staff.php` HTTP 500 or blank output + +Common causes: + +- PHP 8-only syntax deployed on a PHP 7.x host +- missing production files from `Panel/modules/website/pages/` +- missing `includes/billing.php` +- fatal error suppressed by production `display_errors=0` +- non-admin account expecting staff access + +The website bootstrap avoids PHP 8-only constructs and installs a shutdown handler that logs fatal errors with a short reference ID. Staff pages must render through shared website templates and must not point to the Panel activity logger. + +### `staff_services.php` HTTP 500 + +Check: + +- `billing_services` exists +- staff migrations have been run +- expected catalog columns exist or can be added by the idempotent migration runner +- `remote_servers` schema is the current Panel schema +- the page is not assuming `remote_servers.enabled` exists + +The current helper treats missing `remote_servers.enabled` as enabled for display and lets staff assign locations from current Panel remote-server rows. + +### `order.php?service_id=...` HTTP 500 + +Check: + +- `billing_services` exists +- the requested `service_id` exists +- the service is enabled if the `enabled` column exists +- `remote_server_id` contains at least one valid location ID +- configured slot columns are present or defaults are acceptable +- `config_homes` exists when game metadata is needed + +The order page no longer requires `config_homes`; it can render from the service row alone. Missing or disabled services produce a styled unavailable page instead of blank output. + +### Wrong Panel Administration URL + +Website staff administration must link to: + +- `staff.php` + +It must not link to: + +- `home.php?m=administration&p=watch_logger` + +The Panel activity logger is operational administration, not website sales/billing administration. + +### Log Locations + +Production PHP or web-server logs are deployment-specific. Check Apache, Nginx, PHP-FPM, cPanel `error_log`, and PHP `error_log` paths. The local repository does not contain live web-server error logs.