attept 2 at site fix

This commit is contained in:
Frank Harris 2026-06-17 17:36:25 -05:00
parent 60bcc67056
commit cc7bbafb63
23 changed files with 360 additions and 75 deletions

View file

@ -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

View file

@ -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,

View file

@ -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.';

View file

@ -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);

View file

@ -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 {

View file

@ -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;

View file

@ -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 '<!doctype html><html lang="en"><head><meta charset="utf-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
echo '<title>Website Error - Gameservers.World</title>';
echo '<link rel="stylesheet" href="' . website_escape(website_asset('css/site.css')) . '">';
echo '</head><body><main class="site-main"><section class="page-heading"><div class="container">';
echo '<h1>Something went wrong</h1>';
echo '<p>We could not load this page. Please try again or contact support with reference ';
echo website_escape($reference) . '.</p>';
echo '</div></section></main></body></html>';
}
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);

View file

@ -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')],
] : [];
?>
<header class="site-header">
<div class="container header-shell">
@ -44,20 +49,35 @@ $navLinks[] = ['key' => 'cart', 'label' => 'Cart' . ($cartCount > 0 ? ' (' . $ca
<span class="sr-only">Toggle navigation</span>
</button>
<nav class="primary-nav" id="primary-nav" data-nav-menu>
<?php foreach ($navLinks as $link): ?>
<nav class="primary-nav" id="primary-nav" data-nav-menu aria-label="Primary navigation">
<div class="nav-group nav-group-public">
<?php foreach ($publicLinks as $link): ?>
<a class="nav-link<?= $activePage === $link['key'] ? ' is-active' : '' ?>" href="<?= website_escape($link['href']) ?>">
<?= website_escape($link['label']) ?>
</a>
<?php endforeach; ?>
</div>
<div class="nav-group nav-group-account" aria-label="Account navigation">
<?php foreach ($accountLinks as $link): ?>
<a class="nav-link<?= $activePage === $link['key'] ? ' is-active' : '' ?>" href="<?= website_escape($link['href']) ?>">
<?= website_escape($link['label']) ?>
</a>
<?php endforeach; ?>
</div>
<?php if (!empty($staffLinks)): ?>
<div class="nav-group nav-group-staff" aria-label="Staff navigation">
<?php foreach ($staffLinks as $link): ?>
<a class="nav-link<?= $activePage === $link['key'] ? ' is-active' : '' ?>" href="<?= website_escape($link['href']) ?>">
<?= website_escape($link['label']) ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</nav>
<div class="header-actions" data-header-actions>
<a class="button button-secondary" href="<?= website_escape(website_custom_project_url()) ?>">Custom Projects</a>
<a class="button button-primary" href="<?= website_escape(website_control_panel_url()) ?>">Control Panel</a>
<?php if ($currentUser): ?>
<a class="button button-secondary" href="<?= website_escape(website_url('logout.php')) ?>">Logout</a>
<?php endif; ?>
</div>
</div>
</header>

View file

@ -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 {

View file

@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require __DIR__ . '/account.php';

View file

@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require __DIR__ . '/orders.php';

View file

@ -28,7 +28,7 @@ $locations = website_service_locations($service);
$minSlots = website_service_min_slots($service);
$maxSlots = website_service_max_slots($service);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (website_request_method() === 'POST') {
$slots = filter_input(INPUT_POST, 'slots', FILTER_VALIDATE_INT);
$locationId = trim((string)($_POST['location_id'] ?? ''));
$durationMonths = filter_input(INPUT_POST, 'duration_months', FILTER_VALIDATE_INT);

View file

@ -8,7 +8,7 @@ $isStaff = website_current_user_is_staff();
<section class="page-heading">
<div class="container">
<h1>My Account</h1>
<p>Signed in as <?= website_escape($userName) ?>. Use these links to manage servers, order hosting, request support, or open the GSP control panel.</p>
<p>Signed in as <?= website_escape($userName) ?>. Use these links for website billing, server orders, support, staff tools, and the GSP control panel.</p>
</div>
</section>
@ -16,10 +16,13 @@ $isStaff = website_current_user_is_staff();
<div class="container">
<div class="summary-grid">
<article class="summary-card">
<h3>My Servers</h3>
<p>Open the GSP control panel to manage assigned game servers, files, logs, updates, backups, and server state.</p>
<h3>Customer Account</h3>
<p>Review orders, invoices, cart items, and servers tied to your Gameservers.World account.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(panel_url('home.php?m=gamemanager&p=game_monitor')) ?>">Open My Servers</a>
<a class="button button-primary" href="<?= website_escape(website_url('orders.php')) ?>">My Orders</a>
<a class="button button-secondary" href="<?= website_escape(website_url('invoices.php')) ?>">My Invoices</a>
<a class="button button-secondary" href="<?= website_escape(website_url('my_servers.php')) ?>">My Servers</a>
<a class="button button-secondary" href="<?= website_escape(website_cart_url()) ?>">Cart</a>
</div>
</article>
<article class="summary-card">
@ -39,13 +42,23 @@ $isStaff = website_current_user_is_staff();
</article>
<?php if ($isStaff): ?>
<article class="summary-card">
<h3>Staff Access</h3>
<p>Staff links are shown only after account role checks. Server-side authorization still applies inside the Panel.</p>
<h3>Website Staff</h3>
<p>Manage website sales, catalog, prices, locations, orders, invoices, payments, coupons, PayPal settings, and provisioning.</p>
<div class="card-actions">
<a class="button button-secondary" href="<?= website_escape(panel_url('home.php?m=administration&p=watch_logger')) ?>">Panel Administration</a>
<a class="button button-primary" href="<?= website_escape(website_url('staff.php')) ?>">Staff Dashboard</a>
<a class="button button-secondary" href="<?= website_escape(website_url('staff_services.php')) ?>">Manage Services</a>
<a class="button button-secondary" href="<?= website_escape(website_url('staff_orders.php')) ?>">Manage Orders</a>
<a class="button button-secondary" href="<?= website_escape(website_url('staff_invoices.php')) ?>">Manage Invoices</a>
</div>
</article>
<?php endif; ?>
<article class="summary-card">
<h3>Game Server Panel</h3>
<p>Open the GSP control panel for live server controls, files, logs, updates, backups, and operational management.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_control_panel_url()) ?>">Open Control Panel</a>
</div>
</article>
</div>
</div>
</section>

View file

@ -25,6 +25,6 @@ declare(strict_types=1);
</form>
<p><a href="<?= website_escape(website_url('forgot_password.php')) ?>">Forgot your password?</a></p>
<p class="muted">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.</p>
<p class="muted">Need an account? <a href="<?= website_escape(website_register_url($returnPath)) ?>">Create one through the GSP Panel registration page.</a></p>
<p class="muted">Need an account? <a href="<?= website_escape(website_register_url($returnPath)) ?>">Create one for Gameservers.World.</a></p>
</div>
</section>

View file

@ -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) {

View file

@ -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 {

View file

@ -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]);

View file

@ -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.']; }
}

View file

@ -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');

View file

@ -1,2 +1,2 @@
<?php
declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $message=''; $table=website_table('billing_orders'); if($_SERVER['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]);
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]);

View file

@ -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');

View file

@ -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`

View file

@ -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.