'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]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue