Fix login and order

This commit is contained in:
Frank Harris 2026-06-17 14:53:00 -05:00
parent dbecad8606
commit 484a36ce11
22 changed files with 399 additions and 520 deletions

View file

@ -72,10 +72,6 @@ function gsp_website_url(string $path = ''): string {
return $path === '' ? $base : rtrim($base, '/') . '/' . $path;
}
function gsp_panel_to_website_sso_url(string $returnPath = 'serverlist.php'): string {
return 'sso.php?destination=website&return=' . rawurlencode($returnPath);
}
function gsp_discord_invite_url(): string {
return 'https://discord.gg/qt9Hnkj6cv';
}

View file

@ -1,204 +0,0 @@
<?php
declare(strict_types=1);
function gsp_sso_request_scheme(): string
{
$https = $_SERVER['HTTPS'] ?? '';
if ($https !== '' && strtolower((string)$https) !== 'off') {
return 'https';
}
$forwarded = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '';
if ($forwarded !== '') {
return strtolower((string)$forwarded) === 'https' ? 'https' : 'http';
}
return 'http';
}
function gsp_sso_is_secure_request(): bool
{
$host = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
return gsp_sso_request_scheme() === 'https'
|| str_starts_with($host, 'localhost')
|| str_starts_with($host, '127.0.0.1');
}
function gsp_sso_user_agent_hash(): string
{
return hash('sha256', (string)($_SERVER['HTTP_USER_AGENT'] ?? ''));
}
function gsp_sso_client_ip(): string
{
if (function_exists('getClientIPAddress')) {
return (string)getClientIPAddress();
}
return (string)($_SERVER['REMOTE_ADDR'] ?? '');
}
function gsp_sso_hash_token(string $token): string
{
return hash('sha256', $token);
}
function gsp_sso_safe_return_path(string $returnPath, string $default): string
{
$returnPath = trim($returnPath);
if ($returnPath === '') {
return $default;
}
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $returnPath) === 1 || str_starts_with($returnPath, '//')) {
return $default;
}
if (str_contains($returnPath, "\r") || str_contains($returnPath, "\n")) {
return $default;
}
return ltrim($returnPath, '/');
}
function gsp_sso_ensure_table(mysqli $db, string $prefix): void
{
$table = $db->real_escape_string($prefix . 'sso_tokens');
$db->query(
"CREATE TABLE IF NOT EXISTS `{$table}` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`token_hash` CHAR(64) NOT NULL,
`user_id` INT(11) NOT NULL,
`source` VARCHAR(32) NOT NULL,
`destination` VARCHAR(32) NOT NULL,
`created_at` DATETIME NOT NULL,
`expires_at` DATETIME NOT NULL,
`used_at` DATETIME DEFAULT NULL,
`nonce` VARCHAR(64) NOT NULL,
`originating_ip` VARCHAR(64) DEFAULT NULL,
`user_agent_hash` CHAR(64) DEFAULT NULL,
`return_path` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sso_token_hash` (`token_hash`),
KEY `idx_sso_user_destination` (`user_id`, `destination`),
KEY `idx_sso_expires` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
}
function gsp_sso_cleanup(mysqli $db, string $prefix): void
{
gsp_sso_ensure_table($db, $prefix);
$table = $db->real_escape_string($prefix . 'sso_tokens');
$db->query("DELETE FROM `{$table}` WHERE `expires_at` < (UTC_TIMESTAMP() - INTERVAL 10 MINUTE)");
}
function gsp_sso_create_token(mysqli $db, string $prefix, int $userId, string $source, string $destination, string $returnPath, int $ttlSeconds = 60): ?string
{
if (!gsp_sso_is_secure_request()) {
return null;
}
gsp_sso_cleanup($db, $prefix);
$token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$tokenHash = gsp_sso_hash_token($token);
$nonce = bin2hex(random_bytes(16));
$createdAt = gmdate('Y-m-d H:i:s');
$expiresAt = gmdate('Y-m-d H:i:s', time() + max(30, min(60, $ttlSeconds)));
$table = $db->real_escape_string($prefix . 'sso_tokens');
$stmt = $db->prepare(
"INSERT INTO `{$table}`
(`token_hash`, `user_id`, `source`, `destination`, `created_at`, `expires_at`, `nonce`, `originating_ip`, `user_agent_hash`, `return_path`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
if (!$stmt) {
return null;
}
$ip = substr(gsp_sso_client_ip(), 0, 64);
$userAgentHash = gsp_sso_user_agent_hash();
$returnPath = substr($returnPath, 0, 255);
$stmt->bind_param('sissssssss', $tokenHash, $userId, $source, $destination, $createdAt, $expiresAt, $nonce, $ip, $userAgentHash, $returnPath);
$ok = $stmt->execute();
$stmt->close();
return $ok ? $token : null;
}
function gsp_sso_validate_token(mysqli $db, string $prefix, string $token, string $destination): array
{
if (!gsp_sso_is_secure_request()) {
return ['success' => false, 'error' => 'SSO requires HTTPS.'];
}
if (preg_match('/^[A-Za-z0-9_-]{32,128}$/', $token) !== 1) {
return ['success' => false, 'error' => 'Invalid SSO token.'];
}
gsp_sso_cleanup($db, $prefix);
$tokenHash = gsp_sso_hash_token($token);
$table = $db->real_escape_string($prefix . 'sso_tokens');
$stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `token_hash` = ? LIMIT 1");
if (!$stmt) {
return ['success' => false, 'error' => 'SSO is unavailable.'];
}
$stmt->bind_param('s', $tokenHash);
$stmt->execute();
$result = $stmt->get_result();
$row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
$stmt->close();
if (!$row) {
return ['success' => false, 'error' => 'Invalid SSO token.'];
}
if (!hash_equals((string)$row['token_hash'], $tokenHash)) {
return ['success' => false, 'error' => 'Invalid SSO token.'];
}
if ((string)$row['destination'] !== $destination) {
return ['success' => false, 'error' => 'Invalid SSO destination.'];
}
if (!empty($row['used_at'])) {
return ['success' => false, 'error' => 'This SSO link has already been used.'];
}
if (strtotime((string)$row['expires_at'] . ' UTC') < time()) {
return ['success' => false, 'error' => 'This SSO link has expired.'];
}
$currentAgentHash = gsp_sso_user_agent_hash();
if (!empty($row['user_agent_hash']) && !hash_equals((string)$row['user_agent_hash'], $currentAgentHash)) {
return ['success' => false, 'error' => 'This SSO link is not valid for this browser.'];
}
$usedAt = gmdate('Y-m-d H:i:s');
$mark = $db->prepare("UPDATE `{$table}` SET `used_at` = ? WHERE `id` = ? AND `used_at` IS NULL");
if (!$mark) {
return ['success' => false, 'error' => 'SSO is unavailable.'];
}
$id = (int)$row['id'];
$mark->bind_param('si', $usedAt, $id);
$mark->execute();
$updated = $mark->affected_rows === 1;
$mark->close();
if (!$updated) {
return ['success' => false, 'error' => 'This SSO link has already been used.'];
}
return [
'success' => true,
'user_id' => (int)$row['user_id'],
'source' => (string)$row['source'],
'destination' => (string)$row['destination'],
'return_path' => (string)($row['return_path'] ?? ''),
];
}

View file

@ -63,24 +63,4 @@ $install_queries[1] = array(
KEY `idx_logger_home` (`home_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;");
$install_queries[2] = array(
"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."sso_tokens`
(
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`token_hash` CHAR(64) NOT NULL,
`user_id` INT(11) NOT NULL,
`source` VARCHAR(32) NOT NULL,
`destination` VARCHAR(32) NOT NULL,
`created_at` DATETIME NOT NULL,
`expires_at` DATETIME NOT NULL,
`used_at` DATETIME DEFAULT NULL,
`nonce` VARCHAR(64) NOT NULL,
`originating_ip` VARCHAR(64) DEFAULT NULL,
`user_agent_hash` CHAR(64) DEFAULT NULL,
`return_path` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sso_token_hash` (`token_hash`),
KEY `idx_sso_user_destination` (`user_id`, `destination`),
KEY `idx_sso_expires` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
?>

View file

@ -29,7 +29,7 @@ function exec_ogp_module()
$projectRequestUrl = htmlspecialchars(gsp_project_request_url(), ENT_QUOTES, 'UTF-8');
$discordInviteUrl = htmlspecialchars(gsp_discord_invite_url(), ENT_QUOTES, 'UTF-8');
$serverStatusUrl = htmlspecialchars(gsp_server_status_url(), ENT_QUOTES, 'UTF-8');
$orderAnotherUrl = htmlspecialchars(gsp_panel_to_website_sso_url('serverlist.php'), ENT_QUOTES, 'UTF-8');
$orderAnotherUrl = htmlspecialchars(gsp_website_url('serverlist.php'), ENT_QUOTES, 'UTF-8');
$theme = isset($settings['theme']) ? $settings['theme'] : 'SimpleBootstrap';
$themeBase = 'themes/' . htmlspecialchars($theme, ENT_QUOTES, 'UTF-8') . '/images/icons/';
$cards = array(

View file

@ -15,6 +15,7 @@ This module is the public Gameservers.World sales and documentation website.
Panel/modules/website/
index.php
serverlist.php
cart.php
docs.php
login.php
pricing.php
@ -34,6 +35,7 @@ Panel/modules/website/
pages/
home.php
game_servers.php
cart.php
documentation.php
pricing.php
locations.php
@ -72,22 +74,19 @@ Effects:
- `serverlist.php` shows a clean fallback message instead of a fatal include error
- shared navigation never crashes because billing config is missing
## Shared Accounts and SSO
## Shared Accounts
The Panel user table is the identity source for the website. Website login checks
the same `users` table and legacy Panel password hash format instead of creating
a second password database.
The website and Panel run on different parent domains, so PHP session cookies are
not shared. Authenticated navigation uses short-lived one-time SSO tokens stored
in `OGP_DB_PREFIXsso_tokens`:
not shared. SSO is deferred in the current implementation. Users can use the same
username and password on both sites, but the website and Panel keep separate
sessions. Control Panel and staff links point directly to the configured Panel URL.
- website to Panel: `Panel/modules/website/sso.php` creates a token and redirects to `Panel/sso.php`
- Panel to website: `Panel/sso.php` creates a token and redirects to `Panel/modules/website/sso.php`
- tokens are random, stored only as SHA-256 hashes, expire in 30-60 seconds, and are marked used after one successful validation
- passwords, password hashes, permanent API keys, and PHP session IDs are never placed in URLs
Production SSO requires HTTPS. Localhost is allowed only for development testing.
`sso.php` endpoints are compatibility redirects only. They do not create tokens or
sessions.
## Ordering
@ -96,15 +95,16 @@ entry point:
- `order.php?service_id=...`
That page validates `service_id` server-side against enabled catalog rows before
continuing. Logged-out customers are sent through `login.php` and returned to the
same service after successful login. The removed `billing/order.php` route is
obsolete and should not be used for customer-facing links.
That page validates `service_id` server-side against enabled catalog rows, then
lets anonymous visitors configure slots and location before adding the service to
the session cart. Login or registration is required only when the customer proceeds
to checkout. The removed `billing/order.php` route is obsolete and should not be
used for customer-facing links.
The website owns catalog display, login-return intent, pricing display, and
checkout entry. Payment approval and final server provisioning remain server-side
responsibilities; browser requests must not call private provisioning methods
directly.
The website owns catalog display, cart storage, checkout intent, pricing display,
and customer confirmation. Payment approval and final server provisioning remain
server-side responsibilities; browser requests must not call private provisioning
methods directly.
## Documentation source

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? '');
if ($action === 'remove') {
website_cart_remove((string)($_POST['cart_key'] ?? ''));
header('Location: ' . website_cart_url(), true, 302);
exit;
}
if ($action === 'checkout') {
if (website_cart_count() === 0) {
$error = 'Your cart is empty.';
} elseif (!website_is_logged_in()) {
$_SESSION['website_checkout_intent'] = true;
$_SESSION['website_login_return'] = 'cart.php?checkout=1';
header('Location: ' . website_login_url('cart.php?checkout=1'), true, 302);
exit;
} else {
website_log_activity('Website checkout requested', (int)($_SESSION['website_user_id'] ?? 0), 'checkout_requested');
$message = 'Checkout is ready for account validation, but the payment gateway is not connected in this repository checkout. Please contact support to complete this order.';
}
}
}
if (isset($_GET['checkout']) && (string)$_GET['checkout'] === '1') {
if (website_cart_count() === 0) {
$error = 'Your cart is empty.';
} elseif (!website_is_logged_in()) {
$_SESSION['website_checkout_intent'] = true;
$_SESSION['website_login_return'] = 'cart.php?checkout=1';
header('Location: ' . website_login_url('cart.php?checkout=1'), true, 302);
exit;
} else {
$message = 'You are logged in and your cart is preserved. Payment checkout still needs the active payment runtime before public orders can be completed.';
}
}
website_render(
'cart.php',
[
'activePage' => 'cart',
'pageTitle' => 'Cart - Gameservers.World',
'metaDescription' => 'Review your Gameservers.World server cart before checkout.',
'canonicalPath' => 'cart.php',
'items' => website_cart_items(),
'cartTotal' => website_cart_total(),
'message' => $message,
'error' => $error,
]
);

View file

@ -19,7 +19,6 @@ return [
// Active panel URL. Do not point the public site at /panel/ unless that route is real.
'panel_url' => 'https://panel.iaregamer.com/',
'login_url' => 'https://panel.iaregamer.com/',
'panel_sso_url' => 'https://panel.iaregamer.com/sso.php',
'company' => [
'name' => 'Runlevel Systems',

View file

@ -3,9 +3,6 @@
declare(strict_types=1);
require_once __DIR__ . '/paths.php';
if (is_readable(WEBSITE_PANEL_INCLUDE_DIR . '/sso.php')) {
require_once WEBSITE_PANEL_INCLUDE_DIR . '/sso.php';
}
if (defined('GSP_WEBSITE_BOOTSTRAPPED')) {
return;
@ -44,7 +41,6 @@ $websiteDefaults = [
'billing_base_url' => '/billing',
'panel_url' => 'https://panel.iaregamer.com/',
'login_url' => 'https://panel.iaregamer.com/',
'panel_sso_url' => 'https://panel.iaregamer.com/sso.php',
'company' => [
'name' => 'Runlevel Systems',
'url' => 'https://runlevelsystems.com/',
@ -476,6 +472,9 @@ function website_authenticate_user(string $login, string $password): ?array
if (!$user || !website_verify_panel_password($user, $password)) {
return null;
}
if ((string)($user['users_role'] ?? '') === 'banned') {
return null;
}
return $user;
}
@ -543,7 +542,7 @@ function website_log_activity(string $message, int $userId = 0, string $eventTyp
}
$safeTable = $db->real_escape_string($table);
$ip = substr((function_exists('gsp_sso_client_ip') ? gsp_sso_client_ip() : (string)($_SERVER['REMOTE_ADDR'] ?? '')), 0, 255);
$ip = substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 255);
$stmt = $db->prepare(
"INSERT INTO `{$safeTable}` (`date`, `user_id`, `ip`, `message`, `source_type`, `category`, `event_type`, `severity`)
VALUES (FROM_UNIXTIME(UNIX_TIMESTAMP(), '%d-%m-%Y %H:%i:%s'), ?, ?, ?, 'website', 'authentication', ?, 'info')"
@ -560,15 +559,16 @@ function website_log_activity(string $message, int $userId = 0, string $eventTyp
function website_safe_return_path(string $returnPath, string $default = 'index.php'): string
{
if (function_exists('gsp_sso_safe_return_path')) {
return gsp_sso_safe_return_path($returnPath, $default);
}
if ($returnPath === '' || preg_match('#^[a-z][a-z0-9+.-]*://#i', $returnPath) === 1 || str_starts_with($returnPath, '//')) {
return $default;
}
return ltrim($returnPath, '/');
$returnPath = ltrim($returnPath, '/');
if (str_contains($returnPath, "\0") || str_starts_with($returnPath, '../') || str_contains($returnPath, '/../')) {
return $default;
}
return $returnPath;
}
function website_login_url(string $returnPath = ''): string
@ -580,15 +580,9 @@ function website_login_url(string $returnPath = ''): string
return website_url($path);
}
function website_panel_sso_url(string $returnPath = 'home.php?m=dashboard&p=dashboard'): string
{
$path = 'sso.php?destination=panel&return=' . rawurlencode(website_safe_return_path($returnPath, 'home.php?m=dashboard&p=dashboard'));
return website_url($path);
}
function website_control_panel_url(string $returnPath = 'home.php?m=dashboard&p=dashboard'): string
{
return website_is_logged_in() ? website_panel_sso_url($returnPath) : website_login_url('panel');
return panel_url(website_safe_return_path($returnPath, 'home.php?m=dashboard&p=dashboard'));
}
function website_order_url(int|string $serviceId): string
@ -596,6 +590,21 @@ function website_order_url(int|string $serviceId): string
return website_url('order.php?service_id=' . rawurlencode((string)$serviceId));
}
function website_cart_url(): string
{
return website_url('cart.php');
}
function website_checkout_url(): string
{
return website_url('cart.php?checkout=1');
}
function website_register_url(string $returnPath = 'cart.php'): string
{
return panel_url('index.php?m=register');
}
function website_fetch_service_by_id(int $serviceId): ?array
{
$db = website_db();
@ -638,6 +647,96 @@ function website_fetch_service_by_id(int $serviceId): ?array
return $service;
}
function website_service_name(array $service): string
{
$name = trim((string)($service['cfg_game_name'] ?? ''));
if ($name === '') {
$name = trim((string)($service['service_name'] ?? ''));
}
return $name === '' ? 'Game Server' : $name;
}
function website_service_min_slots(array $service): int
{
foreach (['min_slots', 'minimum_slots', 'slots_min'] as $column) {
if (isset($service[$column]) && (int)$service[$column] > 0) {
return (int)$service[$column];
}
}
$pricing = website_config('pricing', []);
return max(1, (int)($pricing['standard_min_slots'] ?? 16));
}
function website_service_max_slots(array $service): int
{
foreach (['max_slots', 'maximum_slots', 'slots_max', 'max_players'] as $column) {
if (isset($service[$column]) && (int)$service[$column] > 0) {
return (int)$service[$column];
}
}
return 0;
}
function website_service_locations(array $service): array
{
$raw = trim((string)($service['remote_server_id'] ?? ''));
if ($raw === '') {
return [];
}
$locations = [];
foreach (preg_split('/\s*,\s*/', $raw) ?: [] as $remoteServerId) {
$remoteServerId = trim($remoteServerId);
if ($remoteServerId === '' || !ctype_digit($remoteServerId)) {
continue;
}
$locations[$remoteServerId] = 'Location ' . $remoteServerId;
}
return $locations;
}
function website_cart_items(): array
{
website_start_session();
return is_array($_SESSION['website_cart'] ?? null) ? $_SESSION['website_cart'] : [];
}
function website_cart_count(): int
{
return count(website_cart_items());
}
function website_cart_add(array $item): void
{
website_start_session();
if (!isset($_SESSION['website_cart']) || !is_array($_SESSION['website_cart'])) {
$_SESSION['website_cart'] = [];
}
$key = bin2hex(random_bytes(8));
$_SESSION['website_cart'][$key] = $item;
}
function website_cart_remove(string $key): void
{
website_start_session();
if (isset($_SESSION['website_cart'][$key])) {
unset($_SESSION['website_cart'][$key]);
}
}
function website_cart_total(): float
{
$total = 0.0;
foreach (website_cart_items() as $item) {
$total += (float)($item['monthly_total'] ?? 0);
}
return $total;
}
function website_billing_docs_root(): ?string
{
if (is_dir(WEBSITE_BILLING_DOCS_DIR)) {

View file

@ -40,17 +40,20 @@ $currentUser = website_current_user();
<li><a href="<?= website_escape(website_url('account.php')) ?>">My Account</a></li>
<li><a href="<?= website_escape(website_url('serverlist.php')) ?>">Order a Server</a></li>
<li><a href="<?= website_escape(website_control_panel_url()) ?>">Control Panel</a></li>
<li><a href="<?= website_escape(website_panel_sso_url('home.php?m=gamemanager&p=game_monitor')) ?>">My Servers</a></li>
<li><a href="<?= website_escape(panel_url('home.php?m=gamemanager&p=game_monitor')) ?>">My Servers</a></li>
<li><a href="<?= website_escape(website_cart_url()) ?>">Cart</a></li>
<li><a href="<?= website_escape(website_url('logout.php')) ?>">Log Out</a></li>
<?php else: ?>
<li><a href="<?= website_escape(website_login_url()) ?>">Account Login</a></li>
<li><a href="<?= website_escape(website_register_url()) ?>">Create Account</a></li>
<li><a href="<?= website_escape(website_url('serverlist.php')) ?>">Order a Server</a></li>
<li><a href="<?= website_escape(website_control_panel_url()) ?>">Control Panel</a></li>
<li><a href="<?= website_escape(website_cart_url()) ?>">Cart</a></li>
<?php endif; ?>
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Server Guides</a></li>
<li><a href="<?= website_escape(website_custom_project_url()) ?>">Request Custom Work</a></li>
<?php if ($currentUser && website_current_user_is_staff()): ?>
<li><a href="<?= website_escape(website_panel_sso_url('home.php?m=administration&p=watch_logger')) ?>">Staff Tools</a></li>
<li><a href="<?= website_escape(panel_url('home.php?m=administration&p=watch_logger')) ?>">Staff Tools</a></li>
<?php endif; ?>
<?php if ($discordUrl !== ''): ?>
<li><a href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a></li>

View file

@ -3,6 +3,8 @@
declare(strict_types=1);
$activePage = $activePage ?? '';
$currentUser = website_current_user();
$cartCount = website_cart_count();
$navLinks = [
['key' => 'home', 'label' => 'Home', 'href' => website_url('index.php')],
['key' => 'servers', 'label' => 'Game Servers', 'href' => website_url('serverlist.php')],
@ -11,6 +13,15 @@ $navLinks = [
['key' => 'locations', 'label' => 'Locations', 'href' => website_url('locations.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('cart.php')];
$navLinks[] = ['key' => 'servers_panel', 'label' => 'My Servers', 'href' => panel_url('home.php?m=gamemanager&p=game_monitor')];
} else {
$navLinks[] = ['key' => 'account', 'label' => 'Login', 'href' => website_login_url()];
$navLinks[] = ['key' => 'register', 'label' => 'Create Account', 'href' => website_register_url()];
}
$navLinks[] = ['key' => 'cart', 'label' => 'Cart' . ($cartCount > 0 ? ' (' . $cartCount . ')' : ''), 'href' => website_cart_url()];
?>
<header class="site-header">
<div class="container header-shell">
@ -40,6 +51,9 @@ $navLinks = [
<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

@ -9,7 +9,7 @@ $returnPath = website_safe_return_path((string)($_GET['return'] ?? $_POST['retur
if (website_is_logged_in()) {
if ($returnPath === 'panel') {
header('Location: ' . website_panel_sso_url(), true, 302);
header('Location: ' . website_control_panel_url(), true, 302);
exit;
}
header('Location: ' . website_url($returnPath), true, 302);
@ -27,7 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
unset($_SESSION['website_login_return']);
if ($returnPath === 'panel') {
header('Location: ' . website_panel_sso_url(), true, 302);
header('Location: ' . website_control_panel_url(), true, 302);
exit;
}

View file

@ -6,6 +6,7 @@ require_once __DIR__ . '/includes/bootstrap.php';
$serviceId = filter_input(INPUT_GET, 'service_id', FILTER_VALIDATE_INT);
$service = $serviceId ? website_fetch_service_by_id((int)$serviceId) : null;
$error = '';
if (!$service) {
website_log_activity('Invalid or unavailable service_id requested from website order flow', (int)($_SESSION['website_user_id'] ?? 0), 'invalid_service');
@ -23,14 +24,45 @@ if (!$service) {
exit;
}
if (!website_is_logged_in()) {
$_SESSION['website_pending_order_service_id'] = (int)$service['service_id'];
$_SESSION['website_login_return'] = 'order.php?service_id=' . (int)$service['service_id'];
header('Location: ' . website_login_url('order.php?service_id=' . (int)$service['service_id']), true, 302);
exit;
$locations = website_service_locations($service);
$minSlots = website_service_min_slots($service);
$maxSlots = website_service_max_slots($service);
if ($_SERVER['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);
if ($slots === false || $slots === null || $slots < $minSlots) {
$error = 'Select at least ' . $minSlots . ' slots for this service.';
} elseif ($maxSlots > 0 && $slots > $maxSlots) {
$error = 'This service supports a maximum of ' . $maxSlots . ' slots.';
} elseif (empty($locations) || !array_key_exists($locationId, $locations)) {
$error = 'Select an available location for this service.';
} elseif (!in_array((int)$durationMonths, [1, 3, 6, 12], true)) {
$error = 'Select a valid billing duration.';
} else {
$priceMonthly = (float)($service['price_monthly'] ?? 0);
$monthlyTotal = $priceMonthly > 0 ? $priceMonthly : 0.0;
website_cart_add([
'service_id' => (int)$service['service_id'],
'service_name' => website_service_name($service),
'slots' => (int)$slots,
'location_id' => $locationId,
'location_name' => $locations[$locationId],
'duration_months' => (int)$durationMonths,
'price_monthly' => $priceMonthly,
'monthly_total' => $monthlyTotal,
'added_at' => time(),
]);
website_log_activity('Website cart item added for service_id ' . (int)$service['service_id'], (int)($_SESSION['website_user_id'] ?? 0), 'cart_item_added');
header('Location: ' . website_cart_url(), true, 302);
exit;
}
}
website_log_activity('Website order flow started for service_id ' . (int)$service['service_id'], (int)$_SESSION['website_user_id'], 'order_started');
website_log_activity('Website order flow opened for service_id ' . (int)$service['service_id'], (int)($_SESSION['website_user_id'] ?? 0), 'order_started');
website_render(
'order.php',
[
@ -39,6 +71,9 @@ website_render(
'metaDescription' => 'Configure your Gameservers.World game server order.',
'canonicalPath' => 'order.php',
'service' => $service,
'error' => null,
'error' => $error,
'locations' => $locations,
'minSlots' => $minSlots,
'maxSlots' => $maxSlots,
]
);

View file

@ -19,7 +19,7 @@ $isStaff = website_current_user_is_staff();
<h3>My Servers</h3>
<p>Open the GSP control panel to manage assigned game servers, files, logs, updates, backups, and server state.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_panel_sso_url('home.php?m=gamemanager&p=game_monitor')) ?>">Open My Servers</a>
<a class="button button-primary" href="<?= website_escape(panel_url('home.php?m=gamemanager&p=game_monitor')) ?>">Open My Servers</a>
</div>
</article>
<article class="summary-card">
@ -42,7 +42,7 @@ $isStaff = website_current_user_is_staff();
<h3>Staff Access</h3>
<p>Staff links are shown only after account role checks. Server-side authorization still applies inside the Panel.</p>
<div class="card-actions">
<a class="button button-secondary" href="<?= website_escape(website_panel_sso_url('home.php?m=administration&p=watch_logger')) ?>">Panel Administration</a>
<a class="button button-secondary" href="<?= website_escape(panel_url('home.php?m=administration&p=watch_logger')) ?>">Panel Administration</a>
</div>
</article>
<?php endif; ?>

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
?>
<section class="page-heading">
<div class="container">
<h1>Cart</h1>
<p>Review selected server packages. You can configure and add servers before logging in; login or account creation is required when you proceed to checkout.</p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($error !== ''): ?>
<div class="alert warning"><?= website_escape($error) ?></div>
<?php endif; ?>
<?php if ($message !== ''): ?>
<div class="alert info"><?= website_escape($message) ?></div>
<?php endif; ?>
<?php if (empty($items)): ?>
<div class="empty-state">
<h2>Your cart is empty</h2>
<p>Browse the catalog to choose a game server, slots, and location.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
</div>
</div>
<?php else: ?>
<div class="summary-grid">
<?php foreach ($items as $key => $item): ?>
<article class="summary-card">
<h3><?= website_escape((string)($item['service_name'] ?? 'Game Server')) ?></h3>
<p>Service ID: <?= website_escape((string)($item['service_id'] ?? '')) ?></p>
<p>Slots: <?= website_escape((string)($item['slots'] ?? '')) ?></p>
<p>Location: <?= website_escape((string)($item['location_name'] ?? '')) ?></p>
<p>Billing duration: <?= website_escape((string)($item['duration_months'] ?? 1)) ?> month(s)</p>
<p><strong><?= ((float)($item['monthly_total'] ?? 0) > 0) ? '$' . website_escape(number_format((float)$item['monthly_total'], 2)) . ' / month' : 'Contact for pricing' ?></strong></p>
<form method="post" action="<?= website_escape(website_cart_url()) ?>">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="cart_key" value="<?= website_escape((string)$key) ?>">
<button class="button button-secondary" type="submit">Remove</button>
</form>
</article>
<?php endforeach; ?>
</div>
<div class="summary-card" style="margin-top: 18px;">
<h3>Checkout</h3>
<p>Estimated monthly total: <strong><?= $cartTotal > 0 ? '$' . website_escape(number_format((float)$cartTotal, 2)) : 'Contact for pricing' ?></strong></p>
<p class="muted">Prices and service availability are revalidated server-side before payment or provisioning. Adding an item to the cart does not create a running server.</p>
<div class="card-actions">
<form method="post" action="<?= website_escape(website_cart_url()) ?>">
<input type="hidden" name="action" value="checkout">
<button class="button button-primary" type="submit">Proceed to Checkout</button>
</form>
<?php if (!website_is_logged_in()): ?>
<a class="button button-secondary" href="<?= website_escape(website_login_url('cart.php?checkout=1')) ?>">Log In</a>
<a class="button button-secondary" href="<?= website_escape(website_register_url('cart.php')) ?>">Create Account</a>
<?php endif; ?>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Continue Shopping</a>
</div>
</div>
<?php endif; ?>
</div>
</section>

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
<input id="password" name="password" type="password" autocomplete="current-password" required>
<button class="button button-primary" type="submit">Log In</button>
</form>
<p class="muted">After login, Control Panel and server-management links use a short-lived one-time SSO handoff. Passwords and PHP session IDs are never passed between domains.</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>
</div>
</section>

View file

@ -25,18 +25,20 @@ endif;
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$description = trim((string)($service['description'] ?? ''));
$price = (float)($service['price_monthly'] ?? 0);
$minSlots = (int)($service['min_slots'] ?? $service['minimum_slots'] ?? website_config('pricing', [])['standard_min_slots'] ?? 16);
$maxSlots = (int)($service['max_slots'] ?? $service['maximum_slots'] ?? 0);
$selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
?>
<section class="page-heading">
<div class="container">
<h1>Configure <?= website_escape($serviceName) ?></h1>
<p><?= website_escape($description !== '' ? $description : 'Review this server plan before checkout. Pricing and provisioning are validated server-side from the active catalog.') ?></p>
<p><?= website_escape($description !== '' ? $description : 'Configure this server package before adding it to your cart. Login is only required when you proceed to checkout.') ?></p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($error !== ''): ?>
<div class="alert warning"><?= website_escape($error) ?></div>
<?php endif; ?>
<div class="summary-grid">
<article class="summary-card">
<h3>Plan</h3>
@ -52,16 +54,35 @@ $maxSlots = (int)($service['max_slots'] ?? $service['maximum_slots'] ?? 0);
</article>
<article class="summary-card">
<h3>Checkout boundary</h3>
<p>This website validates the catalog service and keeps your shared account identity. Payment and final provisioning must complete server-side before the Panel creates a running game server.</p>
<p>You can add this server to your cart before logging in. Payment and final provisioning must complete server-side before the Panel creates a running game server.</p>
<p class="muted">The legacy <code>billing/order.php</code> route is no longer used.</p>
</article>
</div>
<div class="alert info" style="margin-top: 18px;">
Checkout/payment handlers are not present in this repository checkout. Contact support to complete this order or connect the active payment module before enabling public checkout.
</div>
<div class="card-actions" style="margin-top: 18px;">
<a class="button button-primary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Back to Catalog</a>
</div>
<form class="website-form summary-card" method="post" action="<?= website_escape(website_order_url((int)$service['service_id'])) ?>" style="margin-top: 18px;">
<h3>Server configuration</h3>
<label for="slots">Slots</label>
<input id="slots" name="slots" type="number" min="<?= website_escape((string)$minSlots) ?>"<?= $maxSlots > 0 ? ' max="' . website_escape((string)$maxSlots) . '"' : '' ?> value="<?= website_escape((string)$selectedSlots) ?>" required>
<label for="location_id">Location</label>
<select id="location_id" name="location_id" required>
<?php foreach ($locations as $locationId => $locationName): ?>
<option value="<?= website_escape((string)$locationId) ?>"<?= (string)($_POST['location_id'] ?? '') === (string)$locationId ? ' selected' : '' ?>><?= website_escape($locationName) ?></option>
<?php endforeach; ?>
</select>
<label for="duration_months">Billing duration</label>
<select id="duration_months" name="duration_months" required>
<?php foreach ([1, 3, 6, 12] as $duration): ?>
<option value="<?= website_escape((string)$duration) ?>"<?= (int)($_POST['duration_months'] ?? 1) === $duration ? ' selected' : '' ?>><?= website_escape((string)$duration) ?> month<?= $duration === 1 ? '' : 's' ?></option>
<?php endforeach; ?>
</select>
<p class="muted">The website stores this selection in your cart and revalidates service, slots, location, and price during checkout.</p>
<div class="card-actions">
<button class="button button-primary" type="submit">Add to Cart</button>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Back to Catalog</a>
</div>
</form>
</div>
</section>

View file

@ -4,78 +4,15 @@ declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$db = website_db();
$prefix = website_table_prefix();
$destination = trim((string)($_GET['destination'] ?? 'panel'));
$returnPath = website_safe_return_path((string)($_GET['return'] ?? ''), 'home.php?m=dashboard&p=dashboard');
if (!$db instanceof mysqli || $prefix === '') {
http_response_code(503);
echo 'SSO is temporarily unavailable.';
website_log_activity('Deprecated website SSO endpoint redirected to direct login/navigation', (int)($_SESSION['website_user_id'] ?? 0), 'sso_deprecated_redirect');
if ($destination === 'panel') {
header('Location: ' . panel_url($returnPath), true, 302);
exit;
}
$token = trim((string)($_GET['token'] ?? ''));
if ($token !== '') {
if (!function_exists('gsp_sso_validate_token')) {
http_response_code(503);
echo 'SSO is not configured.';
exit;
}
$validation = gsp_sso_validate_token($db, $prefix, $token, 'website');
if (empty($validation['success'])) {
website_log_activity('Website SSO failed: ' . (string)$validation['error'], 0, 'sso_failure');
http_response_code(403);
echo website_escape((string)$validation['error']);
exit;
}
$user = website_panel_user_by_id((int)$validation['user_id']);
if (!$user) {
http_response_code(403);
echo 'SSO user is no longer available.';
exit;
}
website_set_user_session($user);
website_log_activity('Website SSO login succeeded for ' . (string)$user['users_login'], (int)$user['user_id'], 'sso_success');
$returnPath = website_safe_return_path((string)$validation['return_path'], 'index.php');
header('Location: ' . website_url($returnPath), true, 302);
exit;
}
$destination = trim((string)($_GET['destination'] ?? ''));
if ($destination !== 'panel') {
http_response_code(400);
echo 'Invalid SSO destination.';
exit;
}
$user = website_current_user();
if (!$user) {
$_SESSION['website_login_return'] = 'panel';
header('Location: ' . website_login_url('panel'), true, 302);
exit;
}
if (!function_exists('gsp_sso_create_token')) {
http_response_code(503);
echo 'SSO is not configured.';
exit;
}
$returnPath = website_safe_return_path((string)($_GET['return'] ?? 'home.php?m=dashboard&p=dashboard'), 'home.php?m=dashboard&p=dashboard');
$newToken = gsp_sso_create_token($db, $prefix, (int)$user['user_id'], 'website', 'panel', $returnPath, 60);
if ($newToken === null) {
http_response_code(403);
echo 'SSO requires HTTPS.';
exit;
}
website_log_activity('Website-to-panel SSO token created', (int)$user['user_id'], 'sso_token_created');
$panelSsoUrl = trim((string)website_config('panel_sso_url', ''));
if ($panelSsoUrl === '') {
$panelSsoUrl = panel_url('sso.php');
}
header('Location: ' . $panelSsoUrl . '?token=' . rawurlencode($newToken), true, 302);
header('Location: ' . website_url('index.php'), true, 302);
exit;

View file

@ -3,139 +3,19 @@
declare(strict_types=1);
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/helpers.php';
require_once __DIR__ . '/includes/html_functions.php';
require_once __DIR__ . '/includes/sso.php';
startSession();
$destination = trim((string)($_GET['destination'] ?? ''));
$returnPath = trim((string)($_GET['return'] ?? 'serverlist.php'));
define('CONFIG_FILE', __DIR__ . '/includes/config.inc.php');
require_once CONFIG_FILE;
$mysqli = @mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null);
if (!$mysqli instanceof mysqli) {
http_response_code(503);
echo 'SSO is temporarily unavailable.';
exit;
}
@mysqli_set_charset($mysqli, 'utf8mb4');
$prefix = (string)$table_prefix;
$panelReturnDefault = 'home.php?m=dashboard&p=dashboard';
$websiteBase = function_exists('gsp_website_url') ? gsp_website_url() : 'https://gameservers.world/';
function gsp_panel_sso_user(mysqli $db, string $prefix, int $userId): ?array
{
$table = $db->real_escape_string($prefix . 'users');
$stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `user_id` = ? LIMIT 1");
if (!$stmt) {
return null;
}
$stmt->bind_param('i', $userId);
$stmt->execute();
$result = $stmt->get_result();
$user = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
$stmt->close();
return is_array($user) ? $user : null;
$returnPath = ltrim($returnPath, '/');
if ($returnPath === '' || preg_match('#^[a-z][a-z0-9+.-]*://#i', $returnPath) === 1 || str_starts_with($returnPath, '//') || str_contains($returnPath, "\0") || str_starts_with($returnPath, '../') || str_contains($returnPath, '/../')) {
$returnPath = 'serverlist.php';
}
function gsp_panel_sso_api_token(mysqli $db, string $prefix, int $userId): string
{
$table = $db->real_escape_string($prefix . 'api_tokens');
$db->query(
"CREATE TABLE IF NOT EXISTS `{$table}` (
`user_id` int(11) NOT NULL,
`token` varchar(64) NOT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1"
);
$stmt = $db->prepare("SELECT `token` FROM `{$table}` WHERE `user_id` = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('i', $userId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
$stmt->close();
if (is_array($row) && !empty($row['token'])) {
return (string)$row['token'];
}
}
$token = bin2hex(random_bytes(32));
$insert = $db->prepare("INSERT INTO `{$table}` (`user_id`, `token`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `token` = VALUES(`token`)");
if ($insert) {
$insert->bind_param('is', $userId, $token);
$insert->execute();
$insert->close();
}
return $token;
}
function gsp_panel_sso_log(mysqli $db, string $prefix, int $userId, string $message): void
{
$table = $db->real_escape_string($prefix . 'logger');
$ip = $db->real_escape_string(gsp_sso_client_ip());
$message = $db->real_escape_string(substr($message, 0, 1000));
$db->query(
"INSERT INTO `{$table}` (`date`, `user_id`, `ip`, `message`, `source_type`, `category`, `event_type`, `severity`)
VALUES (FROM_UNIXTIME(UNIX_TIMESTAMP(), '%d-%m-%Y %H:%i:%s'), {$userId}, '{$ip}', '{$message}', 'sso', 'authentication', 'sso', 'info')"
);
}
$token = trim((string)($_GET['token'] ?? ''));
if ($token !== '') {
$validation = gsp_sso_validate_token($mysqli, $prefix, $token, 'panel');
if (empty($validation['success'])) {
http_response_code(403);
echo htmlspecialchars((string)$validation['error'], ENT_QUOTES, 'UTF-8');
exit;
}
$user = gsp_panel_sso_user($mysqli, $prefix, (int)$validation['user_id']);
if (!$user) {
http_response_code(403);
echo 'SSO user is no longer available.';
exit;
}
session_regenerate_id(true);
$_SESSION['user_id'] = $user['user_id'];
$_SESSION['users_login'] = $user['users_login'];
$_SESSION['users_passwd'] = $user['users_passwd'];
$_SESSION['users_group'] = $user['users_role'];
$_SESSION['users_lang'] = $user['users_lang'];
$_SESSION['users_theme'] = $user['users_theme'];
$_SESSION['users_api_key'] = gsp_panel_sso_api_token($mysqli, $prefix, (int)$user['user_id']);
gsp_panel_sso_log($mysqli, $prefix, (int)$user['user_id'], 'Panel SSO login succeeded for ' . $user['users_login']);
$returnPath = gsp_sso_safe_return_path((string)$validation['return_path'], $panelReturnDefault);
header('Location: ' . $returnPath, true, 302);
if ($destination === 'website') {
header('Location: ' . gsp_website_url($returnPath), true, 302);
exit;
}
$destination = trim((string)($_GET['destination'] ?? 'website'));
if ($destination !== 'website') {
http_response_code(400);
echo 'Invalid SSO destination.';
exit;
}
if (empty($_SESSION['user_id'])) {
header('Location: index.php', true, 302);
exit;
}
$returnPath = gsp_sso_safe_return_path((string)($_GET['return'] ?? 'serverlist.php'), 'serverlist.php');
$newToken = gsp_sso_create_token($mysqli, $prefix, (int)$_SESSION['user_id'], 'panel', 'website', $returnPath, 60);
if ($newToken === null) {
http_response_code(403);
echo 'SSO requires HTTPS.';
exit;
}
gsp_panel_sso_log($mysqli, $prefix, (int)$_SESSION['user_id'], 'Panel-to-website SSO token created');
header('Location: ' . rtrim($websiteBase, '/') . '/sso.php?token=' . rawurlencode($newToken), true, 302);
header('Location: index.php', true, 302);
exit;

View file

@ -40,9 +40,9 @@ public node status
website account/order entry
-> Panel/modules/website/login.php
-> Panel/modules/website/sso.php and Panel/sso.php
-> Panel/modules/website/sso.php and Panel/sso.php compatibility redirects
-> Panel/modules/website/order.php
-> shared users table and one-time SSO token table
-> shared users table, separate website and Panel sessions
```
## Panel -> Agent XML-RPC
@ -168,14 +168,15 @@ Return shape:
- `mem_percent`
- `disk_percent`
## Website Account, SSO, And Order Entry
## Website Account And Order Entry
| Endpoint | Purpose | Auth / Verification |
|---|---|---|
| `Panel/modules/website/login.php` | create website session from shared Panel user database | username/password checked against Panel hash format |
| `Panel/modules/website/sso.php` | website SSO endpoint | website session or one-time SSO token |
| `Panel/sso.php` | Panel SSO endpoint | Panel session or one-time SSO token |
| `Panel/modules/website/order.php` | validate `service_id` and start order intent | website session for continuation |
| `Panel/modules/website/sso.php` | compatibility redirect for old SSO links | no token/session creation |
| `Panel/sso.php` | compatibility redirect for old SSO links | no token/session creation |
| `Panel/modules/website/order.php` | validate `service_id`, slots, and location before adding to cart | anonymous website session |
| `Panel/modules/website/cart.php` | review cart and require login only at checkout | anonymous website session; website login for checkout |
The old `Website/api/*` and `Website/webhook.php` checkout compatibility files are not present in this checkout. Payment processing must be reconnected and documented before public checkout is enabled.

View file

@ -120,21 +120,20 @@ The scheduler does not call agents directly at runtime. It stores cron lines on
This makes `ogp_api.php` part of the internal scheduler runtime contract.
## Website Account, SSO, And Order Entry
## Website Account And Order Entry
| Endpoint | Auth | Purpose | Parameters | Returns |
|---|---|---|---|---|
| `Panel/modules/website/login.php` | Panel user credentials | create a website session against the shared Panel user table | username/password form | website session and redirect |
| `Panel/modules/website/logout.php` | website session | destroy website session | none | redirect to website home |
| `Panel/modules/website/sso.php?destination=panel` | website session | create a one-time token for Panel login | optional trusted return path | redirect to `Panel/sso.php` |
| `Panel/sso.php?token=...` | one-time SSO token | create normal Panel session | token | redirect to Panel page |
| `Panel/sso.php?destination=website` | Panel session | create a one-time token for website login | optional trusted return path | redirect to website SSO endpoint |
| `Panel/modules/website/sso.php?token=...` | one-time SSO token | create website session | token | redirect to website page |
| `Panel/modules/website/order.php` | website session for checkout continuation | validate catalog service and start order intent | `service_id` | order page or login redirect |
| `Panel/modules/website/sso.php` | none | compatibility redirect for old SSO links | safe `destination` / `return` values | direct website or Panel redirect |
| `Panel/sso.php` | none | compatibility redirect for old Panel-to-website SSO links | safe `destination` / `return` values | direct website or Panel redirect |
| `Panel/modules/website/order.php` | anonymous website session | validate catalog service and configure order intent | `service_id`, slots/location POST | order page or cart redirect |
| `Panel/modules/website/cart.php` | anonymous website session; website login required only for checkout | review cart and begin checkout intent | cart actions | cart page or login redirect |
SSO tokens are stored in `OGP_DB_PREFIXsso_tokens` as SHA-256 hashes, expire in 30-60 seconds, and are marked used after successful validation. Tokens never contain passwords, password hashes, permanent API keys, or PHP session IDs.
SSO is deferred in the current implementation because `gameservers.world` and `panel.iaregamer.com` cannot share one PHP session cookie. Users can use the same Panel-backed credentials on both sites, but website and Panel sessions are separate.
The old `Website/api/create_order.php`, `Website/api/capture_order.php`, `Website/api/log_error.php`, and `Website/webhook.php` compatibility files are not present in this repository checkout. Until an active payment runtime is connected, the website order page validates service intent and sends customers to support rather than claiming checkout is complete.
The old `Website/api/create_order.php`, `Website/api/capture_order.php`, `Website/api/log_error.php`, and `Website/webhook.php` compatibility files are not present in this repository checkout. Until an active payment runtime is connected, the website cart preserves validated order intent and displays a friendly checkout-unavailable message rather than claiming checkout is complete.
### Webhooks
@ -149,7 +148,7 @@ The old `Website/api/create_order.php`, `Website/api/capture_order.php`, `Websit
| token auth | `Panel/ogp_api.php` |
| host allowlist | `api_authorized.hosts`, `api_authorized.fwd_hosts`, `settings/api_hosts.php` |
| role / ownership checks | inside `api_*` handlers in `ogp_api.php` |
| one-time SSO token hash storage | `OGP_DB_PREFIXsso_tokens` |
| website session cart | `$_SESSION['website_cart']` |
## Search Coverage Used For This Document

View file

@ -42,11 +42,11 @@ Commercial billing, provisioning, invoices, orders, transactions, coupons, and p
## Website Ordering Boundary
The active Gameservers.World website no longer links customers to `billing/order.php`. The public catalog uses `Panel/modules/website/order.php?service_id=...` as the order entry point. That page validates the enabled service server-side and sends logged-out users through website login before returning them to the intended service.
The active Gameservers.World website no longer links customers to `billing/order.php`. The public catalog uses `Panel/modules/website/order.php?service_id=...` as the order entry point. That page validates the enabled service server-side and allows anonymous visitors to configure slots/location and add the package to the website session cart.
Payment approval and final provisioning remain server-side responsibilities. The browser must not call private provisioning methods directly, and prices must be read from server-side catalog data rather than query parameters.
In this repository checkout the historical `Panel/modules/billing` runtime is not present, although billing tables and integration references remain. The website order page therefore stops at validated order intent and support handoff until the active checkout/payment runtime is connected.
In this repository checkout the historical `Panel/modules/billing` runtime is not present, although billing tables and integration references remain. The website cart therefore stops at validated order intent and a friendly checkout-unavailable message until the active checkout/payment runtime is connected.
## Admin Workflow

View file

@ -35,24 +35,15 @@ The website module centralizes these helpers in `includes/bootstrap.php`:
The website does not include the billing config loader directly. It reads panel or billing DB values safely, uses them only when needed, and avoids public fatal errors tied to missing config files.
## Shared Accounts and SSO
## Shared Accounts
The website uses the Panel `users` table as the account source of truth. A customer has the same `user_id` on Gameservers.World, the GSP Panel, support, billing, and server orders.
Website login verifies credentials against the existing Panel password hash format. This preserves current Panel login behavior and avoids a second website password database.
`gameservers.world` and `panel.iaregamer.com` cannot share a normal PHP session cookie because they are unrelated parent domains. The bridge is a one-time SSO token:
`gameservers.world` and `panel.iaregamer.com` cannot share a normal PHP session cookie because they are unrelated parent domains. SSO is deferred for this phase. The website and Panel keep separate sessions, and users may log in separately on both sites with the same credentials. Passwords, password hashes, PHP session IDs, and authentication tokens are never passed in URLs.
- website to Panel: `Panel/modules/website/sso.php` creates a token and redirects to `Panel/sso.php`
- Panel to website: `Panel/sso.php` creates a token and redirects back to `Panel/modules/website/sso.php`
- table: `OGP_DB_PREFIXsso_tokens`
- lifetime: 30-60 seconds
- storage: SHA-256 token hash only
- reuse: rejected after `used_at` is set
- URL contents: token only, never passwords, password hashes, API keys, or PHP session IDs
- HTTPS is required in production
Expired tokens are cleaned opportunistically when SSO is used. The administration module also creates the table for fresh installs.
`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.
## Ordering
@ -62,11 +53,13 @@ The current public catalog route is `serverlist.php`. Customer-facing Order butt
The old `billing/order.php` route is obsolete in this repository layout and must not be used for active Gameservers.World links.
`order.php` validates the requested `service_id` server-side against enabled catalog records before allowing the customer to continue. Logged-out customers have the intended order path stored in the website session, are sent to `login.php`, and return to the same service after successful login.
`order.php` validates the requested `service_id` server-side against enabled catalog records before allowing the customer to continue. Anonymous visitors can configure slots and location, add the server package to the session cart, and review the cart before login.
The website owns catalog display, order intent, login-return behavior, checkout entry, and customer confirmation. The Panel owns final provisioning, server assignment to the shared `user_id`, game-home creation, agent handoff, and provisioning state. Public browser requests must not call private provisioning methods directly.
Login or registration is required only at checkout. The cart is stored in the website session and remains available through website login session regeneration. Panel registration is currently linked directly until a website-native registration form is restored.
Checkout/payment handlers are not present in this repository checkout. Until the active payment runtime is connected, `order.php` validates the selected service and sends the customer to support instead of pretending payment or provisioning is available.
The website owns catalog display, cart storage, order intent, login-return behavior, checkout entry, and customer confirmation. The Panel owns final provisioning, server assignment to the shared `user_id`, game-home creation, agent handoff, and provisioning state. Public browser requests must not call private provisioning methods directly.
Checkout/payment handlers are not present in this repository checkout. Until the active payment runtime is connected, `cart.php` preserves the validated cart and shows a friendly checkout-unavailable message instead of pretending payment or provisioning is available.
## Navigation
@ -76,7 +69,7 @@ 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 rely on Panel authorization server-side
The website Control Panel button sends logged-in users through website-to-Panel SSO. Logged-out users go through website login first. The Panel dashboard `Order Another Server` link sends logged-in Panel users through Panel-to-website SSO.
The website main navigation also includes visible `Login`, `Create Account`, and `Cart` entries when appropriate. Control Panel, My Servers, and staff administration links point directly to the configured Panel domain. The Panel dashboard `Order Another Server` link points directly to the website catalog.
## Deployment
@ -101,6 +94,7 @@ Recommended:
- `login.php`
- `account.php`
- `order.php`
- `cart.php`
- `sso.php`
## Pricing and Platform Reference