'rebuilt missing sales and staff pages
This commit is contained in:
parent
484a36ce11
commit
60bcc67056
680 changed files with 33650 additions and 43 deletions
|
|
@ -15,15 +15,42 @@ This module is the public Gameservers.World sales and documentation website.
|
|||
Panel/modules/website/
|
||||
index.php
|
||||
serverlist.php
|
||||
order.php
|
||||
cart.php
|
||||
checkout.php
|
||||
payment_success.php
|
||||
payment_cancel.php
|
||||
docs.php
|
||||
login.php
|
||||
register.php
|
||||
forgot_password.php
|
||||
reset_password.php
|
||||
account.php
|
||||
orders.php
|
||||
invoices.php
|
||||
my_servers.php
|
||||
staff.php
|
||||
staff_services.php
|
||||
staff_locations.php
|
||||
staff_coupons.php
|
||||
staff_orders.php
|
||||
staff_invoices.php
|
||||
staff_payments.php
|
||||
staff_paypal.php
|
||||
staff_settings.php
|
||||
staff_provisioning.php
|
||||
staff_migrations.php
|
||||
webhook.php
|
||||
pricing.php
|
||||
locations.php
|
||||
support.php
|
||||
doc_asset.php
|
||||
api/
|
||||
create_order.php
|
||||
capture_order.php
|
||||
includes/
|
||||
bootstrap.php
|
||||
billing.php
|
||||
footer.php
|
||||
header.php
|
||||
navigation.php
|
||||
|
|
@ -57,7 +84,7 @@ The website uses a central bootstrap instead of scattered relative paths.
|
|||
- `website_order_url(1)`
|
||||
- `documentation_url('minecraft')`
|
||||
|
||||
## Billing and database behavior
|
||||
## Billing, database, and staff behavior
|
||||
|
||||
The public site does not include `Panel/modules/billing/includes/config.inc.php` directly.
|
||||
|
||||
|
|
@ -74,6 +101,20 @@ Effects:
|
|||
- `serverlist.php` shows a clean fallback message instead of a fatal include error
|
||||
- shared navigation never crashes because billing config is missing
|
||||
|
||||
Sales and billing tables are installed through the staff migration runner:
|
||||
|
||||
- `staff_migrations.php`
|
||||
|
||||
The runner is idempotent and uses the current configured Panel table prefix. It
|
||||
creates invoice, order, payment, coupon, password-reset, PayPal webhook, website
|
||||
settings, and provisioning-attempt tables when missing. It also adds missing
|
||||
catalog columns to an existing `billing_services` table.
|
||||
|
||||
Website staff pages are separate from GSP Panel administration. They manage
|
||||
website sales, catalog, pricing, coupons, invoices, payments, PayPal settings,
|
||||
and the paid-order provisioning queue. Access currently requires a Panel admin
|
||||
account.
|
||||
|
||||
## Shared Accounts
|
||||
|
||||
The Panel user table is the identity source for the website. Website login checks
|
||||
|
|
@ -106,6 +147,23 @@ and customer confirmation. Payment approval and final server provisioning remain
|
|||
server-side responsibilities; browser requests must not call private provisioning
|
||||
methods directly.
|
||||
|
||||
Checkout creates invoice/order records only after login or registration. The
|
||||
anonymous cart remains in the website session until that point. PayPal order
|
||||
creation and capture are handled through `api/create_order.php` and
|
||||
`api/capture_order.php`; `webhook.php` verifies PayPal webhook signatures and
|
||||
deduplicates events.
|
||||
|
||||
PayPal credentials must not be committed to source. Preferred runtime settings:
|
||||
|
||||
- `GSP_WEBSITE_PAYPAL_CLIENT_ID`
|
||||
- `GSP_WEBSITE_PAYPAL_CLIENT_SECRET`
|
||||
- `GSP_WEBSITE_PAYPAL_WEBHOOK_ID`
|
||||
|
||||
The staff PayPal page can store fallback settings in `website_settings`, but
|
||||
secrets are masked after save.
|
||||
|
||||
More detail: `docs/modules/website_billing_rebuild.md`.
|
||||
|
||||
## Documentation source
|
||||
|
||||
Customer documentation is read from the existing billing docs directory:
|
||||
|
|
|
|||
8
Panel/modules/website/admin.php
Normal file
8
Panel/modules/website/admin.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_require_staff();
|
||||
header('Location: ' . website_url('staff.php'), true, 302);
|
||||
exit;
|
||||
80
Panel/modules/website/api/capture_order.php
Normal file
80
Panel/modules/website/api/capture_order.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = website_current_user();
|
||||
if (!$user) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'login_required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode((string)file_get_contents('php://input'), true);
|
||||
$invoiceId = (int)($input['invoice_id'] ?? 0);
|
||||
$paypalOrderId = trim((string)($input['order_id'] ?? ''));
|
||||
$config = website_paypal_config();
|
||||
$db = website_db();
|
||||
if (!$config['enabled'] || $paypalOrderId === '' || !$db instanceof mysqli) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$uid = (int)$user['user_id'];
|
||||
$stmt = $db->prepare("SELECT * FROM `{$invoiceTable}` WHERE `invoice_id` = ? AND `user_id` = ? AND `status` = 'due' LIMIT 1");
|
||||
$stmt->bind_param('ii', $invoiceId, $uid);
|
||||
$stmt->execute();
|
||||
$invoice = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
if (!is_array($invoice) || (string)($invoice['paypal_order_id'] ?? '') !== $paypalOrderId) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'invoice_not_found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$access = website_paypal_oauth($config);
|
||||
if (!$access) {
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'paypal_oauth_failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ch = curl_init(website_paypal_api_base($config) . '/v2/checkout/orders/' . rawurlencode($paypalOrderId) . '/capture');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $access],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if (($http !== 200 && $http !== 201) || !is_string($response)) {
|
||||
website_log('PayPal capture failed for invoice ' . $invoiceId);
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'paypal_capture_failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$json = json_decode($response, true);
|
||||
$status = (string)($json['status'] ?? '');
|
||||
$capture = $json['purchase_units'][0]['payments']['captures'][0] ?? [];
|
||||
$captureId = (string)($capture['id'] ?? '');
|
||||
$payerEmail = (string)($json['payer']['email_address'] ?? '');
|
||||
$paidAmount = (float)($capture['amount']['value'] ?? 0);
|
||||
$currency = (string)($capture['amount']['currency_code'] ?? '');
|
||||
|
||||
if ($status !== 'COMPLETED' || $captureId === '' || abs($paidAmount - (float)$invoice['amount']) > 0.01 || $currency !== (string)$invoice['currency']) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => 'payment_not_verified']);
|
||||
exit;
|
||||
}
|
||||
|
||||
website_mark_invoice_paid($invoiceId, 'paypal', $paypalOrderId, $captureId, $payerEmail, ['paypal_status' => $status]);
|
||||
echo json_encode(['status' => 'COMPLETED', 'invoice_id' => $invoiceId]);
|
||||
97
Panel/modules/website/api/create_order.php
Normal file
97
Panel/modules/website/api/create_order.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/includes/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$user = website_current_user();
|
||||
if (!$user) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'login_required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode((string)file_get_contents('php://input'), true);
|
||||
$invoiceId = (int)($input['invoice_id'] ?? 0);
|
||||
$db = website_db();
|
||||
$config = website_paypal_config();
|
||||
|
||||
if (!$config['enabled'] || $config['client_id'] === '' || $config['client_secret'] === '') {
|
||||
http_response_code(503);
|
||||
echo json_encode(['error' => 'paypal_not_configured']);
|
||||
exit;
|
||||
}
|
||||
if (!$db instanceof mysqli || $invoiceId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_invoice']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$stmt = $db->prepare("SELECT * FROM `{$invoiceTable}` WHERE `invoice_id` = ? AND `user_id` = ? AND `status` = 'due' LIMIT 1");
|
||||
$uid = (int)$user['user_id'];
|
||||
$stmt->bind_param('ii', $invoiceId, $uid);
|
||||
$stmt->execute();
|
||||
$invoice = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
if (!is_array($invoice)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'invoice_not_found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$access = website_paypal_oauth($config);
|
||||
if (!$access) {
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'paypal_oauth_failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$amount = number_format((float)$invoice['amount'], 2, '.', '');
|
||||
$currency = (string)$invoice['currency'];
|
||||
$body = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [[
|
||||
'invoice_id' => 'GSW-' . $invoiceId,
|
||||
'custom_id' => (string)$invoiceId,
|
||||
'description' => $config['description_prefix'] . ' #' . $invoiceId,
|
||||
'amount' => ['currency_code' => $currency, 'value' => $amount],
|
||||
]],
|
||||
'application_context' => [
|
||||
'return_url' => website_canonical_url('payment_success.php?invoice_id=' . $invoiceId),
|
||||
'cancel_url' => website_canonical_url('payment_cancel.php?invoice_id=' . $invoiceId),
|
||||
'user_action' => 'PAY_NOW',
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init(website_paypal_api_base($config) . '/v2/checkout/orders');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($body),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $access],
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http !== 201 || !is_string($response)) {
|
||||
website_log('PayPal order create failed for invoice ' . $invoiceId);
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'paypal_order_failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$json = json_decode($response, true);
|
||||
$paypalOrderId = (string)($json['id'] ?? '');
|
||||
if ($paypalOrderId !== '') {
|
||||
$stmt = $db->prepare("UPDATE `{$invoiceTable}` SET `paypal_order_id` = ? WHERE `invoice_id` = ?");
|
||||
$stmt->bind_param('si', $paypalOrderId, $invoiceId);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
echo json_encode(['id' => $paypalOrderId]);
|
||||
|
|
@ -9,13 +9,13 @@ $error = '';
|
|||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
if ($action === 'remove') {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} elseif ($action === 'remove') {
|
||||
website_cart_remove((string)($_POST['cart_key'] ?? ''));
|
||||
header('Location: ' . website_cart_url(), true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'checkout') {
|
||||
} elseif ($action === 'checkout') {
|
||||
if (website_cart_count() === 0) {
|
||||
$error = 'Your cart is empty.';
|
||||
} elseif (!website_is_logged_in()) {
|
||||
|
|
@ -24,8 +24,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
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.';
|
||||
$invoiceId = website_create_invoice_from_cart((int)$_SESSION['website_user_id']);
|
||||
if ($invoiceId === null) {
|
||||
$error = 'We could not create an invoice from this cart. Please contact support.';
|
||||
} else {
|
||||
website_log_activity('Website checkout invoice created #' . $invoiceId, (int)$_SESSION['website_user_id'], 'checkout_requested');
|
||||
header('Location: ' . website_url('checkout.php?invoice_id=' . $invoiceId), true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +45,13 @@ if (isset($_GET['checkout']) && (string)$_GET['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.';
|
||||
$invoiceId = website_create_invoice_from_cart((int)$_SESSION['website_user_id']);
|
||||
if ($invoiceId === null) {
|
||||
$error = 'We could not create an invoice from this cart. Please contact support.';
|
||||
} else {
|
||||
header('Location: ' . website_url('checkout.php?invoice_id=' . $invoiceId), true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
46
Panel/modules/website/checkout.php
Normal file
46
Panel/modules/website/checkout.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$user = website_require_login('cart.php?checkout=1');
|
||||
$invoiceId = filter_input(INPUT_GET, 'invoice_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$invoice = null;
|
||||
$orders = [];
|
||||
$db = website_db();
|
||||
|
||||
if ($db instanceof mysqli && $invoiceId > 0 && website_table_exists(website_table('billing_invoices'))) {
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$orderTable = website_table('billing_orders');
|
||||
$stmt = $db->prepare("SELECT * FROM `{$invoiceTable}` WHERE `invoice_id` = ? AND `user_id` = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$uid = (int)$user['user_id'];
|
||||
$stmt->bind_param('ii', $invoiceId, $uid);
|
||||
$stmt->execute();
|
||||
$invoice = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
}
|
||||
if ($invoice && website_table_exists($orderTable)) {
|
||||
$stmt = $db->prepare("SELECT * FROM `{$orderTable}` WHERE `invoice_id` = ? ORDER BY `order_id` ASC");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('i', $invoiceId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($result instanceof mysqli_result && ($row = $result->fetch_assoc())) {
|
||||
$orders[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
website_render('checkout.php', [
|
||||
'activePage' => 'cart',
|
||||
'pageTitle' => 'Checkout - Gameservers.World',
|
||||
'metaDescription' => 'Pay a Gameservers.World invoice.',
|
||||
'canonicalPath' => 'checkout.php',
|
||||
'invoice' => $invoice,
|
||||
'orders' => $orders,
|
||||
'paypalConfig' => website_paypal_config(),
|
||||
]);
|
||||
54
Panel/modules/website/forgot_password.php
Normal file
54
Panel/modules/website/forgot_password.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$message = '';
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} else {
|
||||
$identifier = trim((string)($_POST['identifier'] ?? ''));
|
||||
$message = 'If an account exists for that username or email, password reset instructions have been sent.';
|
||||
$db = website_db();
|
||||
if ($identifier !== '' && $db instanceof mysqli && website_table_exists(website_table('password_reset_tokens'))) {
|
||||
$usersTable = website_table('users');
|
||||
$stmt = $db->prepare("SELECT `user_id`, `users_email` FROM `{$usersTable}` WHERE `users_login` = ? OR `users_email` = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $identifier, $identifier);
|
||||
$stmt->execute();
|
||||
$user = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
if (is_array($user)) {
|
||||
$rawToken = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $rawToken);
|
||||
$resetTable = website_table('password_reset_tokens');
|
||||
$userId = (int)$user['user_id'];
|
||||
$ip = website_client_ip();
|
||||
$db->query("DELETE FROM `{$resetTable}` WHERE `user_id` = {$userId} AND `used_at` IS NULL");
|
||||
$insert = $db->prepare("INSERT INTO `{$resetTable}` (`user_id`, `token_hash`, `expires_at`, `created_at`, `originating_ip`) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 HOUR), NOW(), ?)");
|
||||
if ($insert) {
|
||||
$insert->bind_param('iss', $userId, $tokenHash, $ip);
|
||||
$insert->execute();
|
||||
$insert->close();
|
||||
website_log_activity('Website password reset requested', $userId, 'password_reset_requested');
|
||||
$resetUrl = website_canonical_url('reset_password.php?token=' . rawurlencode($rawToken));
|
||||
@mail((string)$user['users_email'], 'Gameservers.World password reset', "Use this link to reset your password:\n\n" . $resetUrl . "\n\nThis link expires in 1 hour.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
website_render('forgot_password.php', [
|
||||
'activePage' => 'account',
|
||||
'pageTitle' => 'Forgot Password - Gameservers.World',
|
||||
'metaDescription' => 'Reset your Gameservers.World password.',
|
||||
'canonicalPath' => 'forgot_password.php',
|
||||
'message' => $message,
|
||||
'error' => $error,
|
||||
]);
|
||||
538
Panel/modules/website/includes/billing.php
Normal file
538
Panel/modules/website/includes/billing.php
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (defined('GSP_WEBSITE_BILLING_LOADED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
define('GSP_WEBSITE_BILLING_LOADED', true);
|
||||
|
||||
function website_table(string $baseName): string
|
||||
{
|
||||
return website_table_prefix() . $baseName;
|
||||
}
|
||||
|
||||
function website_client_ip(): string
|
||||
{
|
||||
return substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 64);
|
||||
}
|
||||
|
||||
function website_csrf_token(): string
|
||||
{
|
||||
website_start_session();
|
||||
if (empty($_SESSION['website_csrf_token'])) {
|
||||
$_SESSION['website_csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string)$_SESSION['website_csrf_token'];
|
||||
}
|
||||
|
||||
function website_csrf_field(): string
|
||||
{
|
||||
return '<input type="hidden" name="csrf_token" value="' . website_escape(website_csrf_token()) . '">';
|
||||
}
|
||||
|
||||
function website_verify_csrf(): bool
|
||||
{
|
||||
website_start_session();
|
||||
$token = (string)($_POST['csrf_token'] ?? '');
|
||||
return $token !== '' && hash_equals((string)($_SESSION['website_csrf_token'] ?? ''), $token);
|
||||
}
|
||||
|
||||
function website_require_login(string $returnPath = 'account.php'): ?array
|
||||
{
|
||||
$user = website_current_user();
|
||||
if ($user === null) {
|
||||
$_SESSION['website_login_return'] = website_safe_return_path($returnPath, 'account.php');
|
||||
header('Location: ' . website_login_url($returnPath), true, 302);
|
||||
exit;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
function website_require_staff(): array
|
||||
{
|
||||
$user = website_require_login('staff.php');
|
||||
if ($user === null || !website_current_user_is_staff()) {
|
||||
http_response_code(403);
|
||||
website_render('message.php', [
|
||||
'activePage' => 'account',
|
||||
'pageTitle' => 'Access Denied - Gameservers.World',
|
||||
'heading' => 'Access Denied',
|
||||
'message' => 'This page is available only to authorized website staff.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
function website_column_exists(string $tableName, string $columnName): bool
|
||||
{
|
||||
return isset(website_table_columns($tableName)[$columnName]);
|
||||
}
|
||||
|
||||
function website_billing_migrations(): array
|
||||
{
|
||||
$prefix = website_table_prefix();
|
||||
return [
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}billing_invoices` (
|
||||
`invoice_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`status` VARCHAR(24) NOT NULL DEFAULT 'due',
|
||||
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`currency` CHAR(3) NOT NULL DEFAULT 'USD',
|
||||
`description` VARCHAR(255) DEFAULT NULL,
|
||||
`invoice_date` DATETIME NOT NULL,
|
||||
`due_date` DATETIME DEFAULT NULL,
|
||||
`paid_date` DATETIME DEFAULT NULL,
|
||||
`payment_method` VARCHAR(40) DEFAULT NULL,
|
||||
`payment_txid` VARCHAR(128) DEFAULT NULL,
|
||||
`paypal_order_id` VARCHAR(128) DEFAULT NULL,
|
||||
`paypal_capture_id` VARCHAR(128) DEFAULT NULL,
|
||||
`payer_email` VARCHAR(255) DEFAULT NULL,
|
||||
`coupon_id` INT DEFAULT NULL,
|
||||
`discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`order_id` INT DEFAULT NULL,
|
||||
`metadata_json` TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (`invoice_id`),
|
||||
KEY `idx_invoice_user_status` (`user_id`, `status`),
|
||||
KEY `idx_invoice_payment` (`paypal_order_id`, `paypal_capture_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}billing_orders` (
|
||||
`order_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`invoice_id` INT UNSIGNED DEFAULT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`service_id` INT NOT NULL,
|
||||
`home_id` INT DEFAULT NULL,
|
||||
`home_name` VARCHAR(120) NOT NULL,
|
||||
`remote_server_id` INT DEFAULT NULL,
|
||||
`max_players` INT NOT NULL DEFAULT 0,
|
||||
`qty` INT NOT NULL DEFAULT 1,
|
||||
`invoice_duration` VARCHAR(24) NOT NULL DEFAULT 'month',
|
||||
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'paid',
|
||||
`order_date` DATETIME NOT NULL,
|
||||
`end_date` DATETIME DEFAULT NULL,
|
||||
`payment_txid` VARCHAR(128) DEFAULT NULL,
|
||||
`paid_ts` DATETIME DEFAULT NULL,
|
||||
`provisioning_claim` VARCHAR(64) DEFAULT NULL,
|
||||
`provisioning_error` TEXT DEFAULT NULL,
|
||||
`metadata_json` TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (`order_id`),
|
||||
UNIQUE KEY `uniq_invoice_service` (`invoice_id`, `service_id`, `home_name`),
|
||||
KEY `idx_order_user_status` (`user_id`, `status`),
|
||||
KEY `idx_order_home` (`home_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}billing_payments` (
|
||||
`payment_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`invoice_id` INT UNSIGNED DEFAULT NULL,
|
||||
`user_id` INT DEFAULT NULL,
|
||||
`provider` VARCHAR(40) NOT NULL,
|
||||
`provider_order_id` VARCHAR(128) DEFAULT NULL,
|
||||
`provider_capture_id` VARCHAR(128) DEFAULT NULL,
|
||||
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`currency` CHAR(3) NOT NULL DEFAULT 'USD',
|
||||
`status` VARCHAR(40) NOT NULL,
|
||||
`payer_email` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`metadata_json` TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (`payment_id`),
|
||||
UNIQUE KEY `uniq_provider_capture` (`provider`, `provider_capture_id`),
|
||||
KEY `idx_payment_invoice` (`invoice_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}billing_coupons` (
|
||||
`coupon_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(64) NOT NULL,
|
||||
`name` VARCHAR(120) NOT NULL,
|
||||
`description` TEXT DEFAULT NULL,
|
||||
`discount_type` VARCHAR(16) NOT NULL DEFAULT 'percent',
|
||||
`discount_value` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`starts_at` DATETIME DEFAULT NULL,
|
||||
`expires` DATETIME DEFAULT NULL,
|
||||
`max_uses` INT DEFAULT NULL,
|
||||
`current_uses` INT NOT NULL DEFAULT 0,
|
||||
`minimum_amount` DECIMAL(10,2) DEFAULT NULL,
|
||||
`service_filter_json` TEXT DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`coupon_id`),
|
||||
UNIQUE KEY `uniq_coupon_code` (`code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}website_settings` (
|
||||
`setting_key` VARCHAR(120) NOT NULL,
|
||||
`setting_value` TEXT DEFAULT NULL,
|
||||
`is_secret` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}password_reset_tokens` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`token_hash` CHAR(64) NOT NULL,
|
||||
`expires_at` DATETIME NOT NULL,
|
||||
`used_at` DATETIME DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`originating_ip` VARCHAR(64) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_reset_token_hash` (`token_hash`),
|
||||
KEY `idx_reset_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}payment_webhook_events` (
|
||||
`event_id` VARCHAR(128) NOT NULL,
|
||||
`provider` VARCHAR(40) NOT NULL,
|
||||
`event_type` VARCHAR(120) DEFAULT NULL,
|
||||
`received_at` DATETIME NOT NULL,
|
||||
`processed_at` DATETIME DEFAULT NULL,
|
||||
`status` VARCHAR(40) NOT NULL DEFAULT 'received',
|
||||
`metadata_json` TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (`event_id`, `provider`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}provisioning_attempts` (
|
||||
`attempt_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`order_id` INT UNSIGNED NOT NULL,
|
||||
`status` VARCHAR(40) NOT NULL,
|
||||
`message` TEXT DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`attempt_id`),
|
||||
KEY `idx_provision_order` (`order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
|
||||
];
|
||||
}
|
||||
|
||||
function website_run_billing_migrations(): array
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli) {
|
||||
return ['Database connection is unavailable.'];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach (website_billing_migrations() as $sql) {
|
||||
if (!$db->query($sql)) {
|
||||
$errors[] = $db->error;
|
||||
}
|
||||
}
|
||||
$serviceTable = website_table('billing_services');
|
||||
if (website_table_exists($serviceTable)) {
|
||||
$serviceColumns = [
|
||||
'description' => 'TEXT DEFAULT NULL',
|
||||
'img_url' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'price_monthly' => 'DECIMAL(10,2) NOT NULL DEFAULT 0.00',
|
||||
'slot_min_qty' => 'INT NOT NULL DEFAULT 16',
|
||||
'slot_max_qty' => 'INT NOT NULL DEFAULT 0',
|
||||
'enabled' => 'TINYINT(1) NOT NULL DEFAULT 1',
|
||||
'remote_server_id' => 'VARCHAR(255) DEFAULT NULL',
|
||||
];
|
||||
$existing = website_table_columns($serviceTable);
|
||||
foreach ($serviceColumns as $column => $definition) {
|
||||
if (isset($existing[$column])) {
|
||||
continue;
|
||||
}
|
||||
$safeTable = str_replace('`', '``', $serviceTable);
|
||||
$safeColumn = str_replace('`', '``', $column);
|
||||
if (!$db->query("ALTER TABLE `{$safeTable}` ADD COLUMN `{$safeColumn}` {$definition}")) {
|
||||
$errors[] = $db->error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
function website_setting(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli || !website_table_exists(website_table('website_settings'))) {
|
||||
return $default;
|
||||
}
|
||||
$table = website_table('website_settings');
|
||||
$stmt = $db->prepare("SELECT `setting_value` FROM `{$table}` WHERE `setting_key` = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
return $default;
|
||||
}
|
||||
$stmt->bind_param('s', $key);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
|
||||
$stmt->close();
|
||||
return is_array($row) ? (string)$row['setting_value'] : $default;
|
||||
}
|
||||
|
||||
function website_set_setting(string $key, string $value, bool $secret = false): bool
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli || !website_table_exists(website_table('website_settings'))) {
|
||||
return false;
|
||||
}
|
||||
$table = website_table('website_settings');
|
||||
$isSecret = $secret ? 1 : 0;
|
||||
$stmt = $db->prepare("INSERT INTO `{$table}` (`setting_key`, `setting_value`, `is_secret`, `updated_at`) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE `setting_value` = VALUES(`setting_value`), `is_secret` = VALUES(`is_secret`), `updated_at` = VALUES(`updated_at`)");
|
||||
if (!$stmt) {
|
||||
return false;
|
||||
}
|
||||
$stmt->bind_param('ssi', $key, $value, $isSecret);
|
||||
$ok = $stmt->execute();
|
||||
$stmt->close();
|
||||
return $ok;
|
||||
}
|
||||
|
||||
function website_paypal_config(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => website_setting('paypal_enabled', '0') === '1',
|
||||
'sandbox' => website_setting('paypal_sandbox', '1') === '1',
|
||||
'client_id' => (string)(getenv('GSP_WEBSITE_PAYPAL_CLIENT_ID') ?: website_setting('paypal_client_id', '')),
|
||||
'client_secret' => (string)(getenv('GSP_WEBSITE_PAYPAL_CLIENT_SECRET') ?: website_setting('paypal_client_secret', '')),
|
||||
'webhook_id' => (string)(getenv('GSP_WEBSITE_PAYPAL_WEBHOOK_ID') ?: website_setting('paypal_webhook_id', '')),
|
||||
'currency' => (string)website_setting('paypal_currency', 'USD'),
|
||||
'description_prefix' => (string)website_setting('paypal_description_prefix', 'Gameservers.World order'),
|
||||
];
|
||||
}
|
||||
|
||||
function website_paypal_api_base(array $config): string
|
||||
{
|
||||
return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
}
|
||||
|
||||
function website_paypal_oauth(array $config): ?string
|
||||
{
|
||||
if ($config['client_id'] === '' || $config['client_secret'] === '') {
|
||||
return null;
|
||||
}
|
||||
$ch = curl_init(website_paypal_api_base($config) . '/v1/oauth2/token');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
CURLOPT_USERPWD => $config['client_id'] . ':' . $config['client_secret'],
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($http !== 200 || !is_string($response)) {
|
||||
return null;
|
||||
}
|
||||
$json = json_decode($response, true);
|
||||
return is_array($json) ? (string)($json['access_token'] ?? '') : null;
|
||||
}
|
||||
|
||||
function website_services_have_columns(array $columns): bool
|
||||
{
|
||||
$table = website_table('billing_services');
|
||||
if (!website_table_exists($table)) {
|
||||
return false;
|
||||
}
|
||||
$existing = website_table_columns($table);
|
||||
foreach ($columns as $column) {
|
||||
if (!isset($existing[$column])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function website_fetch_remote_servers(): array
|
||||
{
|
||||
$db = website_db();
|
||||
$table = website_table('remote_servers');
|
||||
if (!$db instanceof mysqli || !website_table_exists($table)) {
|
||||
return [];
|
||||
}
|
||||
$rows = [];
|
||||
$result = @$db->query("SELECT `remote_server_id`, `remote_server_name`, `agent_ip`, `enabled` FROM `{$table}` ORDER BY `remote_server_name` ASC");
|
||||
if ($result instanceof mysqli_result) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
$result->free();
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function website_fetch_invoices_for_user(int $userId): array
|
||||
{
|
||||
$db = website_db();
|
||||
$table = website_table('billing_invoices');
|
||||
if (!$db instanceof mysqli || !website_table_exists($table)) {
|
||||
return [];
|
||||
}
|
||||
$stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `user_id` = ? ORDER BY `invoice_id` DESC");
|
||||
if (!$stmt) {
|
||||
return [];
|
||||
}
|
||||
$stmt->bind_param('i', $userId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$rows = [];
|
||||
while ($result instanceof mysqli_result && ($row = $result->fetch_assoc())) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function website_fetch_orders_for_user(int $userId): array
|
||||
{
|
||||
$db = website_db();
|
||||
$table = website_table('billing_orders');
|
||||
if (!$db instanceof mysqli || !website_table_exists($table)) {
|
||||
return [];
|
||||
}
|
||||
$stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `user_id` = ? ORDER BY `order_id` DESC");
|
||||
if (!$stmt) {
|
||||
return [];
|
||||
}
|
||||
$stmt->bind_param('i', $userId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$rows = [];
|
||||
while ($result instanceof mysqli_result && ($row = $result->fetch_assoc())) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function website_create_invoice_from_cart(int $userId): ?int
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli || !website_table_exists(website_table('billing_invoices')) || !website_table_exists(website_table('billing_orders'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = website_cart_items();
|
||||
if (empty($items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$validItems = [];
|
||||
$total = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$service = website_fetch_service_by_id((int)($item['service_id'] ?? 0));
|
||||
if (!$service) {
|
||||
continue;
|
||||
}
|
||||
$locations = website_service_locations($service);
|
||||
$locationId = (string)($item['location_id'] ?? '');
|
||||
$slots = (int)($item['slots'] ?? 0);
|
||||
$min = website_service_min_slots($service);
|
||||
$max = website_service_max_slots($service);
|
||||
if ($slots < $min || ($max > 0 && $slots > $max) || !isset($locations[$locationId])) {
|
||||
continue;
|
||||
}
|
||||
$price = (float)($service['price_monthly'] ?? 0);
|
||||
$durationMonths = max(1, (int)($item['duration_months'] ?? 1));
|
||||
$line = [
|
||||
'service_id' => (int)$service['service_id'],
|
||||
'service_name' => website_service_name($service),
|
||||
'home_name' => trim((string)($item['server_name'] ?? $item['service_name'] ?? website_service_name($service))),
|
||||
'remote_server_id' => (int)$locationId,
|
||||
'slots' => $slots,
|
||||
'duration_months' => $durationMonths,
|
||||
'price' => $price * $durationMonths,
|
||||
'price_monthly' => $price,
|
||||
];
|
||||
$validItems[] = $line;
|
||||
$total += (float)$line['price'];
|
||||
}
|
||||
if (empty($validItems)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$orderTable = website_table('billing_orders');
|
||||
$currency = website_paypal_config()['currency'] ?: 'USD';
|
||||
$metadata = json_encode(['items' => $validItems], JSON_UNESCAPED_SLASHES);
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
$description = 'Gameservers.World server order';
|
||||
$stmt = $db->prepare("INSERT INTO `{$invoiceTable}` (`user_id`, `status`, `amount`, `currency`, `description`, `invoice_date`, `due_date`, `metadata_json`) VALUES (?, 'due', ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 3 DAY), ?)");
|
||||
if (!$stmt) {
|
||||
throw new RuntimeException('invoice prepare failed');
|
||||
}
|
||||
$stmt->bind_param('idsss', $userId, $total, $currency, $description, $metadata);
|
||||
$stmt->execute();
|
||||
$invoiceId = (int)$db->insert_id;
|
||||
$stmt->close();
|
||||
|
||||
foreach ($validItems as $line) {
|
||||
$status = 'pending_payment';
|
||||
$duration = (string)$line['duration_months'];
|
||||
$stmt = $db->prepare("INSERT INTO `{$orderTable}` (`invoice_id`, `user_id`, `service_id`, `home_name`, `remote_server_id`, `max_players`, `qty`, `invoice_duration`, `price`, `status`, `order_date`, `metadata_json`) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, NOW(), ?)");
|
||||
if (!$stmt) {
|
||||
throw new RuntimeException('order prepare failed');
|
||||
}
|
||||
$lineMeta = json_encode($line, JSON_UNESCAPED_SLASHES);
|
||||
$stmt->bind_param('iiisiisdss', $invoiceId, $userId, $line['service_id'], $line['home_name'], $line['remote_server_id'], $line['slots'], $duration, $line['price'], $status, $lineMeta);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
$_SESSION['website_cart'] = [];
|
||||
website_log_activity('Website invoice created from cart #' . $invoiceId, $userId, 'invoice_created');
|
||||
return $invoiceId;
|
||||
} catch (Throwable $e) {
|
||||
$db->rollback();
|
||||
website_log('Invoice creation failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function website_mark_invoice_paid(int $invoiceId, string $provider, string $providerOrderId, string $captureId, string $payerEmail, array $metadata = []): bool
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli) {
|
||||
return false;
|
||||
}
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$orderTable = website_table('billing_orders');
|
||||
$paymentTable = website_table('billing_payments');
|
||||
if (!website_table_exists($invoiceTable) || !website_table_exists($orderTable) || !website_table_exists($paymentTable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT `user_id`, `amount`, `currency`, `status` FROM `{$invoiceTable}` WHERE `invoice_id` = ? FOR UPDATE");
|
||||
$stmt->bind_param('i', $invoiceId);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
if (!is_array($row) || (string)$row['status'] === 'paid') {
|
||||
$db->commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
$userId = (int)$row['user_id'];
|
||||
$amount = (float)$row['amount'];
|
||||
$currency = (string)$row['currency'];
|
||||
$meta = json_encode($metadata, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$stmt = $db->prepare("UPDATE `{$invoiceTable}` SET `status` = 'paid', `paid_date` = NOW(), `payment_method` = ?, `payment_txid` = ?, `paypal_order_id` = ?, `paypal_capture_id` = ?, `payer_email` = ? WHERE `invoice_id` = ?");
|
||||
$stmt->bind_param('sssssi', $provider, $captureId, $providerOrderId, $captureId, $payerEmail, $invoiceId);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
$paid = 'paid';
|
||||
$provisioning = 'paid';
|
||||
$stmt = $db->prepare("UPDATE `{$orderTable}` SET `status` = ?, `payment_txid` = ?, `paid_ts` = NOW() WHERE `invoice_id` = ? AND `status` IN ('pending_payment', 'paid')");
|
||||
$stmt->bind_param('ssi', $provisioning, $captureId, $invoiceId);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
$stmt = $db->prepare("INSERT IGNORE INTO `{$paymentTable}` (`invoice_id`, `user_id`, `provider`, `provider_order_id`, `provider_capture_id`, `amount`, `currency`, `status`, `payer_email`, `created_at`, `metadata_json`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)");
|
||||
$stmt->bind_param('iisssdssss', $invoiceId, $userId, $provider, $providerOrderId, $captureId, $amount, $currency, $paid, $payerEmail, $meta);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
$db->commit();
|
||||
website_log_activity('Website invoice paid #' . $invoiceId, $userId, 'invoice_paid');
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
$db->rollback();
|
||||
website_log('Payment mark-paid failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,10 @@ foreach ($websiteConfigFiles as $configFile) {
|
|||
}
|
||||
}
|
||||
|
||||
if (is_readable(__DIR__ . '/billing.php')) {
|
||||
require_once __DIR__ . '/billing.php';
|
||||
}
|
||||
|
||||
$websiteDefaults = [
|
||||
'site_name' => 'Gameservers.World',
|
||||
'site_tagline' => 'Developer-backed game hosting for modern and legacy communities, with full server access, daily backups, and optional custom engineering help through Runlevel Systems.',
|
||||
|
|
@ -311,7 +315,7 @@ function website_database_settings(): ?array
|
|||
$merged = array_replace($merged, website_read_php_assignments($billingConfig, $keys));
|
||||
}
|
||||
|
||||
foreach (['db_host', 'db_user', 'db_name', 'table_prefix'] as $requiredKey) {
|
||||
foreach (['db_host', 'db_user', 'db_name'] as $requiredKey) {
|
||||
if (empty($merged[$requiredKey])) {
|
||||
$settings = null;
|
||||
return $settings;
|
||||
|
|
@ -418,7 +422,7 @@ function website_panel_user_by_id(int $userId): ?array
|
|||
{
|
||||
$db = website_db();
|
||||
$prefix = website_table_prefix();
|
||||
if (!$db instanceof mysqli || $prefix === '' || $userId <= 0) {
|
||||
if (!$db instanceof mysqli || $userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -439,7 +443,7 @@ function website_panel_user_by_login(string $login): ?array
|
|||
{
|
||||
$db = website_db();
|
||||
$prefix = website_table_prefix();
|
||||
if (!$db instanceof mysqli || $prefix === '' || $login === '') {
|
||||
if (!$db instanceof mysqli || $login === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -532,7 +536,7 @@ function website_log_activity(string $message, int $userId = 0, string $eventTyp
|
|||
{
|
||||
$db = website_db();
|
||||
$prefix = website_table_prefix();
|
||||
if (!$db instanceof mysqli || $prefix === '') {
|
||||
if (!$db instanceof mysqli) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -602,14 +606,18 @@ function website_checkout_url(): string
|
|||
|
||||
function website_register_url(string $returnPath = 'cart.php'): string
|
||||
{
|
||||
return panel_url('index.php?m=register');
|
||||
$path = 'register.php';
|
||||
if ($returnPath !== '') {
|
||||
$path .= '?return=' . rawurlencode(website_safe_return_path($returnPath, 'cart.php'));
|
||||
}
|
||||
return website_url($path);
|
||||
}
|
||||
|
||||
function website_fetch_service_by_id(int $serviceId): ?array
|
||||
{
|
||||
$db = website_db();
|
||||
$prefix = website_table_prefix();
|
||||
if (!$db instanceof mysqli || $prefix === '' || $serviceId <= 0) {
|
||||
if (!$db instanceof mysqli || $serviceId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -658,7 +666,7 @@ function website_service_name(array $service): string
|
|||
|
||||
function website_service_min_slots(array $service): int
|
||||
{
|
||||
foreach (['min_slots', 'minimum_slots', 'slots_min'] as $column) {
|
||||
foreach (['slot_min_qty', 'min_slots', 'minimum_slots', 'slots_min'] as $column) {
|
||||
if (isset($service[$column]) && (int)$service[$column] > 0) {
|
||||
return (int)$service[$column];
|
||||
}
|
||||
|
|
@ -670,7 +678,7 @@ function website_service_min_slots(array $service): int
|
|||
|
||||
function website_service_max_slots(array $service): int
|
||||
{
|
||||
foreach (['max_slots', 'maximum_slots', 'slots_max', 'max_players'] as $column) {
|
||||
foreach (['slot_max_qty', 'max_slots', 'maximum_slots', 'slots_max', 'max_players'] as $column) {
|
||||
if (isset($service[$column]) && (int)$service[$column] > 0) {
|
||||
return (int)$service[$column];
|
||||
}
|
||||
|
|
@ -687,7 +695,7 @@ function website_service_locations(array $service): array
|
|||
}
|
||||
|
||||
$locations = [];
|
||||
foreach (preg_split('/\s*,\s*/', $raw) ?: [] as $remoteServerId) {
|
||||
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $remoteServerId) {
|
||||
$remoteServerId = trim($remoteServerId);
|
||||
if ($remoteServerId === '' || !ctype_digit($remoteServerId)) {
|
||||
continue;
|
||||
|
|
@ -732,7 +740,7 @@ function website_cart_total(): float
|
|||
{
|
||||
$total = 0.0;
|
||||
foreach (website_cart_items() as $item) {
|
||||
$total += (float)($item['monthly_total'] ?? 0);
|
||||
$total += (float)($item['line_total'] ?? $item['monthly_total'] ?? 0);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
|
@ -805,7 +813,7 @@ function website_service_image_url(string $imageValue): string
|
|||
return website_asset('images/games/' . $fileName);
|
||||
}
|
||||
|
||||
function website_fetch_services(int $limit = 0): array
|
||||
function website_fetch_services(int $limit = 0, bool $includeDisabled = false): array
|
||||
{
|
||||
$db = website_db();
|
||||
if (!$db instanceof mysqli) {
|
||||
|
|
@ -813,24 +821,16 @@ function website_fetch_services(int $limit = 0): array
|
|||
}
|
||||
|
||||
$prefix = website_table_prefix();
|
||||
if ($prefix === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = "SELECT bs.service_id,
|
||||
bs.service_name,
|
||||
bs.description,
|
||||
bs.img_url,
|
||||
bs.price_monthly,
|
||||
bs.remote_server_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 `{$prefix}billing_services` bs
|
||||
LEFT JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
|
||||
WHERE bs.enabled = 1
|
||||
WHERE " . ($includeDisabled ? '1 = 1' : "bs.enabled = 1
|
||||
AND bs.remote_server_id <> ''
|
||||
AND bs.remote_server_id IS NOT NULL
|
||||
AND bs.remote_server_id IS NOT NULL") . "
|
||||
ORDER BY bs.service_name ASC";
|
||||
|
||||
if ($limit > 0) {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ $currentUser = website_current_user();
|
|||
<ul class="footer-links">
|
||||
<?php if ($currentUser): ?>
|
||||
<li><a href="<?= website_escape(website_url('account.php')) ?>">My Account</a></li>
|
||||
<li><a href="<?= website_escape(website_url('orders.php')) ?>">My Orders</a></li>
|
||||
<li><a href="<?= website_escape(website_url('invoices.php')) ?>">My Invoices</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(panel_url('home.php?m=gamemanager&p=game_monitor')) ?>">My Servers</a></li>
|
||||
<li><a href="<?= website_escape(website_url('my_servers.php')) ?>">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: ?>
|
||||
|
|
@ -53,7 +55,7 @@ $currentUser = website_current_user();
|
|||
<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(panel_url('home.php?m=administration&p=watch_logger')) ?>">Staff Tools</a></li>
|
||||
<li><a href="<?= website_escape(website_url('staff.php')) ?>">Staff Dashboard</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if ($discordUrl !== ''): ?>
|
||||
<li><a href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a></li>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@ $navLinks = [
|
|||
];
|
||||
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')];
|
||||
$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')];
|
||||
}
|
||||
} else {
|
||||
$navLinks[] = ['key' => 'account', 'label' => 'Login', 'href' => website_login_url()];
|
||||
$navLinks[] = ['key' => 'register', 'label' => 'Create Account', 'href' => website_register_url()];
|
||||
|
|
|
|||
5
Panel/modules/website/invoices.php
Normal file
5
Panel/modules/website/invoices.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
$user = website_require_login('invoices.php');
|
||||
website_render('invoices.php', ['activePage'=>'invoices','pageTitle'=>'My Invoices - Gameservers.World','metaDescription'=>'View your Gameservers.World invoices.','canonicalPath'=>'invoices.php','invoices'=>website_fetch_invoices_for_user((int)$user['user_id'])]);
|
||||
|
|
@ -17,6 +17,9 @@ if (website_is_logged_in()) {
|
|||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} else {
|
||||
$login = trim((string)($_POST['login'] ?? ''));
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
$user = website_authenticate_user($login, $password);
|
||||
|
|
@ -37,6 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|
||||
website_log_activity('Website login failed for ' . $login, 0, 'website_login_failure');
|
||||
$error = 'Invalid username or password.';
|
||||
}
|
||||
}
|
||||
|
||||
website_render(
|
||||
|
|
|
|||
15
Panel/modules/website/my_servers.php
Normal file
15
Panel/modules/website/my_servers.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$user = website_require_login('my_servers.php');
|
||||
$orders = website_fetch_orders_for_user((int)$user['user_id']);
|
||||
|
||||
website_render('my_servers.php', [
|
||||
'activePage' => 'servers',
|
||||
'pageTitle' => 'My Servers - Gameservers.World',
|
||||
'canonicalPath' => 'my_servers.php',
|
||||
'user' => $user,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
|
|
@ -33,7 +33,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$locationId = trim((string)($_POST['location_id'] ?? ''));
|
||||
$durationMonths = filter_input(INPUT_POST, 'duration_months', FILTER_VALIDATE_INT);
|
||||
|
||||
if ($slots === false || $slots === null || $slots < $minSlots) {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} elseif ($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.';
|
||||
|
|
@ -44,15 +46,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
} else {
|
||||
$priceMonthly = (float)($service['price_monthly'] ?? 0);
|
||||
$monthlyTotal = $priceMonthly > 0 ? $priceMonthly : 0.0;
|
||||
$lineTotal = $monthlyTotal * (int)$durationMonths;
|
||||
website_cart_add([
|
||||
'service_id' => (int)$service['service_id'],
|
||||
'service_name' => website_service_name($service),
|
||||
'server_name' => trim((string)($_POST['server_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,
|
||||
'line_total' => $lineTotal,
|
||||
'added_at' => time(),
|
||||
]);
|
||||
|
||||
|
|
|
|||
5
Panel/modules/website/orders.php
Normal file
5
Panel/modules/website/orders.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
$user = website_require_login('orders.php');
|
||||
website_render('orders.php', ['activePage'=>'orders','pageTitle'=>'My Orders - Gameservers.World','metaDescription'=>'View your Gameservers.World orders.','canonicalPath'=>'orders.php','orders'=>website_fetch_orders_for_user((int)$user['user_id'])]);
|
||||
|
|
@ -32,11 +32,14 @@ declare(strict_types=1);
|
|||
<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>Server name: <?= website_escape((string)($item['server_name'] ?? $item['service_name'] ?? '')) ?></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>
|
||||
<p>Due at checkout: <?= ((float)($item['line_total'] ?? $item['monthly_total'] ?? 0) > 0) ? '$' . website_escape(number_format((float)($item['line_total'] ?? $item['monthly_total']), 2)) : 'Contact for pricing' ?></p>
|
||||
<form method="post" action="<?= website_escape(website_cart_url()) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<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>
|
||||
|
|
@ -47,10 +50,11 @@ declare(strict_types=1);
|
|||
|
||||
<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>Estimated amount due: <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()) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<input type="hidden" name="action" value="checkout">
|
||||
<button class="button button-primary" type="submit">Proceed to Checkout</button>
|
||||
</form>
|
||||
|
|
|
|||
50
Panel/modules/website/pages/checkout.php
Normal file
50
Panel/modules/website/pages/checkout.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Checkout</h1><p>Review your invoice and complete payment.</p></div></section>
|
||||
<section class="section"><div class="container">
|
||||
<?php if (!$invoice): ?>
|
||||
<div class="empty-state"><h2>Invoice unavailable</h2><p>We could not load this invoice for your account.</p><a class="button button-primary" href="<?= website_escape(website_cart_url()) ?>">Return to Cart</a></div>
|
||||
<?php else: ?>
|
||||
<div class="summary-grid">
|
||||
<article class="summary-card"><h3>Invoice #<?= website_escape((string)$invoice['invoice_id']) ?></h3><p>Status: <?= website_escape((string)$invoice['status']) ?></p><p>Total: <strong><?= website_escape((string)$invoice['currency']) ?> <?= website_escape(number_format((float)$invoice['amount'], 2)) ?></strong></p></article>
|
||||
<article class="summary-card"><h3>Payment</h3><p>PayPal is <?= !empty($paypalConfig['enabled']) ? 'enabled' : 'not enabled' ?>.</p><p class="muted">Payment is verified server-side before orders enter provisioning.</p></article>
|
||||
</div>
|
||||
<div class="summary-card" style="margin-top:18px;">
|
||||
<h3>Items</h3>
|
||||
<?php foreach ($orders as $order): ?><p><?= website_escape((string)$order['home_name']) ?> - <?= website_escape((string)$order['max_players']) ?> slots - status <?= website_escape((string)$order['status']) ?></p><?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ((string)$invoice['status'] === 'paid'): ?>
|
||||
<div class="alert info" style="margin-top:18px;">This invoice has already been paid.</div>
|
||||
<?php elseif (!empty($paypalConfig['enabled']) && $paypalConfig['client_id'] !== ''): ?>
|
||||
<div class="summary-card" style="margin-top:18px;">
|
||||
<h3>PayPal Checkout</h3>
|
||||
<div id="paypal-button-container"></div>
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=<?= website_escape($paypalConfig['client_id']) ?>¤cy=<?= website_escape($paypalConfig['currency']) ?>&intent=capture"></script>
|
||||
<script>
|
||||
paypal.Buttons({
|
||||
createOrder: function() {
|
||||
return fetch('<?= website_escape(website_url('api/create_order.php')) ?>', {
|
||||
method: 'post',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({invoice_id: <?= (int)$invoice['invoice_id'] ?>})
|
||||
}).then(function(res) { return res.json(); }).then(function(data) {
|
||||
if (!data.id) { throw new Error(data.error || 'Could not create PayPal order'); }
|
||||
return data.id;
|
||||
});
|
||||
},
|
||||
onApprove: function(data) {
|
||||
return fetch('<?= website_escape(website_url('api/capture_order.php')) ?>', {
|
||||
method: 'post',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({invoice_id: <?= (int)$invoice['invoice_id'] ?>, order_id: data.orderID})
|
||||
}).then(function(res) { return res.json(); }).then(function(result) {
|
||||
window.location.href = '<?= website_escape(website_url('payment_success.php?invoice_id=' . (int)$invoice['invoice_id'])) ?>';
|
||||
});
|
||||
}
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert warning" style="margin-top:18px;">Online payment is not enabled yet. Contact support to complete this invoice.</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div></section>
|
||||
12
Panel/modules/website/pages/forgot_password.php
Normal file
12
Panel/modules/website/pages/forgot_password.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Forgot Password</h1><p>Enter your username or email address.</p></div></section>
|
||||
<section class="section"><div class="container narrow-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; ?>
|
||||
<form class="website-form" method="post" action="<?= website_escape(website_url('forgot_password.php')) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<label for="identifier">Username or Email</label>
|
||||
<input id="identifier" name="identifier" type="text" required>
|
||||
<button class="button button-primary" type="submit">Send Reset Link</button>
|
||||
</form>
|
||||
</div></section>
|
||||
7
Panel/modules/website/pages/invoices.php
Normal file
7
Panel/modules/website/pages/invoices.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>My Invoices</h1><p>Invoices for your Gameservers.World account.</p></div></section>
|
||||
<section class="section"><div class="container">
|
||||
<?php if (empty($invoices)): ?><div class="empty-state"><h2>No invoices yet</h2><p>Your invoices will appear here after checkout.</p></div><?php else: ?>
|
||||
<div class="summary-grid"><?php foreach ($invoices as $invoice): ?><article class="summary-card"><h3>Invoice #<?= website_escape((string)$invoice['invoice_id']) ?></h3><p>Status: <?= website_escape((string)$invoice['status']) ?></p><p><?= website_escape((string)$invoice['currency']) ?> <?= website_escape(number_format((float)$invoice['amount'], 2)) ?></p><a class="button button-primary" href="<?= website_escape(website_url('checkout.php?invoice_id=' . (int)$invoice['invoice_id'])) ?>">View Invoice</a></article><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
</div></section>
|
||||
|
|
@ -15,6 +15,7 @@ declare(strict_types=1);
|
|||
<div class="alert warning"><?= website_escape($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form class="website-form" method="post" action="<?= website_escape(website_url('login.php')) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<input type="hidden" name="return" value="<?= website_escape($returnPath) ?>">
|
||||
<label for="login">Username</label>
|
||||
<input id="login" name="login" type="text" autocomplete="username" required>
|
||||
|
|
@ -22,6 +23,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><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>
|
||||
</div>
|
||||
|
|
|
|||
19
Panel/modules/website/pages/message.php
Normal file
19
Panel/modules/website/pages/message.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1><?= website_escape($heading ?? 'Notice') ?></h1>
|
||||
<p><?= website_escape($message ?? '') ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="card-actions">
|
||||
<a class="button button-primary" href="<?= website_escape(website_url('index.php')) ?>">Home</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
54
Panel/modules/website/pages/my_servers.php
Normal file
54
Panel/modules/website/pages/my_servers.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$orders = is_array($orders ?? null) ? $orders : [];
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>My Servers</h1>
|
||||
<p>Review website orders tied to your account. Use the GSP control panel for live server controls, files, logs, backups, and operational management.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="card-actions" style="margin-bottom: 1rem;">
|
||||
<a class="button button-primary" href="<?= website_escape(panel_url('home.php?m=gamemanager&p=game_monitor')) ?>">Open GSP Control Panel</a>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Order Another Server</a>
|
||||
</div>
|
||||
|
||||
<?php if (empty($orders)): ?>
|
||||
<div class="notice-card">
|
||||
<h2>No Website Server Orders Yet</h2>
|
||||
<p>Orders created through Gameservers.World will appear here after checkout. Existing Panel-assigned servers remain available in the GSP control panel.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Server</th>
|
||||
<th>Status</th>
|
||||
<th>Slots</th>
|
||||
<th>Home ID</th>
|
||||
<th>Ordered</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td>#<?= (int)($order['order_id'] ?? 0) ?></td>
|
||||
<td><?= website_escape($order['home_name'] ?? 'Game Server') ?></td>
|
||||
<td><?= website_escape($order['status'] ?? 'unknown') ?></td>
|
||||
<td><?= (int)($order['max_players'] ?? 0) ?></td>
|
||||
<td><?= (int)($order['home_id'] ?? 0) ?: 'Pending' ?></td>
|
||||
<td><?= website_escape($order['order_date'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -55,12 +55,16 @@ $selectedSlots = max((int)$minSlots, (int)($_POST['slots'] ?? $minSlots));
|
|||
<article class="summary-card">
|
||||
<h3>Checkout boundary</h3>
|
||||
<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>
|
||||
<p class="muted">Return to the game-server catalog or contact support if this package should be available.</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<form class="website-form summary-card" method="post" action="<?= website_escape(website_order_url((int)$service['service_id'])) ?>" style="margin-top: 18px;">
|
||||
<?= website_csrf_field() ?>
|
||||
<h3>Server configuration</h3>
|
||||
<label for="server_name">Server Name</label>
|
||||
<input id="server_name" name="server_name" type="text" maxlength="120" value="<?= website_escape((string)($_POST['server_name'] ?? $serviceName)) ?>" required>
|
||||
|
||||
<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>
|
||||
|
||||
|
|
|
|||
7
Panel/modules/website/pages/orders.php
Normal file
7
Panel/modules/website/pages/orders.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>My Orders</h1><p>Server orders associated with your shared account.</p></div></section>
|
||||
<section class="section"><div class="container">
|
||||
<?php if (empty($orders)): ?><div class="empty-state"><h2>No orders yet</h2><p>Paid server orders will appear here.</p></div><?php else: ?>
|
||||
<div class="summary-grid"><?php foreach ($orders as $order): ?><article class="summary-card"><h3><?= website_escape((string)$order['home_name']) ?></h3><p>Order #<?= website_escape((string)$order['order_id']) ?></p><p>Status: <?= website_escape((string)$order['status']) ?></p><p>Home ID: <?= website_escape((string)($order['home_id'] ?? 'pending')) ?></p></article><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
</div></section>
|
||||
28
Panel/modules/website/pages/register.php
Normal file
28
Panel/modules/website/pages/register.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Create Account</h1>
|
||||
<p>Create one account for Gameservers.World orders and GSP Panel access.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="container narrow-container">
|
||||
<?php if ($error !== ''): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
|
||||
<form class="website-form" method="post" action="<?= website_escape(website_url('register.php')) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<input type="hidden" name="return" value="<?= website_escape($returnPath) ?>">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" required>
|
||||
<button class="button button-primary" type="submit">Create Account</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
19
Panel/modules/website/pages/reset_password.php
Normal file
19
Panel/modules/website/pages/reset_password.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Reset Password</h1><p>Choose a new password for your shared account.</p></div></section>
|
||||
<section class="section"><div class="container narrow-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 ($valid): ?>
|
||||
<form class="website-form" method="post" action="<?= website_escape(website_url('reset_password.php')) ?>">
|
||||
<?= website_csrf_field() ?>
|
||||
<input type="hidden" name="token" value="<?= website_escape($token) ?>">
|
||||
<label for="password">New Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" required>
|
||||
<button class="button button-primary" type="submit">Reset Password</button>
|
||||
</form>
|
||||
<?php elseif ($message === ''): ?>
|
||||
<div class="alert warning">This reset link is invalid or expired.</div>
|
||||
<?php endif; ?>
|
||||
</div></section>
|
||||
18
Panel/modules/website/pages/staff.php
Normal file
18
Panel/modules/website/pages/staff.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Website Staff Dashboard</h1><p>Manage the sales website, catalog, billing, payments, and provisioning queue.</p></div></section>
|
||||
<section class="section"><div class="container"><div class="summary-grid">
|
||||
<?php foreach ([
|
||||
['Manage Game Services','staff_services.php','Catalog, prices, slot limits, images, and locations.'],
|
||||
['Manage Server Locations','staff_locations.php','Panel remote-server locations available to website catalog services.'],
|
||||
['Manage Coupons','staff_coupons.php','Discount codes and usage limits.'],
|
||||
['Manage Orders','staff_orders.php','Customer order state and provisioning status.'],
|
||||
['Manage Invoices','staff_invoices.php','Invoice status and payment references.'],
|
||||
['Manage Payments','staff_payments.php','Provider captures and payment audit records.'],
|
||||
['PayPal Settings','staff_paypal.php','Protected payment configuration.'],
|
||||
['Website Settings','staff_settings.php','Public URLs, project links, and deployment configuration notes.'],
|
||||
['Provisioning Queue','staff_provisioning.php','Paid orders waiting for installation or retry.'],
|
||||
['Run Migrations','staff_migrations.php','Create or update website billing tables.'],
|
||||
] as $card): ?>
|
||||
<article class="summary-card"><h3><?= website_escape($card[0]) ?></h3><p><?= website_escape($card[2]) ?></p><a class="button button-primary" href="<?= website_escape(website_url($card[1])) ?>">Open</a></article>
|
||||
<?php endforeach; ?>
|
||||
</div></div></section>
|
||||
7
Panel/modules/website/pages/staff_coupons.php
Normal file
7
Panel/modules/website/pages/staff_coupons.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Manage Coupons</h1><p>Create and update percentage discount codes.</p></div></section>
|
||||
<section class="section"><div class="container">
|
||||
<?php if($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?><?php if($error): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
|
||||
<form class="website-form summary-card" method="post"><?= website_csrf_field() ?><h3>Add or Update Coupon</h3><label>Code</label><input name="code" required><label>Name</label><input name="name" required><label>Discount Percent</label><input name="discount_value" type="number" step="0.01" min="0" max="100" required><label>Max Uses</label><input name="max_uses" type="number" min="0"><label><input type="checkbox" name="is_active" value="1" checked> Active</label><button class="button button-primary" type="submit">Save Coupon</button></form>
|
||||
<div class="summary-grid" style="margin-top:18px;"><?php foreach($coupons as $c): ?><article class="summary-card"><h3><?= website_escape((string)$c['code']) ?></h3><p><?= website_escape((string)$c['name']) ?></p><p><?= website_escape((string)$c['discount_value']) ?>% off</p><p>Uses: <?= website_escape((string)$c['current_uses']) ?> / <?= website_escape((string)($c['max_uses'] ?? 'unlimited')) ?></p><p>Status: <?= ((int)$c['is_active']===1?'active':'inactive') ?></p></article><?php endforeach; ?></div>
|
||||
</div></section>
|
||||
2
Panel/modules/website/pages/staff_invoices.php
Normal file
2
Panel/modules/website/pages/staff_invoices.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Invoices</h1><p>Latest website invoices.</p></div></section><section class="section"><div class="container"><div class="summary-grid"><?php foreach($invoices as $i): ?><article class="summary-card"><h3>#<?= website_escape((string)$i['invoice_id']) ?> <?= website_escape((string)$i['status']) ?></h3><p>User: <?= website_escape((string)$i['user_id']) ?></p><p><?= website_escape((string)$i['currency']) ?> <?= website_escape(number_format((float)$i['amount'],2)) ?></p><p>Payment: <?= website_escape((string)($i['payment_txid'] ?? '')) ?></p></article><?php endforeach; ?></div></div></section>
|
||||
45
Panel/modules/website/pages/staff_locations.php
Normal file
45
Panel/modules/website/pages/staff_locations.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$remoteServers = is_array($remoteServers ?? null) ? $remoteServers : [];
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Server Locations</h1>
|
||||
<p>Review Panel remote-server locations available to website catalog services. Assign locations to services from Manage Game Services.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php if (empty($remoteServers)): ?>
|
||||
<div class="notice-card">
|
||||
<h2>No Remote Servers Found</h2>
|
||||
<p>The website could not read configured Panel remote servers from the current database.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>IP/Host</th>
|
||||
<th>Enabled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($remoteServers as $server): ?>
|
||||
<tr>
|
||||
<td><?= (int)($server['remote_server_id'] ?? 0) ?></td>
|
||||
<td><?= website_escape($server['remote_server_name'] ?? '') ?></td>
|
||||
<td><?= website_escape($server['agent_ip'] ?? '') ?></td>
|
||||
<td><?= ((int)($server['enabled'] ?? 0) === 1) ? 'Yes' : 'No' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
7
Panel/modules/website/pages/staff_migrations.php
Normal file
7
Panel/modules/website/pages/staff_migrations.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Website Billing Migrations</h1><p>Create missing website billing, order, payment, settings, reset, webhook, and provisioning tables using the current table prefix.</p></div></section>
|
||||
<section class="section"><div class="container narrow-container">
|
||||
<?php if ($ran): ?><div class="alert <?= empty($errors) ? 'info' : 'warning' ?>"><?= empty($errors) ? 'Migrations completed.' : website_escape(implode(' ', $errors)) ?></div><?php endif; ?>
|
||||
<form class="website-form" method="post"><?= website_csrf_field() ?><button class="button button-primary" type="submit">Run Idempotent Migrations</button></form>
|
||||
<p class="muted">Rollback: export/drop the created website billing tables only after confirming no customer data must be preserved.</p>
|
||||
</div></section>
|
||||
2
Panel/modules/website/pages/staff_orders.php
Normal file
2
Panel/modules/website/pages/staff_orders.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Orders</h1><p>Latest website orders.</p></div></section><section class="section"><div class="container"><div class="summary-grid"><?php foreach($orders as $o): ?><article class="summary-card"><h3>#<?= website_escape((string)$o['order_id']) ?> <?= website_escape((string)$o['home_name']) ?></h3><p>Status: <?= website_escape((string)$o['status']) ?></p><p>User: <?= website_escape((string)$o['user_id']) ?></p><p>Home ID: <?= website_escape((string)($o['home_id'] ?? 'pending')) ?></p></article><?php endforeach; ?></div></div></section>
|
||||
2
Panel/modules/website/pages/staff_payments.php
Normal file
2
Panel/modules/website/pages/staff_payments.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Payments</h1><p>Latest payment records.</p></div></section><section class="section"><div class="container"><div class="summary-grid"><?php foreach($payments as $p): ?><article class="summary-card"><h3>#<?= website_escape((string)$p['payment_id']) ?> <?= website_escape((string)$p['provider']) ?></h3><p>Status: <?= website_escape((string)$p['status']) ?></p><p><?= website_escape((string)$p['currency']) ?> <?= website_escape(number_format((float)$p['amount'],2)) ?></p><p>Capture: <?= website_escape((string)($p['provider_capture_id'] ?? '')) ?></p></article><?php endforeach; ?></div></div></section>
|
||||
15
Panel/modules/website/pages/staff_paypal.php
Normal file
15
Panel/modules/website/pages/staff_paypal.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>PayPal Settings</h1><p>Store active payment settings without exposing secrets in source code.</p></div></section>
|
||||
<section class="section"><div class="container narrow-container">
|
||||
<?php if($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?><?php if($error): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
|
||||
<form class="website-form" method="post"><?= website_csrf_field() ?>
|
||||
<label><input type="checkbox" name="enabled" value="1" <?= $config['enabled']?'checked':'' ?>> Enable PayPal</label>
|
||||
<label><input type="checkbox" name="sandbox" value="1" <?= $config['sandbox']?'checked':'' ?>> Sandbox mode</label>
|
||||
<label>Client ID</label><input name="client_id" value="<?= website_escape($config['client_id']) ?>">
|
||||
<label>Client Secret</label><input name="client_secret" type="password" placeholder="<?= $config['client_secret']!==''?'Saved - enter a new value to replace':'Not configured' ?>">
|
||||
<label>Webhook ID</label><input name="webhook_id" type="password" placeholder="<?= $config['webhook_id']!==''?'Saved - enter a new value to replace':'Not configured' ?>">
|
||||
<label>Currency</label><input name="currency" maxlength="3" value="<?= website_escape($config['currency']) ?>">
|
||||
<label>Description Prefix</label><input name="description_prefix" value="<?= website_escape($config['description_prefix']) ?>">
|
||||
<button class="button button-primary" type="submit">Save PayPal Settings</button>
|
||||
</form><p class="muted">Environment variables override stored values: GSP_WEBSITE_PAYPAL_CLIENT_ID, GSP_WEBSITE_PAYPAL_CLIENT_SECRET, GSP_WEBSITE_PAYPAL_WEBHOOK_ID.</p>
|
||||
</div></section>
|
||||
2
Panel/modules/website/pages/staff_provisioning.php
Normal file
2
Panel/modules/website/pages/staff_provisioning.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Provisioning Queue</h1><p>Paid and failed orders awaiting Panel provisioning integration.</p></div></section><section class="section"><div class="container"><?php if($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?><div class="summary-grid"><?php foreach($orders as $o): ?><article class="summary-card"><h3>#<?= website_escape((string)$o['order_id']) ?> <?= website_escape((string)$o['home_name']) ?></h3><p>Status: <?= website_escape((string)$o['status']) ?></p><p>Service: <?= website_escape((string)$o['service_id']) ?> Location: <?= website_escape((string)($o['remote_server_id'] ?? '')) ?></p><p><?= website_escape((string)($o['provisioning_error'] ?? '')) ?></p><?php if(in_array((string)$o['status'], ['failed','paid'], true)): ?><form method="post"><?= website_csrf_field() ?><input type="hidden" name="order_id" value="<?= (int)$o['order_id'] ?>"><button class="button button-secondary" type="submit">Queue Retry</button></form><?php endif; ?></article><?php endforeach; ?></div></div></section>
|
||||
18
Panel/modules/website/pages/staff_services.php
Normal file
18
Panel/modules/website/pages/staff_services.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php declare(strict_types=1); ?>
|
||||
<section class="page-heading"><div class="container"><h1>Manage Game Services</h1><p>Update public catalog status, prices, slot limits, images, and location availability.</p></div></section>
|
||||
<section class="section"><div class="container">
|
||||
<?php if ($message): ?><div class="alert info"><?= website_escape($message) ?></div><?php endif; ?><?php if ($error): ?><div class="alert warning"><?= website_escape($error) ?></div><?php endif; ?>
|
||||
<?php if (empty($services)): ?><div class="empty-state"><h2>No services found</h2><p>Run migrations and import or create billing services before editing catalog data.</p></div><?php else: ?>
|
||||
<form method="post" class="website-form"><?= website_csrf_field() ?><div class="summary-grid">
|
||||
<?php foreach ($services as $service): $sid=(int)$service['service_id']; $selected=array_flip(preg_split('/\s+/', trim((string)$service['remote_server_id'])) ?: []); ?>
|
||||
<article class="summary-card"><h3>#<?= $sid ?> <?= website_escape(website_service_name($service)) ?></h3>
|
||||
<label><input type="checkbox" name="service[<?= $sid ?>][enabled]" value="1" <?= ((int)($service['enabled']??1)===1?'checked':'') ?>> Enabled</label>
|
||||
<label>Name</label><input name="service[<?= $sid ?>][service_name]" value="<?= website_escape((string)($service['service_name']??'')) ?>">
|
||||
<label>Description</label><textarea name="service[<?= $sid ?>][description]"><?= website_escape((string)($service['description']??'')) ?></textarea>
|
||||
<label>Min Slots</label><input type="number" name="service[<?= $sid ?>][slot_min_qty]" value="<?= website_escape((string)website_service_min_slots($service)) ?>">
|
||||
<label>Max Slots</label><input type="number" name="service[<?= $sid ?>][slot_max_qty]" value="<?= website_escape((string)website_service_max_slots($service)) ?>">
|
||||
<label>Monthly Price</label><input type="number" step="0.01" name="service[<?= $sid ?>][price_monthly]" value="<?= website_escape((string)($service['price_monthly']??0)) ?>">
|
||||
<label>Image URL</label><input name="service[<?= $sid ?>][img_url]" value="<?= website_escape((string)($service['img_url']??'')) ?>">
|
||||
<label>Locations</label><?php foreach ($remoteServers as $rs): $rid=(string)$rs['remote_server_id']; ?><label><input type="checkbox" name="service[<?= $sid ?>][locations][]" value="<?= website_escape($rid) ?>" <?= isset($selected[$rid])?'checked':'' ?>> <?= website_escape((string)$rs['remote_server_name']) ?></label><?php endforeach; ?>
|
||||
</article><?php endforeach; ?></div><button class="button button-primary" type="submit">Save Services</button></form><?php endif; ?>
|
||||
</div></section>
|
||||
33
Panel/modules/website/pages/staff_settings.php
Normal file
33
Panel/modules/website/pages/staff_settings.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
?>
|
||||
<section class="page-heading">
|
||||
<div class="container">
|
||||
<h1>Website Settings</h1>
|
||||
<p>Review key public website settings. Edit deployment-specific values in protected website configuration files or approved staff pages.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="summary-grid">
|
||||
<article class="summary-card">
|
||||
<h3>Public URL</h3>
|
||||
<p><?= website_escape(website_public_base_url() ?: 'Auto-detected from request') ?></p>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>Panel URL</h3>
|
||||
<p><?= website_escape(panel_url()) ?></p>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>Project Requests</h3>
|
||||
<p><?= website_escape(website_custom_project_url()) ?></p>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<h3>PayPal</h3>
|
||||
<p>Use the PayPal Settings page for payment configuration. Secrets are not displayed here.</p>
|
||||
<a class="button button-secondary" href="<?= website_escape(website_url('staff_paypal.php')) ?>">Open PayPal Settings</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
4
Panel/modules/website/payment_cancel.php
Normal file
4
Panel/modules/website/payment_cancel.php
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
website_render('message.php', ['activePage'=>'cart','pageTitle'=>'Payment Cancelled - Gameservers.World','heading'=>'Payment Cancelled','message'=>'Your payment was cancelled. Your invoice remains available if you want to try again.']);
|
||||
4
Panel/modules/website/payment_success.php
Normal file
4
Panel/modules/website/payment_success.php
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
website_render('message.php', ['activePage'=>'cart','pageTitle'=>'Payment Received - Gameservers.World','heading'=>'Payment Received','message'=>'Thank you. If PayPal verification has completed, your order is now queued for provisioning.']);
|
||||
80
Panel/modules/website/register.php
Normal file
80
Panel/modules/website/register.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
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');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} else {
|
||||
$username = trim((string)($_POST['username'] ?? ''));
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
$confirm = (string)($_POST['confirm_password'] ?? '');
|
||||
$db = website_db();
|
||||
|
||||
if (!$db instanceof mysqli) {
|
||||
$error = 'Registration is temporarily unavailable.';
|
||||
} elseif (!preg_match('/^[A-Za-z0-9_.-]{3,40}$/', $username)) {
|
||||
$error = 'Choose a username using 3-40 letters, numbers, dots, underscores, or dashes.';
|
||||
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = 'Enter a valid email address.';
|
||||
} elseif (strlen($password) < 8) {
|
||||
$error = 'Use at least 8 characters for your password.';
|
||||
} elseif ($password !== $confirm) {
|
||||
$error = 'Passwords do not match.';
|
||||
} elseif (website_panel_user_by_login($username) !== null) {
|
||||
$error = 'That username is not available.';
|
||||
} else {
|
||||
$usersTable = website_table('users');
|
||||
$hash = md5($password);
|
||||
$role = 'user';
|
||||
$columns = website_table_columns($usersTable);
|
||||
$insertColumns = ['users_login', 'users_passwd', 'users_email', 'users_role'];
|
||||
$values = [$username, $hash, $email, $role];
|
||||
if (isset($columns['users_lang'])) {
|
||||
$insertColumns[] = 'users_lang';
|
||||
$values[] = 'English';
|
||||
}
|
||||
if (isset($columns['users_theme'])) {
|
||||
$insertColumns[] = 'users_theme';
|
||||
$values[] = 'SimpleBootstrap';
|
||||
}
|
||||
$columnSql = '`' . implode('`, `', array_map(static fn($column) => str_replace('`', '``', $column), $insertColumns)) . '`';
|
||||
$placeholders = implode(', ', array_fill(0, count($values), '?'));
|
||||
$stmt = $db->prepare("INSERT INTO `{$usersTable}` ({$columnSql}) VALUES ({$placeholders})");
|
||||
if ($stmt) {
|
||||
$types = str_repeat('s', count($values));
|
||||
$bindValues = [];
|
||||
foreach ($values as $index => $value) {
|
||||
$bindValues[$index] = &$values[$index];
|
||||
}
|
||||
$stmt->bind_param($types, ...$bindValues);
|
||||
if ($stmt->execute()) {
|
||||
$user = website_panel_user_by_login($username);
|
||||
if ($user) {
|
||||
website_set_user_session($user);
|
||||
website_log_activity('Website registration succeeded for ' . $username, (int)$user['user_id'], 'website_registration');
|
||||
header('Location: ' . website_url($returnPath), true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
$error = 'Could not create the account. Please contact support.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
website_render('register.php', [
|
||||
'activePage' => 'account',
|
||||
'pageTitle' => 'Create Account - Gameservers.World',
|
||||
'metaDescription' => 'Create a Gameservers.World account.',
|
||||
'canonicalPath' => 'register.php',
|
||||
'error' => $error,
|
||||
'returnPath' => $returnPath,
|
||||
]);
|
||||
64
Panel/modules/website/reset_password.php
Normal file
64
Panel/modules/website/reset_password.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
$rawToken = trim((string)($_GET['token'] ?? $_POST['token'] ?? ''));
|
||||
$tokenHash = $rawToken !== '' ? hash('sha256', $rawToken) : '';
|
||||
$error = '';
|
||||
$message = '';
|
||||
$valid = false;
|
||||
|
||||
$db = website_db();
|
||||
if ($db instanceof mysqli && $tokenHash !== '' && website_table_exists(website_table('password_reset_tokens'))) {
|
||||
$table = website_table('password_reset_tokens');
|
||||
$stmt = $db->prepare("SELECT `id`, `user_id` FROM `{$table}` WHERE `token_hash` = ? AND `used_at` IS NULL AND `expires_at` > NOW() LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $tokenHash);
|
||||
$stmt->execute();
|
||||
$tokenRow = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
$valid = is_array($tokenRow);
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $valid) {
|
||||
if (!website_verify_csrf()) {
|
||||
$error = 'Your form expired. Please try again.';
|
||||
} else {
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
$confirm = (string)($_POST['confirm_password'] ?? '');
|
||||
if (strlen($password) < 8) {
|
||||
$error = 'Use at least 8 characters for your password.';
|
||||
} elseif ($password !== $confirm) {
|
||||
$error = 'Passwords do not match.';
|
||||
} else {
|
||||
$usersTable = website_table('users');
|
||||
$resetTable = website_table('password_reset_tokens');
|
||||
$hash = md5($password);
|
||||
$userId = (int)$tokenRow['user_id'];
|
||||
$stmt = $db->prepare("UPDATE `{$usersTable}` SET `users_passwd` = ? WHERE `user_id` = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('si', $hash, $userId);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
$db->query("UPDATE `{$resetTable}` SET `used_at` = NOW() WHERE `id` = " . (int)$tokenRow['id']);
|
||||
website_log_activity('Website password reset completed', $userId, 'password_reset_completed');
|
||||
$message = 'Your password has been reset. You can now log in.';
|
||||
$valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
website_render('reset_password.php', [
|
||||
'activePage' => 'account',
|
||||
'pageTitle' => 'Reset Password - Gameservers.World',
|
||||
'metaDescription' => 'Choose a new Gameservers.World password.',
|
||||
'canonicalPath' => 'reset_password.php',
|
||||
'error' => $error,
|
||||
'message' => $message,
|
||||
'valid' => $valid,
|
||||
'token' => $rawToken,
|
||||
]);
|
||||
5
Panel/modules/website/staff.php
Normal file
5
Panel/modules/website/staff.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
website_require_staff();
|
||||
website_render('staff.php', ['activePage'=>'staff','pageTitle'=>'Website Staff - Gameservers.World','metaDescription'=>'Gameservers.World website staff dashboard.','canonicalPath'=>'staff.php']);
|
||||
7
Panel/modules/website/staff_coupons.php
Normal file
7
Panel/modules/website/staff_coupons.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
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.';}}}
|
||||
$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]);
|
||||
2
Panel/modules/website/staff_invoices.php
Normal file
2
Panel/modules/website/staff_invoices.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $rows=[]; $table=website_table('billing_invoices'); if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` ORDER BY `invoice_id` DESC LIMIT 100"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$rows[]=$row; } website_render('staff_invoices.php',['activePage'=>'staff','pageTitle'=>'Invoices - Gameservers.World','canonicalPath'=>'staff_invoices.php','invoices'=>$rows]);
|
||||
14
Panel/modules/website/staff_locations.php
Normal file
14
Panel/modules/website/staff_locations.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_require_staff();
|
||||
$remoteServers = website_fetch_remote_servers();
|
||||
|
||||
website_render('staff_locations.php', [
|
||||
'activePage' => 'staff',
|
||||
'pageTitle' => 'Server Locations - Gameservers.World',
|
||||
'canonicalPath' => 'staff_locations.php',
|
||||
'remoteServers' => $remoteServers,
|
||||
]);
|
||||
10
Panel/modules/website/staff_migrations.php
Normal file
10
Panel/modules/website/staff_migrations.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
website_require_staff();
|
||||
$ran = false; $errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (website_verify_csrf()) { $errors = website_run_billing_migrations(); $ran = true; }
|
||||
else { $errors = ['Invalid CSRF token.']; }
|
||||
}
|
||||
website_render('staff_migrations.php', ['activePage'=>'staff','pageTitle'=>'Website Migrations - Gameservers.World','canonicalPath'=>'staff_migrations.php','ran'=>$ran,'errors'=>$errors]);
|
||||
2
Panel/modules/website/staff_orders.php
Normal file
2
Panel/modules/website/staff_orders.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $rows=[]; $table=website_table('billing_orders'); if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` ORDER BY `order_id` DESC LIMIT 100"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$rows[]=$row; } website_render('staff_orders.php',['activePage'=>'staff','pageTitle'=>'Orders - Gameservers.World','canonicalPath'=>'staff_orders.php','orders'=>$rows]);
|
||||
2
Panel/modules/website/staff_payments.php
Normal file
2
Panel/modules/website/staff_payments.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
declare(strict_types=1); require_once __DIR__ . '/includes/bootstrap.php'; website_require_staff(); $db=website_db(); $rows=[]; $table=website_table('billing_payments'); if($db instanceof mysqli && website_table_exists($table)){ $r=$db->query("SELECT * FROM `{$table}` ORDER BY `payment_id` DESC LIMIT 100"); while($r instanceof mysqli_result && ($row=$r->fetch_assoc()))$rows[]=$row; } website_render('staff_payments.php',['activePage'=>'staff','pageTitle'=>'Payments - Gameservers.World','canonicalPath'=>'staff_payments.php','payments'=>$rows]);
|
||||
20
Panel/modules/website/staff_paypal.php
Normal file
20
Panel/modules/website/staff_paypal.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
website_require_staff();
|
||||
$message=''; $error='';
|
||||
if ($_SERVER['REQUEST_METHOD']==='POST') {
|
||||
if(!website_verify_csrf()) $error='Invalid CSRF token.';
|
||||
else {
|
||||
website_set_setting('paypal_enabled', !empty($_POST['enabled'])?'1':'0');
|
||||
website_set_setting('paypal_sandbox', !empty($_POST['sandbox'])?'1':'0');
|
||||
website_set_setting('paypal_client_id', trim((string)$_POST['client_id']), false);
|
||||
if(trim((string)($_POST['client_secret']??''))!=='') website_set_setting('paypal_client_secret', trim((string)$_POST['client_secret']), true);
|
||||
if(trim((string)($_POST['webhook_id']??''))!=='') website_set_setting('paypal_webhook_id', trim((string)$_POST['webhook_id']), true);
|
||||
website_set_setting('paypal_currency', strtoupper(substr(trim((string)$_POST['currency']),0,3)));
|
||||
website_set_setting('paypal_description_prefix', trim((string)$_POST['description_prefix']));
|
||||
website_log_activity('Website staff updated PayPal settings', (int)$_SESSION['website_user_id'], 'paypal_settings_updated');
|
||||
$message='PayPal settings saved. Rotate any credentials found in backup-website before production use.';
|
||||
}
|
||||
}
|
||||
website_render('staff_paypal.php',['activePage'=>'staff','pageTitle'=>'PayPal Settings - Gameservers.World','canonicalPath'=>'staff_paypal.php','config'=>website_paypal_config(),'message'=>$message,'error'=>$error]);
|
||||
2
Panel/modules/website/staff_provisioning.php
Normal file
2
Panel/modules/website/staff_provisioning.php
Normal file
|
|
@ -0,0 +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]);
|
||||
20
Panel/modules/website/staff_services.php
Normal file
20
Panel/modules/website/staff_services.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
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_verify_csrf()) { $error='Invalid CSRF token.'; }
|
||||
elseif ($db instanceof mysqli && isset($_POST['service']) && is_array($_POST['service'])) {
|
||||
$table = website_table('billing_services');
|
||||
foreach ($_POST['service'] as $sid => $svc) {
|
||||
$serviceId=(int)$sid; $enabled=!empty($svc['enabled'])?1:0; $name=trim((string)($svc['service_name']??'')); $price=(float)($svc['price_monthly']??0); $min=max(1,(int)($svc['slot_min_qty']??1)); $max=max($min,(int)($svc['slot_max_qty']??$min)); $img=trim((string)($svc['img_url']??'')); $desc=trim((string)($svc['description']??'')); $locs=implode(' ', array_map('intval', (array)($svc['locations']??[])));
|
||||
$stmt=$db->prepare("UPDATE `{$table}` SET `service_name`=?, `description`=?, `remote_server_id`=?, `slot_min_qty`=?, `slot_max_qty`=?, `price_monthly`=?, `img_url`=?, `enabled`=? WHERE `service_id`=?");
|
||||
if($stmt){$stmt->bind_param('sssiidsii',$name,$desc,$locs,$min,$max,$price,$img,$enabled,$serviceId);$stmt->execute();$stmt->close();}
|
||||
}
|
||||
website_log_activity('Website staff updated service catalog', (int)$_SESSION['website_user_id'], 'staff_services_updated');
|
||||
$message='Services updated.';
|
||||
}
|
||||
}
|
||||
$services=website_fetch_services(0, true); $remoteServers=website_fetch_remote_servers();
|
||||
website_render('staff_services.php',['activePage'=>'staff','pageTitle'=>'Manage Services - Gameservers.World','canonicalPath'=>'staff_services.php','message'=>$message,'error'=>$error,'services'=>$services,'remoteServers'=>$remoteServers]);
|
||||
12
Panel/modules/website/staff_settings.php
Normal file
12
Panel/modules/website/staff_settings.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
website_require_staff();
|
||||
|
||||
website_render('staff_settings.php', [
|
||||
'activePage' => 'staff',
|
||||
'pageTitle' => 'Website Settings - Gameservers.World',
|
||||
'canonicalPath' => 'staff_settings.php',
|
||||
]);
|
||||
81
Panel/modules/website/webhook.php
Normal file
81
Panel/modules/website/webhook.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
http_response_code(200);
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$event = json_decode($raw, true);
|
||||
if (!is_array($event)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$config = website_paypal_config();
|
||||
$eventId = (string)($event['id'] ?? '');
|
||||
$type = (string)($event['event_type'] ?? '');
|
||||
$db = website_db();
|
||||
if (!$config['enabled'] || $config['webhook_id'] === '' || $eventId === '' || !$db instanceof mysqli || !website_table_exists(website_table('payment_webhook_events'))) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$access = website_paypal_oauth($config);
|
||||
if (!$access) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = array_change_key_case(function_exists('getallheaders') ? (getallheaders() ?: []) : [], CASE_UPPER);
|
||||
$verify = [
|
||||
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
|
||||
'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
|
||||
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
|
||||
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
|
||||
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
|
||||
'webhook_id' => $config['webhook_id'],
|
||||
'webhook_event' => $event,
|
||||
];
|
||||
$ch = curl_init(website_paypal_api_base($config) . '/v1/notifications/verify-webhook-signature');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($verify),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $access],
|
||||
CURLOPT_TIMEOUT => 20,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$verified = $http === 200 && (string)(json_decode((string)$response, true)['verification_status'] ?? '') === 'SUCCESS';
|
||||
|
||||
$table = website_table('payment_webhook_events');
|
||||
$status = $verified ? 'verified' : 'rejected';
|
||||
$meta = $verified ? json_encode(['type' => $type], JSON_UNESCAPED_SLASHES) : null;
|
||||
$stmt = $db->prepare("INSERT IGNORE INTO `{$table}` (`event_id`, `provider`, `event_type`, `received_at`, `status`, `metadata_json`) VALUES (?, 'paypal', ?, NOW(), ?, ?)");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ssss', $eventId, $type, $status, $meta);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
if (!$verified || $type !== 'PAYMENT.CAPTURE.COMPLETED') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$resource = $event['resource'] ?? [];
|
||||
$captureId = (string)($resource['id'] ?? '');
|
||||
$paypalOrderId = (string)($resource['supplementary_data']['related_ids']['order_id'] ?? '');
|
||||
if ($captureId === '' || $paypalOrderId === '') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$invoiceTable = website_table('billing_invoices');
|
||||
$stmt = $db->prepare("SELECT `invoice_id` FROM `{$invoiceTable}` WHERE `paypal_order_id` = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $paypalOrderId);
|
||||
$stmt->execute();
|
||||
$invoice = $stmt->get_result()->fetch_assoc();
|
||||
$stmt->close();
|
||||
if (is_array($invoice)) {
|
||||
website_mark_invoice_paid((int)$invoice['invoice_id'], 'paypal', $paypalOrderId, $captureId, '', ['webhook_event_id' => $eventId]);
|
||||
}
|
||||
}
|
||||
1
backup-website/.htaccess
Normal file
1
backup-website/.htaccess
Normal file
|
|
@ -0,0 +1 @@
|
|||
Require all denied
|
||||
242
backup-website/BILLING_FIX_SUMMARY.md
Normal file
242
backup-website/BILLING_FIX_SUMMARY.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# Billing Invoice/Order Flow - Fix Summary
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The billing system had several critical issues:
|
||||
|
||||
1. **JSON Error**: "Failed to execute 'json' on 'Response': Unexpected end of JSON input" when returning from PayPal payment
|
||||
2. **Cart not clearing**: Items remained in cart after payment (invoices stayed as status='due')
|
||||
3. **No order creation**: Orders were not being created after successful payment
|
||||
4. **Missing renewal flow**: Renewal invoices (linked to existing orders) were not handled
|
||||
5. **Free button errors**: The free/claim button was also experiencing errors
|
||||
|
||||
## Invoice-First Flow (Intended Design)
|
||||
|
||||
The system uses an invoice-first architecture:
|
||||
|
||||
1. **Add to Cart**: Creates INVOICE with status='due', order_id=0 (no order yet)
|
||||
2. **View Cart**: Shows all invoices WHERE status='due'
|
||||
3. **Payment**:
|
||||
- For NEW orders (order_id=0): Mark invoice paid + CREATE new order
|
||||
- For RENEWALS (order_id>0): Mark invoice paid + EXTEND existing order's end_date
|
||||
4. **Provisioning**: Separate step that provisions servers for paid orders
|
||||
|
||||
## Root Causes Identified
|
||||
|
||||
### 1. Missing Function
|
||||
- `process_payment_record()` was called but never defined
|
||||
- Referenced in webhook.php, cart.php (free button), but didn't exist
|
||||
- This prevented any payment processing from completing
|
||||
|
||||
### 2. JSON Response Corruption
|
||||
- `capture_order.php` had PHP errors/warnings during DB operations
|
||||
- These were being output to the response, corrupting the JSON
|
||||
- JavaScript couldn't parse the malformed JSON → "Unexpected end of JSON input"
|
||||
|
||||
### 3. Incomplete Payment Processing
|
||||
- `capture_order.php` was supposed to:
|
||||
- Mark invoices as paid (status: 'due' → 'paid')
|
||||
- Create new orders OR extend existing orders
|
||||
- Link invoices to orders
|
||||
- But the logic was incomplete and had issues
|
||||
|
||||
### 4. Session Compatibility
|
||||
- capture_order.php used `$_SESSION['user_id']`
|
||||
- cart.php used `$_SESSION['website_user_id']`
|
||||
- This mismatch meant user couldn't be identified for payment processing
|
||||
|
||||
### 5. Hardcoded Table Names
|
||||
- capture_order.php used hardcoded "ogp_billing_invoices" and "ogp_billing_orders"
|
||||
- Should use `$table_prefix . "billing_invoices"` for flexibility
|
||||
- Could cause failures if table prefix is different
|
||||
|
||||
## Solutions Implemented
|
||||
|
||||
### 1. Created payment_processor.php Helper
|
||||
**File**: `modules/billing/includes/payment_processor.php`
|
||||
|
||||
**Function**: `process_payment_record($record)`
|
||||
- Accepts payment record from webhook or direct capture
|
||||
- Finds invoices to process by custom_id (invoice_id) or invoice reference
|
||||
- For each invoice:
|
||||
- Marks invoice as paid (status='due' → 'paid')
|
||||
- If NEW order (order_id=0): Creates new order with calculated end_date
|
||||
- If RENEWAL (order_id>0): Extends existing order's end_date by invoice duration
|
||||
- Links invoice to order
|
||||
- Returns true/false and logs all operations
|
||||
- No HTML output (safe to require from webhook/API endpoints)
|
||||
|
||||
### 2. Fixed capture_order.php
|
||||
**File**: `modules/billing/api/capture_order.php`
|
||||
|
||||
**Changes**:
|
||||
- **Disabled error display**: `ini_set('display_errors', '0')` to prevent JSON corruption
|
||||
- **Session compatibility**: Checks both `website_user_id` and `user_id`
|
||||
- **Proper JSON errors**: Returns structured JSON on DB connection failure
|
||||
- **Table prefix usage**: Uses `$table_prefix` instead of hardcoded names
|
||||
- **Complete invoice processing**:
|
||||
- Marks all due invoices as paid
|
||||
- Handles both NEW orders and RENEWALS
|
||||
- Proper end_date calculation (months from qty + invoice_duration)
|
||||
- Links invoices to orders
|
||||
|
||||
### 3. Fixed payment_success.php
|
||||
**File**: `modules/billing/payment_success.php`
|
||||
|
||||
**Changes**:
|
||||
- Requires `payment_processor.php` helper
|
||||
- Displays payment confirmation page
|
||||
- Shows user's recent orders
|
||||
- No longer contains duplicate/incomplete function definitions
|
||||
|
||||
### 4. Fixed webhook.php
|
||||
**File**: `modules/billing/webhook.php`
|
||||
|
||||
**Changes**:
|
||||
- Uses `payment_processor.php` instead of requiring full payment_success.php
|
||||
- Prevents HTML output that would interfere with webhook response
|
||||
- Processes payment record after verification
|
||||
|
||||
### 5. Fixed cart.php Free Button
|
||||
**File**: `modules/billing/cart.php`
|
||||
|
||||
**Changes**:
|
||||
- Uses `payment_processor.php` for consistent processing
|
||||
- Free button now properly:
|
||||
- Marks invoice as paid
|
||||
- Creates order record
|
||||
- Calculates end_date
|
||||
- Processes payment record through shared function
|
||||
|
||||
## Payment Flow (After Fixes)
|
||||
|
||||
### PayPal Payment Flow
|
||||
```
|
||||
1. User clicks "Pay with PayPal" in cart.php
|
||||
↓
|
||||
2. JavaScript calls api/create_order.php
|
||||
→ Creates PayPal order with custom_id = invoice_id
|
||||
↓
|
||||
3. User approves payment on PayPal
|
||||
↓
|
||||
4. JavaScript calls api/capture_order.php
|
||||
→ PayPal captures payment
|
||||
→ capture_order.php:
|
||||
a) Marks invoices as paid (status='due' → 'paid')
|
||||
b) For NEW: Creates order in billing_orders
|
||||
c) For RENEW: Extends existing order's end_date
|
||||
d) Links invoice to order (sets invoice.order_id)
|
||||
→ Returns JSON: { status: "COMPLETED", ... }
|
||||
↓
|
||||
5. JavaScript redirects to payment_success.php
|
||||
→ Shows confirmation page
|
||||
→ Displays order details
|
||||
↓
|
||||
6. PayPal sends webhook to webhook.php (parallel)
|
||||
→ Verifies signature
|
||||
→ Calls process_payment_record()
|
||||
→ Same processing as step 4 (idempotent)
|
||||
↓
|
||||
7. Cart is empty (invoices now have status='paid', not shown)
|
||||
```
|
||||
|
||||
### Free/Claim Flow
|
||||
```
|
||||
1. User clicks "Claim (Free)" button in cart.php
|
||||
↓
|
||||
2. Cart.php POST handler:
|
||||
→ Marks invoice as paid
|
||||
→ Creates order record with calculated end_date
|
||||
→ Links invoice to order
|
||||
→ Creates simulated webhook file
|
||||
→ Calls process_payment_record() for consistency
|
||||
↓
|
||||
3. Redirects to return.php
|
||||
→ Shows payment confirmation
|
||||
↓
|
||||
4. Cart is empty (invoice marked paid)
|
||||
```
|
||||
|
||||
### Renewal Flow
|
||||
```
|
||||
1. User has existing order (order_id > 0)
|
||||
↓
|
||||
2. System creates renewal invoice:
|
||||
→ status = 'due'
|
||||
→ order_id = <existing_order_id>
|
||||
→ qty = renewal months
|
||||
↓
|
||||
3. Invoice appears in cart
|
||||
↓
|
||||
4. User pays (PayPal or Free)
|
||||
↓
|
||||
5. process_payment_record():
|
||||
→ Detects order_id > 0 (renewal)
|
||||
→ Fetches current end_date from existing order
|
||||
→ Calculates new end_date:
|
||||
- If current end_date > now: extend from current end_date
|
||||
- Otherwise: extend from now
|
||||
→ Updates order with new end_date
|
||||
→ Marks invoice as paid
|
||||
↓
|
||||
6. Order subscription extended by renewal period
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before deployment, verify:
|
||||
|
||||
- [ ] Config setup: Copy `config.inc.php.orig` to `config.inc.php` and configure
|
||||
- [ ] Database: Ensure `ogp_billing_invoices` and `ogp_billing_orders` tables exist
|
||||
- [ ] Test NEW order flow:
|
||||
- [ ] Add item to cart (creates invoice with status='due')
|
||||
- [ ] View cart (item appears)
|
||||
- [ ] Click "Claim (Free)" for $0 item (creates order, clears cart)
|
||||
- [ ] Verify order created in billing_orders
|
||||
- [ ] Verify invoice marked paid, linked to order
|
||||
- [ ] Test PayPal flow:
|
||||
- [ ] Add paid item to cart
|
||||
- [ ] Click PayPal button
|
||||
- [ ] Complete payment on PayPal sandbox
|
||||
- [ ] Verify returns to payment_success.php without errors
|
||||
- [ ] Verify order created
|
||||
- [ ] Verify invoice marked paid
|
||||
- [ ] Verify cart is empty
|
||||
- [ ] Test RENEWAL flow:
|
||||
- [ ] Create renewal invoice for existing order
|
||||
- [ ] Pay renewal invoice
|
||||
- [ ] Verify order end_date extended correctly
|
||||
- [ ] Verify invoice marked paid
|
||||
|
||||
## Security Considerations
|
||||
|
||||
All code changes maintain or improve security:
|
||||
|
||||
1. **SQL Injection Protection**: Uses prepared statements where possible
|
||||
2. **Input Validation**: Validates all user inputs (invoice_id, user_id, etc.)
|
||||
3. **Session Security**: Maintains separate website/panel sessions
|
||||
4. **Webhook Verification**: PayPal signature verification still in place
|
||||
5. **Error Logging**: Errors logged, not displayed to users (prevents information leakage)
|
||||
6. **Database Credentials**: Configuration file outside web root (best practice)
|
||||
|
||||
## Files Changed
|
||||
|
||||
1. `modules/billing/includes/payment_processor.php` - NEW
|
||||
2. `modules/billing/api/capture_order.php` - MODIFIED
|
||||
3. `modules/billing/payment_success.php` - MODIFIED
|
||||
4. `modules/billing/webhook.php` - MODIFIED
|
||||
5. `modules/billing/cart.php` - MODIFIED
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Config file required**: System requires `includes/config.inc.php` to be created from .orig template
|
||||
2. **Multi-item cart matching**: If cart has multiple items, all are processed together (could improve to match specific invoice_id)
|
||||
3. **No transaction rollback**: If order creation fails, invoice may still be marked paid (could improve with DB transactions)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. Add database transactions for atomic invoice→order operations
|
||||
2. Improve invoice matching in process_payment_record (more specific matching)
|
||||
3. Add unit tests for payment processing logic
|
||||
4. Add admin UI for viewing/managing invoice-order relationships
|
||||
5. Add email notifications for payment confirmations
|
||||
110
backup-website/COLUMN_RENAME_SUMMARY.md
Normal file
110
backup-website/COLUMN_RENAME_SUMMARY.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Column Rename: finish_date → end_date
|
||||
|
||||
## Overview
|
||||
Renamed the `finish_date` column to `end_date` across the entire billing module for better semantic clarity. The column represents when a server's subscription ends/expires, so "end_date" is more descriptive.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Database Schema
|
||||
1. **module.php** - Line 77
|
||||
- Updated schema definition: `finish_date` DATETIME NULL → `end_date` DATETIME NULL
|
||||
|
||||
2. **migration_to_invoices.sql**
|
||||
- Line 26: Updated AFTER clause in ADD COLUMN statement
|
||||
- Lines 49-60: Updated column conversion logic from VARCHAR to DATETIME
|
||||
- All references to the column name updated
|
||||
|
||||
### PHP Application Code
|
||||
3. **cron-shop.php** (19 occurrences)
|
||||
- Lines 78-80: Updated query conditions checking end_date IS NOT NULL
|
||||
- Lines 97, 121, 124: Updated email notification date formatting
|
||||
- Lines 142, 150-151: Updated suspension query conditions
|
||||
- Lines 218, 226-227: Updated deletion query conditions
|
||||
- Lines 283, 288: Updated legacy code comments and queries
|
||||
- Lines 301, 304: Updated developer notes
|
||||
- Lines 336, 341: Updated suspension logic
|
||||
- Line 395: Updated final cleanup query
|
||||
|
||||
4. **cart.php** (14 occurrences)
|
||||
- Lines 89-106: Updated variable names from $finish_date to $end_date
|
||||
- Line 111: Updated column existence check
|
||||
- Lines 117, 119, 121, 127: Updated SQL UPDATE statements
|
||||
- Line 148-149: Updated audit logging
|
||||
|
||||
5. **my_account.php** (4 occurrences)
|
||||
- Line 128: Updated SELECT query field
|
||||
- Line 328: Updated display formatting (3 references in same line)
|
||||
|
||||
6. **my_servers.php** (2 occurrences)
|
||||
- Line 43: Updated SQL comment
|
||||
- Line 44: Updated column alias
|
||||
|
||||
7. **admin_invoices.php** (1 occurrence)
|
||||
- Line 99: Updated display column
|
||||
|
||||
8. **add_to_cart.php** (10 occurrences)
|
||||
- Lines 134-151: Updated variable names, column checks, INSERT queries, logging
|
||||
|
||||
9. **create_servers.php** (12 occurrences)
|
||||
- Line 244: Updated condition check
|
||||
- Lines 295-296: Updated comments
|
||||
- Lines 301-330: Updated variable names in date calculation logic
|
||||
- Line 342: Updated SET clause in UPDATE query (2 references)
|
||||
|
||||
10. **payment_success.php** (11 occurrences)
|
||||
- Lines 35-102: Updated all references in payment processing logic
|
||||
- Variable renamed: $finish_date_val → $end_date_val
|
||||
- Updated column existence checks and SQL generation
|
||||
|
||||
### Documentation
|
||||
11. **INVOICE_SYSTEM.md** (6 occurrences)
|
||||
- Line 27: Updated field description
|
||||
- Line 67: Updated workflow documentation
|
||||
- Line 74: Updated renewal process
|
||||
- Line 84: Updated expiration logic
|
||||
- Line 113: Updated payment completion notes
|
||||
- Line 124: Updated My Account display notes
|
||||
|
||||
12. **MIGRATION_SUMMARY.md** (4 occurrences)
|
||||
- Line 11: Updated changelog entry
|
||||
- Line 18: Updated bug fix description
|
||||
- Lines 30, 36: Updated cron process descriptions
|
||||
- Line 87: Updated SQL schema example
|
||||
- Line 141: Updated verification notes
|
||||
|
||||
## Database Impact
|
||||
|
||||
### For Fresh Installations
|
||||
- New installations will create the `ogp_billing_orders` table with `end_date` DATETIME NULL
|
||||
|
||||
### For Existing Installations
|
||||
- Run the updated `migration_to_invoices.sql` script
|
||||
- The script will handle the column rename automatically using dynamic SQL:
|
||||
```sql
|
||||
-- Checks if column exists as 'finish_date' and renames to 'end_date'
|
||||
-- Then converts data type from VARCHAR to DATETIME
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
- [x] Module schema updated (module.php)
|
||||
- [x] Migration script updated (migration_to_invoices.sql)
|
||||
- [x] All PHP files using the column updated
|
||||
- [x] All SQL queries updated
|
||||
- [x] All variable names updated
|
||||
- [x] All comments and documentation updated
|
||||
- [x] Verified no remaining `finish_date` references (except log files)
|
||||
|
||||
## Backwards Compatibility
|
||||
⚠️ **BREAKING CHANGE**: This rename requires running the migration script on existing databases.
|
||||
|
||||
**Migration Path:**
|
||||
1. Backup database
|
||||
2. Run updated `migration_to_invoices.sql`
|
||||
3. The script will automatically rename `finish_date` to `end_date`
|
||||
4. Verify column exists: `SHOW COLUMNS FROM ogp_billing_orders LIKE 'end_date';`
|
||||
|
||||
## Notes
|
||||
- Log files may still contain old references to `finish_date` - this is expected and harmless
|
||||
- The semantic meaning of the column is unchanged (server expiration date)
|
||||
- All date calculations remain identical
|
||||
- No functional changes, only naming improvement for clarity
|
||||
364
backup-website/COUPON_SYSTEM.md
Normal file
364
backup-website/COUPON_SYSTEM.md
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
# Coupon System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The billing module now includes a comprehensive coupon system that allows administrators to create discount codes that customers can apply to their orders. The system supports:
|
||||
|
||||
- **Percentage-based discounts** (e.g., 10%, 25%, 50% off)
|
||||
- **One-time or permanent discounts** (one-time applies to first invoice only, permanent applies to all renewals)
|
||||
- **Game-specific filtering** (apply coupons to all games or specific games only)
|
||||
- **Usage limits** (optional maximum number of uses per coupon)
|
||||
- **Expiration dates** (optional expiry date for time-limited promotions)
|
||||
- **Automatic usage tracking** (system tracks how many times each coupon has been used)
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Table: `ogp_billing_coupons`
|
||||
|
||||
The main coupon table stores all coupon definitions:
|
||||
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_coupons` (
|
||||
`coupon_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`description` TEXT,
|
||||
`discount_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||
`usage_type` ENUM('one_time', 'permanent') NOT NULL DEFAULT 'one_time',
|
||||
`game_filter_type` ENUM('all_games', 'specific_games') NOT NULL DEFAULT 'all_games',
|
||||
`game_filter_list` TEXT COMMENT 'JSON array of game keys',
|
||||
`max_uses` INT(11) DEFAULT NULL COMMENT 'NULL for unlimited',
|
||||
`current_uses` INT(11) NOT NULL DEFAULT 0,
|
||||
`expires` DATETIME DEFAULT NULL,
|
||||
`created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_by` INT(11) DEFAULT NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`coupon_id`),
|
||||
UNIQUE KEY `idx_code` (`code`)
|
||||
);
|
||||
```
|
||||
|
||||
### Updated Tables
|
||||
|
||||
#### `ogp_billing_invoices`
|
||||
Added columns:
|
||||
- `coupon_id` INT(11) - Links to the coupon used
|
||||
- `discount_amount` DECIMAL(10,2) - Actual discount amount applied
|
||||
|
||||
#### `ogp_billing_orders`
|
||||
Added columns:
|
||||
- `coupon_id` INT(11) - Links to the coupon used (for permanent discounts)
|
||||
- `discount_amount` DECIMAL(10,2) - Discount amount for renewals
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Run the SQL migration:**
|
||||
```bash
|
||||
mysql -u [username] -p [database_name] < modules/billing/create_coupons_table.sql
|
||||
```
|
||||
|
||||
2. **Verify installation:**
|
||||
- Check that the `ogp_billing_coupons` table exists
|
||||
- Verify that `coupon_id` and `discount_amount` columns were added to both `ogp_billing_invoices` and `ogp_billing_orders`
|
||||
|
||||
## Admin Interface
|
||||
|
||||
### Accessing Coupon Management
|
||||
|
||||
1. Log in as an administrator
|
||||
2. Navigate to `/modules/billing/admin.php`
|
||||
3. Click on "Manage Coupons" button
|
||||
4. Or go directly to `/modules/billing/admin_coupons.php`
|
||||
|
||||
### Creating a New Coupon
|
||||
|
||||
1. On the Manage Coupons page, scroll to "Add New Coupon" section
|
||||
2. Fill in the required fields:
|
||||
- **Coupon Code**: Unique alphanumeric code (e.g., "SUMMER2025", "WELCOME10")
|
||||
- **Display Name**: User-friendly name shown in admin interface
|
||||
- **Description**: Internal notes about the coupon
|
||||
- **Discount Percentage**: Number between 0-100 (e.g., 25 for 25% off)
|
||||
- **Usage Type**:
|
||||
- **One Time**: Discount applies only to the first invoice
|
||||
- **Permanent**: Discount applies to initial order AND all future renewals
|
||||
- **Apply To**:
|
||||
- **All Games**: Works for any game server
|
||||
- **Specific Games**: Works only for selected games
|
||||
- **Maximum Uses**: Optional limit on total uses (blank = unlimited)
|
||||
- **Expiration Date**: Optional expiry date (blank = never expires)
|
||||
|
||||
3. Click "Add Coupon" to save
|
||||
|
||||
### Example Coupons
|
||||
|
||||
#### Welcome Discount (One-Time, All Games)
|
||||
```
|
||||
Code: WELCOME10
|
||||
Name: Welcome 10% Off
|
||||
Discount: 10%
|
||||
Usage Type: One Time
|
||||
Apply To: All Games
|
||||
Max Uses: (unlimited)
|
||||
Expires: (none)
|
||||
```
|
||||
|
||||
#### Arma Series Promotion (Permanent, Specific Games)
|
||||
```
|
||||
Code: ARMA25
|
||||
Name: Arma Series 25% Off
|
||||
Discount: 25%
|
||||
Usage Type: Permanent
|
||||
Apply To: Specific Games
|
||||
- arma2_win32
|
||||
- arma2oa_win32
|
||||
- arma3_linux32
|
||||
- arma3_linux64
|
||||
- arma3_win64
|
||||
- arma-reforger_linux64
|
||||
- arma-reforger_win64
|
||||
Max Uses: 100
|
||||
Expires: 2025-12-31
|
||||
```
|
||||
|
||||
### Editing Coupons
|
||||
|
||||
1. On the Manage Coupons page, find the coupon in the list
|
||||
2. Click the "Edit" button
|
||||
3. Modify any fields (except code uniqueness is enforced)
|
||||
4. Click "Save Changes"
|
||||
|
||||
### Deactivating Coupons
|
||||
|
||||
1. Click "Edit" on the coupon
|
||||
2. Uncheck the "Active" checkbox
|
||||
3. Click "Save Changes"
|
||||
|
||||
Note: Deactivating prevents new uses but doesn't affect existing orders.
|
||||
|
||||
### Deleting Coupons
|
||||
|
||||
1. Find the coupon in the list
|
||||
2. Click "Delete" button
|
||||
3. Confirm the deletion
|
||||
|
||||
Warning: This permanently removes the coupon. Orders that used it will retain the discount but lose the coupon reference.
|
||||
|
||||
## Customer Usage
|
||||
|
||||
### Applying a Coupon
|
||||
|
||||
1. Customer adds items to cart at `/modules/billing/cart.php`
|
||||
2. In the coupon section, enter coupon code in the input field
|
||||
3. Click "Apply Coupon"
|
||||
4. If valid, a success message appears showing:
|
||||
- Coupon code
|
||||
- Discount percentage
|
||||
- Whether it's one-time or permanent
|
||||
5. Cart totals update automatically with discounted prices
|
||||
6. Proceed to checkout with PayPal as normal
|
||||
|
||||
### Coupon Validation
|
||||
|
||||
The system validates:
|
||||
- ✅ Code exists and is active
|
||||
- ✅ Coupon hasn't expired
|
||||
- ✅ Usage limit hasn't been reached
|
||||
- ✅ Game matches filter (if game-specific)
|
||||
|
||||
Error messages shown if:
|
||||
- ❌ Code is invalid or expired
|
||||
- ❌ Usage limit reached
|
||||
- ❌ Coupon doesn't apply to games in cart
|
||||
|
||||
### Removing a Coupon
|
||||
|
||||
1. On cart page, click "Remove" button next to active coupon
|
||||
2. Cart prices revert to original amounts
|
||||
|
||||
## Coupon Behavior
|
||||
|
||||
### One-Time Coupons
|
||||
|
||||
- Applied to the initial invoice only
|
||||
- When order is renewed, renewal invoice uses original price
|
||||
- Coupon is cleared from session after first payment
|
||||
- Example: "WELCOME10" gives 10% off first month only
|
||||
|
||||
### Permanent Coupons
|
||||
|
||||
- Applied to initial invoice AND stored in order record
|
||||
- When order is renewed, the discount is automatically applied to renewal invoices
|
||||
- Coupon stays associated with the order forever
|
||||
- Example: "VIP50" gives 50% off forever for that specific server
|
||||
|
||||
### Game Filtering
|
||||
|
||||
#### All Games
|
||||
- Coupon applies to any game server in the cart
|
||||
- All cart items receive the discount
|
||||
|
||||
#### Specific Games
|
||||
- Coupon checks each cart item's `home_name` field
|
||||
- Only matching games receive the discount
|
||||
- Uses partial string matching (e.g., "arma3" matches "arma3_linux64")
|
||||
- Non-matching games show original price
|
||||
|
||||
Example:
|
||||
```
|
||||
Cart contains:
|
||||
1. Arma 3 Server → ARMA25 coupon applies (25% off)
|
||||
2. Minecraft Server → ARMA25 doesn't apply (full price)
|
||||
3. Arma Reforger → ARMA25 applies (25% off)
|
||||
|
||||
Total discount = 25% off Arma servers only
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Session Storage
|
||||
|
||||
Coupons are stored in `$_SESSION['applied_coupon']` when applied:
|
||||
```php
|
||||
$_SESSION['applied_coupon'] = [
|
||||
'coupon_id' => 1,
|
||||
'code' => 'ARMA25',
|
||||
'discount_percent' => 25.00,
|
||||
'usage_type' => 'permanent',
|
||||
'game_filter_type' => 'specific_games',
|
||||
'game_filter_list' => '["arma3_linux64","arma2_win32"]',
|
||||
// ... other fields
|
||||
];
|
||||
```
|
||||
|
||||
### Cart Calculation
|
||||
|
||||
In `cart.php`, the `couponAppliesTo()` function checks if a coupon applies to a specific game:
|
||||
|
||||
```php
|
||||
function couponAppliesTo($coupon, $game_name) {
|
||||
if (!$coupon || $coupon['game_filter_type'] === 'all_games') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($coupon['game_filter_type'] === 'specific_games') {
|
||||
$allowed_games = json_decode($coupon['game_filter_list'], true);
|
||||
foreach ($allowed_games as $allowed_game) {
|
||||
if (stripos($game_name, $allowed_game) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
Discount calculation:
|
||||
```php
|
||||
$rowtotal = $row['amount'] * $row['qty'] * $row['max_players'];
|
||||
|
||||
if ($applied_coupon && couponAppliesTo($applied_coupon, $row['home_name'])) {
|
||||
$discountPercent = floatval($applied_coupon['discount_percent']);
|
||||
$itemDiscount = ($rowtotal * $discountPercent) / 100;
|
||||
$rowtotal = $rowtotal - $itemDiscount;
|
||||
}
|
||||
```
|
||||
|
||||
### Payment Processing
|
||||
|
||||
In `api/capture_order.php`, when PayPal payment completes:
|
||||
|
||||
1. Coupon info is retrieved from session
|
||||
2. Invoices are updated with `coupon_id`
|
||||
3. Coupon usage count is incremented
|
||||
4. For one-time coupons, cleared from session
|
||||
5. For permanent coupons, stored in order record
|
||||
|
||||
```php
|
||||
// Update invoice with coupon
|
||||
UPDATE ogp_billing_invoices
|
||||
SET status='paid', coupon_id=?, discount_amount=?
|
||||
WHERE user_id=? AND status='due'
|
||||
|
||||
// Increment usage count
|
||||
UPDATE ogp_billing_coupons
|
||||
SET current_uses = current_uses + 1
|
||||
WHERE coupon_id = ?
|
||||
|
||||
// For permanent coupons, store in order
|
||||
INSERT INTO ogp_billing_orders (
|
||||
..., coupon_id, discount_amount
|
||||
) VALUES (
|
||||
..., ?, ?
|
||||
)
|
||||
```
|
||||
|
||||
## Display
|
||||
|
||||
### Cart Page
|
||||
- Shows applied coupon with code and percentage
|
||||
- Displays success/error messages
|
||||
- Updates prices in real-time
|
||||
|
||||
### My Servers Page
|
||||
- Shows original price (strikethrough)
|
||||
- Shows discounted price (bold)
|
||||
- Shows coupon code and percentage (green text)
|
||||
|
||||
### Admin Invoices Page
|
||||
- Same display as My Servers
|
||||
- Visible to administrators for all orders
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Coupon not applying
|
||||
- Check if code is typed correctly (case-sensitive)
|
||||
- Verify coupon is active in admin panel
|
||||
- Check expiration date hasn't passed
|
||||
- Verify usage limit hasn't been reached
|
||||
- For game-specific coupons, ensure game matches filter
|
||||
|
||||
### Discount not showing after payment
|
||||
- Check `discount_amount` column exists in both tables
|
||||
- Verify coupon_id was saved to invoice/order
|
||||
- Clear browser cache and refresh page
|
||||
|
||||
### Permanent coupon not applying to renewals
|
||||
- Verify `usage_type` is set to "permanent"
|
||||
- Check order record has `coupon_id` populated
|
||||
- Ensure renewal invoice creation copies coupon from order
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Code uniqueness**: System enforces unique coupon codes
|
||||
2. **Usage tracking**: Prevents abuse by tracking total uses
|
||||
3. **Expiration**: Automatic validation prevents expired coupon use
|
||||
4. **Admin-only creation**: Only admins can create/edit coupons
|
||||
5. **SQL injection protection**: All inputs are sanitized with `mysqli_real_escape_string()`
|
||||
6. **CSRF protection**: Admin forms include CSRF tokens
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features for future development:
|
||||
- Minimum purchase amount requirements
|
||||
- First-time customer restrictions
|
||||
- User-specific coupons (assign to individual users)
|
||||
- Combination rules (allow/prevent stacking)
|
||||
- Auto-generated unique codes for campaigns
|
||||
- Email notification when coupon is used
|
||||
- Analytics dashboard for coupon performance
|
||||
- Referral system integration
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the troubleshooting section above
|
||||
2. Review error logs in `/modules/billing/logs/`
|
||||
3. Verify database schema matches documentation
|
||||
4. Contact system administrator
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-29
|
||||
**Version**: 1.0
|
||||
**Module**: Billing/Coupons
|
||||
247
backup-website/FIXES_APPLIED.md
Normal file
247
backup-website/FIXES_APPLIED.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Billing Module Fixes - Complete Report
|
||||
|
||||
**Date**: November 10, 2025
|
||||
**Branch**: copilot/fix-billing-module-errors
|
||||
**Status**: ✅ COMPLETE
|
||||
|
||||
## Issues Resolved
|
||||
|
||||
### 1. Critical Syntax Error in cart.php ✅
|
||||
|
||||
**Problem**:
|
||||
- cart.php had a missing closing brace on line 98 (coupon validation logic)
|
||||
- This caused a complete failure of the cart page
|
||||
- PHP parser error: "Unclosed '{' on line 98"
|
||||
- Even debug mode (cart.php?debug_cart=1) failed
|
||||
|
||||
**Root Cause**:
|
||||
- The `else` block starting at line 107 (handling database connection for coupon validation) was not properly closed
|
||||
- The if statement on line 113 (`if ($coupon_result && mysqli_num_rows($coupon_result) === 1)`) was inside the else block
|
||||
- Missing closing brace after the coupon validation logic completed
|
||||
|
||||
**Fix Applied**:
|
||||
- Added missing closing brace at line 181
|
||||
- Properly closes the else block from line 107
|
||||
- Brace structure now balances correctly (22 opening, 22 closing)
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
$ php -l cart.php
|
||||
No syntax errors detected in cart.php
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cat data/debug_cart.log
|
||||
[2025-11-10 03:16:07] SHUTDOWN: no error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. VS Code "Undefined Variable" Warnings ✅
|
||||
|
||||
**Problem**:
|
||||
- VS Code showed warnings: "$table_prefix is unassigned"
|
||||
- Similar warnings for $db_host, $db_user, $db_pass, $db_name
|
||||
- These warnings appeared even though config.inc.php was properly included
|
||||
- Affected developer experience and code review
|
||||
|
||||
**Root Cause**:
|
||||
- IDEs like VS Code don't trace through dynamic `require_once` includes
|
||||
- Variables defined in config.inc.php were not visible to static analysis
|
||||
- This is a limitation of IDE static analysis, not an actual code error
|
||||
|
||||
**Fix Applied**:
|
||||
- Added PHPDoc `@var` annotations after config.inc.php includes
|
||||
- Annotations help IDEs understand variable scope
|
||||
- Pattern used:
|
||||
```php
|
||||
// Variables from config.inc.php (helps IDEs understand scope)
|
||||
/** @var string $db_host Database host */
|
||||
/** @var string $db_user Database user */
|
||||
/** @var string $db_pass Database password */
|
||||
/** @var string $db_name Database name */
|
||||
/** @var string $table_prefix Table prefix for database tables */
|
||||
```
|
||||
|
||||
**Files Updated** (16 total):
|
||||
|
||||
**Main Website Files**:
|
||||
1. cart.php
|
||||
2. add_to_cart.php
|
||||
3. admin_coupons.php
|
||||
4. my_servers.php
|
||||
5. my_account.php
|
||||
6. renew_server.php
|
||||
7. forgot_password.php
|
||||
8. reset_password.php
|
||||
9. login.php
|
||||
10. register.php
|
||||
11. serverlist.php
|
||||
12. payment_success.php
|
||||
13. order.php
|
||||
|
||||
**Include Files**:
|
||||
14. includes/admin_auth.php
|
||||
15. includes/payment_processor.php
|
||||
16. includes/menu.php
|
||||
|
||||
**Coverage**: 16 out of 25 files using $table_prefix now have PHPDoc annotations (64%)
|
||||
|
||||
---
|
||||
|
||||
### 3. Housekeeping ✅
|
||||
|
||||
**Added to .gitignore**:
|
||||
- `modules/billing/data/*.log` - Prevents debug logs from being committed
|
||||
|
||||
---
|
||||
|
||||
## Validation Results
|
||||
|
||||
### Syntax Validation
|
||||
- ✅ All 36 PHP files in modules/billing/ pass syntax check
|
||||
- ✅ No parse errors detected
|
||||
- ✅ All brace pairs balanced correctly
|
||||
|
||||
### Functional Testing
|
||||
- ✅ cart.php loads without errors
|
||||
- ✅ Debug mode (cart.php?debug_cart=1) works correctly
|
||||
- ✅ Debug log shows "no error" status
|
||||
- ✅ Shutdown function executes properly
|
||||
|
||||
### Code Quality
|
||||
- ✅ PHPDoc annotations added for IDE support
|
||||
- ✅ All key user-facing files updated
|
||||
- ✅ No changes to business logic
|
||||
- ✅ Minimal, surgical changes only
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Commit 1: Fix cart.php syntax error and add PHPDoc hints
|
||||
- modules/billing/cart.php (syntax fix + PHPDoc)
|
||||
- modules/billing/add_to_cart.php (PHPDoc)
|
||||
- modules/billing/admin_coupons.php (PHPDoc)
|
||||
- modules/billing/my_servers.php (PHPDoc)
|
||||
- modules/billing/my_account.php (PHPDoc)
|
||||
- modules/billing/renew_server.php (PHPDoc)
|
||||
- modules/billing/forgot_password.php (PHPDoc)
|
||||
- modules/billing/reset_password.php (PHPDoc)
|
||||
|
||||
### Commit 2: Add PHPDoc hints to additional files
|
||||
- modules/billing/login.php (PHPDoc)
|
||||
- modules/billing/register.php (PHPDoc)
|
||||
- modules/billing/serverlist.php (PHPDoc)
|
||||
- modules/billing/payment_success.php (PHPDoc)
|
||||
- modules/billing/order.php (PHPDoc)
|
||||
- modules/billing/includes/admin_auth.php (PHPDoc)
|
||||
- modules/billing/includes/payment_processor.php (PHPDoc)
|
||||
- modules/billing/includes/menu.php (PHPDoc)
|
||||
|
||||
### Commit 3: Add billing data logs to gitignore
|
||||
- .gitignore (added modules/billing/data/*.log)
|
||||
|
||||
**Total Files Changed**: 17 files
|
||||
**Total Lines Changed**: ~120 lines (mostly documentation)
|
||||
**Breaking Changes**: None
|
||||
**Business Logic Changes**: None
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
To fully test the cart functionality in a live environment:
|
||||
|
||||
1. **Configure Database Connection**:
|
||||
- Edit `modules/billing/includes/config.inc.php`
|
||||
- Set correct database credentials
|
||||
- Ensure $table_prefix matches your panel installation
|
||||
|
||||
2. **Test Basic Cart Access**:
|
||||
```
|
||||
http://yoursite.com/modules/billing/cart.php
|
||||
```
|
||||
- Should redirect to login if not authenticated
|
||||
- Should show cart after login
|
||||
|
||||
3. **Test Debug Mode**:
|
||||
```
|
||||
http://yoursite.com/modules/billing/cart.php?debug_cart=1
|
||||
```
|
||||
- Should display detailed error messages
|
||||
- Check data/debug_cart.log for shutdown messages
|
||||
|
||||
4. **Test Coupon Functionality**:
|
||||
- Add items to cart
|
||||
- Apply a test coupon code
|
||||
- Verify discount calculation
|
||||
- Verify coupon validation (expiry, usage limits, game filters)
|
||||
|
||||
5. **Test PayPal Integration**:
|
||||
- Complete checkout flow
|
||||
- Verify PayPal buttons render
|
||||
- Test payment capture
|
||||
|
||||
---
|
||||
|
||||
## Notes for Developers
|
||||
|
||||
### About $table_prefix Variable
|
||||
- Defined in `modules/billing/includes/config.inc.php`
|
||||
- Default value: `"gsp_"`
|
||||
- Used for database table prefixes
|
||||
- Must match the panel installation's table prefix
|
||||
|
||||
### About PHPDoc Annotations
|
||||
- These are ONLY for IDE support
|
||||
- Do NOT change runtime behavior
|
||||
- Safe to add to all files that include config.inc.php
|
||||
- Pattern is consistent across all files
|
||||
|
||||
### Standalone Architecture
|
||||
The billing module is designed to be standalone and relocatable:
|
||||
- Uses ONLY standard PHP libraries (mysqli, json, curl, session)
|
||||
- Does NOT include panel files (like includes/functions.php)
|
||||
- Connects directly to MySQL using mysqli_connect()
|
||||
- Can be deployed on same machine as panel OR external web host
|
||||
- Sessions are separate: "gameservers_website" namespace
|
||||
|
||||
---
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### Files That Could Benefit from PHPDoc (Not Critical)
|
||||
These files use $table_prefix but don't have PHPDoc annotations yet:
|
||||
- admin_invoices.php (4 uses)
|
||||
- adminserverlist.php (8 uses)
|
||||
- cart_old.php (4 uses)
|
||||
- check_table.php (4 uses)
|
||||
- create_servers.php (4 uses) - NOTE: This is a panel module, uses OGP_DB_PREFIX
|
||||
- cron-shop.php (30 uses) - NOTE: This is a panel cron job
|
||||
- server_status.php (4 uses)
|
||||
- test_db_connection.php (9 uses)
|
||||
|
||||
These can be updated in a future enhancement if needed.
|
||||
|
||||
### create_servers.php Note
|
||||
This file is actually a PANEL module (not a standalone billing website file):
|
||||
- Uses panel's $db object
|
||||
- Includes panel files (includes/lib_remote.php)
|
||||
- Uses OGP_DB_PREFIX placeholder in some queries
|
||||
- Inconsistently uses {$table_prefix} in a few places
|
||||
- Should eventually be updated to use OGP_DB_PREFIX consistently
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **All issues resolved successfully**
|
||||
|
||||
The billing module is now functional with:
|
||||
1. cart.php working correctly (syntax error fixed)
|
||||
2. VS Code warnings suppressed (PHPDoc added)
|
||||
3. Debug logging configured properly
|
||||
4. All files validated for syntax correctness
|
||||
|
||||
The changes are minimal, surgical, and follow the repository guidelines for standalone billing module architecture.
|
||||
190
backup-website/INVOICE_FIRST_FLOW.md
Normal file
190
backup-website/INVOICE_FIRST_FLOW.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Invoice-First Billing Flow
|
||||
|
||||
## Overview
|
||||
The billing system now follows an **invoice-first** workflow where invoices are created BEFORE orders. Orders are only created after successful payment.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Add to Cart (order.php → add_to_cart.php)
|
||||
**What happens:**
|
||||
- User clicks "Add to Cart" button on order page
|
||||
- System creates a **billing_invoices** record with:
|
||||
- `status` = 'due'
|
||||
- `order_id` = 0 (no order exists yet)
|
||||
- All server details (service_id, home_name, ip, max_players, passwords, etc.)
|
||||
- Customer details (name, email from ogp_users)
|
||||
- Pricing (amount, qty, invoice_duration)
|
||||
- `due_date` = now + 3 days
|
||||
|
||||
**Database changes:**
|
||||
- INSERT into `ogp_billing_invoices`
|
||||
- NO changes to `ogp_billing_orders` (order doesn't exist yet)
|
||||
|
||||
### 2. Cart Display (cart.php)
|
||||
**What shows:**
|
||||
- Query: `SELECT * FROM ogp_billing_invoices WHERE status = 'due' AND user_id = ?`
|
||||
- Displays all **unpaid invoices** (status='due')
|
||||
- Shows invoice_id, home_name, ip, max_players, amount, qty
|
||||
- Free items show "Claim (Free)" button
|
||||
- Paid items show PayPal button
|
||||
|
||||
**Actions available:**
|
||||
- Delete invoice (removes from cart, no order cleanup needed)
|
||||
- Pay invoice (via PayPal or Free button)
|
||||
|
||||
### 3. Payment (PayPal or Free)
|
||||
|
||||
#### 3a. Free/Claim Flow (cart.php POST handler)
|
||||
**When:** User clicks "Claim (Free)" or admin clicks "Create (Free)"
|
||||
|
||||
**What happens:**
|
||||
1. Mark invoice as paid:
|
||||
- UPDATE `ogp_billing_invoices` SET status='paid', paid_date=NOW()
|
||||
2. Create order record:
|
||||
- Calculate end_date (qty * invoice_duration)
|
||||
- INSERT into `ogp_billing_orders` with status='paid'
|
||||
- Get new order_id from INSERT
|
||||
3. Link invoice to order:
|
||||
- UPDATE `ogp_billing_invoices` SET order_id=? WHERE invoice_id=?
|
||||
|
||||
**Database changes:**
|
||||
- UPDATE `ogp_billing_invoices`: status='due' → 'paid', paid_date=NOW(), order_id=(new)
|
||||
- INSERT `ogp_billing_orders`: New record with status='paid', end_date calculated
|
||||
|
||||
#### 3b. PayPal Flow (api/capture_order.php)
|
||||
**When:** User pays via PayPal
|
||||
|
||||
**What should happen:**
|
||||
1. PayPal sends capture webhook
|
||||
2. System marks invoice as paid (same as Free flow)
|
||||
3. System creates order record (same as Free flow)
|
||||
4. System links invoice to order (same as Free flow)
|
||||
|
||||
**Database changes:** (Same as Free flow above)
|
||||
|
||||
### 4. Server Provisioning (create_servers.php)
|
||||
**What happens:**
|
||||
- Cron job or manual trigger finds orders with status='paid'
|
||||
- Creates actual game server (home_id)
|
||||
- Updates order: status='paid' → 'installed', home_id=(assigned)
|
||||
|
||||
**Database changes:**
|
||||
- UPDATE `ogp_billing_orders`: status='paid' → 'installed', home_id=(assigned)
|
||||
|
||||
## Status Values
|
||||
|
||||
### Invoice Status
|
||||
- **'due'** - Unpaid invoice (shows in cart)
|
||||
- **'paid'** - Paid invoice (payment confirmed)
|
||||
- **'cancelled'** - Deleted/cancelled invoice
|
||||
|
||||
### Order Status
|
||||
- **'paid'** - Payment confirmed, awaiting provisioning
|
||||
- **'installed'** - Server provisioned and running
|
||||
- **'suspended'** - Server stopped for non-payment
|
||||
- **'expired'** - Service ended
|
||||
|
||||
## Database Schema
|
||||
|
||||
### ogp_billing_invoices (INVOICE-FIRST)
|
||||
```sql
|
||||
invoice_id INT AUTO_INCREMENT PRIMARY KEY
|
||||
order_id INT DEFAULT 0 -- Links to order AFTER payment (0 = not yet paid)
|
||||
user_id INT NOT NULL
|
||||
service_id INT NOT NULL -- Server package being purchased
|
||||
home_name VARCHAR(255) -- Server name
|
||||
ip INT -- IP assignment
|
||||
max_players INT -- Player count
|
||||
remote_control_password VARCHAR(255) -- Server RCON password
|
||||
ftp_password VARCHAR(255) -- FTP password
|
||||
customer_name VARCHAR(255) -- Billing name
|
||||
customer_email VARCHAR(255) -- Billing email
|
||||
amount FLOAT(15,2) -- Total price
|
||||
currency VARCHAR(3) DEFAULT 'USD'
|
||||
status VARCHAR(16) DEFAULT 'due' -- 'due', 'paid', 'cancelled'
|
||||
invoice_date DATETIME DEFAULT NOW()
|
||||
due_date DATETIME -- Payment deadline
|
||||
paid_date DATETIME -- When paid
|
||||
payment_txid VARCHAR(255) -- PayPal transaction ID
|
||||
payment_method VARCHAR(50) -- 'paypal', 'free', etc.
|
||||
description VARCHAR(500) -- Invoice description
|
||||
invoice_duration VARCHAR(16) DEFAULT 'month' -- 'month', 'year', 'day'
|
||||
qty INT DEFAULT 1 -- Quantity/duration multiplier
|
||||
```
|
||||
|
||||
### ogp_billing_orders (ORDER-AFTER-PAYMENT)
|
||||
```sql
|
||||
order_id INT AUTO_INCREMENT PRIMARY KEY
|
||||
user_id INT NOT NULL
|
||||
service_id INT NOT NULL
|
||||
home_name VARCHAR(255)
|
||||
home_id VARCHAR(255) -- Panel game server ID (after provisioning)
|
||||
ip INT
|
||||
max_players INT
|
||||
qty INT
|
||||
invoice_duration VARCHAR(16)
|
||||
price FLOAT(15,2)
|
||||
remote_control_password VARCHAR(255)
|
||||
ftp_password VARCHAR(255)
|
||||
status VARCHAR(16) DEFAULT 'paid' -- 'paid', 'installed', 'suspended', 'expired'
|
||||
order_date DATETIME DEFAULT NOW()
|
||||
end_date DATETIME -- Subscription expiration
|
||||
payment_txid VARCHAR(255)
|
||||
paid_ts DATETIME
|
||||
```
|
||||
|
||||
## Key Differences from Old Flow
|
||||
|
||||
### OLD (Order-First)
|
||||
1. Add to cart → Create ORDER (status='in-cart')
|
||||
2. View cart → Show orders WHERE status='in-cart'
|
||||
3. Pay → UPDATE order status='in-cart' → 'paid'
|
||||
4. Provision → UPDATE order status='paid' → 'installed'
|
||||
|
||||
### NEW (Invoice-First)
|
||||
1. Add to cart → Create INVOICE (status='due', order_id=0)
|
||||
2. View cart → Show invoices WHERE status='due'
|
||||
3. Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order
|
||||
4. Provision → UPDATE order status='paid' → 'installed'
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clean Separation:** Invoices = payment requests, Orders = actual services
|
||||
2. **Better Audit Trail:** Invoice IDs never change, order IDs created only after payment
|
||||
3. **Renewal Support:** Can create multiple invoices for same order (renewals)
|
||||
4. **Cart Simplicity:** Cart only shows unpaid invoices (single source of truth)
|
||||
5. **Payment History:** All payments have invoice records, even free ones
|
||||
|
||||
## Migration Notes
|
||||
|
||||
**Existing orders with status='in-cart' need to be migrated:**
|
||||
```sql
|
||||
-- Convert existing cart items to invoices
|
||||
INSERT INTO ogp_billing_invoices (
|
||||
order_id, user_id, service_id, home_name, ip, max_players,
|
||||
remote_control_password, ftp_password, customer_name, customer_email,
|
||||
amount, status, invoice_duration, qty, description
|
||||
)
|
||||
SELECT
|
||||
0, -- No order exists yet
|
||||
o.user_id,
|
||||
o.service_id,
|
||||
o.home_name,
|
||||
o.ip,
|
||||
o.max_players,
|
||||
o.remote_control_password,
|
||||
o.ftp_password,
|
||||
CONCAT(u.users_fname, ' ', u.users_lname),
|
||||
u.users_email,
|
||||
o.price,
|
||||
'due', -- Convert 'in-cart' to 'due'
|
||||
o.invoice_duration,
|
||||
o.qty,
|
||||
CONCAT('Migrated cart item: ', o.home_name)
|
||||
FROM ogp_billing_orders o
|
||||
LEFT JOIN ogp_users u ON o.user_id = u.user_id
|
||||
WHERE o.status = 'in-cart';
|
||||
|
||||
-- Delete old cart items (now converted to invoices)
|
||||
DELETE FROM ogp_billing_orders WHERE status = 'in-cart';
|
||||
```
|
||||
133
backup-website/INVOICE_SYSTEM.md
Normal file
133
backup-website/INVOICE_SYSTEM.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Billing System - Invoice-Based Architecture
|
||||
|
||||
## Overview
|
||||
The billing system now uses a **dual-table architecture** separating orders (ongoing services) from invoices (payment records).
|
||||
|
||||
## Database Tables
|
||||
|
||||
### 1. `ogp_billing_services`
|
||||
**Purpose:** Available game server packages/products
|
||||
**Key Fields:**
|
||||
- `service_id` - Unique identifier
|
||||
- `service_name` - Display name
|
||||
- `remote_server_id` - Target server(s)
|
||||
- `price_monthly`, `price_year` - Pricing tiers
|
||||
- `enabled` - Availability flag
|
||||
|
||||
### 2. `ogp_billing_orders` (formerly just cart items)
|
||||
**Purpose:** Active game server instances (ongoing services)
|
||||
**Key Fields:**
|
||||
- `order_id` - Unique identifier
|
||||
- `user_id` - Owner
|
||||
- `service_id` - Product reference
|
||||
- `home_id` - Panel game home ID (after provisioning)
|
||||
- `home_name` - Server name
|
||||
- `status` - Current state (see Status Flow below)
|
||||
- `order_date` - When created
|
||||
- `end_date` - Expiration date
|
||||
- `payment_txid` - Last payment transaction
|
||||
- `paid_ts` - Last payment timestamp
|
||||
|
||||
**Status Values:**
|
||||
- `in-cart` - User added to cart, not yet paid
|
||||
- `paid` - Payment received, awaiting provisioning
|
||||
- `installed` - ✅ Server provisioned and running
|
||||
- `suspended` - Server stopped due to non-payment
|
||||
- `expired` - Service ended
|
||||
- `renew` - Renewal pending in cart
|
||||
|
||||
### 3. `ogp_billing_invoices` (NEW)
|
||||
**Purpose:** Payment records (one invoice per payment)
|
||||
**Key Fields:**
|
||||
- `invoice_id` - Unique identifier
|
||||
- `order_id` - Links to the server order
|
||||
- `user_id` - Customer
|
||||
- `customer_name` - Full name
|
||||
- `customer_email` - Email address
|
||||
- `amount` - Total due
|
||||
- `currency` - USD, EUR, etc.
|
||||
- `status` - `unpaid` or `paid`
|
||||
- `invoice_date` - When created
|
||||
- `due_date` - Payment deadline
|
||||
- `paid_date` - When paid
|
||||
- `payment_txid` - PayPal/Stripe transaction ID
|
||||
- `payment_method` - PayPal, Stripe, etc.
|
||||
- `description` - Invoice line items
|
||||
- `invoice_duration` - Billing period (month/year)
|
||||
- `qty` - Quantity/duration multiplier
|
||||
|
||||
## Workflow
|
||||
|
||||
### Initial Purchase
|
||||
1. User selects game server package → Creates row in `billing_orders` (status: `in-cart`)
|
||||
2. System creates `billing_invoices` entry (status: `unpaid`, linked to order_id)
|
||||
3. Cart page shows unpaid invoices
|
||||
4. User pays → Invoice status becomes `paid`, order status becomes `paid`
|
||||
5. Provisioning happens → Order status becomes `installed`
|
||||
6. Server is active until `end_date`
|
||||
|
||||
### Renewal Process
|
||||
1. User clicks "Renew" on active server (My Account page)
|
||||
2. System creates NEW invoice in `billing_invoices` (status: `unpaid`, same order_id)
|
||||
3. Cart shows the unpaid renewal invoice
|
||||
4. User pays → Invoice status becomes `paid`
|
||||
5. Order `end_date` is extended by the renewal period
|
||||
|
||||
### Cron Automation (`cron-shop.php`)
|
||||
The cron job checks invoice status to manage servers:
|
||||
|
||||
**7 days before expiration:**
|
||||
- Check if order has unpaid invoice for upcoming period
|
||||
- If NO unpaid invoice exists → Create one (status: `unpaid`)
|
||||
- Email customer about upcoming renewal
|
||||
|
||||
**On expiration (end_date reached):**
|
||||
- Check if order has unpaid invoice
|
||||
- If YES → Suspend server (stop, disable FTP, unassign from user)
|
||||
- Order status → `suspended`
|
||||
|
||||
**7 days after suspension:**
|
||||
- If still unpaid → Delete server permanently
|
||||
- Order status → `expired`
|
||||
|
||||
## Key Advantages
|
||||
|
||||
1. **Clear Payment History:** Each invoice represents one payment
|
||||
2. **Audit Trail:** Can track when/how much each renewal cost
|
||||
3. **Flexible Pricing:** Can adjust price per renewal (discounts, promotions)
|
||||
4. **Multi-Payment Support:** One order can have many invoices
|
||||
5. **Accurate Status:** Order status reflects server state, invoice status reflects payment
|
||||
6. **No Race Conditions:** Webhook updates invoice, provisioning updates order
|
||||
|
||||
## Cart Logic
|
||||
|
||||
**Cart page displays:**
|
||||
- All invoices with `status = 'unpaid'` for the current user
|
||||
- Groups by order_id to show which server each invoice is for
|
||||
- Total amount = SUM of all unpaid invoice amounts
|
||||
|
||||
**After payment:**
|
||||
- Invoice `status` → `paid`
|
||||
- Invoice `paid_date` → NOW()
|
||||
- Invoice `payment_txid` → transaction ID from PayPal/Stripe
|
||||
- Order `status` → `paid` (if new order) or `end_date` extended (if renewal)
|
||||
|
||||
## My Account Logic
|
||||
|
||||
**Show Invoices Section:**
|
||||
- Group invoices by status (unpaid, paid, overdue)
|
||||
- Display invoice_date, amount, status
|
||||
- Link to view invoice details
|
||||
|
||||
**Show Current Servers Section:**
|
||||
- Display orders with `status = 'installed'`
|
||||
- Show end_date (expiration)
|
||||
- "Renew" button creates new invoice
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Run `migration_to_invoices.sql` on existing installations
|
||||
- Creates `billing_invoices` table
|
||||
- Adds missing columns to `billing_orders`
|
||||
- Migrates existing paid orders to have invoices
|
||||
- Removes obsolete `billing_carts` table
|
||||
223
backup-website/LOGGING_CHANGES_SUMMARY.md
Normal file
223
backup-website/LOGGING_CHANGES_SUMMARY.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# PayPal Payment Flow Logging Enhancement - Summary
|
||||
|
||||
## Problem Addressed
|
||||
|
||||
Users were experiencing intermittent errors when clicking "Pay from PayPal" button:
|
||||
- **JSON parsing errors**
|
||||
- **HTTP ERROR 500**
|
||||
- **"Currently unable to handle this request"** errors
|
||||
|
||||
These errors would "flip-flop" between different types, making diagnosis difficult without proper logging.
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
Added comprehensive logging throughout the entire PayPal payment flow to capture:
|
||||
- All request/response data
|
||||
- Error details with full context
|
||||
- Unique request IDs for tracking
|
||||
- Database operations and results
|
||||
- Client-side JavaScript errors
|
||||
|
||||
## What Changed
|
||||
|
||||
### Modified Files
|
||||
|
||||
1. **`api/create_order.php`** - Enhanced with comprehensive logging
|
||||
- Logs every step of order creation
|
||||
- Captures request data, OAuth process, PayPal API calls
|
||||
- Returns request IDs in error messages for tracking
|
||||
- Logs to: `logs/paypal_create_order.log`
|
||||
|
||||
2. **`api/capture_order.php`** - Enhanced existing logging
|
||||
- Logs payment capture process
|
||||
- Tracks database operations (invoice updates, order creation)
|
||||
- Captures all error conditions
|
||||
- Logs to: `logs/paypal_capture.log`
|
||||
|
||||
3. **`cart.php`** - Improved client-side error handling
|
||||
- Better error messages with reference IDs
|
||||
- Enhanced console logging for debugging
|
||||
- Sends errors to server for centralized logging
|
||||
- Better user feedback during payment process
|
||||
|
||||
4. **`api/log_error.php`** - NEW: Client error logging endpoint
|
||||
- Captures JavaScript errors from browser
|
||||
- Logs to: `logs/client_errors.log`
|
||||
|
||||
### New Files
|
||||
|
||||
1. **`PAYPAL_DEBUGGING_GUIDE.md`** - Comprehensive debugging guide
|
||||
- How to read logs
|
||||
- Common issues and solutions
|
||||
- Request flow documentation
|
||||
- Monitoring commands
|
||||
|
||||
2. **`QUICK_DEBUG_REFERENCE.md`** - Quick reference card
|
||||
- Common commands
|
||||
- Error patterns
|
||||
- Quick fixes
|
||||
- Troubleshooting checklist
|
||||
|
||||
## How to Use
|
||||
|
||||
### When an error occurs:
|
||||
|
||||
1. **User will see an error message with a reference ID**, for example:
|
||||
```
|
||||
Failed to create order: API error 500 (Ref: req_abc123)
|
||||
```
|
||||
|
||||
2. **Search the logs for that reference ID**:
|
||||
```bash
|
||||
cd /home/runner/work/GSP/GSP/modules/billing/logs
|
||||
grep "req_abc123" paypal_create_order.log
|
||||
```
|
||||
|
||||
3. **Review the full request flow** to identify where it failed
|
||||
|
||||
4. **Refer to the debugging guide** for common solutions
|
||||
|
||||
### Monitor logs in real-time:
|
||||
|
||||
```bash
|
||||
cd /home/runner/work/GSP/GSP/modules/billing/logs
|
||||
tail -f paypal_*.log
|
||||
```
|
||||
|
||||
### Check for errors:
|
||||
|
||||
```bash
|
||||
cd /home/runner/work/GSP/GSP/modules/billing/logs
|
||||
grep -i error paypal_create_order.log
|
||||
grep -i failed paypal_capture.log
|
||||
```
|
||||
|
||||
## Log Files
|
||||
|
||||
All logs are written to: `/modules/billing/logs/`
|
||||
|
||||
| Log File | Purpose | When Created |
|
||||
|----------|---------|--------------|
|
||||
| `paypal_create_order.log` | Order creation requests | When user clicks "Pay with PayPal" |
|
||||
| `paypal_capture.log` | Payment capture process | After PayPal approval, during payment capture |
|
||||
| `client_errors.log` | JavaScript/browser errors | When browser encounters errors |
|
||||
|
||||
## Request Tracking
|
||||
|
||||
Each request has a unique ID:
|
||||
- **Create order**: `req_XXXXXXXXXXXXX`
|
||||
- **Capture order**: `cap_XXXXXXXXXXXXX`
|
||||
|
||||
These IDs:
|
||||
- Appear in error messages shown to users
|
||||
- Are logged in every log entry for that request
|
||||
- Can be used to track a request through the entire flow
|
||||
|
||||
## Log Entry Format
|
||||
|
||||
```
|
||||
[TIMESTAMP] [REQUEST_ID] LOG_LABEL
|
||||
key => value
|
||||
key => value
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
[2025-10-29 21:30:00] [req_abc123] OAUTH_SUCCESS
|
||||
token_length => 1024
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
## What Gets Logged
|
||||
|
||||
### Create Order Flow (`api/create_order.php`):
|
||||
- ✓ Incoming request data (amount, currency, items)
|
||||
- ✓ JSON parsing status
|
||||
- ✓ OAuth token acquisition
|
||||
- ✓ PayPal order creation request/response
|
||||
- ✓ All error conditions with full details
|
||||
|
||||
### Capture Order Flow (`api/capture_order.php`):
|
||||
- ✓ Payment capture request
|
||||
- ✓ OAuth process
|
||||
- ✓ Database connection status
|
||||
- ✓ Invoice update queries and results
|
||||
- ✓ Order creation/renewal operations
|
||||
- ✓ All error conditions with full details
|
||||
|
||||
### Client-Side (`cart.php` → `log_error.php`):
|
||||
- ✓ JavaScript errors
|
||||
- ✓ PayPal SDK errors
|
||||
- ✓ Network failures
|
||||
- ✓ JSON parsing errors
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Full Visibility**: Every step of payment flow is now logged
|
||||
2. **Easy Troubleshooting**: Request IDs link user reports to log entries
|
||||
3. **Root Cause Analysis**: Can identify exactly where and why failures occur
|
||||
4. **Pattern Detection**: Can identify if errors are consistent or intermittent
|
||||
5. **Better User Experience**: Users get reference IDs to report issues
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Monitor the logs** after deploying this change
|
||||
2. **Analyze error patterns** to identify the root cause
|
||||
3. **Review common errors** in the debugging guide
|
||||
4. **Fix underlying issues** once identified
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Full Guide**: `PAYPAL_DEBUGGING_GUIDE.md`
|
||||
- **Quick Reference**: `QUICK_DEBUG_REFERENCE.md`
|
||||
- **This Summary**: `LOGGING_CHANGES_SUMMARY.md`
|
||||
|
||||
## Testing
|
||||
|
||||
The logging system has been tested and verified to work correctly. All components:
|
||||
- ✓ Write to correct log files
|
||||
- ✓ Include proper timestamps and request IDs
|
||||
- ✓ Format data correctly
|
||||
- ✓ Handle errors gracefully
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Log Rotation
|
||||
|
||||
Logs will grow over time. Consider setting up log rotation:
|
||||
|
||||
```bash
|
||||
# Manual rotation
|
||||
cd /home/runner/work/GSP/GSP/modules/billing/logs
|
||||
gzip paypal_create_order.log
|
||||
mv paypal_create_order.log.gz paypal_create_order.$(date +%Y%m%d).log.gz
|
||||
touch paypal_create_order.log
|
||||
```
|
||||
|
||||
Or use `logrotate` (see `PAYPAL_DEBUGGING_GUIDE.md` for details).
|
||||
|
||||
### Monitoring
|
||||
|
||||
Set up automated monitoring to alert on:
|
||||
- High error rates
|
||||
- Specific error patterns (OAuth failures, DB connection issues)
|
||||
- Unusual request volumes
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues or need help interpreting logs:
|
||||
|
||||
1. Check `PAYPAL_DEBUGGING_GUIDE.md` for common issues
|
||||
2. Review `QUICK_DEBUG_REFERENCE.md` for quick fixes
|
||||
3. Provide log excerpts (with request IDs) when asking for help
|
||||
|
||||
## Changes Made By
|
||||
|
||||
- Enhanced logging system - Added 2025-10-29
|
||||
- Documentation created - 2025-10-29
|
||||
- Testing completed - 2025-10-29
|
||||
|
||||
---
|
||||
|
||||
**The intermittent JSON/HTTP 500 errors should now be fully traceable and debuggable with this comprehensive logging system.**
|
||||
201
backup-website/MIGRATION_SUMMARY.md
Normal file
201
backup-website/MIGRATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Billing System Migration Summary
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. `module.php` - Database Schema
|
||||
**Changes:**
|
||||
- Removed all legacy `ALTER TABLE` migration queries (db_version reset to 1)
|
||||
- Updated to single clean install with current schema
|
||||
- Added `ogp_billing_invoices` table definition
|
||||
- Added missing columns to `billing_orders`: `order_date`, `payment_txid`, `paid_ts`
|
||||
- Changed `end_date` from VARCHAR to DATETIME
|
||||
- Removed obsolete columns: `cart_id`, `extended`
|
||||
- Removed `billing_carts` table (replaced by invoices)
|
||||
- Added proper indexes for performance
|
||||
|
||||
### 2. `cron-shop.php` - Server Lifecycle Automation
|
||||
**Fixed Logic Errors:**
|
||||
- OLD BUG: Was deleting servers with `status='paid'` or `status='installed'` if end_date was close
|
||||
- NEW: Only processes servers based on **invoice payment status**, not just order status
|
||||
- Now uses `billing_invoices` table to determine if payment is due
|
||||
|
||||
**New 3-Step Process:**
|
||||
1. **Create Renewal Invoices** (7 days before expiration)
|
||||
- Find `installed` servers expiring soon
|
||||
- Check if unpaid invoice exists
|
||||
- If not, create renewal invoice
|
||||
- Send email reminder
|
||||
|
||||
2. **Suspend Servers** (on expiration with unpaid invoice)
|
||||
- Find `installed` servers past end_date
|
||||
- Check if they have unpaid invoices
|
||||
- Stop server, disable FTP, unassign from user
|
||||
- Status → `suspended`
|
||||
|
||||
3. **Delete Servers** (7 days after suspension)
|
||||
- Find `suspended` servers 7+ days past end_date
|
||||
- Still have unpaid invoices
|
||||
- Permanently delete files and database
|
||||
- Status → `deleted`
|
||||
|
||||
## New Files Created
|
||||
|
||||
### 1. `migration_to_invoices.sql`
|
||||
**Purpose:** Upgrade existing installations
|
||||
**What it does:**
|
||||
- Adds new columns to `billing_orders`
|
||||
- Creates `billing_invoices` table
|
||||
- Migrates existing paid orders to have invoice records
|
||||
- Removes obsolete `billing_carts` table
|
||||
- Adds performance indexes
|
||||
|
||||
### 2. `INVOICE_SYSTEM.md`
|
||||
**Purpose:** Documentation
|
||||
**Contents:**
|
||||
- Table schemas explained
|
||||
- Workflow diagrams
|
||||
- Status field definitions
|
||||
- Cron automation logic
|
||||
- Migration instructions
|
||||
|
||||
## SQL for Fresh Install
|
||||
|
||||
The `module.php` now contains clean CREATE TABLE statements for:
|
||||
|
||||
### ogp_billing_services
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_services` (
|
||||
service_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
service_name VARCHAR(255),
|
||||
remote_server_id VARCHAR(255),
|
||||
price_monthly FLOAT(15,4),
|
||||
enabled INT DEFAULT 1,
|
||||
... [other fields]
|
||||
);
|
||||
```
|
||||
|
||||
### ogp_billing_orders
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_orders` (
|
||||
order_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
service_id INT NOT NULL,
|
||||
home_name VARCHAR(255),
|
||||
home_id VARCHAR(255),
|
||||
status VARCHAR(16) DEFAULT 'in-cart',
|
||||
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
end_date DATETIME NULL,
|
||||
payment_txid VARCHAR(255) NULL,
|
||||
paid_ts DATETIME NULL,
|
||||
... [other fields]
|
||||
KEY (user_id),
|
||||
KEY (status),
|
||||
KEY (home_id)
|
||||
);
|
||||
```
|
||||
|
||||
### ogp_billing_invoices (NEW)
|
||||
```sql
|
||||
CREATE TABLE `ogp_billing_invoices` (
|
||||
invoice_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
customer_name VARCHAR(255),
|
||||
customer_email VARCHAR(255),
|
||||
amount FLOAT(15,2),
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
status VARCHAR(16) DEFAULT 'unpaid',
|
||||
invoice_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
due_date DATETIME NULL,
|
||||
paid_date DATETIME NULL,
|
||||
payment_txid VARCHAR(255),
|
||||
payment_method VARCHAR(50),
|
||||
description VARCHAR(500),
|
||||
invoice_duration VARCHAR(16),
|
||||
qty INT DEFAULT 1,
|
||||
KEY (order_id),
|
||||
KEY (user_id),
|
||||
KEY (status),
|
||||
KEY (due_date)
|
||||
);
|
||||
```
|
||||
|
||||
## Migration Steps for Existing Installations
|
||||
|
||||
1. **Backup Database**
|
||||
```bash
|
||||
mysqldump -u root -p ogp_panel > backup_before_invoice_migration.sql
|
||||
```
|
||||
|
||||
2. **Run Migration Script**
|
||||
```bash
|
||||
mysql -u root -p ogp_panel < modules/billing/migration_to_invoices.sql
|
||||
```
|
||||
|
||||
3. **Verify Tables**
|
||||
```sql
|
||||
SHOW TABLES LIKE 'ogp_billing%';
|
||||
-- Should show: billing_services, billing_orders, billing_invoices
|
||||
|
||||
DESCRIBE ogp_billing_orders;
|
||||
-- Should have: order_date, payment_txid, paid_ts, end_date (DATETIME)
|
||||
|
||||
DESCRIBE ogp_billing_invoices;
|
||||
-- Should exist with all invoice fields
|
||||
```
|
||||
|
||||
4. **Test Cron Job**
|
||||
```bash
|
||||
cd /path/to/ogp/web
|
||||
php modules/billing/cron-shop.php
|
||||
```
|
||||
|
||||
5. **Check Logs**
|
||||
```sql
|
||||
SELECT * FROM ogp_logger WHERE type LIKE '%BILLING-CRON%' ORDER BY date DESC LIMIT 20;
|
||||
```
|
||||
|
||||
## Key Improvements
|
||||
|
||||
1. **Accurate Server Management**
|
||||
- Servers only suspended if they have **unpaid invoices**
|
||||
- Active paid servers are never touched
|
||||
- Clear separation between order state and payment state
|
||||
|
||||
2. **Audit Trail**
|
||||
- Every payment creates an invoice record
|
||||
- Can track payment history per server
|
||||
- Know exactly when/why server was suspended
|
||||
|
||||
3. **Flexible Pricing**
|
||||
- Each renewal can have different price
|
||||
- Support for discounts and promotions
|
||||
- Currency per invoice (multi-currency support ready)
|
||||
|
||||
4. **Better Customer Experience**
|
||||
- Clear invoice emails with due dates
|
||||
- 7-day warning before expiration
|
||||
- 7-day grace period before deletion
|
||||
|
||||
## Status Field Values Reference
|
||||
|
||||
### billing_orders.status
|
||||
- `in-cart` - Initial state, unpaid
|
||||
- `paid` - Payment received, awaiting provisioning
|
||||
- `installed` - Server active and running ✅
|
||||
- `suspended` - Stopped due to non-payment
|
||||
- `deleted` - Permanently removed
|
||||
- `expired` - Service ended
|
||||
- `renew` - Renewal in cart (legacy, now uses invoices)
|
||||
|
||||
### billing_invoices.status
|
||||
- `unpaid` - Invoice created, awaiting payment
|
||||
- `paid` - Invoice paid successfully
|
||||
|
||||
## Next Steps for Implementation
|
||||
|
||||
1. Update cart.php to show invoices instead of orders
|
||||
2. Update my_account.php "Renew" button to create invoices
|
||||
3. Update payment success flow to mark invoices paid
|
||||
4. Add invoice viewing page
|
||||
5. Test full workflow: order → pay → renew → pay renewal
|
||||
282
backup-website/PAYMENT_IMPLEMENTATION_SUMMARY.md
Normal file
282
backup-website/PAYMENT_IMPLEMENTATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# Payment System Implementation Summary
|
||||
**Date:** November 5, 2025
|
||||
**Status:** ✅ COMPLETED - Ready for Testing
|
||||
|
||||
## What Was Done
|
||||
|
||||
### 1. **Updated Copilot Instructions** ✅
|
||||
- Added explicit standalone/relocatable requirements for `modules/billing/`
|
||||
- Emphasized: NEVER include panel files, use only standard PHP mysqli
|
||||
- Documented that billing module can be deployed on separate web host
|
||||
- All URLs must be root-relative (no `/modules/billing/` in runtime paths)
|
||||
|
||||
### 2. **Documented Status Values** ✅
|
||||
**Invoice Status** (`ogp_billing_invoices.status`):
|
||||
- `due` - Unpaid invoice, awaiting payment
|
||||
- `paid` - Invoice paid, order created
|
||||
- `pending` - Legacy status (some admin pages use this)
|
||||
- `renew` - Renewal invoice
|
||||
|
||||
**Order Status** (`ogp_billing_orders.status`):
|
||||
- `paid` - Payment received, awaiting server provisioning (panel auto-creates and marks `active`)
|
||||
- `active` - Server provisioned and running
|
||||
- `suspended` - Payment overdue, server stopped (grace period)
|
||||
- `deleted` - Server permanently removed
|
||||
- `renew` - Active but needs renewal payment
|
||||
|
||||
### 3. **Rebuilt Cart System** ✅
|
||||
**File:** `modules/billing/cart.php`
|
||||
|
||||
**Features:**
|
||||
- Displays all unpaid invoices (`status='due'`) for logged-in user
|
||||
- Shows: Game type, server name, duration, quantity, price
|
||||
- Professional table layout with totals
|
||||
- PayPal JS SDK integration (client-side payment)
|
||||
- Calls `/api/capture_order.php` backend after PayPal approval
|
||||
- Handles empty cart gracefully
|
||||
- Uses only standard mysqli (standalone compatible)
|
||||
|
||||
**Payment Flow:**
|
||||
1. User clicks PayPal button
|
||||
2. PayPal JS SDK creates order and processes payment
|
||||
3. On approval, calls our `/api/capture_order.php` with order_id
|
||||
4. Backend marks invoices paid, creates orders
|
||||
5. Redirects to `/payment_success.php`
|
||||
|
||||
### 4. **Rewrote Payment Capture Backend** ✅
|
||||
**File:** `modules/billing/api/capture_order.php` (old version backed up as `.backup`)
|
||||
|
||||
**Features:**
|
||||
- Simplified from 461 lines to ~250 lines
|
||||
- Clean output buffering (prevents JSON corruption)
|
||||
- Comprehensive logging to `logs/payment_capture.log`
|
||||
- Verifies PayPal order capture
|
||||
- Marks all `due` invoices as `paid`
|
||||
- Creates `billing_orders` records with `status='paid'`
|
||||
- Stores full PayPal response JSON in `paypal_data` column
|
||||
- Returns minimal JSON response (no truncation issues)
|
||||
|
||||
**Security:**
|
||||
- No output before JSON response
|
||||
- Validates session user_id
|
||||
- Logs all steps for debugging/audit trail
|
||||
- Stores PayPal transaction ID for refunds
|
||||
|
||||
### 5. **Enhanced Success Page** ✅
|
||||
**File:** `modules/billing/payment_success.php`
|
||||
|
||||
**Features:**
|
||||
- Professional confirmation page with success icon
|
||||
- Shows recent orders with details
|
||||
- Explains next steps (panel auto-provisioning)
|
||||
- Links to account management and order pages
|
||||
- Uses only standard mysqli (standalone compatible)
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Required Tables (Already Exist)
|
||||
- ✅ `ogp_billing_invoices` - Stores invoices (due/paid)
|
||||
- ✅ `ogp_billing_orders` - Stores orders (paid/active/suspended/deleted)
|
||||
- ✅ `ogp_billing_services` - Game server packages/pricing
|
||||
- ✅ `ogp_billing_coupons` - Discount coupons
|
||||
|
||||
### New Column Required
|
||||
**Run this SQL:**
|
||||
```sql
|
||||
ALTER TABLE `ogp_billing_orders`
|
||||
ADD COLUMN `paypal_data` TEXT NULL AFTER `payment_txid`
|
||||
COMMENT 'Full PayPal API response JSON for tracking/refunds';
|
||||
```
|
||||
**File:** `modules/billing/add_paypal_data_column.sql`
|
||||
|
||||
## Payment Flow Diagram
|
||||
|
||||
```
|
||||
User → order.php (select server)
|
||||
↓
|
||||
add_to_cart.php (create invoice with status='due')
|
||||
↓
|
||||
cart.php (show unpaid invoices + PayPal button)
|
||||
↓
|
||||
PayPal Checkout (user pays)
|
||||
↓
|
||||
api/capture_order.php (backend processing):
|
||||
- Verify PayPal payment
|
||||
- Mark invoices status='paid'
|
||||
- Create orders with status='paid'
|
||||
- Store PayPal JSON data
|
||||
↓
|
||||
payment_success.php (confirmation)
|
||||
↓
|
||||
User logs into Panel
|
||||
↓
|
||||
Panel auto-provisions servers (paid → active)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### PayPal Credentials
|
||||
**Location:** `modules/billing/api/capture_order.php` (lines 44-45)
|
||||
```php
|
||||
$sandbox = true; // Set to false for live
|
||||
$client_id = 'YOUR_CLIENT_ID';
|
||||
$client_secret = 'YOUR_CLIENT_SECRET';
|
||||
```
|
||||
|
||||
**Also update in:** `modules/billing/cart.php` (line 47)
|
||||
|
||||
### Database Connection
|
||||
**Location:** `modules/billing/includes/config.inc.php`
|
||||
```php
|
||||
$db_host = "your_host";
|
||||
$db_user = "your_user";
|
||||
$db_pass = "your_password";
|
||||
$db_name = "panel";
|
||||
$table_prefix = "ogp_";
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Pre-Test Setup
|
||||
- [ ] Run SQL: `add_paypal_data_column.sql`
|
||||
- [ ] Verify PayPal sandbox credentials are set
|
||||
- [ ] Confirm database connection works
|
||||
- [ ] Ensure user is logged in (session has `website_user_id`)
|
||||
|
||||
### Test Flow
|
||||
1. **Order Creation**
|
||||
- [ ] Go to `/order.php`
|
||||
- [ ] Select a game server
|
||||
- [ ] Configure settings
|
||||
- [ ] Click "Add to Cart"
|
||||
- [ ] Verify invoice created in `ogp_billing_invoices` with `status='due'`
|
||||
|
||||
2. **Cart Display**
|
||||
- [ ] Go to `/cart.php`
|
||||
- [ ] Verify invoice(s) displayed with correct details
|
||||
- [ ] Verify total amount is correct
|
||||
- [ ] Verify PayPal button appears
|
||||
|
||||
3. **Payment Processing**
|
||||
- [ ] Click PayPal button
|
||||
- [ ] Complete sandbox payment
|
||||
- [ ] Check `logs/payment_capture.log` for processing details
|
||||
- [ ] Verify no JSON errors in browser console
|
||||
- [ ] Verify redirected to `/payment_success.php`
|
||||
|
||||
4. **Database Verification**
|
||||
- [ ] Check `ogp_billing_invoices`: `status='paid'`, `payment_txid` set
|
||||
- [ ] Check `ogp_billing_orders`: New record with `status='paid'`
|
||||
- [ ] Check `paypal_data` column contains JSON
|
||||
- [ ] Verify `order_id` in invoice links to order
|
||||
|
||||
5. **Success Page**
|
||||
- [ ] Verify order(s) displayed
|
||||
- [ ] Verify correct amounts shown
|
||||
- [ ] Verify all links work
|
||||
|
||||
6. **Panel Provisioning** (Future - Not Implemented Yet)
|
||||
- [ ] Log into panel
|
||||
- [ ] Panel detects orders with `status='paid'`
|
||||
- [ ] Panel creates game server homes
|
||||
- [ ] Panel updates order `status='active'`
|
||||
|
||||
## What's NOT Done Yet (Todo)
|
||||
|
||||
### High Priority
|
||||
- [ ] **Email Notifications** - Send confirmation email after payment
|
||||
- [ ] **Invoice History Page** - Show user's paid invoices (`my_invoices.php`)
|
||||
- [ ] **Suspended Status Support** - Verify cron job handles suspended orders correctly
|
||||
|
||||
### Medium Priority
|
||||
- [ ] **Refund System** - Admin interface to issue PayPal refunds using stored JSON data
|
||||
- [ ] **Webhook Support** - Add PayPal webhook handler for payment verification (more secure than client-side)
|
||||
- [ ] **Coupon Application** - Apply discount coupons during checkout
|
||||
|
||||
### Low Priority
|
||||
- [ ] **Multi-currency Support** - Currently USD only
|
||||
- [ ] **Tax Calculation** - Add tax/VAT support
|
||||
- [ ] **Payment Plans** - Recurring subscriptions via PayPal
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Core Payment Files
|
||||
- ✅ `modules/billing/cart.php` - Complete rewrite
|
||||
- ✅ `modules/billing/api/capture_order.php` - Simplified rewrite (old backed up)
|
||||
- ✅ `modules/billing/payment_success.php` - Enhanced with order display
|
||||
|
||||
### Configuration
|
||||
- ✅ `.github/copilot-instructions.md` - Added standalone/relocatable requirements
|
||||
|
||||
### Database
|
||||
- ✅ `modules/billing/add_paypal_data_column.sql` - New migration file
|
||||
|
||||
### Existing Files (Not Modified)
|
||||
- `modules/billing/add_to_cart.php` - Already working correctly
|
||||
- `modules/billing/order.php` - Already working correctly
|
||||
- `modules/billing/includes/config.inc.php` - Config file (no changes needed)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: JSON Parse Error
|
||||
**Cause:** Output before JSON response (whitespace, errors, warnings)
|
||||
**Fix:** Check `logs/payment_capture.log` for errors. Ensure `ob_start()` at top of `capture_order.php`
|
||||
|
||||
### Issue: No Orders Created
|
||||
**Cause:** User not logged in or session lost
|
||||
**Fix:** Verify session contains `website_user_id` or `user_id`
|
||||
|
||||
### Issue: Invoices Not Marked Paid
|
||||
**Cause:** Database connection failed or SQL error
|
||||
**Fix:** Check `logs/payment_capture.log` for database errors
|
||||
|
||||
### Issue: PayPal Button Doesn't Appear
|
||||
**Cause:** Empty cart or JS error
|
||||
**Fix:** Check browser console. Verify invoices exist with `status='due'`
|
||||
|
||||
### Issue: 500 Error on capture_order.php
|
||||
**Cause:** PHP error in capture script
|
||||
**Fix:** Check `logs/payment_capture.log` and PHP error logs
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
### Same Host Deployment
|
||||
Files already at correct location: `modules/billing/`
|
||||
|
||||
### External Host Deployment
|
||||
1. Copy entire `modules/billing/` directory to external web host
|
||||
2. Deploy at website root (not in subdirectory)
|
||||
3. Update `includes/config.inc.php` with panel database credentials
|
||||
4. Ensure external host can connect to panel database (firewall/network)
|
||||
5. Update PayPal return URLs to external domain
|
||||
|
||||
## Security Considerations
|
||||
|
||||
✅ **Implemented:**
|
||||
- Output buffering prevents JSON corruption
|
||||
- SQL injection protection (mysqli_real_escape_string)
|
||||
- Session validation (user_id required)
|
||||
- PayPal OAuth token authentication
|
||||
- Comprehensive audit logging
|
||||
|
||||
⚠️ **Recommended (Not Implemented):**
|
||||
- CSRF token validation on payment endpoints
|
||||
- Rate limiting on API endpoints
|
||||
- PayPal webhook signature verification
|
||||
- IP whitelisting for admin functions
|
||||
|
||||
## Support & Maintenance
|
||||
|
||||
### Log Files
|
||||
- `modules/billing/logs/payment_capture.log` - Payment processing log
|
||||
- `modules/billing/logs/add_to_cart.log` - Cart/invoice creation log
|
||||
- `modules/billing/logs/site.log` - General site log
|
||||
|
||||
### Key Functions
|
||||
- `capture_order.php::log_payment()` - Payment logging function
|
||||
- Database schema in `create_invoices_table.sql`
|
||||
|
||||
### Contact
|
||||
For issues or questions, refer to:
|
||||
- GitHub repo: `GameServerPanel/GSP` branch `Panel-unstable`
|
||||
- This summary: `modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md`
|
||||
316
backup-website/PAYPAL_DEBUGGING_GUIDE.md
Normal file
316
backup-website/PAYPAL_DEBUGGING_GUIDE.md
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
# PayPal Payment Flow Debugging Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to diagnose and troubleshoot PayPal payment errors using the comprehensive logging system that has been added to the payment flow.
|
||||
|
||||
## Problem Being Addressed
|
||||
|
||||
Users were experiencing intermittent errors when clicking "Pay from PayPal" button:
|
||||
- JSON parsing errors
|
||||
- HTTP ERROR 500
|
||||
- "Currently unable to handle this request" errors
|
||||
|
||||
These errors would "flip-flop" between different error types, making it difficult to diagnose the root cause.
|
||||
|
||||
## Log Files Location
|
||||
|
||||
All logs are stored in: `/modules/billing/logs/`
|
||||
|
||||
### Available Log Files
|
||||
|
||||
1. **`paypal_create_order.log`** - Logs all PayPal order creation requests
|
||||
- When: Created when user clicks "Pay with PayPal" button
|
||||
- Contains: Request data, OAuth tokens, PayPal API responses
|
||||
|
||||
2. **`paypal_capture.log`** - Logs all payment capture attempts
|
||||
- When: Created when PayPal redirects user back after approving payment
|
||||
- Contains: Capture requests, database operations, order creation
|
||||
|
||||
3. **`client_errors.log`** - Logs JavaScript errors from browser
|
||||
- When: Created when browser encounters errors during checkout
|
||||
- Contains: Client-side errors, PayPal SDK issues, network failures
|
||||
|
||||
## How to Debug Payment Issues
|
||||
|
||||
### Step 1: Identify the Request
|
||||
|
||||
Each request has a unique ID for tracking:
|
||||
- Create order requests: `req_XXXXX`
|
||||
- Capture order requests: `cap_XXXXX`
|
||||
|
||||
Look for these IDs in error messages shown to users.
|
||||
|
||||
### Step 2: Check the Logs
|
||||
|
||||
#### For "Failed to create order" errors:
|
||||
|
||||
```bash
|
||||
tail -100 /modules/billing/logs/paypal_create_order.log
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `JSON_DECODE_ERROR` - Invalid input from cart.php
|
||||
- `OAUTH_CURL_ERROR` or `OAUTH_HTTP_ERROR` - Can't connect to PayPal
|
||||
- `CREATE_ORDER_HTTP_ERROR` - PayPal rejected the order
|
||||
|
||||
#### For "Payment capture failed" errors:
|
||||
|
||||
```bash
|
||||
tail -100 /modules/billing/logs/paypal_capture.log
|
||||
```
|
||||
|
||||
Look for:
|
||||
- `OAUTH_*_ERROR` - Authentication issues
|
||||
- `CAPTURE_HTTP_ERROR` - PayPal rejected capture
|
||||
- `DB_CONNECTION_FAILED` - Database issues
|
||||
- `UPDATE_INVOICES_FAILED` - Can't mark invoices as paid
|
||||
- `ORDER_CREATE_FAILED` - Can't create order record
|
||||
|
||||
#### For client-side errors:
|
||||
|
||||
```bash
|
||||
tail -100 /modules/billing/logs/client_errors.log
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Network errors (fetch failed)
|
||||
- PayPal SDK errors
|
||||
- JSON parsing errors
|
||||
|
||||
### Step 3: Common Issues and Solutions
|
||||
|
||||
#### Issue: OAuth fails (OAUTH_HTTP_ERROR)
|
||||
|
||||
**Log entry example:**
|
||||
```
|
||||
[2025-10-29 21:30:00] [req_12345] OAUTH_HTTP_ERROR
|
||||
http_code => 401
|
||||
```
|
||||
|
||||
**Cause:** Invalid PayPal credentials
|
||||
|
||||
**Solution:** Check that `$client_id` and `$client_secret` in `api/create_order.php` and `api/capture_order.php` are correct.
|
||||
|
||||
---
|
||||
|
||||
#### Issue: JSON decode error
|
||||
|
||||
**Log entry example:**
|
||||
```
|
||||
[2025-10-29 21:30:00] [req_12345] JSON_DECODE_ERROR
|
||||
error => Syntax error
|
||||
```
|
||||
|
||||
**Cause:** Malformed JSON from cart.php or corrupted request
|
||||
|
||||
**Solution:**
|
||||
1. Check the `RAW_INPUT` entry before the error
|
||||
2. Verify cart.php is sending valid JSON
|
||||
3. Check for PHP errors that might corrupt the output
|
||||
|
||||
---
|
||||
|
||||
#### Issue: PayPal returns error creating order
|
||||
|
||||
**Log entry example:**
|
||||
```
|
||||
[2025-10-29 21:30:00] [req_12345] CREATE_ORDER_HTTP_ERROR
|
||||
http_code => 400
|
||||
response => {"name":"INVALID_REQUEST","details":[{"issue":"..."}]}
|
||||
```
|
||||
|
||||
**Cause:** Invalid order data sent to PayPal
|
||||
|
||||
**Solution:**
|
||||
1. Look at `PAYPAL_ORDER_PAYLOAD` entry to see what was sent
|
||||
2. Common issues:
|
||||
- Invalid amount format (must be 2 decimals)
|
||||
- Invalid currency code
|
||||
- Malformed items array
|
||||
- Invalid URLs (return_url, cancel_url must be absolute URLs)
|
||||
|
||||
---
|
||||
|
||||
#### Issue: Database connection failed
|
||||
|
||||
**Log entry example:**
|
||||
```
|
||||
[2025-10-29 21:30:00] [cap_12345] DB_CONNECTION_FAILED
|
||||
error => Access denied for user
|
||||
```
|
||||
|
||||
**Cause:** Can't connect to database
|
||||
|
||||
**Solution:**
|
||||
1. Check database credentials in `includes/config.inc.php`
|
||||
2. Verify database server is running
|
||||
3. Check database permissions
|
||||
|
||||
---
|
||||
|
||||
#### Issue: Invoice update failed
|
||||
|
||||
**Log entry example:**
|
||||
```
|
||||
[2025-10-29 21:30:00] [cap_12345] UPDATE_INVOICES_FAILED
|
||||
error => Table 'ogp_billing_invoices' doesn't exist
|
||||
```
|
||||
|
||||
**Cause:** Database schema issue
|
||||
|
||||
**Solution:**
|
||||
1. Verify table exists and has correct name
|
||||
2. Check `$table_prefix` variable in config
|
||||
3. Run database migrations if needed
|
||||
|
||||
## Log Entry Structure
|
||||
|
||||
Each log entry includes:
|
||||
|
||||
```
|
||||
[TIMESTAMP] [REQUEST_ID] LOG_LABEL
|
||||
key => value
|
||||
key => value
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
- **TIMESTAMP**: When the event occurred (Y-m-d H:i:s format)
|
||||
- **REQUEST_ID**: Unique identifier for tracking the request
|
||||
- **LOG_LABEL**: What happened (e.g., OAUTH_SUCCESS, CREATE_ORDER_FAILED)
|
||||
- **Data**: Relevant data for the event (arrays/objects pretty-printed)
|
||||
|
||||
## Request Flow with Logging
|
||||
|
||||
### Creating an Order
|
||||
|
||||
1. User clicks "Pay with PayPal" in cart.php
|
||||
2. JavaScript calls `api/create_order.php`
|
||||
3. Logs generated:
|
||||
- `REQUEST_START` - Initial request info
|
||||
- `RAW_INPUT` - What was received
|
||||
- `PARSED_INPUT` - Decoded data
|
||||
- `OAUTH_REQUEST_START` - Starting OAuth
|
||||
- `OAUTH_RESPONSE` - OAuth result
|
||||
- `OAUTH_SUCCESS` or `OAUTH_*_ERROR`
|
||||
- `CREATE_ORDER_REQUEST_START` - Sending to PayPal
|
||||
- `CREATE_ORDER_RESPONSE` - PayPal's response
|
||||
- `CREATE_ORDER_SUCCESS` or `CREATE_ORDER_*_ERROR`
|
||||
|
||||
### Capturing Payment
|
||||
|
||||
1. User approves payment on PayPal
|
||||
2. PayPal redirects back to site
|
||||
3. JavaScript calls `api/capture_order.php`
|
||||
4. Logs generated:
|
||||
- `REQUEST_START` - Initial request
|
||||
- `RAW_INPUT` - Order ID received
|
||||
- `PARSED_INPUT` - Decoded data
|
||||
- `OAUTH_*` - Authentication steps
|
||||
- `CAPTURE_REQUEST_START` - Starting capture
|
||||
- `CAPTURE_RESPONSE` - PayPal's response
|
||||
- `CAPTURE_SUCCESS` or `CAPTURE_*_ERROR`
|
||||
- `PAYMENT_DETAILS` - Extracted transaction info
|
||||
- `STARTING_DB_PROCESSING` - Beginning database work
|
||||
- `DB_CONNECTED` - Database ready
|
||||
- `SESSION_INFO` - User session details
|
||||
- `PROCESSING_INVOICES` - Starting invoice processing
|
||||
- `UPDATE_INVOICES_*` - Invoice update results
|
||||
- `PROCESSING_INVOICE` - For each invoice
|
||||
- `NEW_ORDER_DETECTED` or `RENEWAL_DETECTED`
|
||||
- `ORDER_CREATE_*` or `ORDER_EXTENDED_*`
|
||||
- `PROCESSING_COMPLETE` - Done
|
||||
|
||||
## Monitoring Tips
|
||||
|
||||
### Watch logs in real-time
|
||||
|
||||
```bash
|
||||
# Watch create order logs
|
||||
tail -f /modules/billing/logs/paypal_create_order.log
|
||||
|
||||
# Watch capture logs
|
||||
tail -f /modules/billing/logs/paypal_capture.log
|
||||
|
||||
# Watch all logs
|
||||
tail -f /modules/billing/logs/*.log
|
||||
```
|
||||
|
||||
### Filter for errors only
|
||||
|
||||
```bash
|
||||
grep -i error /modules/billing/logs/paypal_create_order.log
|
||||
grep -i failed /modules/billing/logs/paypal_capture.log
|
||||
```
|
||||
|
||||
### Find specific request by ID
|
||||
|
||||
```bash
|
||||
grep "req_abc123" /modules/billing/logs/paypal_create_order.log
|
||||
grep "cap_xyz789" /modules/billing/logs/paypal_capture.log
|
||||
```
|
||||
|
||||
### Count successful vs failed requests
|
||||
|
||||
```bash
|
||||
grep -c "CREATE_ORDER_SUCCESS" /modules/billing/logs/paypal_create_order.log
|
||||
grep -c "CREATE_ORDER.*ERROR" /modules/billing/logs/paypal_create_order.log
|
||||
```
|
||||
|
||||
## Log Rotation
|
||||
|
||||
Logs will grow over time. Consider implementing log rotation:
|
||||
|
||||
```bash
|
||||
# Archive old logs
|
||||
cd /modules/billing/logs
|
||||
gzip paypal_create_order.log
|
||||
mv paypal_create_order.log.gz paypal_create_order.$(date +%Y%m%d).log.gz
|
||||
touch paypal_create_order.log
|
||||
```
|
||||
|
||||
Or use logrotate:
|
||||
|
||||
```
|
||||
/path/to/modules/billing/logs/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0644 www-data www-data
|
||||
}
|
||||
```
|
||||
|
||||
## Error Messages to Users
|
||||
|
||||
When errors occur, users now see messages with request IDs:
|
||||
|
||||
- "Failed to create order: API error 500 (Ref: req_abc123)"
|
||||
- "Payment capture failed: oauth_fail (Ref: cap_xyz789)"
|
||||
|
||||
Use these reference IDs to search the logs for the full details.
|
||||
|
||||
## Getting Help
|
||||
|
||||
When reporting issues, include:
|
||||
|
||||
1. The exact error message shown to user (including Ref ID)
|
||||
2. Relevant log entries (search by Ref ID)
|
||||
3. What the user was trying to do
|
||||
4. Whether it's consistent or intermittent
|
||||
5. Browser console output (F12 → Console tab)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- PayPal API Documentation: https://developer.paypal.com/api/rest/
|
||||
- PayPal Sandbox Testing: https://developer.paypal.com/developer/accounts/
|
||||
- PayPal Error Codes: https://developer.paypal.com/api/rest/reference/orders/v2/errors/
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-10-29
|
||||
- Added comprehensive logging to create_order.php
|
||||
- Enhanced logging in capture_order.php
|
||||
- Added client-side error logging
|
||||
- Created debugging guide
|
||||
186
backup-website/QUICK_DEBUG_REFERENCE.md
Normal file
186
backup-website/QUICK_DEBUG_REFERENCE.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# PayPal Payment Flow - Quick Debug Reference
|
||||
|
||||
## Quick Commands
|
||||
|
||||
### View recent errors:
|
||||
```bash
|
||||
cd /home/runner/work/GSP/GSP/modules/billing/logs
|
||||
|
||||
# Last 50 lines of create order log
|
||||
tail -50 paypal_create_order.log
|
||||
|
||||
# Last 50 lines of capture log
|
||||
tail -50 paypal_capture.log
|
||||
|
||||
# Last 50 lines of client errors
|
||||
tail -50 client_errors.log
|
||||
```
|
||||
|
||||
### Watch logs live:
|
||||
```bash
|
||||
# In terminal, run:
|
||||
tail -f /home/runner/work/GSP/GSP/modules/billing/logs/paypal_*.log
|
||||
```
|
||||
|
||||
### Search for specific error:
|
||||
```bash
|
||||
# Find all OAuth errors
|
||||
grep "OAUTH.*ERROR" paypal_create_order.log paypal_capture.log
|
||||
|
||||
# Find database errors
|
||||
grep "DB.*FAILED" paypal_capture.log
|
||||
|
||||
# Find a specific request by ID
|
||||
grep "req_12345" paypal_create_order.log
|
||||
```
|
||||
|
||||
## Common Error Patterns
|
||||
|
||||
### ❌ "JSON error" or "unable to handle this request"
|
||||
|
||||
**What to check:**
|
||||
1. Browser console (F12 → Console tab) for JavaScript errors
|
||||
2. `client_errors.log` for client-side issues
|
||||
3. `paypal_create_order.log` for `JSON_DECODE_ERROR`
|
||||
|
||||
**Quick fix:**
|
||||
- Check if cart items are valid
|
||||
- Verify amount calculations are correct
|
||||
- Look for PHP errors that might corrupt JSON output
|
||||
|
||||
---
|
||||
|
||||
### ❌ HTTP ERROR 500
|
||||
|
||||
**What to check:**
|
||||
1. `paypal_create_order.log` for `CREATE_ORDER_HTTP_ERROR`
|
||||
2. `paypal_capture.log` for `CAPTURE_HTTP_ERROR`
|
||||
3. Look for `OAUTH.*ERROR` entries
|
||||
|
||||
**Quick fix:**
|
||||
- Verify PayPal credentials are correct
|
||||
- Check PayPal API status: https://www.paypal-status.com/
|
||||
- Verify sandbox vs live mode settings match credentials
|
||||
|
||||
---
|
||||
|
||||
### ❌ Payment seems successful but no order created
|
||||
|
||||
**What to check:**
|
||||
1. `paypal_capture.log` for `DB_CONNECTION_FAILED`
|
||||
2. Look for `UPDATE_INVOICES_FAILED`
|
||||
3. Check `ORDER_CREATE_FAILED`
|
||||
|
||||
**Quick fix:**
|
||||
- Verify database connection settings
|
||||
- Check if `ogp_billing_invoices` table exists
|
||||
- Verify `ogp_billing_orders` table exists
|
||||
- Check table permissions
|
||||
|
||||
---
|
||||
|
||||
### ❌ Intermittent failures (works sometimes, fails sometimes)
|
||||
|
||||
**What to check:**
|
||||
1. Compare successful vs failed requests in logs
|
||||
2. Look for timeout errors (`CURL.*ERROR`)
|
||||
3. Check for database connection pool exhaustion
|
||||
|
||||
**Quick fix:**
|
||||
- Check server load/resources
|
||||
- Verify network connectivity to PayPal API
|
||||
- Check for rate limiting
|
||||
|
||||
## Log File Locations
|
||||
|
||||
```
|
||||
/home/runner/work/GSP/GSP/modules/billing/logs/
|
||||
├── paypal_create_order.log # Order creation (when clicking "Pay")
|
||||
├── paypal_capture.log # Payment capture (after PayPal approval)
|
||||
└── client_errors.log # JavaScript/browser errors
|
||||
```
|
||||
|
||||
## Request ID Format
|
||||
|
||||
- Create order: `req_XXXXXXXXXXXXX`
|
||||
- Capture order: `cap_XXXXXXXXXXXXX`
|
||||
|
||||
When user sees an error with `(Ref: req_abc123)`, search logs for that ID.
|
||||
|
||||
## Important Log Labels
|
||||
|
||||
### Create Order Flow:
|
||||
- `REQUEST_START` → `RAW_INPUT` → `PARSED_INPUT`
|
||||
- `OAUTH_REQUEST_START` → `OAUTH_SUCCESS`
|
||||
- `CREATE_ORDER_REQUEST_START` → `CREATE_ORDER_SUCCESS`
|
||||
|
||||
### Capture Order Flow:
|
||||
- `REQUEST_START` → `PARSED_INPUT`
|
||||
- `OAUTH_SUCCESS` → `CAPTURE_SUCCESS`
|
||||
- `DB_CONNECTED` → `PROCESSING_INVOICES`
|
||||
- `ORDER_CREATED_SUCCESS` or `ORDER_EXTENDED_SUCCESS`
|
||||
|
||||
### Error Labels:
|
||||
- `*_ERROR` - Something went wrong
|
||||
- `*_FAILED` - Operation failed
|
||||
- `INVALID_*` - Invalid input/data
|
||||
|
||||
## Browser Console Debugging
|
||||
|
||||
1. Open cart page
|
||||
2. Press F12 to open DevTools
|
||||
3. Go to Console tab
|
||||
4. Click "Pay with PayPal"
|
||||
5. Watch for:
|
||||
- Red error messages
|
||||
- `PayPal Error:` logs
|
||||
- Network errors (check Network tab)
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
When testing payments:
|
||||
|
||||
- [ ] Check browser console for errors
|
||||
- [ ] Note the Ref ID if error occurs
|
||||
- [ ] Check `paypal_create_order.log` for the request
|
||||
- [ ] Check `paypal_capture.log` if got past order creation
|
||||
- [ ] Verify database tables exist and have data
|
||||
- [ ] Check PayPal sandbox account activity
|
||||
|
||||
## Need More Help?
|
||||
|
||||
See full guide: `PAYPAL_DEBUGGING_GUIDE.md`
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
- PayPal credentials: `api/create_order.php` and `api/capture_order.php`
|
||||
- Lines 5-6: `$client_id` and `$client_secret`
|
||||
- Line 4: `$sandbox` (true/false)
|
||||
|
||||
- Database config: `includes/config.inc.php`
|
||||
- `$db_host`, `$db_user`, `$db_pass`, `$db_name`
|
||||
- `$table_prefix`
|
||||
|
||||
## Status Checklist for Issues
|
||||
|
||||
When user reports error:
|
||||
|
||||
1. **Get details:**
|
||||
- [ ] What error message did they see?
|
||||
- [ ] What was the Ref ID (if shown)?
|
||||
- [ ] Can they reproduce it?
|
||||
|
||||
2. **Check logs:**
|
||||
- [ ] Find the request by Ref ID
|
||||
- [ ] Look for ERROR or FAILED labels
|
||||
- [ ] Check surrounding context (before/after)
|
||||
|
||||
3. **Verify config:**
|
||||
- [ ] PayPal credentials valid?
|
||||
- [ ] Database connection working?
|
||||
- [ ] Correct sandbox/live mode?
|
||||
|
||||
4. **Test:**
|
||||
- [ ] Try creating test order
|
||||
- [ ] Watch logs in real-time
|
||||
- [ ] Check database for created records
|
||||
287
backup-website/README_COUPON_UPDATE.md
Normal file
287
backup-website/README_COUPON_UPDATE.md
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# Billing Module Standalone & Coupon System - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This update addresses two major requirements:
|
||||
|
||||
1. **Standalone Billing Module**: The billing module can now operate independently from the panel, either on the same server or on a separate web host.
|
||||
2. **Enhanced Coupon System**: A comprehensive coupon system with game filters, usage tracking, and permanent/one-time discount options.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Standalone Database Connection (Critical Fix)
|
||||
|
||||
**Problem**: The billing module was trying to use panel database functions that don't exist when deployed on a separate server, causing PayPal payment processing to fail with "Unexpected end of JSON input" error.
|
||||
|
||||
**Solution**:
|
||||
- Removed all `require_once` statements that reference panel files like `includes/database_mysqli.php`
|
||||
- Replaced panel database functions with native mysqli functions
|
||||
- Created standalone `config.inc.php` file for database credentials
|
||||
- Updated `api/capture_order.php` to use `mysqli_connect()` instead of `createDatabaseConnection()`
|
||||
|
||||
**Files Modified**:
|
||||
- `.github/copilot-instructions.md` - Added standalone requirement documentation
|
||||
- `modules/billing/includes/config.inc.php` - Created from template (should be gitignored in production)
|
||||
- `modules/billing/api/capture_order.php` - Fixed database connection
|
||||
|
||||
### 2. Enhanced Coupon System
|
||||
|
||||
**Features Implemented**:
|
||||
- ✅ Create, edit, delete coupons through admin interface
|
||||
- ✅ Percentage-based discounts (0-100%)
|
||||
- ✅ One-time vs. permanent discount types
|
||||
- ✅ Game-specific filtering (all games or specific games)
|
||||
- ✅ Usage limits and tracking
|
||||
- ✅ Expiration dates
|
||||
- ✅ Coupon application in cart with real-time price updates
|
||||
- ✅ Automatic discount application on payment
|
||||
- ✅ Discount display in My Servers and Admin Invoices views
|
||||
|
||||
**Files Created**:
|
||||
- `modules/billing/create_coupons_table.sql` - Database schema
|
||||
- `modules/billing/admin_coupons.php` - Admin management interface
|
||||
- `modules/billing/COUPON_SYSTEM.md` - Comprehensive documentation
|
||||
|
||||
**Files Modified**:
|
||||
- `modules/billing/admin.php` - Added "Manage Coupons" link
|
||||
- `modules/billing/cart.php` - Added coupon application form and discount logic
|
||||
- `modules/billing/api/capture_order.php` - Apply coupons on payment, track usage
|
||||
- `modules/billing/my_servers.php` - Display discount information
|
||||
- `modules/billing/admin_invoices.php` - Display discount information
|
||||
|
||||
### 3. Database Schema Updates
|
||||
|
||||
**New Table**: `ogp_billing_coupons`
|
||||
```sql
|
||||
- coupon_id (primary key)
|
||||
- code (unique)
|
||||
- name, description
|
||||
- discount_percent
|
||||
- usage_type (one_time/permanent)
|
||||
- game_filter_type (all_games/specific_games)
|
||||
- game_filter_list (JSON array of game keys)
|
||||
- max_uses, current_uses
|
||||
- expires, is_active
|
||||
```
|
||||
|
||||
**Updated Tables**:
|
||||
- `ogp_billing_invoices`: Added `coupon_id`, `discount_amount`
|
||||
- `ogp_billing_orders`: Added `coupon_id`, `discount_amount`
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
### Prerequisites
|
||||
- MySQL/MariaDB database
|
||||
- PHP 7.4 or higher
|
||||
- Existing billing module installation
|
||||
|
||||
### Step 1: Create Configuration File
|
||||
|
||||
If deploying on a separate server (not co-located with panel):
|
||||
|
||||
```bash
|
||||
cd modules/billing/includes/
|
||||
cp config.inc.php.orig config.inc.php
|
||||
```
|
||||
|
||||
Edit `config.inc.php` with your database credentials:
|
||||
```php
|
||||
$db_host = "your-db-host";
|
||||
$db_user = "your-db-user";
|
||||
$db_pass = "your-db-password";
|
||||
$db_name = "your-db-name";
|
||||
$table_prefix = "ogp_";
|
||||
```
|
||||
|
||||
**Important**: Add `config.inc.php` to `.gitignore` to prevent committing sensitive credentials.
|
||||
|
||||
### Step 2: Run Database Migration
|
||||
|
||||
```bash
|
||||
mysql -u [username] -p [database] < modules/billing/create_coupons_table.sql
|
||||
```
|
||||
|
||||
Or import via phpMyAdmin.
|
||||
|
||||
### Step 3: Verify Installation
|
||||
|
||||
1. Log in as admin: `/modules/billing/admin.php`
|
||||
2. Click "Manage Coupons"
|
||||
3. You should see the coupon management interface with 2 sample coupons
|
||||
|
||||
### Step 4: Test Coupon System
|
||||
|
||||
1. Create a test coupon or use existing "WELCOME10"
|
||||
2. Add a server to cart: `/modules/billing/order.php`
|
||||
3. View cart: `/modules/billing/cart.php`
|
||||
4. Apply coupon code
|
||||
5. Verify discount is calculated correctly
|
||||
6. Complete payment (or use free server button if admin)
|
||||
7. Check My Servers page for discount display
|
||||
|
||||
## Usage
|
||||
|
||||
### For Administrators
|
||||
|
||||
**Create a Coupon**:
|
||||
1. Navigate to Admin → Manage Coupons
|
||||
2. Scroll to "Add New Coupon" form
|
||||
3. Fill in details:
|
||||
- Code (e.g., "SUMMER25")
|
||||
- Discount percentage (e.g., 25 for 25% off)
|
||||
- Usage type (one-time or permanent)
|
||||
- Game filter (all games or specific)
|
||||
4. Click "Add Coupon"
|
||||
|
||||
**Monitor Usage**:
|
||||
- View current uses vs. max uses in coupon list
|
||||
- Edit or deactivate coupons as needed
|
||||
- Delete expired or unused coupons
|
||||
|
||||
### For Customers
|
||||
|
||||
**Apply a Coupon**:
|
||||
1. Add servers to cart
|
||||
2. On cart page, find "Have a coupon code?" section
|
||||
3. Enter coupon code
|
||||
4. Click "Apply Coupon"
|
||||
5. Prices update automatically
|
||||
6. Proceed to PayPal checkout
|
||||
|
||||
**View Discounts**:
|
||||
- Cart page shows applied discount
|
||||
- My Servers page shows original price (strikethrough) and discounted price
|
||||
- Coupon code displayed with percentage
|
||||
|
||||
## Coupon Types Explained
|
||||
|
||||
### One-Time Coupons
|
||||
- Applied to first invoice only
|
||||
- Renewals use original price
|
||||
- Example: "WELCOME10" for new customers
|
||||
|
||||
### Permanent Coupons
|
||||
- Applied to initial purchase AND all renewals
|
||||
- Discount stored in order record
|
||||
- Example: "VIP50" for permanent 50% off
|
||||
|
||||
### Game Filters
|
||||
|
||||
**All Games**:
|
||||
- Coupon applies to any game in cart
|
||||
- Simplest option for general promotions
|
||||
|
||||
**Specific Games**:
|
||||
- Define list of game keys
|
||||
- Only matching games get discount
|
||||
- Uses partial matching (e.g., "arma3" matches "arma3_linux64")
|
||||
- Example: Arma-only promotion
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PayPal Payment Returns JSON Error
|
||||
|
||||
**Symptom**: "Unexpected end of JSON input" on cart page after PayPal payment
|
||||
|
||||
**Cause**: Missing `config.inc.php` or incorrect database credentials
|
||||
|
||||
**Fix**:
|
||||
1. Check `/modules/billing/includes/config.inc.php` exists
|
||||
2. Verify credentials are correct
|
||||
3. Test database connection: `/modules/billing/test_db_connection.php`
|
||||
4. Check error logs: `/modules/billing/logs/` and server error log
|
||||
|
||||
### Coupon Not Applying
|
||||
|
||||
**Checks**:
|
||||
- Code is correct (case-sensitive)
|
||||
- Coupon is active
|
||||
- Not expired
|
||||
- Usage limit not reached
|
||||
- Game matches filter (for game-specific coupons)
|
||||
|
||||
### Discount Not Showing After Payment
|
||||
|
||||
**Checks**:
|
||||
- Database schema includes `discount_amount` columns
|
||||
- `coupon_id` was saved to invoice/order
|
||||
- Clear browser cache
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Sensitive Files**: Add `modules/billing/includes/config.inc.php` to `.gitignore`
|
||||
2. **Database Credentials**: Use read-only credentials if possible (billing only needs read/write to billing tables)
|
||||
3. **CSRF Protection**: All admin forms include CSRF tokens
|
||||
4. **Input Sanitization**: All user inputs are sanitized with `mysqli_real_escape_string()`
|
||||
5. **SQL Injection**: Parameterized queries or escaped strings throughout
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
modules/billing/
|
||||
├── api/
|
||||
│ ├── capture_order.php (Modified - standalone DB connection)
|
||||
│ └── create_order.php
|
||||
├── includes/
|
||||
│ ├── config.inc.php (Created - DB config)
|
||||
│ └── config.inc.php.orig (Template)
|
||||
├── admin_coupons.php (Created - Coupon management UI)
|
||||
├── admin_invoices.php (Modified - Show discounts)
|
||||
├── cart.php (Modified - Coupon application)
|
||||
├── my_servers.php (Modified - Show discounts)
|
||||
├── admin.php (Modified - Added coupon link)
|
||||
├── create_coupons_table.sql (Created - DB schema)
|
||||
└── COUPON_SYSTEM.md (Created - Documentation)
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Database migration ran successfully
|
||||
- [ ] Admin can access coupon management page
|
||||
- [ ] Can create new coupon (all games)
|
||||
- [ ] Can create game-specific coupon
|
||||
- [ ] Can edit existing coupon
|
||||
- [ ] Can delete coupon
|
||||
- [ ] Customer can apply coupon in cart
|
||||
- [ ] Cart prices update with discount
|
||||
- [ ] Free server creation works (if admin)
|
||||
- [ ] PayPal payment processes successfully
|
||||
- [ ] Coupon usage count increments
|
||||
- [ ] One-time coupon clears after payment
|
||||
- [ ] Permanent coupon stays in order
|
||||
- [ ] Discount shows on My Servers page
|
||||
- [ ] Discount shows on Admin Invoices page
|
||||
- [ ] Expired coupons are rejected
|
||||
- [ ] Max uses limit is enforced
|
||||
- [ ] Game filter works correctly
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. Coupons are percentage-based only (no fixed-amount discounts)
|
||||
2. No minimum purchase requirement
|
||||
3. No user-specific targeting (all users can use any active coupon)
|
||||
4. No coupon stacking (one coupon per order)
|
||||
5. Game matching uses partial string match (may need refinement)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Fixed-amount coupons (e.g., $5 off)
|
||||
- Minimum purchase requirements
|
||||
- User-specific or group-specific coupons
|
||||
- Referral system integration
|
||||
- Automatic coupon generation for campaigns
|
||||
- Analytics dashboard
|
||||
- Email notifications on coupon usage
|
||||
|
||||
## Support & Documentation
|
||||
|
||||
- Full documentation: `modules/billing/COUPON_SYSTEM.md`
|
||||
- Copilot instructions: `.github/copilot-instructions.md`
|
||||
- Issue tracker: GitHub Issues
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0
|
||||
**Date**: October 29, 2025
|
||||
**Author**: Copilot Agent
|
||||
**Tested**: Manual testing completed
|
||||
266
backup-website/RECENT_FIXES_SUMMARY.md
Normal file
266
backup-website/RECENT_FIXES_SUMMARY.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
# Recent Fixes & Enhancements Summary
|
||||
**Date:** November 10, 2025
|
||||
|
||||
## Critical Fixes Completed ✅
|
||||
|
||||
### 1. PayPal Payment Capture Session Issue (FIXED)
|
||||
**Problem:** Payment capture was failing with `NO_USER_SESSION` error even though user was logged in.
|
||||
|
||||
**Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `gameservers_website` session where the user_id is stored.
|
||||
|
||||
**Solution:** Added `session_name("gameservers_website")` before `session_start()` in `capture_order.php`.
|
||||
|
||||
**File Modified:** `modules/billing/api/capture_order.php` (line ~148)
|
||||
|
||||
**Test Steps:**
|
||||
1. Log into the billing site
|
||||
2. Add a server to cart
|
||||
3. Click PayPal checkout button
|
||||
4. Complete payment in PayPal sandbox
|
||||
5. Verify payment completes successfully and redirects to success page
|
||||
6. Check `modules/billing/logs/payment_capture.log` - should no longer show `NO_USER_SESSION` error
|
||||
|
||||
---
|
||||
|
||||
### 2. Cart Page Debug Logging Removed (COMPLETED)
|
||||
**What Was Removed:**
|
||||
- Shutdown function that logged to `data/debug_cart.log`
|
||||
- `?debug_cart=1` parameter handling
|
||||
- Debug error display code
|
||||
|
||||
**File Modified:** `modules/billing/cart.php` (lines 1-30)
|
||||
|
||||
**Result:** Cart page now runs in production mode without debug overhead.
|
||||
|
||||
---
|
||||
|
||||
### 3. Cart Page Header/Footer Consistency (FIXED)
|
||||
**Problem:** Cart page had different fonts and styling than other billing pages; missing footer entirely.
|
||||
|
||||
**Solutions Applied:**
|
||||
1. Added `include(__DIR__ . '/includes/top.php');` before menu
|
||||
2. Added `include(__DIR__ . '/includes/footer.php');` at page end
|
||||
3. Removed global `font-family` and `background` override from inline CSS
|
||||
4. Added favicon links to match other pages
|
||||
|
||||
**Files Modified:**
|
||||
- `modules/billing/cart.php` (head section and body closing)
|
||||
|
||||
**Result:** Cart page now has consistent header/menu/footer with rest of billing module.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Enhancements Started 📚
|
||||
|
||||
### 4. Minecraft Documentation Updated (TEMPLATE CREATED)
|
||||
**What Was Added:**
|
||||
- Comprehensive **Ports section** with table showing all ports (TCP 25565, UDP 25565, TCP 25575, UDP 19132)
|
||||
- Port purposes clearly explained
|
||||
- Firewall configuration examples for multiple platforms
|
||||
- Security notes for RCON and port protection
|
||||
- Enhanced navigation with icons (🔌 Ports, ⚙️ Startup Parameters, 🔧 Troubleshooting)
|
||||
|
||||
**File Modified:** `modules/billing/docs/minecraft/index.php`
|
||||
|
||||
**Template Pattern Established:**
|
||||
- ✅ Quick Info section (at top)
|
||||
- ✅ Ports section with complete table
|
||||
- ✅ Installation steps
|
||||
- ✅ Configuration examples
|
||||
- ✅ Startup Parameters section (already excellent)
|
||||
- ✅ Troubleshooting section (already comprehensive)
|
||||
- ✅ Performance optimization
|
||||
- ✅ Security best practices
|
||||
|
||||
---
|
||||
|
||||
## Remaining Documentation Work 📋
|
||||
|
||||
### Games Needing Full Port/Parameter/Troubleshooting Docs
|
||||
|
||||
The following games need their `docs/{game}/index.php` files updated with the Minecraft template pattern:
|
||||
|
||||
#### High Priority Games (Popular):
|
||||
1. **Counter-Strike: Global Offensive** (`csgo/`)
|
||||
2. **Team Fortress 2** (`tf2/`)
|
||||
3. **Garry's Mod** (`garrysmod/`)
|
||||
4. **Rust** (`rust/`)
|
||||
5. **ARK: Survival Evolved** (`arkse/`)
|
||||
6. **Terraria** (`terraria/`)
|
||||
7. **Valheim** (`valheim/`)
|
||||
8. **7 Days to Die** (`7daystodie/`)
|
||||
9. **DayZ** (`dayz/`)
|
||||
10. **Left 4 Dead 2** (`left4dead2/`)
|
||||
|
||||
#### Medium Priority:
|
||||
11. Counter-Strike Source (`css/`)
|
||||
12. Arma 3 (`arma3/`)
|
||||
13. Squad (`squad/`)
|
||||
14. Insurgency Sandstorm (`insurgencysandstorm/`)
|
||||
15. Space Engineers (`space_engineers/`)
|
||||
16. Conan Exiles (`conanexiles/`)
|
||||
17. The Forest (`theforest/`)
|
||||
18. Don't Starve Together (`dontstarvetogether/`)
|
||||
19. Factorio (`factorio/`)
|
||||
20. TeamSpeak 3 (`teamspeak3/`)
|
||||
|
||||
#### Lower Priority (Legacy/Niche):
|
||||
21. All remaining games in `modules/billing/docs/`
|
||||
|
||||
---
|
||||
|
||||
### Research Needed Per Game
|
||||
|
||||
For each game, research and document:
|
||||
|
||||
1. **All Network Ports:**
|
||||
- Game port (TCP/UDP)
|
||||
- Query port
|
||||
- RCON/Admin port
|
||||
- Voice chat ports (if applicable)
|
||||
- Steam port (if Steam-based)
|
||||
- Additional service ports (web interfaces, etc.)
|
||||
|
||||
2. **Startup Parameters:**
|
||||
- Command-line flags
|
||||
- Memory allocation
|
||||
- Server configuration switches
|
||||
- Performance optimization flags
|
||||
|
||||
3. **Common Issues (from internet research):**
|
||||
- "Server won't start" specific to that game
|
||||
- Connection problems
|
||||
- Performance/lag issues specific to game engine
|
||||
- Mod/plugin conflicts
|
||||
- Save corruption issues
|
||||
- Update/patch problems
|
||||
|
||||
4. **Game-Specific Configuration:**
|
||||
- Main config file locations
|
||||
- Critical settings
|
||||
- Player limits
|
||||
- World/map settings
|
||||
|
||||
---
|
||||
|
||||
### Documentation Template Structure
|
||||
|
||||
Each game's `index.php` should follow this structure:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* {Game Name} Server Documentation
|
||||
*/
|
||||
?>
|
||||
|
||||
<!-- Navigation with icons -->
|
||||
<div style="background: #1e3a5f...">
|
||||
<h3>📚 Quick Navigation</h3>
|
||||
<div>
|
||||
<a href="#quick-info">Quick Info</a>
|
||||
<a href="#ports">🔌 Ports</a>
|
||||
<a href="#installation">Installation</a>
|
||||
<a href="#configuration">Configuration</a>
|
||||
<a href="#parameters">⚙️ Startup Parameters</a>
|
||||
<a href="#troubleshooting">🔧 Troubleshooting</a>
|
||||
<a href="#performance">Performance</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>{Game Name} Server Hosting Guide</h1>
|
||||
|
||||
<h2 id="quick-info">Quick Info</h2>
|
||||
<!-- Key stats in styled box -->
|
||||
|
||||
<h2 id="ports">🔌 Network Ports Used</h2>
|
||||
<!-- Table with all ports, protocols, purposes, required/optional -->
|
||||
<!-- Firewall examples -->
|
||||
<!-- Port security notes -->
|
||||
|
||||
<h2 id="installation">Installation & Setup</h2>
|
||||
<!-- Step-by-step installation -->
|
||||
|
||||
<h2 id="configuration">Server Configuration</h2>
|
||||
<!-- Config file examples -->
|
||||
|
||||
<h2 id="parameters">⚙️ Startup Parameters</h2>
|
||||
<!-- Command-line flags -->
|
||||
<!-- Parameter explanations -->
|
||||
|
||||
<h2 id="troubleshooting">🔧 Troubleshooting</h2>
|
||||
<!-- Common Issues section -->
|
||||
<!-- Server Won't Start -->
|
||||
<!-- Connection Problems -->
|
||||
<!-- Performance Issues -->
|
||||
<!-- Game-specific problems -->
|
||||
|
||||
<h2 id="performance">Performance Optimization</h2>
|
||||
<!-- Optimization tips -->
|
||||
|
||||
<!-- Additional Resources -->
|
||||
<!-- Important Notes -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### PayPal Payment Flow:
|
||||
- [ ] Log into billing site
|
||||
- [ ] Add server to cart
|
||||
- [ ] Apply coupon (optional)
|
||||
- [ ] Click PayPal button
|
||||
- [ ] Complete sandbox payment
|
||||
- [ ] Verify success page loads
|
||||
- [ ] Check invoice marked as paid in database
|
||||
- [ ] Verify no `NO_USER_SESSION` in `logs/payment_capture.log`
|
||||
|
||||
### Cart Page:
|
||||
- [ ] Cart page loads with correct header/menu (same font as index.php)
|
||||
- [ ] Footer appears with timestamp
|
||||
- [ ] Favicon displays in browser tab
|
||||
- [ ] Remove item (trash icon) works via AJAX
|
||||
- [ ] Cart refreshes without full page reload after removal
|
||||
- [ ] Database row hard-deleted (invoice removed from table)
|
||||
|
||||
### Documentation:
|
||||
- [ ] Navigate to `/docs.php` (or docs index)
|
||||
- [ ] Click on Minecraft documentation
|
||||
- [ ] Verify new Ports section displays correctly
|
||||
- [ ] Verify navigation links jump to correct sections
|
||||
- [ ] Test on mobile/tablet for responsive layout
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Priority Order)
|
||||
|
||||
1. **Test PayPal payment flow end-to-end** (sandbox environment)
|
||||
2. **Verify cart removal functionality** (AJAX + database deletion)
|
||||
3. **Begin documentation expansion:**
|
||||
- Start with top 10 popular games
|
||||
- Research ports/parameters/issues for each
|
||||
- Update docs using Minecraft template
|
||||
- Test navigation and layout
|
||||
4. **Consider automation:**
|
||||
- Script to validate all game docs have required sections
|
||||
- Port information database/reference
|
||||
- Common troubleshooting template generator
|
||||
|
||||
---
|
||||
|
||||
## Files Modified in This Session
|
||||
|
||||
1. `modules/billing/api/capture_order.php` - Fixed session name issue
|
||||
2. `modules/billing/cart.php` - Removed debug logging, fixed header/footer
|
||||
3. `modules/billing/docs/minecraft/index.php` - Added ports section, enhanced navigation
|
||||
|
||||
## Files to Review
|
||||
|
||||
- `modules/billing/logs/payment_capture.log` - Check for successful captures
|
||||
- `modules/billing/data/debug_cart.log` - Should no longer be written to
|
||||
- Database table `{$table_prefix}billing_invoices` - Verify removals are hard-deleted
|
||||
|
||||
---
|
||||
|
||||
**End of Summary**
|
||||
176
backup-website/STATUS_REPORT.md
Normal file
176
backup-website/STATUS_REPORT.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Billing Module Status Report
|
||||
**Date:** November 7, 2025
|
||||
**Branch:** copilot/update-billing-table-prefix
|
||||
|
||||
## ✅ Completed Tasks
|
||||
|
||||
### 1. Table Prefix Updates
|
||||
- **Status:** ✅ COMPLETE
|
||||
- **Changes:**
|
||||
- All SQL files updated to use hardcoded `gsp_` prefix
|
||||
- `config.inc.php` default changed from `ogp_` to `gsp_`
|
||||
- Panel tables (like `ogp_users`) correctly left unchanged
|
||||
- All references properly updated in:
|
||||
- create_invoices_table.sql
|
||||
- create_coupons_table.sql
|
||||
- migration_to_invoices.sql
|
||||
- add_paypal_data_column.sql
|
||||
- add_service_id_column.sql
|
||||
- fix_invoices_table_columns.sql
|
||||
|
||||
### 2. Documentation System
|
||||
- **Status:** ✅ COMPLETE
|
||||
- **Implementation:**
|
||||
- New `/modules/billing/docs.php` browser created
|
||||
- Category-based organization (game, panel, mods, troubleshooting, other)
|
||||
- Each doc folder contains:
|
||||
- `index.php` - Documentation content
|
||||
- `metadata.json` - Category, name, description, order
|
||||
- `icon.png/jpg` - Visual icon
|
||||
- Smart sorting by category and order number
|
||||
- Clean, dark-themed UI matching site design
|
||||
- Back button navigation
|
||||
- "Documentation" link added to main menu
|
||||
- Old docs preserved in `/docs_old/` for reference
|
||||
- Complete README.md with instructions
|
||||
|
||||
**Example Documentation Created:**
|
||||
- Minecraft Server Guide (game category)
|
||||
- Getting Started (panel category)
|
||||
- Common Issues & Solutions (troubleshooting category)
|
||||
|
||||
### 3. PayPal Integration
|
||||
- **Status:** ✅ COMPLETE (Core Functionality)
|
||||
- **Components:**
|
||||
- `api/create_order.php` - Creates PayPal orders with comprehensive logging
|
||||
- `api/capture_order.php` - Captures payments and marks invoices paid
|
||||
- `webhook.php` - Handles PayPal webhooks with signature verification
|
||||
- All use standalone mysqli (no panel dependencies)
|
||||
- Full logging system for debugging
|
||||
- Secure error handling
|
||||
|
||||
**Payment Flow:**
|
||||
1. User views cart with unpaid invoices
|
||||
2. Clicks PayPal button → creates order via API
|
||||
3. Completes payment on PayPal
|
||||
4. capture_order.php marks invoices paid, creates orders
|
||||
5. Webhook confirms payment asynchronously
|
||||
6. Success page shows confirmation
|
||||
|
||||
## ⚠️ Partially Complete
|
||||
|
||||
### Coupon System
|
||||
- **Status:** ⚠️ BACKEND READY, FRONTEND MISSING
|
||||
- **What Exists:**
|
||||
- ✅ Database schema (`gsp_billing_coupons` table)
|
||||
- ✅ Admin interface (`admin_coupons.php`)
|
||||
- ✅ Coupon CRUD operations
|
||||
- ✅ Fields in invoices/orders for coupon tracking
|
||||
- ✅ Comprehensive documentation (COUPON_SYSTEM.md)
|
||||
|
||||
- **What's Missing:**
|
||||
- ❌ Coupon input/validation in cart.php
|
||||
- ❌ Discount calculation in checkout
|
||||
- ❌ Session storage of applied coupons
|
||||
- ❌ Coupon usage tracking on payment
|
||||
|
||||
**Impact:** Coupons can be created by admins but customers cannot apply them during checkout.
|
||||
|
||||
**Recommendation:** The problem statement asks to "verify all the paypal payment works and is complete with coupons". The PayPal payment WORKS but coupon integration in the checkout flow needs to be implemented to match the COUPON_SYSTEM.md documentation.
|
||||
|
||||
## 📋 Other Findings
|
||||
|
||||
### Inconsistencies Found
|
||||
|
||||
1. **Mixed URL Patterns**
|
||||
- Some files use absolute URLs correctly
|
||||
- create_order.php has hardcoded site base URL instead of using config
|
||||
- Recommendation: Use `$SITE_BASE_URL` from config consistently
|
||||
|
||||
2. **Session Namespaces**
|
||||
- Most files use `website_user_id` session variable
|
||||
- Some fallback to `user_id`
|
||||
- Recommendation: Standardize on `website_user_id`
|
||||
|
||||
3. **Error Handling**
|
||||
- Most files have good error handling
|
||||
- A few older files could use try/catch blocks
|
||||
- Recommendation: Audit older PHP files for error handling
|
||||
|
||||
4. **Documentation Markdown Files**
|
||||
- Multiple .md files in root of billing module
|
||||
- Could be consolidated or moved to docs folder
|
||||
- Recommendation: Create a `/docs/developer/` category for technical docs
|
||||
|
||||
### SQL Files Status
|
||||
All SQL files properly use `gsp_` prefix:
|
||||
- ✅ create_invoices_table.sql
|
||||
- ✅ create_coupons_table.sql
|
||||
- ✅ migration_to_invoices.sql
|
||||
- ✅ add_paypal_data_column.sql
|
||||
- ✅ add_service_id_column.sql
|
||||
- ✅ fix_invoices_table_columns.sql
|
||||
|
||||
### Configuration Files
|
||||
- ✅ `config.inc.php` - Default prefix is `gsp_`
|
||||
- ✅ Standalone compatible (no panel includes)
|
||||
- ✅ Database connection using mysqli
|
||||
|
||||
## 🎯 Recommended Next Steps
|
||||
|
||||
### Priority 1: Complete Coupon Integration
|
||||
To match COUPON_SYSTEM.md documentation, implement in cart.php:
|
||||
1. Add coupon input field
|
||||
2. AJAX endpoint to validate and apply coupons
|
||||
3. Discount calculation in cart totals
|
||||
4. Store applied coupon in session
|
||||
5. Pass coupon to payment processor
|
||||
6. Update invoices with coupon_id on payment
|
||||
7. Increment usage counter
|
||||
8. Handle one-time vs permanent coupons
|
||||
|
||||
### Priority 2: Testing
|
||||
1. Test PayPal sandbox end-to-end
|
||||
2. Test invoice creation → cart → payment → success
|
||||
3. Test webhook signature verification
|
||||
4. Test error scenarios (payment failure, timeout, etc.)
|
||||
5. Once coupons implemented, test coupon application
|
||||
|
||||
### Priority 3: Documentation
|
||||
1. Move developer .md files to `/docs/developer/` category
|
||||
2. Create user-facing coupon documentation in docs system
|
||||
3. Add payment troubleshooting guide
|
||||
|
||||
### Priority 4: Code Quality
|
||||
1. Audit older PHP files for error handling
|
||||
2. Standardize session variable names
|
||||
3. Use config SITE_BASE_URL consistently
|
||||
4. Add input validation where missing
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
### What Works Now
|
||||
- ✅ Table prefixes corrected to `gsp_`
|
||||
- ✅ Documentation system fully functional
|
||||
- ✅ PayPal payment processing complete
|
||||
- ✅ Coupon admin management ready
|
||||
- ✅ Standalone deployment compatible
|
||||
|
||||
### What Needs Work
|
||||
- ❌ Coupon checkout integration
|
||||
- ⚠️ Some minor inconsistencies (URLs, sessions)
|
||||
- ⚠️ Testing needed for full payment flow
|
||||
|
||||
### Files Modified in This PR
|
||||
- SQL files (6 files) - table prefix updates
|
||||
- config.inc.php - default prefix change
|
||||
- docs.php (new) - documentation browser
|
||||
- docs/ folder - restructured with examples
|
||||
- includes/menu.php - added Documentation link
|
||||
- STATUS_REPORT.md (this file)
|
||||
|
||||
### Files in docs_old/ (preserved for reference)
|
||||
- 206 game markdown files
|
||||
- Old docs.php, server.php, game.php
|
||||
- all_hostable_games_union.csv
|
||||
|
||||
339
backup-website/TESTING_CHECKLIST.md
Normal file
339
backup-website/TESTING_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# Testing Checklist for Billing Invoice/Order Flow Fixes
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Database Setup**
|
||||
- [ ] Verify `ogp_billing_invoices` table exists
|
||||
- [ ] Verify `ogp_billing_orders` table exists
|
||||
- [ ] Verify tables have all required columns (see create_invoices_table.sql)
|
||||
|
||||
2. **Configuration**
|
||||
- [ ] Copy `modules/billing/includes/config.inc.php.orig` to `modules/billing/includes/config.inc.php`
|
||||
- [ ] Update database credentials in config.inc.php
|
||||
- [ ] Verify `$table_prefix` is set correctly (default: "ogp_")
|
||||
- [ ] Verify `$SITE_DATA_DIR` path is writable
|
||||
|
||||
3. **PayPal Configuration**
|
||||
- [ ] Verify sandbox client_id and client_secret in api/create_order.php
|
||||
- [ ] Verify sandbox client_id and client_secret in api/capture_order.php
|
||||
- [ ] Verify webhook_id in webhook.php
|
||||
|
||||
## Test 1: Add to Cart (Invoice Creation)
|
||||
|
||||
**Test NEW Order Flow**
|
||||
|
||||
1. Navigate to order.php
|
||||
2. Select a game server configuration
|
||||
3. Set price to $0.00 for testing (or use regular price)
|
||||
4. Fill in all required fields
|
||||
5. Click "Add to Cart"
|
||||
|
||||
**Expected Results:**
|
||||
- [ ] Redirects to cart.php
|
||||
- [ ] Item appears in cart
|
||||
- [ ] Database check: Invoice created in `ogp_billing_invoices`
|
||||
- [ ] status = 'due'
|
||||
- [ ] order_id = 0 (no order yet)
|
||||
- [ ] user_id matches logged-in user
|
||||
- [ ] amount, qty, service_id populated correctly
|
||||
|
||||
**Verification SQL:**
|
||||
```sql
|
||||
SELECT * FROM ogp_billing_invoices WHERE status='due' ORDER BY invoice_id DESC LIMIT 5;
|
||||
```
|
||||
|
||||
## Test 2: Free Button (Manual Order Creation)
|
||||
|
||||
**Test Free/Claim Flow**
|
||||
|
||||
1. Ensure you have item in cart with amount = 0.00
|
||||
2. Click "Claim (Free)" button
|
||||
|
||||
**Expected Results:**
|
||||
- [ ] Redirects to return.php
|
||||
- [ ] Shows payment confirmation
|
||||
- [ ] Invoice marked as paid
|
||||
- [ ] Order created
|
||||
- [ ] Cart is empty
|
||||
|
||||
**Verification SQL:**
|
||||
```sql
|
||||
-- Check invoice was marked paid
|
||||
SELECT invoice_id, status, paid_date, order_id FROM ogp_billing_invoices
|
||||
WHERE status='paid' ORDER BY invoice_id DESC LIMIT 1;
|
||||
|
||||
-- Check order was created
|
||||
SELECT order_id, user_id, status, end_date, payment_txid FROM ogp_billing_orders
|
||||
ORDER BY order_id DESC LIMIT 1;
|
||||
|
||||
-- Verify link
|
||||
SELECT i.invoice_id, i.order_id, o.order_id
|
||||
FROM ogp_billing_invoices i
|
||||
LEFT JOIN ogp_billing_orders o ON i.order_id = o.order_id
|
||||
WHERE i.status='paid' ORDER BY i.invoice_id DESC LIMIT 5;
|
||||
```
|
||||
|
||||
**Check Logs:**
|
||||
```bash
|
||||
tail -50 modules/billing/logs/site.log | grep -E "(payment|free_create)"
|
||||
```
|
||||
|
||||
## Test 3: PayPal Payment Flow
|
||||
|
||||
**Test PayPal Checkout**
|
||||
|
||||
1. Add paid item to cart (e.g., $5.00)
|
||||
2. Click PayPal button in cart
|
||||
3. Should redirect to PayPal sandbox
|
||||
4. Login with sandbox buyer account
|
||||
5. Approve payment
|
||||
6. Should return to payment_success.php
|
||||
|
||||
**Expected Results:**
|
||||
- [ ] PayPal button renders correctly
|
||||
- [ ] Creates PayPal order (check browser console for order ID)
|
||||
- [ ] Redirects to PayPal sandbox
|
||||
- [ ] After approval, returns to payment_success.php
|
||||
- [ ] No JavaScript errors in console
|
||||
- [ ] No "Unexpected end of JSON input" error
|
||||
- [ ] Invoice marked as paid
|
||||
- [ ] Order created
|
||||
- [ ] Cart is empty
|
||||
|
||||
**Browser Console Checks:**
|
||||
```
|
||||
Look for:
|
||||
✓ "PayPal cart debug: ..." - Shows cart data
|
||||
✓ "Creating order..." - Order creation started
|
||||
✓ "Order created." - Order creation succeeded
|
||||
✓ "Capturing payment..." - Capture started
|
||||
✗ Any errors - Should be none
|
||||
```
|
||||
|
||||
**Verification SQL:**
|
||||
```sql
|
||||
-- Check invoice
|
||||
SELECT invoice_id, status, paid_date, payment_txid, payment_method, order_id
|
||||
FROM ogp_billing_invoices
|
||||
WHERE payment_method='paypal'
|
||||
ORDER BY invoice_id DESC LIMIT 1;
|
||||
|
||||
-- Check order
|
||||
SELECT order_id, user_id, status, price, end_date, payment_txid
|
||||
FROM ogp_billing_orders
|
||||
WHERE payment_txid LIKE '%'
|
||||
ORDER BY order_id DESC LIMIT 1;
|
||||
```
|
||||
|
||||
**Check API Logs:**
|
||||
```bash
|
||||
# Check create_order.php payload
|
||||
cat modules/billing/data/create_order_payload.log
|
||||
|
||||
# Check corrected URLs
|
||||
cat modules/billing/data/corrected_urls.log
|
||||
|
||||
# Check for errors
|
||||
cat modules/billing/data/create_order_errors.log
|
||||
```
|
||||
|
||||
## Test 4: Webhook Processing
|
||||
|
||||
**Test Webhook Handler**
|
||||
|
||||
1. Trigger a PayPal payment (from Test 3)
|
||||
2. PayPal will send webhook to webhook.php
|
||||
|
||||
**Expected Results:**
|
||||
- [ ] Webhook receives POST from PayPal
|
||||
- [ ] Signature verification succeeds
|
||||
- [ ] Payment record processed
|
||||
- [ ] Invoice marked paid (if not already)
|
||||
- [ ] Order created/updated (if not already)
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Check webhook log
|
||||
tail -50 modules/billing/data/webhook.log
|
||||
|
||||
# Check for payment processing
|
||||
grep "process_payment" modules/billing/data/webhook.log
|
||||
```
|
||||
|
||||
**Check Data Files:**
|
||||
```bash
|
||||
ls -lah modules/billing/data/*.json
|
||||
cat modules/billing/data/INV-*.json # Check payment record format
|
||||
```
|
||||
|
||||
## Test 5: Renewal Flow
|
||||
|
||||
**Setup Renewal Invoice**
|
||||
|
||||
1. Create a test order manually:
|
||||
```sql
|
||||
INSERT INTO ogp_billing_orders (
|
||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
price, remote_control_password, ftp_password, status, order_date, end_date,
|
||||
payment_txid, paid_ts
|
||||
) VALUES (
|
||||
1, 1, 'Test Server', 1, 10, 1, 'month',
|
||||
5.00, 'rconpass', 'ftppass', 'paid', NOW(), DATE_ADD(NOW(), INTERVAL 1 MONTH),
|
||||
'TEST-INITIAL', NOW()
|
||||
);
|
||||
```
|
||||
|
||||
2. Get the order_id from the insert:
|
||||
```sql
|
||||
SELECT LAST_INSERT_ID();
|
||||
```
|
||||
|
||||
3. Create renewal invoice:
|
||||
```sql
|
||||
INSERT INTO ogp_billing_invoices (
|
||||
order_id, user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
amount, status, customer_name, customer_email, due_date, description
|
||||
) VALUES (
|
||||
LAST_INSERT_ID(), -- Use order_id from step 2
|
||||
1, 1, 'Test Server', 1, 10, 1, 'month',
|
||||
5.00, 'due', 'Test User', 'test@test.com', DATE_ADD(NOW(), INTERVAL 3 DAY),
|
||||
'Renewal invoice'
|
||||
);
|
||||
```
|
||||
|
||||
**Test Renewal Payment**
|
||||
|
||||
1. Log in as user who owns the order
|
||||
2. View cart - should show renewal invoice
|
||||
3. Pay using free button or PayPal
|
||||
|
||||
**Expected Results:**
|
||||
- [ ] Invoice marked as paid
|
||||
- [ ] Original order's end_date extended by 1 month
|
||||
- [ ] No duplicate order created
|
||||
- [ ] Invoice.order_id still points to original order
|
||||
|
||||
**Verification SQL:**
|
||||
```sql
|
||||
-- Check order end_date was extended
|
||||
SELECT order_id, end_date, status, payment_txid
|
||||
FROM ogp_billing_orders
|
||||
WHERE order_id = <order_id_from_step_2>;
|
||||
|
||||
-- Should show end_date = original end_date + 1 month
|
||||
|
||||
-- Check invoice
|
||||
SELECT invoice_id, order_id, status, paid_date
|
||||
FROM ogp_billing_invoices
|
||||
WHERE order_id = <order_id_from_step_2>;
|
||||
|
||||
-- Should show paid invoice linked to same order_id
|
||||
```
|
||||
|
||||
## Test 6: Error Handling
|
||||
|
||||
**Test Invalid Scenarios**
|
||||
|
||||
1. **Missing session**: Try to pay without being logged in
|
||||
- [ ] Should redirect to login or show error
|
||||
|
||||
2. **Database connection failure**: Temporarily break DB config
|
||||
- [ ] capture_order.php should return JSON error, not crash
|
||||
- [ ] Error should be logged
|
||||
|
||||
3. **PayPal API failure**: Use invalid credentials
|
||||
- [ ] Should show error in console
|
||||
- [ ] Should log error
|
||||
- [ ] Should not corrupt database
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: "Config file not found"
|
||||
**Solution**: Copy config.inc.php.orig to config.inc.php
|
||||
|
||||
### Issue: "Table doesn't exist"
|
||||
**Solution**: Run create_invoices_table.sql
|
||||
|
||||
### Issue: "Permission denied writing to data/"
|
||||
**Solution**:
|
||||
```bash
|
||||
chmod 775 modules/billing/data
|
||||
chown www-data:www-data modules/billing/data # Or your web server user
|
||||
```
|
||||
|
||||
### Issue: "PayPal button doesn't render"
|
||||
**Solution**: Check browser console for errors, verify client_id
|
||||
|
||||
### Issue: "Unexpected end of JSON input"
|
||||
**Solution**:
|
||||
- Check PHP error log: `tail -f /var/log/php/error.log`
|
||||
- Verify display_errors=0 in capture_order.php
|
||||
- Check for syntax errors: `php -l api/capture_order.php`
|
||||
|
||||
### Issue: "Cart still shows items after payment"
|
||||
**Solution**:
|
||||
- Check if invoice status changed to 'paid'
|
||||
- Check if process_payment_record was called
|
||||
- Check logs for errors
|
||||
|
||||
## Performance Testing
|
||||
|
||||
**Test with Multiple Items**
|
||||
|
||||
1. Add 5 items to cart
|
||||
2. Pay with PayPal
|
||||
3. Verify all 5 invoices marked paid
|
||||
4. Verify all 5 orders created
|
||||
5. Verify all linked correctly
|
||||
|
||||
**Test Concurrent Payments**
|
||||
|
||||
1. Add item to cart in two different browsers (same user)
|
||||
2. Attempt to pay both simultaneously
|
||||
3. Verify both process correctly
|
||||
4. Check for race conditions
|
||||
|
||||
## Security Testing
|
||||
|
||||
**Test SQL Injection**
|
||||
|
||||
1. Try adding special characters to form fields
|
||||
2. Try manipulating invoice_id in POST requests
|
||||
3. Verify all inputs are sanitized/escaped
|
||||
|
||||
**Test Session Hijacking**
|
||||
|
||||
1. Try accessing cart with invalid session
|
||||
2. Try paying for someone else's invoice
|
||||
3. Verify proper authorization checks
|
||||
|
||||
**Test Webhook Signature**
|
||||
|
||||
1. Send fake webhook without valid signature
|
||||
2. Verify it's rejected
|
||||
3. Check logs for security events
|
||||
|
||||
## Cleanup
|
||||
|
||||
After testing, clean up test data:
|
||||
|
||||
```sql
|
||||
-- Remove test invoices
|
||||
DELETE FROM ogp_billing_invoices WHERE customer_email = 'test@test.com';
|
||||
|
||||
-- Remove test orders
|
||||
DELETE FROM ogp_billing_orders WHERE remote_control_password = 'rconpass';
|
||||
```
|
||||
|
||||
## Sign-off
|
||||
|
||||
- [ ] All tests passed
|
||||
- [ ] No errors in logs
|
||||
- [ ] Documentation reviewed
|
||||
- [ ] Security checks completed
|
||||
- [ ] Ready for production deployment
|
||||
|
||||
**Tested by**: _______________
|
||||
**Date**: _______________
|
||||
**Environment**: _______________ (Dev/Staging/Production)
|
||||
**Notes**: _______________
|
||||
165
backup-website/_archived/CONFIGURATION.md
Normal file
165
backup-website/_archived/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Website Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `_website` folder is now a standalone site with centralized database configuration. All database connection settings are managed in a single location: `includes/config.inc.php`.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
_website/
|
||||
├── includes/
|
||||
│ ├── config.inc.php # Central database configuration
|
||||
│ └── README.md # Documentation for includes directory
|
||||
├── db.php # Database connection (loads config.inc.php)
|
||||
├── login.php # Uses db.php
|
||||
├── logout.php # Uses db.php
|
||||
├── cart.php # Uses db.php
|
||||
├── order.php # Uses db.php
|
||||
├── serverlist.php # Uses db.php
|
||||
└── ...other files
|
||||
```
|
||||
|
||||
## Configuration File
|
||||
|
||||
### Location
|
||||
`_website/includes/config.inc.php`
|
||||
|
||||
### Contents
|
||||
```php
|
||||
<?php
|
||||
$db_host="localhost"; // Database server hostname
|
||||
$db_user="localuser"; // Database username
|
||||
$db_pass="password"; // Database password
|
||||
$db_name="panel"; // Database name
|
||||
$table_prefix="ogp_"; // Table prefix
|
||||
$db_type="mysql"; // Database type
|
||||
?>
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Configuration Loading**
|
||||
- Website files include `db.php`
|
||||
- `db.php` loads `includes/config.inc.php`
|
||||
- Configuration variables are available to all files
|
||||
|
||||
2. **Configuration Flow**
|
||||
```
|
||||
includes/config.inc.php → db.php → website files
|
||||
```
|
||||
|
||||
3. **Database Connection**
|
||||
- `db.php` uses the configuration variables to establish a connection
|
||||
- Returns `$db` variable containing the mysqli connection
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### For Standalone Use
|
||||
|
||||
1. **Copy the _website folder** to your web server
|
||||
2. **Edit configuration**:
|
||||
```bash
|
||||
nano _website/includes/config.inc.php
|
||||
```
|
||||
3. **Update database credentials**:
|
||||
- Set `$db_host` to your database server
|
||||
- Set `$db_user` to your database username
|
||||
- Set `$db_pass` to your database password
|
||||
- Set `$db_name` to your database name
|
||||
4. **Verify permissions**:
|
||||
```bash
|
||||
chmod 600 _website/includes/config.inc.php
|
||||
```
|
||||
|
||||
### For Panel Integration
|
||||
|
||||
The configuration in `_website/includes/config.inc.php` should match the panel's configuration in `/includes/config.inc.php` to ensure both the website and panel access the same database.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **File Permissions**: Set `config.inc.php` to read-only for the web server user
|
||||
```bash
|
||||
chmod 600 includes/config.inc.php
|
||||
```
|
||||
|
||||
2. **Web Server Configuration**: Ensure the `includes/` directory is not directly accessible via HTTP
|
||||
```apache
|
||||
<Directory "/path/to/_website/includes">
|
||||
Require all denied
|
||||
</Directory>
|
||||
```
|
||||
|
||||
3. **Backup Configuration**: Keep a secure backup of your configuration file
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Errors
|
||||
|
||||
If you see database connection errors:
|
||||
|
||||
1. **Verify credentials** in `includes/config.inc.php`
|
||||
2. **Check database server** is running
|
||||
3. **Verify database exists**
|
||||
4. **Check user permissions** in the database
|
||||
|
||||
### File Not Found Errors
|
||||
|
||||
If you see errors about missing `config.inc.php`:
|
||||
|
||||
1. **Verify the file exists** at `_website/includes/config.inc.php`
|
||||
2. **Check file permissions** are readable by the web server
|
||||
3. **Verify path** in `db.php` uses `__DIR__` for relative paths
|
||||
|
||||
### Include Errors
|
||||
|
||||
If website files can't include `db.php`:
|
||||
|
||||
1. **Check file paths** are correct
|
||||
2. **Verify `db.php`** exists in the `_website/` root
|
||||
3. **Check PHP include paths** in php.ini if needed
|
||||
|
||||
## Migration from Old Configuration
|
||||
|
||||
The old `db.php` had hardcoded credentials:
|
||||
```php
|
||||
// OLD (hardcoded)
|
||||
$servername = "panel.iaregamer.com";
|
||||
$username = "remoteuser";
|
||||
```
|
||||
|
||||
The new `db.php` uses centralized config:
|
||||
```php
|
||||
// NEW (centralized)
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
$servername = $db_host;
|
||||
$username = $db_user;
|
||||
```
|
||||
|
||||
**No changes needed** to files that include `db.php` - they work automatically with the new configuration.
|
||||
|
||||
## Files Using Database Connection
|
||||
|
||||
The following files include `db.php` and use the centralized configuration:
|
||||
- `login.php` - User authentication
|
||||
- `logout.php` - Session termination
|
||||
- `cart.php` - Shopping cart
|
||||
- `order.php` - Order processing
|
||||
- `serverlist.php` - Server listings
|
||||
- `adminserverlist.php` - Admin server management
|
||||
- `test_db_connection.php` - Database testing
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Single Source of Truth**: All database settings in one file
|
||||
2. **Easy Configuration**: Change settings in one place
|
||||
3. **Portable**: Copy folder and update one config file
|
||||
4. **Secure**: Configuration separate from code
|
||||
5. **Maintainable**: Easy to update and manage
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions about the configuration, please refer to:
|
||||
- `includes/README.md` - Detailed information about includes directory
|
||||
- Main project documentation
|
||||
- Panel configuration at `/includes/config.inc.php`
|
||||
383
backup-website/_archived/FEATURES.md
Normal file
383
backup-website/_archived/FEATURES.md
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
# Website Features Documentation
|
||||
|
||||
This document describes the new features added to the GameServers.World website (_website folder).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Password Reset System](#password-reset-system)
|
||||
2. [My Servers Dashboard](#my-servers-dashboard)
|
||||
3. [Server Status Page](#server-status-page)
|
||||
4. [UI Improvements](#ui-improvements)
|
||||
5. [Apache Configuration](#apache-configuration)
|
||||
|
||||
---
|
||||
|
||||
## Password Reset System
|
||||
|
||||
A complete password reset workflow has been implemented to allow users to recover their accounts.
|
||||
|
||||
### Files Created
|
||||
|
||||
- **forgot_password.php** - Request password reset
|
||||
- **reset_password.php** - Reset password with token
|
||||
|
||||
### How It Works
|
||||
|
||||
1. User visits the login page and clicks "Forgot Password?"
|
||||
2. User enters their username or email on `forgot_password.php`
|
||||
3. System generates a secure token and stores it in `ogp_password_reset_tokens` table
|
||||
4. Email is sent with reset link (falls back to displaying link if email fails)
|
||||
5. User clicks link and is taken to `reset_password.php?token=XXX`
|
||||
6. User enters new password (min 8 characters)
|
||||
7. Password is updated using both MD5 (panel compatibility) and modern hash (if shadow column exists)
|
||||
8. Token is marked as used
|
||||
|
||||
### Database Table
|
||||
|
||||
The system automatically creates this table if it doesn't exist:
|
||||
|
||||
```sql
|
||||
CREATE TABLE ogp_password_reset_tokens (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token VARCHAR(64) NOT NULL,
|
||||
expires DATETIME NOT NULL,
|
||||
used TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_token (token),
|
||||
INDEX idx_user_id (user_id)
|
||||
)
|
||||
```
|
||||
|
||||
### Security Features
|
||||
|
||||
- Tokens expire after 1 hour
|
||||
- Tokens can only be used once
|
||||
- Secure random token generation (64 hex characters)
|
||||
- Password requirements enforced (min 8 chars)
|
||||
- Passwords hashed with both MD5 (panel) and bcrypt (modern)
|
||||
- User enumeration protection (doesn't reveal if account exists)
|
||||
|
||||
### Email Configuration
|
||||
|
||||
The system uses PHP's `mail()` function. For production:
|
||||
|
||||
1. Configure your server's mail system (sendmail, postfix, etc.)
|
||||
2. Or integrate with an email service (SendGrid, Mailgun, etc.)
|
||||
3. Update the email headers in `forgot_password.php` as needed
|
||||
|
||||
---
|
||||
|
||||
## My Servers Dashboard
|
||||
|
||||
A user dashboard showing all active game servers with renewal options.
|
||||
|
||||
### File Created
|
||||
|
||||
- **my_servers.php** - User's server management dashboard
|
||||
- **renew_server.php** - Server renewal page
|
||||
|
||||
### Features
|
||||
|
||||
- **Server List**: Shows all servers owned by logged-in user
|
||||
- **Server Details**: Name, game type, location, status
|
||||
- **Expiration Tracking**: Shows expiration date for each server
|
||||
- **Status Indicators**: Active, Inactive, Expired
|
||||
- **Renewal Links**: Quick access to renew each server
|
||||
- **Empty State**: Helpful message when user has no servers
|
||||
|
||||
### Access
|
||||
|
||||
- Menu link "My Servers" appears when user is logged in
|
||||
- Requires authentication via `login_required.php`
|
||||
|
||||
### Database Query
|
||||
|
||||
Joins multiple tables:
|
||||
- `ogp_home` - Server instances
|
||||
- `ogp_remote_servers` - Server locations
|
||||
- `ogp_game_configs` - Game information
|
||||
- `ogp_billing_orders` - Order/expiration data
|
||||
- `ogp_billing_services` - Service pricing
|
||||
|
||||
---
|
||||
|
||||
## Server Status Page
|
||||
|
||||
Public page showing real-time status of all game server infrastructure.
|
||||
|
||||
### File Created
|
||||
|
||||
- **server_status.php** - Server infrastructure status
|
||||
|
||||
### Features
|
||||
|
||||
- **Real-time Status**: Online, Offline, Maintenance, Unknown
|
||||
- **Resource Usage**: CPU, Memory, Disk usage percentages
|
||||
- **Uptime Display**: How long each server has been running
|
||||
- **Last Updated**: Time since last status update
|
||||
- **Color-coded Badges**: Visual status indicators
|
||||
- **Notes Support**: Display maintenance or status messages
|
||||
|
||||
### Database Table
|
||||
|
||||
Automatically creates table if it doesn't exist:
|
||||
|
||||
```sql
|
||||
CREATE TABLE ogp_server_status (
|
||||
status_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
remote_server_id INT NOT NULL,
|
||||
server_name VARCHAR(255) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
status ENUM('online', 'offline', 'maintenance') DEFAULT 'offline',
|
||||
cpu_usage DECIMAL(5,2),
|
||||
memory_usage DECIMAL(5,2),
|
||||
disk_usage DECIMAL(5,2),
|
||||
uptime VARCHAR(50),
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
notes TEXT,
|
||||
INDEX idx_remote_server (remote_server_id),
|
||||
UNIQUE KEY unique_server (remote_server_id)
|
||||
)
|
||||
```
|
||||
|
||||
### Server Updates
|
||||
|
||||
The page displays data from `ogp_server_status`. Servers should update this table:
|
||||
|
||||
```php
|
||||
// Example server update code (run on each server periodically)
|
||||
$stmt = $db->prepare("INSERT INTO ogp_server_status
|
||||
(remote_server_id, server_name, ip_address, status, cpu_usage, memory_usage, disk_usage, uptime, notes)
|
||||
VALUES (?, ?, ?, 'online', ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
cpu_usage = VALUES(cpu_usage),
|
||||
memory_usage = VALUES(memory_usage),
|
||||
disk_usage = VALUES(disk_usage),
|
||||
uptime = VALUES(uptime),
|
||||
notes = VALUES(notes),
|
||||
last_updated = NOW()");
|
||||
```
|
||||
|
||||
### Access
|
||||
|
||||
- Link in footer: "Server Status"
|
||||
- Public page (no login required)
|
||||
|
||||
---
|
||||
|
||||
## UI Improvements
|
||||
|
||||
### Server List Page
|
||||
|
||||
**Before**: "Order Server" was a plain link
|
||||
**After**: Styled as a button with gradient background
|
||||
|
||||
```html
|
||||
<a href="order.php?service_id=X" class="gsw-btn"
|
||||
style="display:inline-block;padding:12px 24px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;text-decoration:none;border-radius:8px;font-weight:600;transition:transform 0.2s;">
|
||||
Order Now
|
||||
</a>
|
||||
```
|
||||
|
||||
### Order Page
|
||||
|
||||
**Fixed**: Game images now display correctly
|
||||
- Changed from `src="<?php echo $img_url; ?>"`
|
||||
- To `src="../<?php echo $img_url; ?>"`
|
||||
- Assumes images are stored relative to panel root
|
||||
|
||||
### Login Page
|
||||
|
||||
**Added**: "Forgot Password?" link next to Register link
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
**Added**: "My Servers" link for logged-in users
|
||||
- Only visible when user is authenticated
|
||||
- Positioned between "Game Servers" and "Cart"
|
||||
|
||||
### Footer
|
||||
|
||||
**Added**: "Server Status" link
|
||||
- Public access to infrastructure status
|
||||
- Positioned in footer with other utility links
|
||||
|
||||
---
|
||||
|
||||
## Apache Configuration
|
||||
|
||||
Three Apache virtual host configuration files have been created in the GSP root directory.
|
||||
|
||||
### Files Created
|
||||
|
||||
- **panel.conf** - Panel dashboard configuration
|
||||
- **website.conf** - Storefront website configuration
|
||||
- **fileserver.conf** - File server configuration
|
||||
- **APACHE_SETUP.md** - Detailed installation guide
|
||||
|
||||
### panel.conf
|
||||
|
||||
Main Open Game Panel dashboard:
|
||||
- Domain: panel.yourdomain.com
|
||||
- Document Root: /var/www/GSP
|
||||
- PHP settings optimized for panel operations
|
||||
- Security headers enabled
|
||||
|
||||
### website.conf
|
||||
|
||||
GameServers.World storefront:
|
||||
- Domain: gameservers.world
|
||||
- Document Root: /var/www/GSP/_website
|
||||
- Protected includes and data directories
|
||||
- Static asset caching
|
||||
- Compression enabled
|
||||
- Separate session handling
|
||||
|
||||
### fileserver.conf
|
||||
|
||||
Game file distribution:
|
||||
- Domain: files.yourdomain.com
|
||||
- Document Root: /var/www/fileserver
|
||||
- Directory browsing enabled
|
||||
- Large file support
|
||||
- Script execution disabled in uploads
|
||||
- Bandwidth limiting support (optional)
|
||||
|
||||
### Installation
|
||||
|
||||
See `APACHE_SETUP.md` for complete installation instructions including:
|
||||
- Copying configuration files
|
||||
- Enabling sites and modules
|
||||
- SSL/HTTPS setup with Let's Encrypt
|
||||
- DNS configuration
|
||||
- Firewall rules
|
||||
- Troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Password Reset
|
||||
|
||||
1. Visit `login.php`
|
||||
2. Click "Forgot Password?"
|
||||
3. Enter username or email
|
||||
4. Check email or view on-screen link (development mode)
|
||||
5. Click reset link
|
||||
6. Enter new password (min 8 chars)
|
||||
7. Confirm password matches
|
||||
8. Submit and verify redirect to login
|
||||
|
||||
### My Servers
|
||||
|
||||
1. Login as a user with servers
|
||||
2. Click "My Servers" in navigation
|
||||
3. Verify all servers are listed
|
||||
4. Check expiration dates
|
||||
5. Click "Renew" on a server
|
||||
6. Verify renewal page displays correctly
|
||||
|
||||
### Server Status
|
||||
|
||||
1. Visit footer link "Server Status"
|
||||
2. Verify all remote servers are displayed
|
||||
3. Check status badges (color coding)
|
||||
4. Verify "Last Updated" formatting
|
||||
5. Confirm public access (no login required)
|
||||
|
||||
### UI Changes
|
||||
|
||||
1. Visit `serverlist.php`
|
||||
2. Verify "Order Now" displays as styled button
|
||||
3. Click button to go to `order.php`
|
||||
4. Verify game images display correctly
|
||||
5. Check footer has "Server Status" link
|
||||
6. Login and verify "My Servers" appears in menu
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Password Reset
|
||||
|
||||
- ✅ Tokens expire after 1 hour
|
||||
- ✅ One-time use tokens
|
||||
- ✅ Secure random generation
|
||||
- ✅ User enumeration protection
|
||||
- ✅ Password strength requirements
|
||||
- ⚠️ Email delivery depends on server mail config
|
||||
|
||||
### My Servers
|
||||
|
||||
- ✅ Login required
|
||||
- ✅ User can only see own servers
|
||||
- ✅ SQL injection prevention with prepared statements
|
||||
- ✅ XSS prevention with htmlspecialchars()
|
||||
|
||||
### Server Status
|
||||
|
||||
- ✅ Read-only public page
|
||||
- ✅ No sensitive information exposed
|
||||
- ✅ SQL injection prevention
|
||||
- ℹ️ Server updates should be authenticated (implement separately)
|
||||
|
||||
### Apache Configs
|
||||
|
||||
- ✅ Security headers enabled
|
||||
- ✅ Sensitive directories protected
|
||||
- ✅ Directory listing disabled (except fileserver)
|
||||
- ✅ HTTPS configurations ready
|
||||
- ⚠️ Update domain names before deployment
|
||||
- ⚠️ Configure SSL certificates for production
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Password Reset
|
||||
- Email template customization
|
||||
- Integration with email service provider
|
||||
- Rate limiting for reset requests
|
||||
- SMS/2FA backup recovery
|
||||
|
||||
### My Servers
|
||||
- Server control buttons (start/stop/restart)
|
||||
- Real-time server metrics
|
||||
- Configuration editor
|
||||
- File manager integration
|
||||
- Console access
|
||||
- Backup/restore functionality
|
||||
|
||||
### Server Status
|
||||
- Automated server monitoring agent
|
||||
- Alert notifications
|
||||
- Historical uptime graphs
|
||||
- Incident history
|
||||
- Scheduled maintenance display
|
||||
- Status API for external monitoring
|
||||
|
||||
### General
|
||||
- User profile management
|
||||
- Invoice history
|
||||
- Support ticket system
|
||||
- Knowledge base integration
|
||||
- Multi-language support
|
||||
- Dark/light theme toggle
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
1. Check the main GSP documentation
|
||||
2. Review Apache configuration in `APACHE_SETUP.md`
|
||||
3. Check PHP error logs
|
||||
4. Verify database connectivity
|
||||
5. Ensure proper file permissions
|
||||
|
||||
## License
|
||||
|
||||
All new features follow the same license as the main Open Game Panel project.
|
||||
180
backup-website/_archived/IMPLEMENTATION_SUMMARY.md
Normal file
180
backup-website/_archived/IMPLEMENTATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Website Login Implementation - Summary
|
||||
|
||||
## Task Completed
|
||||
Successfully implemented login functionality for the website (_website/) that authenticates users against the panel database (ogp_users table) while maintaining separate sessions.
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. `_website/login.php` (NEW - 223 lines)
|
||||
Full-featured login page with:
|
||||
- Modern, responsive UI design
|
||||
- Authentication against panel DB using MD5 (panel-compatible)
|
||||
- Separate website session: `gameservers_website`
|
||||
- Input validation and sanitization
|
||||
- Error and success message display
|
||||
- Automatic redirect after successful login
|
||||
- Login attempt logging
|
||||
- Already-logged-in detection and redirect
|
||||
|
||||
**Key Features:**
|
||||
- SQL injection prevention via `mysqli_real_escape_string()`
|
||||
- XSS prevention via `htmlspecialchars()` in output
|
||||
- Password verification using MD5 (matching panel's method)
|
||||
- Clean separation from panel session
|
||||
- Responsive design that works on mobile and desktop
|
||||
|
||||
### 2. `_website/logout.php` (NEW - 23 lines)
|
||||
Clean logout functionality:
|
||||
- Destroys website session properly
|
||||
- Clears session cookies
|
||||
- Logs logout events
|
||||
- Redirects to homepage
|
||||
|
||||
### 3. `_website/index.php` (MODIFIED)
|
||||
Updated homepage with:
|
||||
- Session management initialization
|
||||
- Header with login status display
|
||||
- "Welcome, [username]!" message when logged in
|
||||
- Login/Logout button in header
|
||||
- Maintains original design with minimal changes
|
||||
|
||||
**Changes Made:**
|
||||
- Added session initialization at top (4 lines)
|
||||
- Added proper HTML structure (DOCTYPE, html, head tags)
|
||||
- Added header section with login/logout UI (19 lines)
|
||||
- Converted from heredoc to regular HTML output
|
||||
- All styling preserved with additions for header
|
||||
|
||||
### 4. `_website/README_LOGIN.md` (NEW - Documentation)
|
||||
Comprehensive documentation covering:
|
||||
- Overview of implementation
|
||||
- File descriptions
|
||||
- Session management details
|
||||
- Security features
|
||||
- Database requirements
|
||||
- Usage instructions for users and developers
|
||||
- Future enhancement suggestions
|
||||
- Alignment with project guidelines
|
||||
|
||||
### 5. `_website/test_db_connection.php` (NEW - Test Script)
|
||||
Database testing utility that checks:
|
||||
- Database connection status
|
||||
- ogp_users table existence
|
||||
- Table structure verification
|
||||
- User count
|
||||
- Required columns presence
|
||||
- MD5 hashing functionality
|
||||
- Session functionality
|
||||
|
||||
**⚠️ Warning in file:** Must be deleted before production deployment
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Session Management
|
||||
- **Website Session Name:** `gameservers_website`
|
||||
- **Panel Session Name:** `opengamepanel_web` (unchanged)
|
||||
- **Complete separation:** Users can be logged into one without the other
|
||||
|
||||
### Session Variables Set on Login
|
||||
```php
|
||||
$_SESSION['website_user_id'] // User ID from ogp_users
|
||||
$_SESSION['website_username'] // Username
|
||||
$_SESSION['website_user_role'] // User role (admin, user, etc.)
|
||||
$_SESSION['website_user_email'] // User email
|
||||
$_SESSION['website_login_time'] // Timestamp of login
|
||||
```
|
||||
|
||||
### Database Requirements
|
||||
- Access to `ogp_users` table
|
||||
- Required fields: `user_id`, `users_login`, `users_passwd`, `users_role`, `users_email`
|
||||
- Uses existing `db.php` connection
|
||||
|
||||
### Security Measures Implemented
|
||||
1. **SQL Injection Prevention:** `mysqli_real_escape_string()` on all user input
|
||||
2. **XSS Prevention:** `htmlspecialchars()` on all output
|
||||
3. **Session Isolation:** Separate session name prevents conflicts
|
||||
4. **Password Compatibility:** MD5 hashing matches panel's method
|
||||
5. **Logging:** All login/logout events logged via `logger()` function
|
||||
6. **Input Validation:** Empty field checking
|
||||
7. **Already-Logged-In Check:** Prevents duplicate sessions
|
||||
|
||||
### Code Quality
|
||||
- All files pass PHP syntax validation (`php -l`)
|
||||
- Follows existing code conventions
|
||||
- Minimal changes to existing files
|
||||
- Clean, readable code with comments
|
||||
- Responsive design
|
||||
|
||||
## Testing Performed
|
||||
|
||||
### Automated Testing
|
||||
✅ PHP syntax validation on all files
|
||||
✅ File structure verification
|
||||
✅ Git commit verification
|
||||
|
||||
### Manual Testing Required
|
||||
⚠️ Requires live database connection:
|
||||
- Login with valid credentials
|
||||
- Login with invalid credentials
|
||||
- Already-logged-in redirect
|
||||
- Logout functionality
|
||||
- Session persistence across page loads
|
||||
- Use `test_db_connection.php` to verify database setup
|
||||
|
||||
## Alignment with Project Guidelines
|
||||
|
||||
From `.github/copilot-instructions.md`:
|
||||
|
||||
✅ **Website ↔ Panel on same host:** Uses panel DB for authentication
|
||||
✅ **Sessions remain separate:** Different session names
|
||||
✅ **Auth compatibility:** MD5 hashing matches panel
|
||||
✅ **No-Code Planning:** Documented approach before implementation
|
||||
✅ **Repository-first:** Reused existing `db.php`, `logger()` function
|
||||
✅ **Minimal changes:** Surgical modifications to index.php only
|
||||
✅ **Security considerations:** SQL injection, XSS prevention
|
||||
|
||||
## File Size Summary
|
||||
- `login.php`: 7,282 bytes (223 lines)
|
||||
- `logout.php`: 567 bytes (23 lines)
|
||||
- `index.php`: Modified from 3,961 to 5,381 bytes (+1,420 bytes, +37 lines)
|
||||
- `README_LOGIN.md`: 4,041 bytes (documentation)
|
||||
- `test_db_connection.php`: 4,970 bytes (test utility)
|
||||
- `IMPLEMENTATION_SUMMARY.md`: This file (documentation)
|
||||
|
||||
**Total New Code:** ~17,000 bytes across 3 new PHP files
|
||||
|
||||
## Next Steps
|
||||
|
||||
### For Testing
|
||||
1. Run `test_db_connection.php` to verify database connectivity
|
||||
2. Test login with valid panel credentials
|
||||
3. Verify session persistence
|
||||
4. Test logout functionality
|
||||
5. **Delete `test_db_connection.php` after testing**
|
||||
|
||||
### For Production
|
||||
1. Remove or restrict access to `test_db_connection.php`
|
||||
2. Consider adding rate limiting for failed login attempts
|
||||
3. Optional: Add CSRF token protection
|
||||
4. Optional: Implement modern password hashing with transparent upgrade
|
||||
5. Monitor `logfile.txt` for login activity
|
||||
|
||||
### Future Enhancements (Optional)
|
||||
- Password hashing upgrade (bcrypt/argon2)
|
||||
- CSRF protection
|
||||
- Rate limiting (IP-based, like panel's ban_list)
|
||||
- "Remember Me" functionality
|
||||
- Two-factor authentication
|
||||
- Password reset flow integration
|
||||
- Session timeout management
|
||||
|
||||
## Conclusion
|
||||
|
||||
The implementation successfully provides a clean, secure login system for the website that authenticates against the panel database while maintaining complete session separation. The code follows best practices, includes comprehensive documentation, and is ready for testing with a live database connection.
|
||||
|
||||
All requirements from the problem statement have been met:
|
||||
✅ Clone index page structure
|
||||
✅ Create login page
|
||||
✅ Authenticate against panel DB
|
||||
✅ Create separate login session
|
||||
✅ Maintain panel compatibility
|
||||
109
backup-website/_archived/README_LOGIN.md
Normal file
109
backup-website/_archived/README_LOGIN.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Website Login Implementation
|
||||
|
||||
## Overview
|
||||
This implementation adds login functionality to the website that authenticates users against the panel's database (ogp_users table) while maintaining separate sessions for the website and panel.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### 1. `_website/login.php` (NEW)
|
||||
- Full-featured login page with modern UI
|
||||
- Authenticates against panel DB using MD5 password hashing (panel-compatible)
|
||||
- Creates separate website session using `gameservers_website` session name
|
||||
- Logs all login attempts via logger() function
|
||||
- Session variables set:
|
||||
- `$_SESSION['website_user_id']` - User ID from ogp_users
|
||||
- `$_SESSION['website_username']` - Username
|
||||
- `$_SESSION['website_user_role']` - User role (admin, user, etc.)
|
||||
- `$_SESSION['website_user_email']` - User email
|
||||
- `$_SESSION['website_login_time']` - Timestamp of login
|
||||
|
||||
### 2. `_website/logout.php` (NEW)
|
||||
- Cleanly destroys website session
|
||||
- Logs logout events
|
||||
- Redirects to homepage after logout
|
||||
- Properly clears session cookies
|
||||
|
||||
### 3. `_website/index.php` (MODIFIED)
|
||||
- Added session management at the top
|
||||
- Added header with Login/Logout button and user greeting
|
||||
- Shows "Welcome, [username]!" when logged in
|
||||
- Maintains same visual design with added header
|
||||
|
||||
## Session Management
|
||||
|
||||
### Separate Sessions
|
||||
- **Website Session**: `gameservers_website` (this implementation)
|
||||
- **Panel Session**: `opengamepanel_web` (existing panel)
|
||||
|
||||
These sessions are completely separate - users can be logged into one without being logged into the other.
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **SQL Injection Prevention**: Uses `mysqli_real_escape_string()` for input sanitization
|
||||
2. **Password Hashing**: Compatible with panel's MD5 hashing (legacy but matches panel)
|
||||
3. **Session Isolation**: Separate session name prevents conflicts with panel
|
||||
4. **XSS Prevention**: Uses `htmlspecialchars()` for output escaping
|
||||
5. **Logging**: All login/logout events are logged via logger() function
|
||||
|
||||
## Database Requirements
|
||||
|
||||
Requires connection to panel database with access to:
|
||||
- `ogp_users` table (fields: user_id, users_login, users_passwd, users_role, users_email)
|
||||
- Connection configured in `db.php`
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users:
|
||||
1. Visit `_website/login.php` to login
|
||||
2. Enter panel credentials (username/password)
|
||||
3. After successful login, redirected to homepage with session active
|
||||
4. Click "Logout" button to end session
|
||||
|
||||
### For Developers:
|
||||
Check if user is logged in:
|
||||
```php
|
||||
session_name("gameservers_website");
|
||||
session_start();
|
||||
|
||||
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
|
||||
// User is logged in
|
||||
$username = $_SESSION['website_username'];
|
||||
$user_id = $_SESSION['website_user_id'];
|
||||
$user_role = $_SESSION['website_user_role'];
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements (Optional)
|
||||
|
||||
1. **Password Hashing Upgrade**: Implement modern bcrypt/argon2 with transparent upgrade on login
|
||||
2. **CSRF Protection**: Add CSRF tokens to login form
|
||||
3. **Rate Limiting**: Add IP-based login attempt limiting (similar to panel's ban_list)
|
||||
4. **Remember Me**: Add persistent login cookie option
|
||||
5. **Password Reset**: Integrate with panel's password reset flow
|
||||
6. **Two-Factor Auth**: Optional 2FA for enhanced security
|
||||
|
||||
## Testing
|
||||
|
||||
All files pass PHP syntax validation:
|
||||
```bash
|
||||
php -l _website/index.php
|
||||
php -l _website/login.php
|
||||
php -l _website/logout.php
|
||||
```
|
||||
|
||||
## Alignment with Copilot Instructions
|
||||
|
||||
This implementation follows the no-code planning guidelines from `.github/copilot-instructions.md`:
|
||||
|
||||
✅ Website uses panel DB for authentication
|
||||
✅ Sessions remain separate (website ≠ panel)
|
||||
✅ Auth compatibility maintained (MD5 hash for panel users)
|
||||
✅ Minimal changes to existing code
|
||||
✅ Repository-first approach (reused existing db.php, logger function)
|
||||
✅ Security considerations (SQL injection prevention, session isolation)
|
||||
|
||||
## Notes
|
||||
|
||||
- Login credentials are the same as panel login (same user table)
|
||||
- Website session does not grant access to panel - separate login required
|
||||
- Logger function from db.php creates logfile.txt for audit trail
|
||||
317
backup-website/_archived/VISUAL_GUIDE.md
Normal file
317
backup-website/_archived/VISUAL_GUIDE.md
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
# Visual Guide - New Website Features
|
||||
|
||||
This document provides a visual description of the new features and UI changes.
|
||||
|
||||
## 1. Login Page Updates
|
||||
|
||||
### Before
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Welcome Back │
|
||||
│ Sign in to your GameServers account│
|
||||
│ │
|
||||
│ Username: [____________] │
|
||||
│ Password: [____________] │
|
||||
│ │
|
||||
│ [ Sign In ] │
|
||||
│ │
|
||||
│ Register │
|
||||
│ ─── or ─── │
|
||||
│ Back to Home | Panel Login │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Welcome Back │
|
||||
│ Sign in to your GameServers account│
|
||||
│ │
|
||||
│ Username: [____________] │
|
||||
│ Password: [____________] │
|
||||
│ │
|
||||
│ [ Sign In ] │
|
||||
│ │
|
||||
│ Register | Forgot Password? ←NEW │
|
||||
│ ─── or ─── │
|
||||
│ Back to Home | Panel Login │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Forgot Password Page (NEW)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Forgot Password │
|
||||
│ Enter your username or email to │
|
||||
│ reset your password │
|
||||
│ │
|
||||
│ Username or Email: │
|
||||
│ [_____________________________] │
|
||||
│ │
|
||||
│ [ Request Password Reset ] │
|
||||
│ │
|
||||
│ Back to Login | Home │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
After submission (success):
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ✓ Password reset instructions have │
|
||||
│ been sent to your email address. │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. Reset Password Page (NEW)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Reset Password │
|
||||
│ Enter your new password │
|
||||
│ │
|
||||
│ New Password: │
|
||||
│ [_____________________________] │
|
||||
│ Must be at least 8 characters long │
|
||||
│ │
|
||||
│ Confirm Password: │
|
||||
│ [_____________________________] │
|
||||
│ │
|
||||
│ [ Reset Password ] │
|
||||
│ │
|
||||
│ Back to Login | Home │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 4. Navigation Menu Updates
|
||||
|
||||
### Before (Not Logged In)
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ GameServers.World [Login] │
|
||||
│ Home | Game Servers | Cart │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### After (Logged In)
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ GameServers.World Welcome, username! [Logout] │
|
||||
│ Home | Game Servers | My Servers ←NEW | Cart │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 5. Server List Page
|
||||
|
||||
### Before
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ [Game Image] │
|
||||
│ Counter-Strike 2 │
|
||||
│ $15.99 Monthly │
|
||||
│ │
|
||||
│ Order Server (link) │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ [Game Image] │
|
||||
│ Counter-Strike 2 │
|
||||
│ $15.99 Monthly │
|
||||
│ │
|
||||
│ ┌────────────┐ │
|
||||
│ │ Order Now │ ←BUTTON │
|
||||
│ └────────────┘ │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
Button styling:
|
||||
- Gradient background (purple/blue)
|
||||
- Rounded corners
|
||||
- Hover effect (lift up)
|
||||
- Better visibility
|
||||
|
||||
## 6. My Servers Page (NEW)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ My Game Servers │
|
||||
├────────────────────────────────────────────────────────────────────────┤
|
||||
│ Server Name │ Game │ Location │ Status │ Expires │ Price │ Action│
|
||||
├──────────────┼─────────┼──────────┼────────┼────────────┼───────┼───────┤
|
||||
│ My CS2 Srv │ CS2 │ US East │ Active │ Nov 22,2025│ $15.99│[Renew]│
|
||||
│ Rust Server │ Rust │ US West │ Active │ Dec 5, 2025│ $19.99│[Renew]│
|
||||
│ Minecraft │ MC │ EU │ Expired│ Oct 1, 2025│ $12.99│[Renew]│
|
||||
└──────────────┴─────────┴──────────┴────────┴────────────┴───────┴───────┘
|
||||
|
||||
Status indicators:
|
||||
- Active: Green badge
|
||||
- Inactive: Red badge
|
||||
- Expired: Red badge
|
||||
```
|
||||
|
||||
Empty state (no servers):
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ My Game Servers │
|
||||
├────────────────────────────────────┤
|
||||
│ │
|
||||
│ You don't have any game servers │
|
||||
│ yet. │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Browse Game Servers │ │
|
||||
│ └──────────────────────┘ │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 7. Renew Server Page (NEW)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Renew Server │
|
||||
├─────────────────────────────────────┤
|
||||
│ Counter-Strike 2 Server │
|
||||
│ │
|
||||
│ ○ 1 Month - $15.99 │
|
||||
│ ○ 1 Year - $159.99 │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ Cancel │
|
||||
│ │ Proceed to Payment │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 8. Server Status Page (NEW)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Server Status │
|
||||
│ Real-time status of our game server infrastructure │
|
||||
├────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Server │Location/IP │Status │CPU │Memory│Disk │Uptime │Updated│
|
||||
├─────────────┼─────────────┼────────────┼──────┼──────┼──────┼───────┼───────┤
|
||||
│ US-East-1 │192.168.1.10 │ [Online] │45.2% │72.1% │38.5% │30 days│2m ago │
|
||||
│ US-West-1 │192.168.1.11 │ [Online] │32.8% │65.3% │42.1% │15 days│1m ago │
|
||||
│ EU-Central-1│192.168.1.12 │[Maintenance]│N/A │N/A │N/A │N/A │Never │
|
||||
│ Asia-1 │192.168.1.13 │ [Offline] │N/A │N/A │N/A │N/A │2h ago │
|
||||
└─────────────┴─────────────┴────────────┴──────┴──────┴──────┴───────┴───────┘
|
||||
|
||||
Server status is updated automatically every 5 minutes.
|
||||
If you experience any issues, please contact support.
|
||||
```
|
||||
|
||||
Status badge colors:
|
||||
- Online: Green
|
||||
- Offline: Red
|
||||
- Maintenance: Orange
|
||||
- Unknown: Gray
|
||||
|
||||
## 9. Footer Updates
|
||||
|
||||
### Before
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ Privacy | TOS | Worlddomination.dev │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### After
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Privacy | TOS | Server Status ←NEW | Worlddomination.dev│
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 10. Order Page Image Fix
|
||||
|
||||
### Before (Broken)
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ [X] Image not found │
|
||||
│ Counter-Strike 2 │
|
||||
│ Description... │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
### After (Fixed)
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ [✓] ┌──────────┐ │
|
||||
│ │ CS2 Image│ │
|
||||
│ └──────────┘ │
|
||||
│ Counter-Strike 2 │
|
||||
│ Description... │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
Image path changed from `images/game.png` to `../images/game.png`
|
||||
|
||||
## Color Scheme
|
||||
|
||||
All pages use consistent styling:
|
||||
|
||||
### Primary Colors
|
||||
- Purple/Blue Gradient: `#667eea` to `#764ba2`
|
||||
- White backgrounds: `#ffffff`
|
||||
- Dark backgrounds: `#0b1020`
|
||||
|
||||
### Status Colors
|
||||
- Success/Active: `#10b981` (Green)
|
||||
- Error/Expired: `#ef4444` (Red)
|
||||
- Warning/Maintenance: `#f59e0b` (Orange)
|
||||
- Info/Unknown: `#6b7280` (Gray)
|
||||
|
||||
### Typography
|
||||
- Font: System fonts (-apple-system, Segoe UI, Roboto, Arial)
|
||||
- Headings: Bold, 1.8rem
|
||||
- Body: 1rem
|
||||
- Small text: 0.9rem
|
||||
|
||||
### Buttons
|
||||
- Primary: Gradient purple/blue
|
||||
- Hover: Lift effect (translateY -2px)
|
||||
- Border radius: 8px
|
||||
- Padding: 12px 24px
|
||||
|
||||
## Responsive Design
|
||||
|
||||
All pages are mobile-responsive:
|
||||
|
||||
### Desktop (> 768px)
|
||||
- Full navigation menu
|
||||
- Side-by-side layouts
|
||||
- Larger form fields
|
||||
|
||||
### Mobile (< 768px)
|
||||
- Stacked navigation
|
||||
- Single column layouts
|
||||
- Touch-friendly buttons
|
||||
- Larger tap targets
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
- Semantic HTML elements
|
||||
- Proper form labels
|
||||
- Keyboard navigation support
|
||||
- Focus indicators
|
||||
- Alt text for images
|
||||
- ARIA labels where needed
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
Tested and compatible with:
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
|
||||
## Performance
|
||||
|
||||
- Compressed CSS/JS
|
||||
- Optimized images
|
||||
- Cached static assets
|
||||
- Minimal database queries
|
||||
- Prepared statements for security and speed
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
Archived files from _website on 2025-10-23 14:20:00
|
||||
|
||||
This folder contains a snapshot of removed documentation and test artifacts moved from the active `_website/` tree.
|
||||
|
||||
Files moved here (original paths):
|
||||
- VISUAL_GUIDE.md
|
||||
- README_LOGIN.md
|
||||
- FEATURES.md
|
||||
- IMPLEMENTATION_SUMMARY.md
|
||||
- CONFIGURATION.md
|
||||
- test_db_connection.php
|
||||
- tools/simulate_webhook.php
|
||||
- ai.php
|
||||
- data/SIMULATED-WEBHOOK-20251022-101500.json
|
||||
|
||||
If you need to restore any of these, copy them back to the original paths.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
The detailed game docs under `_website/docs/games/` were intentionally left in place (they are product-facing).
|
||||
|
||||
Top-level documentation (VISUAL_GUIDE.md, FEATURES.md, IMPLEMENTATION_SUMMARY.md, CONFIGURATION.md, README_LOGIN.md) were archived here and removed from the active site to reduce clutter.
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"moved_at": "2025-10-23T20:25:00Z",
|
||||
"kept": {
|
||||
"logs": "_website/logs/",
|
||||
"docs": "_website/docs/"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"original": "_website/ai.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/ai.php",
|
||||
"size_bytes": null,
|
||||
"note": "archived sample and tools; size omitted"
|
||||
},
|
||||
{
|
||||
"original": "_website/test_db_connection.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/test_db_connection.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/tools/simulate_webhook.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/tools/simulate_webhook.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/tools/check_db_user.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/tools/check_db_user.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/tools/check_invoices_redirect.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/tools/check_invoices_redirect.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/tools/debug_invoices_redirect.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/tools/debug_invoices_redirect.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/tools/check_logout_redirect.php",
|
||||
"archived": "_website/_archived/removed-20251023-202500/tools/check_logout_redirect.php",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/SIMULATED-WEBHOOK-20251022-101500.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/NO-INVOICE.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/NO-INVOICE.json",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/INV-20250825-174311-0a7993.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/INV-20250825-170438-e37518.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/FREE-549-1761246925.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/FREE-549-1761246925.json",
|
||||
"size_bytes": null
|
||||
},
|
||||
{
|
||||
"original": "_website/data/FREE-548-1761171178.json",
|
||||
"archived": "_website/_archived/removed-20251023-202500/data/FREE-548-1761171178.json",
|
||||
"size_bytes": null
|
||||
}
|
||||
]
|
||||
}
|
||||
325
backup-website/_archived/removed-20251023-202500/ai.php
Normal file
325
backup-website/_archived/removed-20251023-202500/ai.php
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
<?php
|
||||
/***********************
|
||||
* Assistant Chat (Full History) — PHP + cURL
|
||||
* - Persistent thread in session
|
||||
* - Full history render with Question / Answer labels
|
||||
* - SSL verification disabled (your hosting constraint)
|
||||
* - Citations: filename + page (when available)
|
||||
***********************/
|
||||
|
||||
// Debug (disable on production)
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
/* ------------------- CONFIG ------------------- */
|
||||
$OPENAI_API_KEY = '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';
|
||||
|
||||
$ASSISTANT_ID = 'asst_RAhtGzcy6higJeMwomZSqVjM'; // <-- set to your existing assistant
|
||||
$OPENAI_BASE_URL = 'https://api.openai.com/v1';
|
||||
$OPENAI_BETA_HDR = 'assistants=v2'; // required for Assistants v2
|
||||
$REQUEST_TIMEOUT = 30; // seconds for cURL calls
|
||||
$RUN_POLL_DELAY = 500000; // microseconds between run polls (0.5s)
|
||||
$RUN_POLL_MAX = 40; // max polls (~20s total); adjust as needed
|
||||
/* ---------------------------------------------- */
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['thread_id'])) {
|
||||
$_SESSION['thread_id'] = null;
|
||||
}
|
||||
|
||||
/** HTML escape helper */
|
||||
function h($v) { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
|
||||
/** Low-level OpenAI request helper */
|
||||
function openai_request($method, $endpoint, $payload = null, $query = []) {
|
||||
global $OPENAI_API_KEY;
|
||||
$url = "https://api.openai.com/v1" . $endpoint;
|
||||
if (!empty($query)) $url .= '?' . http_build_query($query);
|
||||
|
||||
$headers = [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer {$OPENAI_API_KEY}",
|
||||
"OpenAI-Beta: assistants=v2"
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Host requires SSL verification disabled
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
|
||||
if (!is_null($payload)) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
|
||||
$resp = curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new RuntimeException("cURL error: {$err}");
|
||||
}
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
if ($code >= 400) {
|
||||
$msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error';
|
||||
throw new RuntimeException("OpenAI API error ({$code}): {$msg}");
|
||||
}
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/** Create or reuse a per-visitor thread */
|
||||
function ensure_thread_id() {
|
||||
if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id'];
|
||||
$created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]);
|
||||
$tid = $created['id'] ?? null;
|
||||
if (!$tid) throw new RuntimeException('Failed to create thread.');
|
||||
$_SESSION['thread_id'] = $tid;
|
||||
return $tid;
|
||||
}
|
||||
|
||||
/** Add a user message */
|
||||
function add_user_message($thread_id, $text) {
|
||||
openai_request('POST', "/threads/{$thread_id}/messages", [
|
||||
'role' => 'user',
|
||||
'content' => $text,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Start a run */
|
||||
function start_run($thread_id, $assistant_id) {
|
||||
$run = openai_request('POST', "/threads/{$thread_id}/runs", [
|
||||
'assistant_id' => $assistant_id,
|
||||
]);
|
||||
$run_id = $run['id'] ?? null;
|
||||
if (!$run_id) throw new RuntimeException('Failed to start run.');
|
||||
return $run_id;
|
||||
}
|
||||
|
||||
/** Wait for completion (or fail/timeout) */
|
||||
function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) {
|
||||
$terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired'];
|
||||
for ($i = 0; $i < $max_tries; $i++) {
|
||||
usleep($delay_us);
|
||||
$run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}");
|
||||
$status = $run['status'] ?? '';
|
||||
if (in_array($status, $terminal, true)) return $run;
|
||||
}
|
||||
return ['status' => 'timeout'];
|
||||
}
|
||||
|
||||
/** Cache of file_id => filename (per request) */
|
||||
$_FILE_NAME_CACHE = [];
|
||||
|
||||
/** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */
|
||||
function get_file_name_by_id($file_id) {
|
||||
global $_FILE_NAME_CACHE;
|
||||
if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id];
|
||||
$file = openai_request('GET', "/files/{$file_id}");
|
||||
$name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id));
|
||||
$_FILE_NAME_CACHE[$file_id] = $name;
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message text + citations (filename + page if available).
|
||||
* Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]]
|
||||
*/
|
||||
function normalize_messages($messages) {
|
||||
$out = [];
|
||||
if (empty($messages['data']) || !is_array($messages['data'])) return $out;
|
||||
|
||||
// The API returns newest first by default if not specifying; we request 'asc' in fetch.
|
||||
foreach ($messages['data'] as $m) {
|
||||
$role = $m['role'] ?? '';
|
||||
if (!in_array($role, ['user', 'assistant', 'system'], true)) continue;
|
||||
|
||||
if (empty($m['content']) || !is_array($m['content'])) continue;
|
||||
|
||||
$all_text = [];
|
||||
$refs = [];
|
||||
foreach ($m['content'] as $part) {
|
||||
if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) {
|
||||
$all_text[] = $part['text']['value'];
|
||||
|
||||
// Parse annotations for citations (file_citation)
|
||||
$anns = $part['text']['annotations'] ?? [];
|
||||
if (is_array($anns)) {
|
||||
foreach ($anns as $ann) {
|
||||
if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) {
|
||||
$fid = $ann['file_citation']['file_id'];
|
||||
$page = null;
|
||||
|
||||
// Page can appear under different shapes depending on backend. Try common keys:
|
||||
if (isset($ann['file_citation']['page'])) {
|
||||
$page = $ann['file_citation']['page'];
|
||||
} elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) {
|
||||
// Example: ['start' => 5, 'end' => 6]
|
||||
$start = $ann['file_citation']['page_range']['start'] ?? null;
|
||||
$end = $ann['file_citation']['page_range']['end'] ?? null;
|
||||
if ($start && $end && $start !== $end) $page = "{$start}-{$end}";
|
||||
elseif ($start) $page = (string)$start;
|
||||
}
|
||||
// Fetch filename
|
||||
try {
|
||||
$filename = get_file_name_by_id($fid);
|
||||
} catch (Throwable $e) {
|
||||
$filename = $fid;
|
||||
}
|
||||
$refs[] = [
|
||||
'file_id' => $fid,
|
||||
'filename' => $filename,
|
||||
'page' => $page ?? 'n/a',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($all_text)) {
|
||||
$out[] = [
|
||||
'role' => $role,
|
||||
'text' => implode("\n", $all_text),
|
||||
'refs' => $refs,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Fetch conversation (ascending) */
|
||||
function fetch_history($thread_id) {
|
||||
$messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]);
|
||||
return normalize_messages($messages);
|
||||
}
|
||||
|
||||
/* ------------------- HANDLE POST ------------------- */
|
||||
$error = null;
|
||||
$history = [];
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($_POST['reset_thread'])) {
|
||||
$_SESSION['thread_id'] = null;
|
||||
} elseif (isset($_POST['user_input'])) {
|
||||
$user_text = trim((string)$_POST['user_input']);
|
||||
if ($user_text !== '') {
|
||||
$thread_id = ensure_thread_id();
|
||||
add_user_message($thread_id, $user_text);
|
||||
$run_id = start_run($thread_id, $ASSISTANT_ID);
|
||||
$run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $RUN_POLL_DELAY);
|
||||
|
||||
if (($run['status'] ?? '') === 'failed') {
|
||||
$error = 'Assistant run failed.';
|
||||
} elseif (($run['status'] ?? '') === 'requires_action') {
|
||||
// If you later support tool calls, handle them here then submit outputs.
|
||||
} elseif (($run['status'] ?? '') === 'timeout') {
|
||||
$error = 'Assistant timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($_SESSION['thread_id'])) {
|
||||
$history = fetch_history($_SESSION['thread_id']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
// Include top and menu for website UI (session already started above)
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
?>
|
||||
<!-- UI -->
|
||||
<div class="ai-container">
|
||||
<h3>Site Assistant</h3>
|
||||
<p>Type a question below. Press <b>Enter</b> to send, <b>Shift+Enter</b> for a new line.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="ai-alert" style="border:1px solid #c00;">
|
||||
<strong>Error:</strong> <?php echo h($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($_SESSION['thread_id'])): ?>
|
||||
<div class="ai-msg-meta">Thread: <?php echo h($_SESSION['thread_id']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="chat-form" method="post" style="margin:12px 0;">
|
||||
<textarea id="chat-input" name="user_input" rows="3" class="ai-textarea" placeholder="Ask your question..."></textarea>
|
||||
<div style="margin-top:8px; display:flex; gap:8px;">
|
||||
<button type="submit">Send</button>
|
||||
<button type="submit" name="reset_thread" value="1">Reset Conversation</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($history) && is_array($history)): ?>
|
||||
<div style="margin-top:16px; padding:10px; border:1px solid #ccc; border-radius:8px;">
|
||||
<?php foreach ($history as $msg):
|
||||
// Label mapping: user => Question, assistant => Answer, system => (optional)
|
||||
$role = $msg['role'] ?? 'assistant';
|
||||
if ($role === 'user') $label = 'Question';
|
||||
elseif ($role === 'assistant') $label = 'Answer';
|
||||
else $label = ucfirst($role); // e.g., System
|
||||
$text = str_replace("\r\n", "\n", $msg['text'] ?? '');
|
||||
$refs = $msg['refs'] ?? [];
|
||||
?>
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-weight:bold;"><?php echo h($label); ?></div>
|
||||
<div style="white-space:pre-wrap;"><?php echo nl2br(h($text)); ?></div>
|
||||
|
||||
<?php if (!empty($refs)): ?>
|
||||
<div style="margin-top:6px; font-size:12px;">
|
||||
<em>References:</em>
|
||||
<ul style="margin:6px 0 0 18px; padding:0;">
|
||||
<?php foreach ($refs as $r):
|
||||
$fname = $r['filename'] ?? 'file';
|
||||
$page = $r['page'] ?? 'n/a';
|
||||
// If you have your own document links, replace '#' with a real URL.
|
||||
?>
|
||||
<li>
|
||||
<a href="#" title="file_id: <?php echo h($r['file_id']); ?>">
|
||||
<?php echo h($fname); ?> — page <?php echo h($page); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; color:#666;">No messages yet.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top:10px; font-size:12px; color:#555;">
|
||||
Conversation persists until you click “Reset Conversation”.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit on Enter (Shift+Enter = newline) -->
|
||||
<script>
|
||||
(function(){
|
||||
var form = document.getElementById('chat-form');
|
||||
var input = document.getElementById('chat-input');
|
||||
|
||||
input.addEventListener('keydown', function(e){
|
||||
if (e.key === 'Enter') {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.submit();
|
||||
}
|
||||
// if Shift+Enter, allow newline
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"payer": "iaretechnician@gmail.com",
|
||||
"invoice": "FREE-548-1761171178",
|
||||
"custom": "admin_free_create_order_548",
|
||||
"resource_id": "FREE-439c594e1e65",
|
||||
"items": [],
|
||||
"ts": "2025-10-23T00:12:58+02:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"payer": "iaretechnician@gmail.com",
|
||||
"invoice": "FREE-549-1761246925",
|
||||
"custom": "admin_free_create_order_549",
|
||||
"resource_id": "FREE-439c594e1e65",
|
||||
"items": [],
|
||||
"ts": "2025-10-23T00:12:58+02:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "19.99",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": "INV-20250825-170438-e37518",
|
||||
"custom": "user_1234_order_5678",
|
||||
"resource_id": "2V315801FX904340P",
|
||||
"ts": "2025-08-25T17:05:27-04:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "19.99",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": "INV-20250825-174311-0a7993",
|
||||
"custom": "user_1234_order_5678",
|
||||
"resource_id": "2V315801FX904340P",
|
||||
"ts": "2025-08-25T17:05:27-04:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"event_type": "PAYMENT.SALE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "0.48",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": null,
|
||||
"custom": null,
|
||||
"ts": "2025-08-25T16:46:11-04:00"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "9.99",
|
||||
"currency": "USD",
|
||||
"invoice": "INV-20251022-101500-TEST",
|
||||
"resource_id": "SIMULATED12345",
|
||||
"ts": "2025-10-22T10:15:00-04:00",
|
||||
"note": "Simulated webhook write for testing"
|
||||
}
|
||||
10
backup-website/add_paypal_data_column.sql
Normal file
10
backup-website/add_paypal_data_column.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Add paypal_data column to billing_orders table
|
||||
-- This stores the full PayPal response JSON for admin/refund tracking
|
||||
-- Table prefix is hardcoded to gsp_ for standalone billing module
|
||||
|
||||
ALTER TABLE `gsp_billing_orders`
|
||||
ADD COLUMN `paypal_data` TEXT NULL AFTER `payment_txid`;
|
||||
|
||||
-- Update comment
|
||||
ALTER TABLE `gsp_billing_orders`
|
||||
MODIFY COLUMN `paypal_data` TEXT NULL COMMENT 'Full PayPal API response JSON for tracking/refunds';
|
||||
10
backup-website/add_service_id_column.sql
Normal file
10
backup-website/add_service_id_column.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Add missing service_id column to gsp_billing_invoices table
|
||||
-- This column is required to track which service/game plan was purchased
|
||||
-- Table prefix is hardcoded to gsp_ for standalone billing module
|
||||
|
||||
ALTER TABLE `gsp_billing_invoices`
|
||||
ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`;
|
||||
|
||||
-- Add index for better query performance
|
||||
ALTER TABLE `gsp_billing_invoices`
|
||||
ADD KEY `service_id` (`service_id`);
|
||||
200
backup-website/add_to_cart.php
Normal file
200
backup-website/add_to_cart.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
// _website/add_to_cart.php
|
||||
// Handle Add to Cart posts from order.php
|
||||
require_once(__DIR__ . '/bootstrap.php');
|
||||
require_once(__DIR__ . '/includes/login_required.php');
|
||||
require_once(__DIR__ . '/includes/log.php');
|
||||
|
||||
// Variables from config.inc.php (helps IDEs understand scope)
|
||||
/** @var string $db_host Database host */
|
||||
/** @var string $db_user Database user */
|
||||
/** @var string $db_pass Database password */
|
||||
/** @var string $db_name Database name */
|
||||
/** @var string $table_prefix Table prefix for database tables */
|
||||
|
||||
// Start session if not already
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
// Immediate request tracing log (helps confirm the script is hit)
|
||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||
$trace_file = __DIR__ . '/logs/add_to_cart_requests.log';
|
||||
file_put_contents($trace_file, date('c') . " - REQUEST_METHOD=" . ($_SERVER['REQUEST_METHOD'] ?? '') . " URI=" . ($_SERVER['REQUEST_URI'] ?? '') . "\n", FILE_APPEND);
|
||||
|
||||
// Prefer website session id if set (login.php sets website_user_id in debug mode)
|
||||
$user_id = 0;
|
||||
if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) {
|
||||
$user_id = intval($_SESSION['website_user_id']);
|
||||
} elseif (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
|
||||
$user_id = intval($_SESSION['user_id']);
|
||||
}
|
||||
// If we don't have a numeric user_id but have a username, try to resolve it from the panel DB
|
||||
if ($user_id <= 0 && isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) {
|
||||
$uname = trim((string)$_SESSION['website_username']);
|
||||
// attempt to lookup in DB (if connection available later we will set session after connecting)
|
||||
// We'll set a temporary flag to resolve after DB connection is established below
|
||||
$resolve_username_for_user_id = $uname;
|
||||
} else {
|
||||
$resolve_username_for_user_id = null;
|
||||
}
|
||||
/*
|
||||
if ($user_id <= 0) {
|
||||
// Not logged in - redirect to login with return
|
||||
$return = urlencode('/' . trim(str_replace('\\', '/', $_SERVER['REQUEST_URI']), '/'));
|
||||
header('Location: ' . (isset($SITE_BASE_URL) ? $SITE_BASE_URL : '') . '/_website/login.php?return_to=' . $return);
|
||||
exit;
|
||||
}*/
|
||||
|
||||
// Basic validation and normalization
|
||||
$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0;
|
||||
$home_name = isset($_POST['home_name']) ? trim($_POST['home_name']) : '';
|
||||
$ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0;
|
||||
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
|
||||
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
||||
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
|
||||
$remote_control_password = isset($_POST['remote_control_password']) ? $_POST['remote_control_password'] : '';
|
||||
$ftp_password = isset($_POST['ftp_password']) ? $_POST['ftp_password'] : '';
|
||||
|
||||
// Price lookup: try to find service price_monthly
|
||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
if (!$db) {
|
||||
// Log connection error and exit
|
||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||
$trace = __DIR__ . '/logs/add_to_cart.log';
|
||||
file_put_contents($trace, date('c') . " - mysqli_connect failed: " . mysqli_connect_error() . "\n", FILE_APPEND);
|
||||
die('DB connection failed');
|
||||
} else {
|
||||
// Log that config was loaded (mask password)
|
||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||
$trace = __DIR__ . '/logs/add_to_cart.log';
|
||||
$masked_pass = strlen($db_pass) ? '***' : '';
|
||||
file_put_contents($trace, date('c') . " - DB connected host={$db_host} user={$db_user} pass={$masked_pass} db={$db_name}\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
// If we deferred resolving username to user_id, do it now with the DB connection
|
||||
if (!empty($resolve_username_for_user_id) && $db) {
|
||||
$safe_uname = mysqli_real_escape_string($db, $resolve_username_for_user_id);
|
||||
// users_login is the correct column name in this schema
|
||||
$q = mysqli_query($db, "SELECT user_id FROM {$table_prefix}users WHERE users_login = '$safe_uname' LIMIT 1");
|
||||
if ($q && mysqli_num_rows($q) === 1) {
|
||||
$r = mysqli_fetch_assoc($q);
|
||||
$user_id = intval($r['user_id'] ?? 0);
|
||||
// persist into session for subsequent requests
|
||||
if ($user_id > 0) {
|
||||
$_SESSION['website_user_id'] = $user_id;
|
||||
site_log_info('resolved_user_id_from_username', ['username'=>$resolve_username_for_user_id,'user_id'=>$user_id]);
|
||||
// Also resolve and persist the user's role so menus and admin checks are consistent
|
||||
$role_q = mysqli_query($db, "SELECT users_role FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1");
|
||||
if ($role_q && mysqli_num_rows($role_q) === 1) {
|
||||
$role_row = mysqli_fetch_assoc($role_q);
|
||||
$_SESSION['website_user_role'] = $role_row['users_role'] ?? '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
site_log_warn('resolve_user_failed', ['username'=>$resolve_username_for_user_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$price = 0.0;
|
||||
if ($service_id > 0) {
|
||||
$stmt = $db->prepare("SELECT price_monthly, slot_min_qty, slot_max_qty FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('i', $service_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($price_monthly, $slot_min_qty, $slot_max_qty);
|
||||
if ($stmt->fetch()) {
|
||||
$price = floatval($price_monthly);
|
||||
// constrain slots
|
||||
if ($max_players < $slot_min_qty) $max_players = $slot_min_qty;
|
||||
if ($max_players > $slot_max_qty) $max_players = $slot_max_qty;
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into {table_prefix}billing_invoices (NOT orders - invoice created first)
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$status = 'due'; // Invoice status: due (unpaid), paid
|
||||
|
||||
// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs.
|
||||
$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1');
|
||||
|
||||
// Build and execute a simple INSERT using mysqli_query for debugging clarity
|
||||
@mkdir(__DIR__ . '/logs', 0775, true);
|
||||
$logfile = __DIR__ . '/logs/add_to_cart.log';
|
||||
site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]);
|
||||
|
||||
// Get customer name and email from {table_prefix}users
|
||||
$customer_name = '';
|
||||
$customer_email = '';
|
||||
$user_q = mysqli_query($db, "SELECT users_fname, users_lname, users_email FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1");
|
||||
if ($user_q && mysqli_num_rows($user_q) === 1) {
|
||||
$user_row = mysqli_fetch_assoc($user_q);
|
||||
$customer_name = trim(($user_row['users_fname'] ?? '') . ' ' . ($user_row['users_lname'] ?? ''));
|
||||
$customer_email = $user_row['users_email'] ?? '';
|
||||
}
|
||||
|
||||
// Compute due_date = now + 3 days
|
||||
$due_dt = new DateTime('now');
|
||||
$due_dt->modify('+3 days');
|
||||
$due_date = $due_dt->format('Y-m-d H:i:s');
|
||||
|
||||
// Escape values
|
||||
$esc_user_id = intval($user_id);
|
||||
$esc_service_id = intval($service_id);
|
||||
$esc_home_name = mysqli_real_escape_string($db, $home_name);
|
||||
$esc_ip_id = intval($ip_id);
|
||||
$esc_max_players = intval($max_players);
|
||||
$esc_qty = intval($qty);
|
||||
$esc_invoice_duration = mysqli_real_escape_string($db, $invoice_duration);
|
||||
$esc_price = number_format((float)$price, 2, '.', '');
|
||||
$esc_remote_control_password = mysqli_real_escape_string($db, $remote_control_password);
|
||||
$esc_ftp_password = mysqli_real_escape_string($db, $ftp_password);
|
||||
$esc_status = mysqli_real_escape_string($db, $status);
|
||||
$esc_customer_name = mysqli_real_escape_string($db, $customer_name);
|
||||
$esc_customer_email = mysqli_real_escape_string($db, $customer_email);
|
||||
$esc_due_date = mysqli_real_escape_string($db, $due_date);
|
||||
$esc_description = mysqli_real_escape_string($db, "New server: {$home_name}");
|
||||
|
||||
$sql = "INSERT INTO {$table_prefix}billing_invoices (
|
||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
amount, remote_control_password, ftp_password, status, customer_name,
|
||||
customer_email, due_date, description, currency, order_id
|
||||
) VALUES (
|
||||
{$esc_user_id}, {$esc_service_id}, '{$esc_home_name}', {$esc_ip_id},
|
||||
{$esc_max_players}, {$esc_qty}, '{$esc_invoice_duration}', {$esc_price},
|
||||
'{$esc_remote_control_password}', '{$esc_ftp_password}', '{$esc_status}',
|
||||
'{$esc_customer_name}', '{$esc_customer_email}', '{$esc_due_date}',
|
||||
'{$esc_description}', 'USD', 0
|
||||
)";
|
||||
|
||||
site_log_info('add_to_cart_sql', ['sql'=>$sql]);
|
||||
file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due\n", FILE_APPEND);
|
||||
file_put_contents($logfile, date('c') . " - SQL: " . $sql . "\n", FILE_APPEND);
|
||||
|
||||
$res = @mysqli_query($db, $sql);
|
||||
$err_no = mysqli_errno($db);
|
||||
$err = mysqli_error($db);
|
||||
|
||||
if (!$res || $err_no > 0) {
|
||||
site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]);
|
||||
file_put_contents($logfile, date('c') . " - ERROR: " . $err . " (errno: {$err_no})\n", FILE_APPEND);
|
||||
// Log table existence check
|
||||
$tbl_check = mysqli_query($db, "SHOW TABLES LIKE '{$table_prefix}billing_invoices'");
|
||||
$tbl_exists = ($tbl_check && mysqli_num_rows($tbl_check) > 0) ? 'yes' : 'no';
|
||||
site_log_warn('billing_invoices_exists', ['exists'=>$tbl_exists]);
|
||||
file_put_contents($logfile, date('c') . " - Table exists check: {$tbl_exists}\n", FILE_APPEND);
|
||||
|
||||
// Show user-friendly error
|
||||
die("Error adding to cart: " . htmlspecialchars($err) . ". Please contact support.");
|
||||
} else {
|
||||
$insert_id = mysqli_insert_id($db);
|
||||
$affected = mysqli_affected_rows($db);
|
||||
site_log_info('add_to_cart_insert', ['invoice_id'=>$insert_id, 'affected_rows'=>$affected]);
|
||||
file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
// Redirect to cart page
|
||||
header('Location: cart.php');
|
||||
exit;
|
||||
|
||||
?>
|
||||
69
backup-website/admin.php
Normal file
69
backup-website/admin.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
// Admin landing page
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Admin — Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/header.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-wide panel">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<p>Welcome to the admin area. From here you can manage servers, payments, and site settings.</p>
|
||||
|
||||
<div class="admin-flex-wrap">
|
||||
<a class="gsw-btn" href="adminserverlist.php">Manage Servers & Services</a>
|
||||
<a class="gsw-btn" href="./invoices.php">Invoice History</a>
|
||||
<a class="gsw-btn" href="admin_coupons.php">Manage Coupons</a>
|
||||
<a class="gsw-btn" href="admin_config.php">Edit Site Config</a>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3>Quick usage notes</h3>
|
||||
<ul>
|
||||
<li>The <strong>Manage Servers & Services</strong> page allows enabling/disabling nodes and editing service rows.</li>
|
||||
<li>The <strong>Invoice History</strong> page reads JSON payment records from <code><?php echo h($SITE_DATA_DIR); ?></code>.</li>
|
||||
<li>The <strong>Edit Site Config</strong> page edits <code>_website/includes/config.inc.php</code>. Edits create a timestamped backup before saving.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Sandbox account (testing)</h3>
|
||||
<p>Use PayPal sandbox credentials when testing payments. Set your sandbox <code>client_id</code> and <code>client_secret</code> in the runtime config that the payment handlers use (for this site those are in the respective files under <code>_website/api/</code> or in a central config if you moved credentials).</p>
|
||||
<ul>
|
||||
<li>Create a sandbox business account at <a href="https://developer.paypal.com">PayPal Developer</a> and obtain a sandbox client ID/secret.</li>
|
||||
<li>Update the payment handler config and restart the webserver if required.</li>
|
||||
<li>Run a checkout using the PayPal JS button on the checkout page — after payment completes, the webhook will record a JSON file into <code><?php echo h($SITE_DATA_DIR); ?></code>.</li>
|
||||
<li>If you need to simulate a webhook locally, drop a JSON file with the same schema into the <code>data/</code> folder (we added a sample: <code>SIMULATED-WEBHOOK-*.json</code>).</li>
|
||||
</ul>
|
||||
|
||||
<h3>Payments: high-level program flow</h3>
|
||||
<ol>
|
||||
<li>User adds an item and proceeds to checkout (<code>_website/cart.php</code>).</li>
|
||||
<li>The checkout page renders the PayPal JS SDK and calls server-side endpoints (create_order/capture_order).</li>
|
||||
<li>After a successful capture, PayPal sends a webhook event to <code>_website/webhook.php</code> (or the equivalent handler under <code>_website/api/</code>).</li>
|
||||
<li>The webhook verifies the signature, fetches any missing order details, and writes a JSON record to the <code>data/</code> directory (this powers <code>invoices.php</code> and <code>return.php</code>).</li>
|
||||
<li>On successful payment we mark the order as PAID in the JSON and the site UI (invoices/returns) reads those JSONs to render receipts.</li>
|
||||
<li>Admin pages can view invoices at <code>./invoices.php</code> and reconcile or trigger further provisioning via internal panel APIs.</li>
|
||||
</ol>
|
||||
|
||||
<h3>Environment</h3>
|
||||
<table class="cart-table">
|
||||
<tr><th>Site Base URL</th><td><?php echo h($SITE_BASE_URL ?: '(empty — using relative paths)'); ?></td></tr>
|
||||
<tr><th>Data directory</th><td><?php echo h($SITE_DATA_DIR); ?></td></tr>
|
||||
<tr><th>PHP SAPI</th><td><?php echo h(PHP_SAPI); ?></td></tr>
|
||||
<tr><th>Writable?</th><td><?php echo is_writable(__DIR__ . '/data') ? 'yes' : 'no'; ?></td></tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
103
backup-website/admin_config.php
Normal file
103
backup-website/admin_config.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
// Admin config editor — lightweight editor for _website/includes/config.inc.php
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
|
||||
session_start();
|
||||
if (empty($_SESSION['admin_csrf'])) $_SESSION['admin_csrf'] = bin2hex(random_bytes(16));
|
||||
$csrf = $_SESSION['admin_csrf'];
|
||||
|
||||
$cfgPath = __DIR__ . '/includes/config.inc.php';
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
$status = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$token = $_POST['csrf'] ?? '';
|
||||
if (!hash_equals($csrf, (string)$token)) {
|
||||
$status = 'Invalid CSRF token.';
|
||||
} else {
|
||||
if (!is_writable($cfgPath)) {
|
||||
$status = 'Config file not writable: ' . h($cfgPath);
|
||||
} else {
|
||||
// Backup
|
||||
$bakDir = dirname($cfgPath) . '/backups';
|
||||
@mkdir($bakDir, 0775, true);
|
||||
$bakName = $bakDir . '/config.inc.php.' . date('Ymd-His') . '.' . bin2hex(random_bytes(4)) . '.bak';
|
||||
if (!copy($cfgPath, $bakName)) {
|
||||
$status = 'Failed to create backup. Aborting.';
|
||||
} else {
|
||||
$new = $_POST['config_text'] ?? '';
|
||||
// Basic safety: ensure the file still starts with <?php
|
||||
if (strpos(trim($new), '<?php') !== 0) {
|
||||
$status = 'Config must start with <?php';
|
||||
} else {
|
||||
if (file_put_contents($cfgPath, $new) === false) {
|
||||
$status = 'Failed to write config file.';
|
||||
} else {
|
||||
// Post-save syntax check: try to run php -l using a sensible PHP executable.
|
||||
$phpExec = PHP_BINARY ?: (file_exists('C:\\xampp\\php\\php.exe') ? 'C:\\xampp\\php\\php.exe' : null);
|
||||
$lintOk = true;
|
||||
$lintOutput = '';
|
||||
if ($phpExec) {
|
||||
$cmd = escapeshellarg($phpExec) . ' -l ' . escapeshellarg($cfgPath);
|
||||
// execute and capture output
|
||||
$out = null; $rc = null;
|
||||
@exec($cmd . ' 2>&1', $out, $rc);
|
||||
$lintOutput = is_array($out) ? implode("\n", $out) : (string)$out;
|
||||
if ($rc !== 0) {
|
||||
$lintOk = false;
|
||||
}
|
||||
} else {
|
||||
$lintOutput = 'PHP executable not found for linting; skipping post-save syntax check.';
|
||||
}
|
||||
|
||||
if (!$lintOk) {
|
||||
// rollback
|
||||
@copy($bakName, $cfgPath);
|
||||
$status = 'Syntax error detected in saved config. Changes rolled back. Lint output: ' . h($lintOutput);
|
||||
} else {
|
||||
$status = 'Config saved successfully. Backup: ' . basename($bakName) . (strlen($lintOutput) ? ' (lint: '.h($lintOutput).')' : '');
|
||||
// reload values
|
||||
require_once($cfgPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$currentText = '';
|
||||
if (is_readable($cfgPath)) {
|
||||
$currentText = file_get_contents($cfgPath);
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Admin — Edit Config</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/header.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-wide panel">
|
||||
<h1>Edit Site Config</h1>
|
||||
<?php if ($status): ?><div class="panel"><strong><?php echo h($status); ?></strong></div><?php endif; ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||||
<div style="margin-bottom:8px;"><button type="submit">Save Config</button></div>
|
||||
<textarea name="config_text" rows="24" style="width:100%;font-family:monospace;"><?php echo h($currentText); ?></textarea>
|
||||
<div style="margin-top:8px;"><button type="submit">Save Config</button></div>
|
||||
</form>
|
||||
|
||||
<p><small>Backups are stored in <code><?php echo h(dirname($cfgPath) . '/backups'); ?></code></small></p>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
446
backup-website/admin_coupons.php
Normal file
446
backup-website/admin_coupons.php
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
<?php
|
||||
// Admin coupon management page - standalone billing module
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
|
||||
// Variables from config.inc.php (helps IDEs understand scope)
|
||||
/** @var string $db_host Database host */
|
||||
/** @var string $db_user Database user */
|
||||
/** @var string $db_pass Database password */
|
||||
/** @var string $db_name Database name */
|
||||
/** @var string $table_prefix Table prefix for database tables */
|
||||
|
||||
// Start session if not already started by admin_auth
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
if (empty($_SESSION['admin_csrf'])) {
|
||||
// generate a CSRF token with a safe fallback for older PHP builds
|
||||
try {
|
||||
$token = function_exists('random_bytes') ? bin2hex(random_bytes(16)) : null;
|
||||
} catch (Exception $e) {
|
||||
$token = null;
|
||||
}
|
||||
if (empty($token)) {
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$token = bin2hex(openssl_random_pseudo_bytes(16));
|
||||
} else {
|
||||
$token = bin2hex(bin2hex(substr(sha1(uniqid((string)microtime(true), true)), 0, 16)));
|
||||
}
|
||||
}
|
||||
$_SESSION['admin_csrf'] = $token;
|
||||
}
|
||||
$csrf = $_SESSION['admin_csrf'];
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
|
||||
// Connect to database (graceful failure)
|
||||
$db = false;
|
||||
try {
|
||||
// suppress direct output; we'll log errors and show a friendly message
|
||||
$db = @mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[admin_coupons] mysqli_connect exception: ' . $e->getMessage());
|
||||
$db = false;
|
||||
}
|
||||
if (!$db) {
|
||||
$error = 'Database connection failed. Please check your configuration.';
|
||||
error_log('[admin_coupons] DB connect failed for host=' . ($db_host ?? 'unknown') . ' user=' . ($db_user ?? 'unknown') . ' db=' . ($db_name ?? 'unknown') . ' - ' . mysqli_connect_error());
|
||||
}
|
||||
|
||||
$status = '';
|
||||
$error = '';
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$token = $_POST['csrf'] ?? '';
|
||||
if (!hash_equals($csrf, (string)$token)) {
|
||||
$error = 'Invalid CSRF token.';
|
||||
} else {
|
||||
// Add new coupon
|
||||
if (isset($_POST['add_coupon'])) {
|
||||
$code = mysqli_real_escape_string($db, trim($_POST['code']));
|
||||
$name = mysqli_real_escape_string($db, trim($_POST['name']));
|
||||
$description = mysqli_real_escape_string($db, trim($_POST['description']));
|
||||
$discount_percent = floatval($_POST['discount_percent']);
|
||||
$usage_type = mysqli_real_escape_string($db, $_POST['usage_type']);
|
||||
$game_filter_type = mysqli_real_escape_string($db, $_POST['game_filter_type']);
|
||||
$game_filter_list = isset($_POST['game_filter_list']) && $_POST['game_filter_type'] === 'specific_games'
|
||||
? mysqli_real_escape_string($db, json_encode($_POST['game_filter_list']))
|
||||
: 'NULL';
|
||||
$max_uses = !empty($_POST['max_uses']) ? intval($_POST['max_uses']) : 'NULL';
|
||||
$expires = !empty($_POST['expires']) ? "'" . mysqli_real_escape_string($db, $_POST['expires']) . "'" : 'NULL';
|
||||
|
||||
// Validate code is unique
|
||||
$check = mysqli_query($db, "SELECT coupon_id FROM {$table_prefix}billing_coupons WHERE code = '$code'");
|
||||
if (mysqli_num_rows($check) > 0) {
|
||||
$error = "Coupon code '$code' already exists.";
|
||||
} else {
|
||||
$sql = "INSERT INTO {$table_prefix}billing_coupons
|
||||
(code, name, description, discount_percent, usage_type, game_filter_type, game_filter_list, max_uses, expires, is_active)
|
||||
VALUES ('$code', '$name', '$description', $discount_percent, '$usage_type', '$game_filter_type', " .
|
||||
($game_filter_list === 'NULL' ? 'NULL' : "'$game_filter_list'") . ", $max_uses, $expires, 1)";
|
||||
|
||||
if (mysqli_query($db, $sql)) {
|
||||
$status = "Coupon '$code' added successfully.";
|
||||
} else {
|
||||
$error = "Error adding coupon: " . mysqli_error($db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update existing coupon
|
||||
elseif (isset($_POST['update_coupon'])) {
|
||||
$coupon_id = intval($_POST['coupon_id']);
|
||||
$code = mysqli_real_escape_string($db, trim($_POST['code']));
|
||||
$name = mysqli_real_escape_string($db, trim($_POST['name']));
|
||||
$description = mysqli_real_escape_string($db, trim($_POST['description']));
|
||||
$discount_percent = floatval($_POST['discount_percent']);
|
||||
$usage_type = mysqli_real_escape_string($db, $_POST['usage_type']);
|
||||
$game_filter_type = mysqli_real_escape_string($db, $_POST['game_filter_type']);
|
||||
$game_filter_list = isset($_POST['game_filter_list']) && $_POST['game_filter_type'] === 'specific_games'
|
||||
? mysqli_real_escape_string($db, json_encode($_POST['game_filter_list']))
|
||||
: 'NULL';
|
||||
$max_uses = !empty($_POST['max_uses']) ? intval($_POST['max_uses']) : 'NULL';
|
||||
$expires = !empty($_POST['expires']) ? "'" . mysqli_real_escape_string($db, $_POST['expires']) . "'" : 'NULL';
|
||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
$sql = "UPDATE {$table_prefix}billing_coupons SET
|
||||
code = '$code',
|
||||
name = '$name',
|
||||
description = '$description',
|
||||
discount_percent = $discount_percent,
|
||||
usage_type = '$usage_type',
|
||||
game_filter_type = '$game_filter_type',
|
||||
game_filter_list = " . ($game_filter_list === 'NULL' ? 'NULL' : "'$game_filter_list'") . ",
|
||||
max_uses = $max_uses,
|
||||
expires = $expires,
|
||||
is_active = $is_active
|
||||
WHERE coupon_id = $coupon_id";
|
||||
|
||||
if (mysqli_query($db, $sql)) {
|
||||
$status = "Coupon updated successfully.";
|
||||
} else {
|
||||
$error = "Error updating coupon: " . mysqli_error($db);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete coupon
|
||||
elseif (isset($_POST['delete_coupon'])) {
|
||||
$coupon_id = intval($_POST['coupon_id']);
|
||||
if (mysqli_query($db, "DELETE FROM {$table_prefix}billing_coupons WHERE coupon_id = $coupon_id")) {
|
||||
$status = "Coupon deleted successfully.";
|
||||
} else {
|
||||
$error = "Error deleting coupon: " . mysqli_error($db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all available games from server configs
|
||||
$game_options = [];
|
||||
$games_dir = __DIR__ . '/../../config_games/server_configs/';
|
||||
if (is_dir($games_dir)) {
|
||||
$files = scandir($games_dir);
|
||||
foreach ($files as $file) {
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'xml' && strpos($file, '.bak') === false) {
|
||||
$game_key = str_replace('.xml', '', $file);
|
||||
$game_options[] = $game_key;
|
||||
}
|
||||
}
|
||||
sort($game_options);
|
||||
}
|
||||
|
||||
// Get all coupons
|
||||
$coupons_result = mysqli_query($db, "SELECT * FROM {$table_prefix}billing_coupons ORDER BY created_date DESC");
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Admin — Coupon Management</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/header.css">
|
||||
<style>
|
||||
.coupon-form { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 5px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px; box-sizing: border-box; }
|
||||
.form-group textarea { min-height: 60px; }
|
||||
.game-checkboxes { max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: white; }
|
||||
.game-checkboxes label { display: block; margin: 5px 0; font-weight: normal; }
|
||||
.coupon-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
.coupon-table th, .coupon-table td { border: 1px solid #ddd; padding: 10px; text-align: left; }
|
||||
.coupon-table th { background: #4CAF50; color: white; }
|
||||
.coupon-table tr:nth-child(even) { background: #f9f9f9; }
|
||||
.btn { padding: 8px 16px; margin: 2px; cursor: pointer; border: none; border-radius: 3px; }
|
||||
.btn-primary { background: #4CAF50; color: white; }
|
||||
.btn-warning { background: #ff9800; color: white; }
|
||||
.btn-danger { background: #f44336; color: white; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.badge { padding: 3px 8px; border-radius: 3px; font-size: 0.85em; }
|
||||
.badge-active { background: #28a745; color: white; }
|
||||
.badge-inactive { background: #6c757d; color: white; }
|
||||
.badge-onetime { background: #17a2b8; color: white; }
|
||||
.badge-permanent { background: #ffc107; color: black; }
|
||||
</style>
|
||||
<script>
|
||||
function toggleGameFilter(selectEl) {
|
||||
const gameList = document.getElementById('game_filter_list_container');
|
||||
if (selectEl.value === 'specific_games') {
|
||||
gameList.style.display = 'block';
|
||||
} else {
|
||||
gameList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function editCoupon(couponId) {
|
||||
document.getElementById('edit-form-' + couponId).style.display = 'block';
|
||||
document.getElementById('view-row-' + couponId).style.display = 'none';
|
||||
}
|
||||
|
||||
function cancelEdit(couponId) {
|
||||
document.getElementById('edit-form-' + couponId).style.display = 'none';
|
||||
document.getElementById('view-row-' + couponId).style.display = 'table-row';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
?>
|
||||
<div class="container-wide panel">
|
||||
<h1>Coupon Management</h1>
|
||||
|
||||
<?php if ($status): ?>
|
||||
<div class="status success"><?php echo h($status); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="status error"><?php echo h($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Add New Coupon Form -->
|
||||
<h2>Add New Coupon</h2>
|
||||
<form method="POST" class="coupon-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="code">Coupon Code *</label>
|
||||
<input type="text" id="code" name="code" required maxlength="50" placeholder="e.g., SUMMER2025">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Display Name *</label>
|
||||
<input type="text" id="name" name="name" required maxlength="255" placeholder="e.g., Summer Sale 2025">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" placeholder="Optional description for internal use"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="discount_percent">Discount Percentage * (0-100)</label>
|
||||
<input type="number" id="discount_percent" name="discount_percent" required min="0" max="100" step="0.01" value="10">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="usage_type">Usage Type *</label>
|
||||
<select id="usage_type" name="usage_type" required>
|
||||
<option value="one_time">One Time (applies to first invoice only)</option>
|
||||
<option value="permanent">Permanent (applies to all renewals)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="game_filter_type">Apply To *</label>
|
||||
<select id="game_filter_type" name="game_filter_type" required onchange="toggleGameFilter(this)">
|
||||
<option value="all_games">All Games</option>
|
||||
<option value="specific_games">Specific Games</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="game_filter_list_container" class="form-group" style="display:none;">
|
||||
<label>Select Games</label>
|
||||
<div class="game-checkboxes">
|
||||
<?php foreach ($game_options as $game): ?>
|
||||
<label>
|
||||
<input type="checkbox" name="game_filter_list[]" value="<?php echo h($game); ?>">
|
||||
<?php echo h($game); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_uses">Maximum Uses (leave empty for unlimited)</label>
|
||||
<input type="number" id="max_uses" name="max_uses" min="1" placeholder="Unlimited">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="expires">Expiration Date (leave empty for no expiration)</label>
|
||||
<input type="datetime-local" id="expires" name="expires">
|
||||
</div>
|
||||
|
||||
<button type="submit" name="add_coupon" class="btn btn-primary">Add Coupon</button>
|
||||
</form>
|
||||
|
||||
<!-- Existing Coupons Table -->
|
||||
<h2>Existing Coupons</h2>
|
||||
|
||||
<?php if ($coupons_result && mysqli_num_rows($coupons_result) > 0): ?>
|
||||
<table class="coupon-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Discount</th>
|
||||
<th>Type</th>
|
||||
<th>Game Filter</th>
|
||||
<th>Uses</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($coupon = mysqli_fetch_assoc($coupons_result)):
|
||||
$games_filtered = $coupon['game_filter_type'] === 'specific_games'
|
||||
? json_decode($coupon['game_filter_list'], true)
|
||||
: [];
|
||||
?>
|
||||
<!-- View Row -->
|
||||
<tr id="view-row-<?php echo $coupon['coupon_id']; ?>">
|
||||
<td><strong><?php echo h($coupon['code']); ?></strong></td>
|
||||
<td><?php echo h($coupon['name']); ?></td>
|
||||
<td><?php echo h($coupon['discount_percent']); ?>%</td>
|
||||
<td>
|
||||
<span class="badge badge-<?php echo $coupon['usage_type'] === 'permanent' ? 'permanent' : 'onetime'; ?>">
|
||||
<?php echo h(ucfirst(str_replace('_', ' ', $coupon['usage_type']))); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($coupon['game_filter_type'] === 'all_games'): ?>
|
||||
All Games
|
||||
<?php else: ?>
|
||||
<?php echo count($games_filtered); ?> specific games
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($coupon['max_uses']): ?>
|
||||
<?php echo h($coupon['current_uses']); ?> / <?php echo h($coupon['max_uses']); ?>
|
||||
<?php else: ?>
|
||||
<?php echo h($coupon['current_uses']); ?> (unlimited)
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo $coupon['expires'] ? h($coupon['expires']) : 'Never'; ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?php echo $coupon['is_active'] ? 'active' : 'inactive'; ?>">
|
||||
<?php echo $coupon['is_active'] ? 'Active' : 'Inactive'; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="editCoupon(<?php echo $coupon['coupon_id']; ?>)" class="btn btn-warning">Edit</button>
|
||||
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this coupon?');">
|
||||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||||
<input type="hidden" name="coupon_id" value="<?php echo $coupon['coupon_id']; ?>">
|
||||
<button type="submit" name="delete_coupon" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Form Row (hidden by default) -->
|
||||
<tr id="edit-form-<?php echo $coupon['coupon_id']; ?>" style="display:none;">
|
||||
<td colspan="9">
|
||||
<form method="POST" class="coupon-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||||
<input type="hidden" name="coupon_id" value="<?php echo $coupon['coupon_id']; ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Coupon Code</label>
|
||||
<input type="text" name="code" required value="<?php echo h($coupon['code']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Display Name</label>
|
||||
<input type="text" name="name" required value="<?php echo h($coupon['name']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea name="description"><?php echo h($coupon['description']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Discount Percentage</label>
|
||||
<input type="number" name="discount_percent" required min="0" max="100" step="0.01" value="<?php echo h($coupon['discount_percent']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Usage Type</label>
|
||||
<select name="usage_type" required>
|
||||
<option value="one_time" <?php echo $coupon['usage_type'] === 'one_time' ? 'selected' : ''; ?>>One Time</option>
|
||||
<option value="permanent" <?php echo $coupon['usage_type'] === 'permanent' ? 'selected' : ''; ?>>Permanent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Apply To</label>
|
||||
<select name="game_filter_type" required onchange="toggleGameFilter(this)">
|
||||
<option value="all_games" <?php echo $coupon['game_filter_type'] === 'all_games' ? 'selected' : ''; ?>>All Games</option>
|
||||
<option value="specific_games" <?php echo $coupon['game_filter_type'] === 'specific_games' ? 'selected' : ''; ?>>Specific Games</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="display:<?php echo $coupon['game_filter_type'] === 'specific_games' ? 'block' : 'none'; ?>;">
|
||||
<label>Select Games</label>
|
||||
<div class="game-checkboxes">
|
||||
<?php foreach ($game_options as $game): ?>
|
||||
<label>
|
||||
<input type="checkbox" name="game_filter_list[]" value="<?php echo h($game); ?>"
|
||||
<?php echo in_array($game, $games_filtered) ? 'checked' : ''; ?>>
|
||||
<?php echo h($game); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Maximum Uses</label>
|
||||
<input type="number" name="max_uses" min="1" value="<?php echo h($coupon['max_uses']); ?>" placeholder="Unlimited">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Expiration Date</label>
|
||||
<input type="datetime-local" name="expires" value="<?php echo $coupon['expires'] ? date('Y-m-d\TH:i', strtotime($coupon['expires'])) : ''; ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" <?php echo $coupon['is_active'] ? 'checked' : ''; ?>>
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="update_coupon" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" onclick="cancelEdit(<?php echo $coupon['coupon_id']; ?>)" class="btn">Cancel</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<p>No coupons found. Add your first coupon above.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php
|
||||
if ($db) mysqli_close($db);
|
||||
?>
|
||||
166
backup-website/admin_invoices.php
Normal file
166
backup-website/admin_invoices.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
// Admin invoices viewer and editor
|
||||
$session_name = session_name(); session_start();
|
||||
require_once(__DIR__ . '/bootstrap.php');
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
|
||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
if (!$db) die('DB connection failed');
|
||||
|
||||
// Handle POST requests for invoice updates
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['update_invoice'])) {
|
||||
$orderId = intval($_POST['order_id']);
|
||||
$newStatus = mysqli_real_escape_string($db, $_POST['status']);
|
||||
$newPrice = floatval($_POST['price']);
|
||||
|
||||
$sql = "UPDATE {$table_prefix}billing_orders SET status = '$newStatus', price = $newPrice WHERE order_id = $orderId LIMIT 1";
|
||||
mysqli_query($db, $sql);
|
||||
header('Location: admin_invoices.php?updated=' . $orderId);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all orders with coupon information
|
||||
$orders = mysqli_query($db, "SELECT o.*, u.user_name, c.code AS coupon_code, c.discount_percent AS coupon_discount
|
||||
FROM {$table_prefix}billing_orders o
|
||||
LEFT JOIN {$table_prefix}users u ON o.user_id = u.user_id
|
||||
LEFT JOIN {$table_prefix}billing_coupons c ON o.coupon_id = c.coupon_id
|
||||
ORDER BY o.order_id DESC");
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Admin — Invoices</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/header.css">
|
||||
<style>
|
||||
.edit-row { background: #f9f9f9; }
|
||||
.edit-input { width: 80px; padding: 4px; border: 1px solid #ccc; border-radius: 3px; }
|
||||
.edit-select { padding: 4px; border: 1px solid #ccc; border-radius: 3px; }
|
||||
.btn-save { background: #28a745; color: white; border: none; padding: 5px 12px; border-radius: 3px; cursor: pointer; }
|
||||
.btn-save:hover { background: #218838; }
|
||||
.status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: 600; }
|
||||
.status-paid { background: #d4edda; color: #155724; }
|
||||
.status-pending { background: #fff3cd; color: #856404; }
|
||||
.status-in-cart { background: #d1ecf1; color: #0c5460; }
|
||||
.status-expired { background: #f8d7da; color: #721c24; }
|
||||
.status-renew { background: #cce5ff; color: #004085; }
|
||||
.status-installed { background: #d4edda; color: #155724; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/menu.php'); ?>
|
||||
<div class="container-wide panel">
|
||||
<h1>Admin — All Invoices</h1>
|
||||
<?php if (isset($_GET['updated'])): ?>
|
||||
<div style="background: #d4edda; padding: 10px; margin-bottom: 15px; border-radius: 3px; color: #155724;">
|
||||
✓ Invoice #<?php echo h($_GET['updated']); ?> updated successfully.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$orders || mysqli_num_rows($orders) === 0): ?>
|
||||
<p>No invoices found.</p>
|
||||
<?php else: ?>
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>User</th>
|
||||
<th>Home ID</th>
|
||||
<th>Home Name</th>
|
||||
<th>IP</th>
|
||||
<th>Price</th>
|
||||
<th>Duration</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Finish Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($row = mysqli_fetch_assoc($orders)): ?>
|
||||
<tr id="row-<?php echo $row['order_id']; ?>">
|
||||
<td><?php echo h($row['order_id']); ?></td>
|
||||
<td><?php echo h($row['user_name'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo h($row['home_id'] ?? 'N/A'); ?></td>
|
||||
<td><?php echo h($row['home_name']); ?></td>
|
||||
<td><?php echo h($row['ip']); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$price = floatval($row['price']);
|
||||
$discount = floatval($row['discount_amount'] ?? 0);
|
||||
|
||||
if ($discount > 0 && !empty($row['coupon_code'])) {
|
||||
echo '<span style="text-decoration: line-through; color: #999;">$' . number_format($price + $discount, 2) . '</span><br>';
|
||||
echo '<strong>$' . number_format($price, 2) . '</strong>';
|
||||
echo '<br><small style="color: #28a745;">(' . h($row['coupon_code']) . ' -' . number_format($row['coupon_discount'], 0) . '%)</small>';
|
||||
} else {
|
||||
echo '$' . number_format($price, 2);
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo h($row['invoice_duration']); ?></td>
|
||||
<td>
|
||||
<span class="status-badge status-<?php echo h($row['status']); ?>">
|
||||
<?php echo strtoupper(h($row['status'])); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo h($row['order_date']); ?></td>
|
||||
<td><?php echo h($row['end_date'] ?? 'N/A'); ?></td>
|
||||
<td>
|
||||
<button onclick="editRow(<?php echo $row['order_id']; ?>)" class="gsw-btn" style="padding: 4px 10px; font-size: 12px;">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="edit-<?php echo $row['order_id']; ?>" class="edit-row" style="display: none;">
|
||||
<td colspan="11">
|
||||
<form method="post" action="" style="padding: 10px;">
|
||||
<input type="hidden" name="order_id" value="<?php echo $row['order_id']; ?>">
|
||||
<strong>Edit Invoice #<?php echo $row['order_id']; ?></strong>
|
||||
<div style="margin-top: 10px;">
|
||||
<label style="margin-right: 15px;">
|
||||
<strong>Price:</strong>
|
||||
<input type="number" name="price" value="<?php echo $row['price']; ?>" step="0.01" class="edit-input" required>
|
||||
</label>
|
||||
<label style="margin-right: 15px;">
|
||||
<strong>Status:</strong>
|
||||
<select name="status" class="edit-select" required>
|
||||
<option value="in-cart" <?php echo $row['status'] === 'in-cart' ? 'selected' : ''; ?>>IN-CART</option>
|
||||
<option value="paid" <?php echo $row['status'] === 'paid' ? 'selected' : ''; ?>>PAID</option>
|
||||
<option value="installed" <?php echo $row['status'] === 'installed' ? 'selected' : ''; ?>>INSTALLED</option>
|
||||
<option value="renew" <?php echo $row['status'] === 'renew' ? 'selected' : ''; ?>>RENEW</option>
|
||||
<option value="pending" <?php echo $row['status'] === 'pending' ? 'selected' : ''; ?>>PENDING</option>
|
||||
<option value="expired" <?php echo $row['status'] === 'expired' ? 'selected' : ''; ?>>EXPIRED</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" name="update_invoice" class="btn-save">Save Changes</button>
|
||||
<button type="button" onclick="cancelEdit(<?php echo $row['order_id']; ?>)" class="gsw-btn" style="padding: 5px 12px; margin-left: 5px;">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function editRow(orderId) {
|
||||
document.getElementById('row-' + orderId).style.display = 'none';
|
||||
document.getElementById('edit-' + orderId).style.display = 'table-row';
|
||||
}
|
||||
|
||||
function cancelEdit(orderId) {
|
||||
document.getElementById('row-' + orderId).style.display = 'table-row';
|
||||
document.getElementById('edit-' + orderId).style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
59
backup-website/admin_payments.php
Normal file
59
backup-website/admin_payments.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
// Admin payments viewer — lists persisted PayPal webhook JSON files
|
||||
$session_name = session_name(); session_start();
|
||||
require_once(__DIR__ . '/includes/config.inc.php');
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
|
||||
$dataDir = (isset($SITE_DATA_DIR) && $SITE_DATA_DIR) ? $SITE_DATA_DIR : realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data';
|
||||
$files = [];
|
||||
if (is_dir($dataDir)) {
|
||||
foreach (glob($dataDir . '/*.json') as $file) {
|
||||
$files[] = $file;
|
||||
}
|
||||
}
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Admin — Payments</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/header.css">
|
||||
</head>
|
||||
<body>
|
||||
<?php include(__DIR__ . '/includes/top.php'); include(__DIR__ . '/includes/menu.php'); ?>
|
||||
<div class="container-wide panel">
|
||||
<h1>Payments (webhook)</h1>
|
||||
<?php if (!$files): ?>
|
||||
<p>No payment records found in <?php echo h($dataDir); ?></p>
|
||||
<?php else: ?>
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Invoice</th>
|
||||
<th>Amount</th>
|
||||
<th>Payer</th>
|
||||
<th>Date</th>
|
||||
<th>View</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($files as $f): $j = json_decode(file_get_contents($f), true) ?: []; ?>
|
||||
<tr>
|
||||
<td><?php echo h(basename($f)); ?></td>
|
||||
<td><?php echo h($j['invoice'] ?? ($j['custom'] ?? '')); ?></td>
|
||||
<td><?php echo h(($j['currency'] ?? '') . ' ' . number_format((float)($j['amount'] ?? 0),2)); ?></td>
|
||||
<td><?php echo h($j['payer'] ?? ''); ?></td>
|
||||
<td><?php echo h($j['ts'] ?? ''); ?></td>
|
||||
<td><a href="return.php?invoice=<?php echo urlencode($j['invoice'] ?? ($j['custom'] ?? '')); ?>">View</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||
</body>
|
||||
</html>
|
||||
338
backup-website/adminserverlist.php
Normal file
338
backup-website/adminserverlist.php
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Server List - GameServers.World</title>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
// gameservers.world admin — mysqli only, bulk + per-row update, image base URL + small button
|
||||
|
||||
/* === SITE_BASE_URL is loaded from includes/config.inc.php; leave empty to use relative paths === */
|
||||
|
||||
// Include billing bootstrap (loads config and DB helper)
|
||||
require_once(__DIR__ . '/bootstrap.php');
|
||||
|
||||
// Protect this page: require admin
|
||||
require_once(__DIR__ . '/includes/admin_auth.php');
|
||||
|
||||
// Create database connection (admin_auth already validated DB but we need connection for UI ops)
|
||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
if (!$db) {
|
||||
die("Connection failed: " . mysqli_connect_error());
|
||||
}
|
||||
|
||||
// Include top bar and menu
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
|
||||
/* show errors during setup */
|
||||
@ini_set('display_errors','1');
|
||||
error_reporting(E_ALL);
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function esc_mysqli($db, $v){ return $db->real_escape_string($v); }
|
||||
function fetch_all_assoc($db, $sql){
|
||||
$res = $db->query($sql);
|
||||
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
|
||||
}
|
||||
function col_exists($db, $table, $col){
|
||||
$res = $db->query("SHOW COLUMNS FROM `$table` LIKE '".$db->real_escape_string($col)."'");
|
||||
return ($res && $res->num_rows > 0);
|
||||
}
|
||||
function parse_id_list($s){
|
||||
$tokens = preg_split('/\s+/', trim((string)$s));
|
||||
$out = [];
|
||||
foreach ($tokens as $t) {
|
||||
if ($t === '') continue;
|
||||
if (preg_match('/^\d+$/', $t)) $out[] = (int)$t;
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
/* URL helpers for image preview */
|
||||
function is_abs_url($u){ return (bool)preg_match('~^(?:https?:)?//|^data:~i', (string)$u); }
|
||||
function join_base($base, $path){
|
||||
$base = rtrim((string)$base, '/');
|
||||
$path = ltrim((string)$path, '/');
|
||||
return $base !== '' ? $base.'/'.$path : $path;
|
||||
}
|
||||
|
||||
/* which column holds space-separated locations */
|
||||
$locationCol = col_exists($db, "{$table_prefix}billing_services", 'remote_server_id') ? 'remote_server_id' :
|
||||
(col_exists($db, "{$table_prefix}billing_services", 'remote_server') ? 'remote_server' : 'remote_server_id');
|
||||
|
||||
$flash = [];
|
||||
|
||||
/* A) Update global server location enable flags */
|
||||
if (isset($_POST['update_remote_servers'])) {
|
||||
$enabledIds = array_map('intval', $_POST['rs'] ?? []);
|
||||
$enabledSet = array_flip($enabledIds);
|
||||
$allIds = fetch_all_assoc($db, "SELECT remote_server_id FROM {$table_prefix}remote_servers");
|
||||
foreach ($allIds as $row) {
|
||||
$id = (int)$row['remote_server_id'];
|
||||
$e = isset($enabledSet[$id]) ? 1 : 0;
|
||||
$db->query("UPDATE {$table_prefix}remote_servers SET enabled={$e} WHERE remote_server_id={$id}");
|
||||
}
|
||||
$flash[] = "Server locations updated.";
|
||||
}
|
||||
|
||||
/* helper: update one service row from posted array */
|
||||
function update_service_row(mysqli $db, string $locationCol, int $sid, array $svc){
|
||||
$name = esc_mysqli($db, trim($svc['service_name'] ?? ''));
|
||||
$price = esc_mysqli($db, trim($svc['price_monthly'] ?? '0.00'));
|
||||
$img = esc_mysqli($db, trim($svc['img_url'] ?? ''));
|
||||
$en = !empty($svc['enabled']) ? 1 : 0;
|
||||
|
||||
$minSlots = max(1, (int)($svc['slot_min_qty'] ?? 1));
|
||||
$maxSlots = max($minSlots, (int)($svc['slot_max_qty'] ?? $minSlots));
|
||||
|
||||
$selected = [];
|
||||
if (!empty($svc['locations']) && is_array($svc['locations'])) {
|
||||
$selected = array_map('intval', $svc['locations']);
|
||||
$selected = array_values(array_unique($selected));
|
||||
}
|
||||
$primary = isset($svc['primary_location']) ? (int)$svc['primary_location'] : 0;
|
||||
if ($primary && in_array($primary, $selected, true)) {
|
||||
$selected = array_values(array_diff($selected, [$primary]));
|
||||
array_unshift($selected, $primary);
|
||||
}
|
||||
$locList = implode(' ', $selected);
|
||||
$locListEsc = esc_mysqli($db, $locList);
|
||||
|
||||
$sql = "UPDATE {$table_prefix}billing_services
|
||||
SET service_name='{$name}',
|
||||
`{$locationCol}`='{$locListEsc}',
|
||||
slot_min_qty={$minSlots},
|
||||
slot_max_qty={$maxSlots},
|
||||
price_monthly='{$price}',
|
||||
img_url='{$img}',
|
||||
enabled={$en}
|
||||
WHERE service_id={$sid}";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
/* B1) PER-ROW UPDATE */
|
||||
if (isset($_POST['update_single']) && isset($_POST['service']) && is_array($_POST['service'])) {
|
||||
$sid = (int)$_POST['update_single'];
|
||||
if (isset($_POST['service'][$sid])) {
|
||||
update_service_row($db, $locationCol, $sid, $_POST['service'][$sid]);
|
||||
$flash[] = "Service #{$sid} updated.";
|
||||
}
|
||||
}
|
||||
|
||||
/* B2) BULK UPDATE (single button at bottom) */
|
||||
if (isset($_POST['bulk_update']) && !empty($_POST['service']) && is_array($_POST['service'])) {
|
||||
foreach ($_POST['service'] as $sid => $svc) {
|
||||
update_service_row($db, $locationCol, (int)$sid, (array)$svc);
|
||||
}
|
||||
$flash[] = "All edited services have been updated.";
|
||||
}
|
||||
|
||||
/* C) Remove a service (separate small form) */
|
||||
if (isset($_POST['remove_service'], $_POST['service_id_remove'])) {
|
||||
$sid = (int)$_POST['service_id_remove'];
|
||||
$db->query("DELETE FROM {$table_prefix}billing_services WHERE service_id={$sid}");
|
||||
$flash[] = "Service #{$sid} removed.";
|
||||
}
|
||||
|
||||
/* fetch data for UI */
|
||||
$remoteServers = fetch_all_assoc($db, "SELECT remote_server_id, remote_server_name, enabled FROM {$table_prefix}remote_servers ORDER BY remote_server_name");
|
||||
$services = fetch_all_assoc($db, "SELECT service_id, service_name, `{$locationCol}` AS locs, slot_min_qty, slot_max_qty, price_monthly, img_url, enabled FROM {$table_prefix}billing_services ORDER BY service_name");
|
||||
?>
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div class="panel" style="margin-bottom:12px"><?php foreach($flash as $m) echo "<div>".h($m)."</div>"; ?></div>
|
||||
<div class="panel mb-12"><?php foreach($flash as $m) echo "<div>".h($m)."</div>"; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2>Enable/Disable Server Locations (Global)</h2>
|
||||
<form method="post" action="">
|
||||
<input type="hidden" name="update_remote_servers" value="1">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:10px;">
|
||||
<?php foreach ($remoteServers as $rs): ?>
|
||||
<label class="loc-label min-w-240">
|
||||
<input type="checkbox" name="rs[]" value="<?php echo (int)$rs['remote_server_id']; ?>" <?php echo ((int)$rs['enabled']===1?'checked':''); ?>>
|
||||
<b><?php echo h($rs['remote_server_name']); ?></b>
|
||||
<small class="muted">(ID: <?php echo (int)$rs['remote_server_id']; ?>)</small>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="margin-top:10px;"><button type="submit">Update Enabled Servers</button></div>
|
||||
<div class="mt-10"><button type="submit">Update Enabled Servers</button></div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Current Services</h2>
|
||||
<?php if (!$services): ?>
|
||||
<p>No services found.</p>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- SINGLE BULK FORM FOR ALL SERVICES -->
|
||||
<form method="post" action="">
|
||||
|
||||
<table class="center" style="text-align:center;width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Service Name <small class="muted">(ID below)</small></th>
|
||||
<th>Min Slots</th>
|
||||
<th>Max Slots</th>
|
||||
<th>Price (Monthly)</th>
|
||||
<th>Thumbnail URL</th>
|
||||
<th>Preview</th>
|
||||
<th>Update Row</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($services as $row): ?>
|
||||
<?php
|
||||
$sid = (int)$row['service_id'];
|
||||
$selected = parse_id_list($row['locs'] ?? '');
|
||||
$primary = $selected[0] ?? 0; // first ID is "primary"
|
||||
$selSet = array_flip($selected);
|
||||
$imgUrl = trim((string)$row['img_url']);
|
||||
$displayUrl = '';
|
||||
if ($imgUrl !== '') {
|
||||
if (is_abs_url($imgUrl)) {
|
||||
$displayUrl = $imgUrl;
|
||||
} elseif ($SITE_BASE_URL !== '') {
|
||||
$displayUrl = join_base($SITE_BASE_URL, $imgUrl);
|
||||
} else {
|
||||
// Use relative path (local folder)
|
||||
$displayUrl = $imgUrl;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- MAIN ROW (no bottom border) -->
|
||||
<tr>
|
||||
<!-- Enabled first -->
|
||||
<td>
|
||||
<input type="hidden" name="service[<?php echo $sid; ?>][enabled]" value="0">
|
||||
<input type="checkbox" name="service[<?php echo $sid; ?>][enabled]" value="1" <?php echo ((int)$row['enabled']===1?'checked':''); ?>>
|
||||
</td>
|
||||
|
||||
<!-- Service name (with tiny ID under it) -->
|
||||
<td>
|
||||
<input type="text" name="service[<?php echo $sid; ?>][service_name]" value="<?php echo h($row['service_name']); ?>" class="min-w-260">
|
||||
<div class="small-muted">ID: <?php echo $sid; ?></div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" name="service[<?php echo $sid; ?>][slot_min_qty]" value="<?php echo (int)$row['slot_min_qty']; ?>" min="1" step="1" class="w-90">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="number" name="service[<?php echo $sid; ?>][slot_max_qty]" value="<?php echo (int)$row['slot_max_qty']; ?>" min="1" step="1" class="w-90">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="text" name="service[<?php echo $sid; ?>][price_monthly]" value="<?php echo h($row['price_monthly']); ?>" size="8">
|
||||
</td>
|
||||
|
||||
<!-- Thumbnail URL input -->
|
||||
<td>
|
||||
<input type="text" name="service[<?php echo $sid; ?>][img_url]" value="<?php echo h($row['img_url']); ?>" class="min-w-240">
|
||||
</td>
|
||||
|
||||
<!-- Preview (uses BASE + relative path) -->
|
||||
<td>
|
||||
<?php if ($displayUrl !== ''): ?>
|
||||
<img src="<?php echo h($displayUrl); ?>" alt="preview" loading="lazy" class="img-preview" onerror="this.style.display='none'">
|
||||
<?php else: ?>
|
||||
<span class="muted">(no image)</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Per-row Update (smaller) -->
|
||||
<td>
|
||||
<button type="submit" name="update_single" value="<?php echo $sid; ?>" class="btn-small">Update Row</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- LOCATIONS ROW (single bottom divider) -->
|
||||
<tr>
|
||||
<td colspan="8" style="border-bottom:1px solid #f0f0f0; padding:8px 6px; text-align:left;">
|
||||
<div class="locs-box" data-sid="<?php echo $sid; ?>" style="display:flex; flex-wrap:wrap; gap:8px;">
|
||||
<?php foreach ($remoteServers as $rs): ?>
|
||||
<?php
|
||||
$rid = (int)$rs['remote_server_id'];
|
||||
$isChecked = isset($selSet[$rid]);
|
||||
$isPrimary = ($primary === $rid);
|
||||
?>
|
||||
<label class="loc-label">
|
||||
<input type="checkbox" class="locchk" data-sid="<?php echo $sid; ?>"
|
||||
name="service[<?php echo $sid; ?>][locations][]" value="<?php echo $rid; ?>"
|
||||
<?php echo $isChecked ? 'checked' : ''; ?> class="mr-6">
|
||||
<?php echo h($rs['remote_server_name']); ?> (<?php echo $rid; ?>)
|
||||
<span style="margin-left:10px;">
|
||||
<input type="radio" class="locprim" data-sid="<?php echo $sid; ?>"
|
||||
name="service[<?php echo $sid; ?>][primary_location]" value="<?php echo $rid; ?>"
|
||||
<?php echo $isPrimary ? 'checked' : ''; ?> <?php echo $isChecked ? '' : 'disabled'; ?>>
|
||||
<small>Primary</small>
|
||||
</span>
|
||||
<?php if ((int)$rs['enabled'] === 0): ?>
|
||||
<small class="text-danger ml-8">[Globally disabled]</small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top:14px; text-align:right;">
|
||||
<button type="submit" name="bulk_update" value="1">Update All</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<h3 style="margin-top:20px;">Remove a Service</h3>
|
||||
<form method="post" action="" style="display:flex;gap:8px;align-items:center;">
|
||||
<input type="hidden" name="remove_service" value="1">
|
||||
<select name="service_id_remove">
|
||||
<?php foreach ($services as $s): ?>
|
||||
<option value="<?php echo (int)$s['service_id']; ?>">
|
||||
<?php echo h($s['service_name']); ?> (ID: <?php echo (int)$s['service_id']; ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" onclick="return confirm('Remove this service? This cannot be undone.')">Remove</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- JS: Per-row: enable/disable Primary radios based on whether that location is checked -->
|
||||
<script>
|
||||
document.querySelectorAll('.locs-box').forEach(function(box){
|
||||
const sid = box.getAttribute('data-sid');
|
||||
const checks = box.querySelectorAll('input.locchk[data-sid="'+sid+'"]');
|
||||
|
||||
function refreshRadios() {
|
||||
checks.forEach(function(chk){
|
||||
const rid = chk.value;
|
||||
const rad = box.querySelector('input.locprim[data-sid="'+sid+'"][value="'+rid+'"]');
|
||||
if (!rad) return;
|
||||
if (chk.checked) {
|
||||
rad.disabled = false;
|
||||
} else {
|
||||
if (rad.checked) rad.checked = false;
|
||||
rad.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checks.forEach(chk => chk.addEventListener('change', refreshRadios));
|
||||
refreshRadios();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// Close database connection safely
|
||||
billing_maybe_close_db($db);
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
326
backup-website/ai.php
Normal file
326
backup-website/ai.php
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<?php
|
||||
/***********************
|
||||
* Assistant Chat (Full History) — PHP + cURL
|
||||
* - Persistent thread in session
|
||||
* - Full history render with Question / Answer labels
|
||||
* - SSL verification disabled (your hosting constraint)
|
||||
* - Citations: filename + page (when available)
|
||||
***********************/
|
||||
|
||||
// Debug (disable on production)
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
/* ------------------- CONFIG ------------------- */
|
||||
$OPENAI_API_KEY = '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';
|
||||
|
||||
$ASSISTANT_ID = 'asst_RAhtGzcy6higJeMwomZSqVjM'; // <-- set to your existing assistant
|
||||
$OPENAI_BASE_URL = 'https://api.openai.com/v1';
|
||||
$OPENAI_BETA_HDR = 'assistants=v2'; // required for Assistants v2
|
||||
$REQUEST_TIMEOUT = 30; // seconds for cURL calls
|
||||
$RUN_POLL_DELAY = 500000; // microseconds between run polls (0.5s)
|
||||
$RUN_POLL_MAX = 40; // max polls (~20s total); adjust as needed
|
||||
/* ---------------------------------------------- */
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['thread_id'])) {
|
||||
$_SESSION['thread_id'] = null;
|
||||
}
|
||||
|
||||
/** HTML escape helper */
|
||||
function h($v) { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
|
||||
/** Low-level OpenAI request helper */
|
||||
function openai_request($method, $endpoint, $payload = null, $query = []) {
|
||||
global $OPENAI_API_KEY;
|
||||
$url = "https://api.openai.com/v1" . $endpoint;
|
||||
if (!empty($query)) $url .= '?' . http_build_query($query);
|
||||
|
||||
$headers = [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer {$OPENAI_API_KEY}",
|
||||
"OpenAI-Beta: assistants=v2"
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Host requires SSL verification disabled
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
|
||||
if (!is_null($payload)) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
|
||||
$resp = curl_exec($ch);
|
||||
if ($resp === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new RuntimeException("cURL error: {$err}");
|
||||
}
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = json_decode($resp, true);
|
||||
if ($code >= 400) {
|
||||
$msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error';
|
||||
throw new RuntimeException("OpenAI API error ({$code}): {$msg}");
|
||||
}
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/** Create or reuse a per-visitor thread */
|
||||
function ensure_thread_id() {
|
||||
if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id'];
|
||||
$created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]);
|
||||
$tid = $created['id'] ?? null;
|
||||
if (!$tid) throw new RuntimeException('Failed to create thread.');
|
||||
$_SESSION['thread_id'] = $tid;
|
||||
return $tid;
|
||||
}
|
||||
|
||||
/** Add a user message */
|
||||
function add_user_message($thread_id, $text) {
|
||||
openai_request('POST', "/threads/{$thread_id}/messages", [
|
||||
'role' => 'user',
|
||||
'content' => $text,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Start a run */
|
||||
function start_run($thread_id, $assistant_id) {
|
||||
$run = openai_request('POST', "/threads/{$thread_id}/runs", [
|
||||
'assistant_id' => $assistant_id,
|
||||
]);
|
||||
$run_id = $run['id'] ?? null;
|
||||
if (!$run_id) throw new RuntimeException('Failed to start run.');
|
||||
return $run_id;
|
||||
}
|
||||
|
||||
/** Wait for completion (or fail/timeout) */
|
||||
function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) {
|
||||
$terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired'];
|
||||
for ($i = 0; $i < $max_tries; $i++) {
|
||||
usleep($delay_us);
|
||||
$run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}");
|
||||
$status = $run['status'] ?? '';
|
||||
if (in_array($status, $terminal, true)) return $run;
|
||||
}
|
||||
return ['status' => 'timeout'];
|
||||
}
|
||||
|
||||
/** Cache of file_id => filename (per request) */
|
||||
$_FILE_NAME_CACHE = [];
|
||||
|
||||
/** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */
|
||||
function get_file_name_by_id($file_id) {
|
||||
global $_FILE_NAME_CACHE;
|
||||
if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id];
|
||||
$file = openai_request('GET', "/files/{$file_id}");
|
||||
$name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id));
|
||||
$_FILE_NAME_CACHE[$file_id] = $name;
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message text + citations (filename + page if available).
|
||||
* Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]]
|
||||
*/
|
||||
function normalize_messages($messages) {
|
||||
$out = [];
|
||||
if (empty($messages['data']) || !is_array($messages['data'])) return $out;
|
||||
|
||||
// The API returns newest first by default if not specifying; we request 'asc' in fetch.
|
||||
foreach ($messages['data'] as $m) {
|
||||
$role = $m['role'] ?? '';
|
||||
if (!in_array($role, ['user', 'assistant', 'system'], true)) continue;
|
||||
|
||||
if (empty($m['content']) || !is_array($m['content'])) continue;
|
||||
|
||||
$all_text = [];
|
||||
$refs = [];
|
||||
foreach ($m['content'] as $part) {
|
||||
if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) {
|
||||
$all_text[] = $part['text']['value'];
|
||||
|
||||
// Parse annotations for citations (file_citation)
|
||||
$anns = $part['text']['annotations'] ?? [];
|
||||
if (is_array($anns)) {
|
||||
foreach ($anns as $ann) {
|
||||
if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) {
|
||||
$fid = $ann['file_citation']['file_id'];
|
||||
$page = null;
|
||||
|
||||
// Page can appear under different shapes depending on backend. Try common keys:
|
||||
if (isset($ann['file_citation']['page'])) {
|
||||
$page = $ann['file_citation']['page'];
|
||||
} elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) {
|
||||
// Example: ['start' => 5, 'end' => 6]
|
||||
$start = $ann['file_citation']['page_range']['start'] ?? null;
|
||||
$end = $ann['file_citation']['page_range']['end'] ?? null;
|
||||
if ($start && $end && $start !== $end) $page = "{$start}-{$end}";
|
||||
elseif ($start) $page = (string)$start;
|
||||
}
|
||||
// Fetch filename
|
||||
try {
|
||||
$filename = get_file_name_by_id($fid);
|
||||
} catch (Throwable $e) {
|
||||
$filename = $fid;
|
||||
}
|
||||
$refs[] = [
|
||||
'file_id' => $fid,
|
||||
'filename' => $filename,
|
||||
'page' => $page ?? 'n/a',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($all_text)) {
|
||||
$out[] = [
|
||||
'role' => $role,
|
||||
'text' => implode("\n", $all_text),
|
||||
'refs' => $refs,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Fetch conversation (ascending) */
|
||||
function fetch_history($thread_id) {
|
||||
$messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]);
|
||||
return normalize_messages($messages);
|
||||
}
|
||||
|
||||
/* ------------------- HANDLE POST ------------------- */
|
||||
$error = null;
|
||||
$history = [];
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($_POST['reset_thread'])) {
|
||||
$_SESSION['thread_id'] = null;
|
||||
} elseif (isset($_POST['user_input'])) {
|
||||
$user_text = trim((string)$_POST['user_input']);
|
||||
if ($user_text !== '') {
|
||||
$thread_id = ensure_thread_id();
|
||||
add_user_message($thread_id, $user_text);
|
||||
$run_id = start_run($thread_id, $ASSISTANT_ID);
|
||||
$run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $POLL_DELAY_US);
|
||||
|
||||
if (($run['status'] ?? '') === 'failed') {
|
||||
$error = 'Assistant run failed.';
|
||||
} elseif (($run['status'] ?? '') === 'requires_action') {
|
||||
// If you later support tool calls, handle them here then submit outputs.
|
||||
} elseif (($run['status'] ?? '') === 'timeout') {
|
||||
$error = 'Assistant timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($_SESSION['thread_id'])) {
|
||||
$history = fetch_history($_SESSION['thread_id']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
// Include top and menu for website UI (session already started above)
|
||||
include(__DIR__ . '/includes/top.php');
|
||||
include(__DIR__ . '/includes/menu.php');
|
||||
?>
|
||||
<!-- UI -->
|
||||
<div class="ai-container">
|
||||
<h3>Site Assistant</h3>
|
||||
<p>Type a question below. Press <b>Enter</b> to send, <b>Shift+Enter</b> for a new line.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="ai-alert" style="border:1px solid #c00;">
|
||||
<strong>Error:</strong> <?php echo h($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($_SESSION['thread_id'])): ?>
|
||||
<div class="ai-msg-meta">Thread: <?php echo h($_SESSION['thread_id']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="chat-form" method="post" style="margin:12px 0;">
|
||||
<textarea id="chat-input" name="user_input" rows="3" class="ai-textarea" placeholder="Ask your question..."></textarea>
|
||||
<div style="margin-top:8px; display:flex; gap:8px;">
|
||||
<button type="submit">Send</button>
|
||||
<button type="submit" name="reset_thread" value="1">Reset Conversation</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($history) && is_array($history)): ?>
|
||||
<div style="margin-top:16px; padding:10px; border:1px solid #ccc; border-radius:8px;">
|
||||
<?php foreach ($history as $msg):
|
||||
// Label mapping: user => Question, assistant => Answer, system => (optional)
|
||||
$role = $msg['role'] ?? 'assistant';
|
||||
if ($role === 'user') $label = 'Question';
|
||||
elseif ($role === 'assistant') $label = 'Answer';
|
||||
else $label = ucfirst($role); // e.g., System
|
||||
$text = str_replace("\r\n", "\n", $msg['text'] ?? '');
|
||||
$refs = $msg['refs'] ?? [];
|
||||
?>
|
||||
<div style="margin-bottom:14px;">
|
||||
<div style="font-weight:bold;"><?php echo h($label); ?></div>
|
||||
<div style="white-space:pre-wrap;"><?php echo nl2br(h($text)); ?></div>
|
||||
|
||||
<?php if (!empty($refs)): ?>
|
||||
<div style="margin-top:6px; font-size:12px;">
|
||||
<em>References:</em>
|
||||
<ul style="margin:6px 0 0 18px; padding:0;">
|
||||
<?php foreach ($refs as $r):
|
||||
$fname = $r['filename'] ?? 'file';
|
||||
$page = $r['page'] ?? 'n/a';
|
||||
// If you have your own document links, replace '#' with a real URL.
|
||||
?>
|
||||
<li>
|
||||
<a href="#" title="file_id: <?php echo h($r['file_id']); ?>">
|
||||
<?php echo h($fname); ?> — page <?php echo h($page); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; color:#666;">No messages yet.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top:10px; font-size:12px; color:#555;">
|
||||
Conversation persists until you click “Reset Conversation”.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit on Enter (Shift+Enter = newline) -->
|
||||
<script>
|
||||
(function(){
|
||||
var form = document.getElementById('chat-form');
|
||||
var input = document.getElementById('chat-input');
|
||||
|
||||
input.addEventListener('keydown', function(e){
|
||||
if (e.key === 'Enter') {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.submit();
|
||||
}
|
||||
// if Shift+Enter, allow newline
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
305
backup-website/api/capture_order.php
Normal file
305
backup-website/api/capture_order.php
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
<?php
|
||||
/**
|
||||
* PayPal Order Capture Endpoint
|
||||
* Processes PayPal payment, marks invoices paid, creates order records
|
||||
* Standalone billing module - uses only standard PHP mysqli
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../includes/config.inc.php');
|
||||
|
||||
// Prevent any output before JSON
|
||||
ob_start();
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// Setup logging
|
||||
$logDir = __DIR__ . '/../logs';
|
||||
@mkdir($logDir, 0755, true);
|
||||
$logFile = $logDir . '/payment_capture.log';
|
||||
$requestId = uniqid('req_', true);
|
||||
|
||||
function log_payment($label, $data) {
|
||||
global $logFile, $requestId;
|
||||
$entry = "[" . date('Y-m-d H:i:s') . "] [$requestId] $label\n";
|
||||
$entry .= is_array($data) || is_object($data) ? print_r($data, true) : (string)$data;
|
||||
$entry .= "\n" . str_repeat('-', 80) . "\n";
|
||||
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Parse input
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$input = json_decode($rawInput, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
log_payment('JSON_ERROR', json_last_error_msg());
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$paypal_order_id = $input['order_id'] ?? null;
|
||||
if (!$paypal_order_id) {
|
||||
log_payment('MISSING_ORDER_ID', $input);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'missing_order_id', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
log_payment('REQUEST_START', ['order_id' => $paypal_order_id]);
|
||||
|
||||
// PayPal API configuration
|
||||
$sandbox = true;
|
||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
// Get OAuth token
|
||||
$ch = curl_init("$api/v1/oauth2/token");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
CURLOPT_USERPWD => "$client_id:$client_secret",
|
||||
]);
|
||||
$tokenResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
log_payment('OAUTH_FAILED', ['http_code' => $httpCode, 'response' => $tokenResponse]);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'oauth_failed', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$tokenData = json_decode($tokenResponse, true);
|
||||
$accessToken = $tokenData['access_token'] ?? null;
|
||||
|
||||
if (!$accessToken) {
|
||||
log_payment('NO_ACCESS_TOKEN', $tokenData);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'no_access_token', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Capture the PayPal order
|
||||
$ch = curl_init("$api/v2/checkout/orders/$paypal_order_id/capture");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
"Authorization: Bearer $accessToken"
|
||||
],
|
||||
]);
|
||||
$captureResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 201 && $httpCode !== 200) {
|
||||
log_payment('CAPTURE_FAILED', ['http_code' => $httpCode, 'response' => substr($captureResponse, 0, 500)]);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'capture_failed', 'http_code' => $httpCode, 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$captureData = json_decode($captureResponse, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
log_payment('CAPTURE_JSON_ERROR', json_last_error_msg());
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'capture_json_error', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$status = $captureData['status'] ?? null;
|
||||
if ($status !== 'COMPLETED') {
|
||||
log_payment('NOT_COMPLETED', ['status' => $status]);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'payment_not_completed', 'status' => $status, 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Extract transaction ID
|
||||
$txid = $captureData['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
|
||||
$payer_email = $captureData['payer']['email_address'] ?? '';
|
||||
$payer_name = ($captureData['payer']['name']['given_name'] ?? '') . ' ' . ($captureData['payer']['name']['surname'] ?? '');
|
||||
|
||||
// Store full PayPal response as JSON for admin/refund tracking
|
||||
$paypal_json = json_encode($captureData);
|
||||
|
||||
log_payment('PAYMENT_CAPTURED', [
|
||||
'txid' => $txid,
|
||||
'payer_email' => $payer_email,
|
||||
'payer_name' => trim($payer_name)
|
||||
]);
|
||||
|
||||
// Start session to get user_id (use billing website session name)
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name("gameservers_website");
|
||||
session_start();
|
||||
}
|
||||
$user_id = isset($_SESSION['website_user_id']) ? intval($_SESSION['website_user_id']) :
|
||||
(isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0);
|
||||
|
||||
if ($user_id <= 0) {
|
||||
log_payment('NO_USER_SESSION', $_SESSION);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'no_user_session', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
|
||||
if (!$db) {
|
||||
log_payment('DB_CONNECTION_FAILED', mysqli_connect_error());
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$esc_txid = mysqli_real_escape_string($db, $txid);
|
||||
$esc_paypal_json = mysqli_real_escape_string($db, $paypal_json);
|
||||
|
||||
// Apply coupon from session to invoices before marking paid
|
||||
session_start();
|
||||
$coupon_id = isset($_SESSION['cart_coupon_id']) ? intval($_SESSION['cart_coupon_id']) : 0;
|
||||
if ($coupon_id > 0) {
|
||||
// Get unpaid invoices for this user to apply coupon
|
||||
$invoices_query = "SELECT invoice_id, amount FROM {$table_prefix}billing_invoices
|
||||
WHERE user_id=$user_id AND status='due'";
|
||||
$invoices_result = mysqli_query($db, $invoices_query);
|
||||
|
||||
// Get coupon details
|
||||
$coupon_query = "SELECT discount_percent FROM {$table_prefix}billing_coupons
|
||||
WHERE coupon_id=$coupon_id AND is_active=1 LIMIT 1";
|
||||
$coupon_result = mysqli_query($db, $coupon_query);
|
||||
|
||||
if ($coupon_result && mysqli_num_rows($coupon_result) === 1) {
|
||||
$coupon_row = mysqli_fetch_assoc($coupon_result);
|
||||
$discount_percent = floatval($coupon_row['discount_percent']);
|
||||
|
||||
// Update each invoice with coupon
|
||||
while ($inv_row = mysqli_fetch_assoc($invoices_result)) {
|
||||
$inv_id = intval($inv_row['invoice_id']);
|
||||
$inv_amount = floatval($inv_row['amount']);
|
||||
$discount_amt = $inv_amount * ($discount_percent / 100);
|
||||
$new_amount = $inv_amount - $discount_amt;
|
||||
|
||||
$update_coupon_sql = "UPDATE {$table_prefix}billing_invoices
|
||||
SET coupon_id=$coupon_id,
|
||||
discount_amount=" . number_format($discount_amt, 2, '.', '') . ",
|
||||
amount=" . number_format($new_amount, 2, '.', '') . "
|
||||
WHERE invoice_id=$inv_id";
|
||||
mysqli_query($db, $update_coupon_sql);
|
||||
log_payment('COUPON_APPLIED', ['invoice_id' => $inv_id, 'discount' => $discount_amt]);
|
||||
}
|
||||
|
||||
// Increment coupon usage
|
||||
$update_usage_sql = "UPDATE {$table_prefix}billing_coupons
|
||||
SET current_uses = current_uses + 1
|
||||
WHERE coupon_id=$coupon_id";
|
||||
mysqli_query($db, $update_usage_sql);
|
||||
|
||||
// Clear coupon from session
|
||||
unset($_SESSION['cart_coupon_code']);
|
||||
unset($_SESSION['cart_coupon_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all due invoices for this user as paid
|
||||
$updateInvoicesSql = "UPDATE {$table_prefix}billing_invoices
|
||||
SET status='paid', paid_date='$now', payment_txid='$esc_txid', payment_method='paypal'
|
||||
WHERE user_id=$user_id AND status='due'";
|
||||
|
||||
log_payment('UPDATE_INVOICES_SQL', $updateInvoicesSql);
|
||||
$updateResult = mysqli_query($db, $updateInvoicesSql);
|
||||
|
||||
if (!$updateResult) {
|
||||
log_payment('UPDATE_INVOICES_FAILED', mysqli_error($db));
|
||||
mysqli_close($db);
|
||||
ob_clean();
|
||||
echo json_encode(['error' => 'update_invoices_failed', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$affectedInvoices = mysqli_affected_rows($db);
|
||||
log_payment('INVOICES_MARKED_PAID', ['count' => $affectedInvoices]);
|
||||
|
||||
// Get all invoices we just marked paid
|
||||
$getInvoicesSql = "SELECT * FROM {$table_prefix}billing_invoices
|
||||
WHERE user_id=$user_id AND payment_txid='$esc_txid'";
|
||||
$invoicesResult = mysqli_query($db, $getInvoicesSql);
|
||||
|
||||
$ordersCreated = 0;
|
||||
while ($inv = mysqli_fetch_assoc($invoicesResult)) {
|
||||
$invoice_id = intval($inv['invoice_id']);
|
||||
$existing_order_id = intval($inv['order_id'] ?? 0);
|
||||
|
||||
// Skip if invoice already linked to an order (renewal)
|
||||
if ($existing_order_id > 0) {
|
||||
log_payment('RENEWAL_INVOICE', ['invoice_id' => $invoice_id, 'order_id' => $existing_order_id]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new order
|
||||
$service_id = intval($inv['service_id']);
|
||||
$home_name = mysqli_real_escape_string($db, $inv['home_name']);
|
||||
$ip = intval($inv['ip']);
|
||||
$max_players = intval($inv['max_players']);
|
||||
$qty = intval($inv['qty']);
|
||||
$duration = mysqli_real_escape_string($db, $inv['invoice_duration']);
|
||||
$amount = floatval($inv['amount']);
|
||||
$rcon_pw = mysqli_real_escape_string($db, $inv['remote_control_password']);
|
||||
$ftp_pw = mysqli_real_escape_string($db, $inv['ftp_password']);
|
||||
|
||||
// Calculate end_date
|
||||
$end_date = date('Y-m-d H:i:s', strtotime("+$qty $duration"));
|
||||
|
||||
// Insert order with status='paid' (panel will provision and change to 'active')
|
||||
$insertOrderSql = "INSERT INTO {$table_prefix}billing_orders (
|
||||
user_id, service_id, home_name, ip, max_players, qty, invoice_duration,
|
||||
price, remote_control_password, ftp_password, status, order_date, end_date,
|
||||
payment_txid, paid_ts, paypal_data
|
||||
) VALUES (
|
||||
$user_id, $service_id, '$home_name', $ip, $max_players, $qty, '$duration',
|
||||
$amount, '$rcon_pw', '$ftp_pw', 'paid', '$now', '$end_date',
|
||||
'$esc_txid', '$now', '$esc_paypal_json'
|
||||
)";
|
||||
|
||||
log_payment('INSERT_ORDER_SQL', substr($insertOrderSql, 0, 300));
|
||||
|
||||
if (mysqli_query($db, $insertOrderSql)) {
|
||||
$new_order_id = mysqli_insert_id($db);
|
||||
log_payment('ORDER_CREATED', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
|
||||
|
||||
// Link invoice to order
|
||||
$linkSql = "UPDATE {$table_prefix}billing_invoices SET order_id=$new_order_id WHERE invoice_id=$invoice_id";
|
||||
mysqli_query($db, $linkSql);
|
||||
|
||||
$ordersCreated++;
|
||||
} else {
|
||||
log_payment('INSERT_ORDER_FAILED', mysqli_error($db));
|
||||
}
|
||||
}
|
||||
|
||||
mysqli_close($db);
|
||||
|
||||
log_payment('PROCESSING_COMPLETE', [
|
||||
'invoices_paid' => $affectedInvoices,
|
||||
'orders_created' => $ordersCreated,
|
||||
'txid' => $txid
|
||||
]);
|
||||
|
||||
// Return success response
|
||||
ob_clean();
|
||||
echo json_encode([
|
||||
'status' => 'COMPLETED',
|
||||
'order_id' => $paypal_order_id,
|
||||
'txid' => $txid,
|
||||
'invoices_paid' => $affectedInvoices,
|
||||
'orders_created' => $ordersCreated
|
||||
]);
|
||||
266
backup-website/api/create_order.php
Normal file
266
backup-website/api/create_order.php
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
/**
|
||||
* PayPal Create Order API Endpoint
|
||||
* Enhanced with comprehensive logging for debugging
|
||||
*/
|
||||
|
||||
// Ensure all errors are logged, not displayed (to prevent JSON corruption)
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once(__DIR__ . '/../includes/config.inc.php');
|
||||
// create_order for PayPal — adapted to run from _website/api
|
||||
$sandbox = true; // flip to false for Live
|
||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
|
||||
// Setup comprehensive logging
|
||||
$logDir = __DIR__ . '/../logs';
|
||||
@mkdir($logDir, 0755, true);
|
||||
$logFile = $logDir . '/paypal_create_order.log';
|
||||
$requestId = uniqid('req_', true); // Unique request identifier for tracking
|
||||
|
||||
function create_order_log($label, $data) {
|
||||
global $logFile, $requestId;
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$entry = "[$timestamp] [$requestId] $label\n";
|
||||
if (is_array($data) || is_object($data)) {
|
||||
$entry .= print_r($data, true);
|
||||
} else {
|
||||
$entry .= (string)$data;
|
||||
}
|
||||
$entry .= "\n" . str_repeat('-', 80) . "\n";
|
||||
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
create_order_log('REQUEST_START', [
|
||||
'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
|
||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN',
|
||||
]);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Read and parse input
|
||||
$rawInput = file_get_contents('php://input');
|
||||
create_order_log('RAW_INPUT', substr($rawInput, 0, 2000)); // Log first 2000 chars
|
||||
|
||||
$in = json_decode($rawInput, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
create_order_log('JSON_DECODE_ERROR', [
|
||||
'error' => json_last_error_msg(),
|
||||
'raw_input_length' => strlen($rawInput),
|
||||
'raw_input_preview' => substr($rawInput, 0, 500)
|
||||
]);
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_json', 'message' => json_last_error_msg(), 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$in) {
|
||||
$in = [];
|
||||
}
|
||||
|
||||
$amount_in = $in['amount'] ?? '0.00';
|
||||
$currency = $in['currency'] ?? 'USD';
|
||||
$invoice_id = $in['invoice_id'] ?? null;
|
||||
$custom_id = $in['custom_id'] ?? null;
|
||||
$description = $in['description'] ?? 'Order';
|
||||
$return_url = $in['return_url'] ?? null;
|
||||
$cancel_url = $in['cancel_url'] ?? null;
|
||||
$items = (isset($in['items']) && is_array($in['items'])) ? $in['items'] : null;
|
||||
$line_invoices= (isset($in['line_invoices']) && is_array($in['line_invoices'])) ? $in['line_invoices'] : null;
|
||||
|
||||
create_order_log('PARSED_INPUT', [
|
||||
'amount' => $amount_in,
|
||||
'currency' => $currency,
|
||||
'invoice_id' => $invoice_id,
|
||||
'custom_id' => $custom_id,
|
||||
'items_count' => $items ? count($items) : 0,
|
||||
'line_invoices_count' => $line_invoices ? count($line_invoices) : 0
|
||||
]);
|
||||
|
||||
$amount_value = number_format((float)$amount_in, 2, '.', '');
|
||||
if ($items) {
|
||||
$sum = 0.00;
|
||||
foreach ($items as $it) {
|
||||
$qty = isset($it['quantity']) ? (int)$it['quantity'] : 1;
|
||||
$val = isset($it['unit_amount']['value']) ? (float)$it['unit_amount']['value'] : 0.00;
|
||||
$sum += $qty * $val;
|
||||
}
|
||||
$amount_value = number_format($sum, 2, '.', '');
|
||||
create_order_log('AMOUNT_CALCULATED', [
|
||||
'original_amount' => $amount_in,
|
||||
'calculated_from_items' => $amount_value,
|
||||
'items_sum' => $sum
|
||||
]);
|
||||
}
|
||||
|
||||
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
create_order_log('PAYPAL_API_CONFIG', [
|
||||
'sandbox_mode' => $sandbox,
|
||||
'api_base' => $api,
|
||||
'has_client_id' => !empty($client_id),
|
||||
'has_client_secret' => !empty($client_secret)
|
||||
]);
|
||||
|
||||
// Step 1: Get OAuth token
|
||||
create_order_log('OAUTH_REQUEST_START', ['endpoint' => "$api/v1/oauth2/token"]);
|
||||
|
||||
$ch = curl_init("$api/v1/oauth2/token");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
CURLOPT_USERPWD => $client_id . ':' . $client_secret,
|
||||
]);
|
||||
$tok = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_errno = curl_errno($ch);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
create_order_log('OAUTH_RESPONSE', [
|
||||
'http_code' => $http,
|
||||
'curl_errno' => $curl_errno,
|
||||
'curl_error' => $curl_error,
|
||||
'response_length' => strlen($tok),
|
||||
'response_preview' => substr($tok, 0, 200)
|
||||
]);
|
||||
|
||||
if ($curl_errno !== 0) {
|
||||
create_order_log('OAUTH_CURL_ERROR', ['errno' => $curl_errno, 'error' => $curl_error]);
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'oauth_curl_fail', 'details' => $curl_error, 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($http !== 200) {
|
||||
create_order_log('OAUTH_HTTP_ERROR', ['http_code' => $http, 'response' => $tok]);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'oauth_fail', 'http_code' => $http, 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$access = json_decode($tok, true)['access_token'] ?? null;
|
||||
if (!$access) {
|
||||
create_order_log('OAUTH_NO_TOKEN', ['response' => $tok]);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'oauth_no_token', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
create_order_log('OAUTH_SUCCESS', ['token_length' => strlen($access)]);
|
||||
|
||||
// Update site base URL to exclude 'modules/billing'
|
||||
$siteBaseUrl = 'http://gameservers.world';
|
||||
|
||||
create_order_log('URL_PROCESSING_BEFORE', [
|
||||
'return_url' => $return_url,
|
||||
'cancel_url' => $cancel_url,
|
||||
'site_base' => $siteBaseUrl
|
||||
]);
|
||||
|
||||
// Ensure return_url and cancel_url are absolute URLs (relative to site root)
|
||||
if (strpos($return_url, 'http') !== 0) {
|
||||
$return_url = $siteBaseUrl . '/' . ltrim($return_url, '/');
|
||||
}
|
||||
if (strpos($cancel_url, 'http') !== 0) {
|
||||
$cancel_url = $siteBaseUrl . '/' . ltrim($cancel_url, '/');
|
||||
}
|
||||
|
||||
create_order_log('URL_PROCESSING_AFTER', [
|
||||
'return_url' => $return_url,
|
||||
'cancel_url' => $cancel_url
|
||||
]);
|
||||
|
||||
$purchaseUnit = [
|
||||
'amount' => [ 'currency_code' => $currency, 'value' => $amount_value ],
|
||||
'description' => $description,
|
||||
'invoice_id' => $invoice_id,
|
||||
'custom_id' => $custom_id
|
||||
];
|
||||
if ($items) {
|
||||
$purchaseUnit['items'] = $items;
|
||||
$purchaseUnit['amount']['breakdown'] = [ 'item_total' => ['currency_code'=>$currency,'value'=>$amount_value] ];
|
||||
}
|
||||
|
||||
$body = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [ $purchaseUnit ],
|
||||
'application_context' => [ 'return_url'=>$return_url, 'cancel_url'=>$cancel_url, 'user_action'=>'PAY_NOW' ]
|
||||
];
|
||||
|
||||
create_order_log('PAYPAL_ORDER_PAYLOAD', $body);
|
||||
|
||||
// Step 2: Create PayPal order
|
||||
create_order_log('CREATE_ORDER_REQUEST_START', ['endpoint' => "$api/v2/checkout/orders"]);
|
||||
|
||||
$ch = curl_init("$api/v2/checkout/orders");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($body),
|
||||
CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $access ],
|
||||
]);
|
||||
$res = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curl_errno = curl_errno($ch);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
create_order_log('CREATE_ORDER_RESPONSE', [
|
||||
'http_code' => $http,
|
||||
'curl_errno' => $curl_errno,
|
||||
'curl_error' => $curl_error,
|
||||
'response_length' => strlen($res),
|
||||
'response' => substr($res, 0, 1000) // First 1000 chars of response
|
||||
]);
|
||||
|
||||
if ($curl_errno !== 0) {
|
||||
create_order_log('CREATE_ORDER_CURL_ERROR', ['errno' => $curl_errno, 'error' => $curl_error]);
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'create_order_curl_fail', 'details' => $curl_error, 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($http !== 201) {
|
||||
create_order_log('CREATE_ORDER_HTTP_ERROR', [
|
||||
'http_code' => $http,
|
||||
'response' => $res,
|
||||
'payload_sent' => $body
|
||||
]);
|
||||
|
||||
// Try to parse PayPal error response
|
||||
$errorData = json_decode($res, true);
|
||||
http_response_code($http);
|
||||
echo json_encode([
|
||||
'error' => 'create_order_failed',
|
||||
'http_code' => $http,
|
||||
'paypal_error' => $errorData,
|
||||
'request_id' => $requestId
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Success - parse and validate response
|
||||
$orderData = json_decode($res, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
create_order_log('CREATE_ORDER_INVALID_JSON', [
|
||||
'json_error' => json_last_error_msg(),
|
||||
'response' => $res
|
||||
]);
|
||||
http_response_code(502);
|
||||
echo json_encode(['error' => 'invalid_paypal_response', 'request_id' => $requestId]);
|
||||
exit;
|
||||
}
|
||||
|
||||
create_order_log('CREATE_ORDER_SUCCESS', [
|
||||
'order_id' => $orderData['id'] ?? 'UNKNOWN',
|
||||
'status' => $orderData['status'] ?? 'UNKNOWN'
|
||||
]);
|
||||
|
||||
echo $res;
|
||||
|
||||
?>
|
||||
1
backup-website/api/error_log
Normal file
1
backup-website/api/error_log
Normal file
|
|
@ -0,0 +1 @@
|
|||
[10-Nov-2025 23:17:16 UTC] PHP Notice: session_start(): Ignoring session_start() because a session is already active (started from /home/domainpl/gameservers.world/modules/billing/api/capture_order.php on line 142) in /home/domainpl/gameservers.world/modules/billing/api/capture_order.php on line 168
|
||||
44
backup-website/api/log_error.php
Normal file
44
backup-website/api/log_error.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/**
|
||||
* Client-side error logging endpoint
|
||||
* Logs JavaScript errors from the cart page for debugging
|
||||
*/
|
||||
|
||||
// Ensure all errors are logged, not displayed
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Setup logging
|
||||
$logDir = __DIR__ . '/../logs';
|
||||
@mkdir($logDir, 0755, true);
|
||||
$logFile = $logDir . '/client_errors.log';
|
||||
|
||||
function log_client_error($data) {
|
||||
global $logFile;
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$entry = "[$timestamp] CLIENT ERROR\n";
|
||||
$entry .= "IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN') . "\n";
|
||||
$entry .= "User Agent: " . ($_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN') . "\n";
|
||||
if (is_array($data) || is_object($data)) {
|
||||
$entry .= print_r($data, true);
|
||||
} else {
|
||||
$entry .= (string)$data;
|
||||
}
|
||||
$entry .= "\n" . str_repeat('-', 80) . "\n";
|
||||
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
// Read and parse input
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$data = json_decode($rawInput, true);
|
||||
|
||||
if ($data) {
|
||||
log_client_error($data);
|
||||
echo json_encode(['status' => 'logged']);
|
||||
} else {
|
||||
log_client_error(['raw_input' => $rawInput, 'error' => 'Invalid JSON']);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']);
|
||||
}
|
||||
?>
|
||||
114
backup-website/bootstrap.php
Normal file
114
backup-website/bootstrap.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
// modules/billing/bootstrap.php
|
||||
// Central bootstrap for billing website pages. Loads config, provides safe DB helper
|
||||
// and ensures $table_prefix is available.
|
||||
|
||||
// Load configuration (includes/config.inc.php) if present
|
||||
$config_path = __DIR__ . '/includes/config.inc.php';
|
||||
if (file_exists($config_path)) {
|
||||
require_once $config_path;
|
||||
} else {
|
||||
trigger_error('Billing config not found: ' . $config_path, E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Ensure $table_prefix exists (fallback to empty string)
|
||||
if (!isset($table_prefix)) {
|
||||
$table_prefix = '';
|
||||
}
|
||||
|
||||
// Billing DB connection cached in $billing_db
|
||||
if (!isset($billing_db)) {
|
||||
$billing_db = null;
|
||||
}
|
||||
|
||||
// Track whether bootstrap opened the connection (so callers can safely close it)
|
||||
$billing_db_opened_by_bootstrap = false;
|
||||
|
||||
/**
|
||||
* Get a mysqli connection for billing pages.
|
||||
* - Reuses global $db if already created by other code.
|
||||
* - Tries to open a new connection using config variables if needed.
|
||||
* - Returns null on failure.
|
||||
*/
|
||||
function billing_get_db()
|
||||
{
|
||||
global $billing_db, $db, $db_host, $db_user, $db_pass, $db_name, $billing_db_opened_by_bootstrap;
|
||||
if (!empty($billing_db) && ($billing_db instanceof mysqli)) {
|
||||
return $billing_db;
|
||||
}
|
||||
if (!empty($db) && ($db instanceof mysqli)) {
|
||||
$billing_db = $db;
|
||||
return $billing_db;
|
||||
}
|
||||
// Try to connect (suppress warnings; caller may check return value)
|
||||
$conn = @mysqli_connect($db_host ?? null, $db_user ?? null, $db_pass ?? null, $db_name ?? null);
|
||||
if ($conn) {
|
||||
// Set charset when available
|
||||
if (function_exists('mysqli_set_charset')) {
|
||||
@mysqli_set_charset($conn, 'utf8mb4');
|
||||
}
|
||||
$billing_db = $conn;
|
||||
$billing_db_opened_by_bootstrap = true;
|
||||
return $billing_db;
|
||||
}
|
||||
// Leave $billing_db as null
|
||||
$billing_db = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close DB connection only if it was opened by bootstrap. If the connection
|
||||
* is shared (created by other code) this function will not close it.
|
||||
*/
|
||||
function billing_maybe_close_db($conn)
|
||||
{
|
||||
global $billing_db, $billing_db_opened_by_bootstrap;
|
||||
if (!($conn instanceof mysqli)) return;
|
||||
if (!empty($billing_db_opened_by_bootstrap) && $billing_db === $conn) {
|
||||
@mysqli_close($conn);
|
||||
$billing_db = null;
|
||||
$billing_db_opened_by_bootstrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Small helper wrappers commonly used across billing pages
|
||||
if (!function_exists('esc_mysqli')) {
|
||||
function esc_mysqli($db, $v)
|
||||
{
|
||||
if ($db instanceof mysqli) {
|
||||
return $db->real_escape_string((string)$v);
|
||||
}
|
||||
return addslashes((string)$v);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('fetch_all_assoc')) {
|
||||
function fetch_all_assoc($db, $sql)
|
||||
{
|
||||
if (!($db instanceof mysqli)) return [];
|
||||
$res = $db->query($sql);
|
||||
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('col_exists')) {
|
||||
function col_exists($db, $table, $col)
|
||||
{
|
||||
if (!($db instanceof mysqli)) return false;
|
||||
$t = $db->real_escape_string($table);
|
||||
$c = $db->real_escape_string($col);
|
||||
$res = $db->query("SHOW COLUMNS FROM `{$t}` LIKE '{$c}'");
|
||||
return ($res && $res->num_rows > 0);
|
||||
}
|
||||
}
|
||||
|
||||
// expose a convenience variable for scripts that expect $db
|
||||
// Do not overwrite an existing $db if present
|
||||
if (!isset($db) || !($db instanceof mysqli)) {
|
||||
$maybe = billing_get_db();
|
||||
if ($maybe instanceof mysqli) {
|
||||
$db = $maybe;
|
||||
}
|
||||
}
|
||||
|
||||
// End bootstrap
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue