From 2e587941c264dd6ebf89c0f7e00daf0c1807fab8 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Fri, 22 May 2026 12:52:50 +0000 Subject: [PATCH] Initial commit --- .gitignore | 5 + .htaccess | 117 ++ .htaccess.bak | 117 ++ admin/index.php | 3191 +++++++++++++++++++++++++++++++++ admin/login.php | 54 + api/admin.php | 971 ++++++++++ api/billing.php | 91 + api/broadcast.php | 89 + api/cashout.php | 148 ++ api/cashout_method_types.php | 73 + api/chat.php | 124 ++ api/game_aliases.php | 65 + api/login.php | 37 + api/logout.php | 5 + api/me.php | 35 + api/my_activity.php | 76 + api/my_purchases.php | 17 + api/payment_settings.php | 44 + api/payout_methods.php | 75 + api/payout_process.php | 153 ++ api/platform_accounts.php | 92 + api/platforms.php | 93 + api/purchase.php | 163 ++ api/referrals.php | 255 +++ api/register.php | 18 + api/resend_verify.php | 16 + assets/img/egame99.svg | 31 + assets/img/firekirin.svg | 30 + assets/img/icon-192.png | Bin 0 -> 1088 bytes assets/img/icon-512.png | Bin 0 -> 3856 bytes assets/img/logo-icon.svg | 38 + assets/img/milkyway.svg | 35 + assets/img/noble777.svg | 31 + assets/img/og-image.svg | 46 + assets/img/pandamaster.svg | 27 + assets/img/ultrapanda.svg | 30 + assets/img/vblink777.svg | 27 + bump_version.php | 52 + favicon.ico | Bin 0 -> 136 bytes favicon.svg | 5 + fix_broadcast.php | 28 + games/index.php | 232 +++ get_location.php | 108 ++ index.php | 3283 ++++++++++++++++++++++++++++++++++ install.php | 206 +++ manifest.json | 43 + phpcheck.php | 25 + ref_test.php | 26 + referral_setup.php | 96 + robots.txt | 23 + sitemap.xml | 21 + test.php | 6 + test_login.php | 18 + test_mail.php | 91 + verify.php | 66 + 55 files changed, 10748 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 .htaccess.bak create mode 100644 admin/index.php create mode 100644 admin/login.php create mode 100644 api/admin.php create mode 100644 api/billing.php create mode 100644 api/broadcast.php create mode 100644 api/cashout.php create mode 100644 api/cashout_method_types.php create mode 100644 api/chat.php create mode 100644 api/game_aliases.php create mode 100644 api/login.php create mode 100644 api/logout.php create mode 100644 api/me.php create mode 100644 api/my_activity.php create mode 100644 api/my_purchases.php create mode 100644 api/payment_settings.php create mode 100644 api/payout_methods.php create mode 100644 api/payout_process.php create mode 100644 api/platform_accounts.php create mode 100644 api/platforms.php create mode 100644 api/purchase.php create mode 100644 api/referrals.php create mode 100644 api/register.php create mode 100644 api/resend_verify.php create mode 100644 assets/img/egame99.svg create mode 100644 assets/img/firekirin.svg create mode 100644 assets/img/icon-192.png create mode 100644 assets/img/icon-512.png create mode 100644 assets/img/logo-icon.svg create mode 100644 assets/img/milkyway.svg create mode 100644 assets/img/noble777.svg create mode 100644 assets/img/og-image.svg create mode 100644 assets/img/pandamaster.svg create mode 100644 assets/img/ultrapanda.svg create mode 100644 assets/img/vblink777.svg create mode 100644 bump_version.php create mode 100644 favicon.ico create mode 100644 favicon.svg create mode 100644 fix_broadcast.php create mode 100644 games/index.php create mode 100644 get_location.php create mode 100644 index.php create mode 100644 install.php create mode 100644 manifest.json create mode 100644 phpcheck.php create mode 100644 ref_test.php create mode 100644 referral_setup.php create mode 100644 robots.txt create mode 100644 sitemap.xml create mode 100644 test.php create mode 100644 test_login.php create mode 100644 test_mail.php create mode 100644 verify.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c90ad4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.log +.DS_Store +*.swp + +uploads/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..381e984 --- /dev/null +++ b/.htaccess @@ -0,0 +1,117 @@ +# ══════════════════════════════════════════════════════════ +# TomTomGames Security Configuration +# ══════════════════════════════════════════════════════════ + +Options -Indexes -Includes +ServerSignature Off + +# ── Block all sensitive file types ─────────────────────── + + Order allow,deny + Deny from all + + +# ── Block direct access to sensitive PHP files ─────────── + + Order allow,deny + Deny from all + + +# ── Block access to includes and vendor folders ────────── + + RewriteEngine On + RewriteRule ^includes/ - [F,L] + RewriteRule ^vendor/ - [F,L] + RewriteRule ^mail_queue/ - [F,L] + RewriteRule ^\.git/ - [F,L] + + +# ── Block common attack vectors ────────────────────────── + + RewriteEngine On + + # Block SQL injection attempts in query strings + RewriteCond %{QUERY_STRING} (union|select|insert|drop|delete|update|cast|exec|declare|char|convert|truncate).*= [NC,OR] + RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR] + RewriteCond %{QUERY_STRING} \.\./\.\. [NC,OR] + RewriteCond %{QUERY_STRING} (javascript|vbscript|expression|applet|meta|xml|blink|link|iframe|input|embed|script|object|marquee) [NC] + RewriteRule .* - [F,L] + + # Block base64 encoded attacks + RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR] + RewriteCond %{QUERY_STRING} base64_(en|de)code[^(]*\([^)]*\) [NC] + RewriteRule .* - [F,L] + + # Block common exploit scanners and bad bots + RewriteCond %{HTTP_USER_AGENT} (nikto|sqlmap|havij|nessus|masscan|zgrab|python-requests/2\.6|libwww-perl|wget|curl\/7\.[0-4]) [NC] + RewriteRule .* - [F,L] + + +# ── Block access to WordPress paths (scanners look for these) ── + + RewriteRule ^wp-admin - [F,L] + RewriteRule ^wp-login - [F,L] + RewriteRule ^xmlrpc - [F,L] + RewriteRule ^\.env - [F,L] + RewriteRule ^composer\. - [F,L] + + +# ── Security Headers ────────────────────────────────────── + + # Prevent MIME type sniffing + Header always set X-Content-Type-Options "nosniff" + + # Prevent clickjacking + Header always set X-Frame-Options "DENY" + + # XSS protection + Header always set X-XSS-Protection "1; mode=block" + + # Referrer policy + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Permissions policy — disable dangerous browser features + Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()" + + # Content Security Policy + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://web.squarecdn.com https://sandbox.web.squarecdn.com https://js.squareup.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; connect-src 'self' https: wss:; frame-src 'none'; object-src 'none'" + + # Strict Transport Security — force HTTPS for 1 year + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + + # Remove server info headers + Header unset Server + Header unset X-Powered-By + + +# ── Canonical HTTPS + non-www redirect ─────────────────── + + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L] + + +# ── Block PHP execution in uploads folder (if it exists) ─ + + RewriteRule ^uploads/.*\.php$ - [F,L] + + +# ── Gzip compression ────────────────────────────────────── + + AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml + + +# ── Browser caching ─────────────────────────────────────── + + ExpiresActive On + ExpiresByType text/html "access plus 1 hour" + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/webp "access plus 1 month" + ExpiresByType application/json "access plus 1 day" + diff --git a/.htaccess.bak b/.htaccess.bak new file mode 100644 index 0000000..1424f36 --- /dev/null +++ b/.htaccess.bak @@ -0,0 +1,117 @@ +# ══════════════════════════════════════════════════════════ +# TomTomGames Security Configuration +# ══════════════════════════════════════════════════════════ + +Options -Indexes -Includes +ServerSignature Off + +# ── Block all sensitive file types ─────────────────────── + + Order allow,deny + Deny from all + + +# ── Block direct access to sensitive PHP files ─────────── + + Order allow,deny + Deny from all + + +# ── Block access to includes and vendor folders ────────── + + RewriteEngine On + RewriteRule ^includes/ - [F,L] + RewriteRule ^vendor/ - [F,L] + RewriteRule ^mail_queue/ - [F,L] + RewriteRule ^\.git/ - [F,L] + + +# ── Block common attack vectors ────────────────────────── + + RewriteEngine On + + # Block SQL injection attempts in query strings + RewriteCond %{QUERY_STRING} (union|select|insert|drop|delete|update|cast|exec|declare|char|convert|truncate).*= [NC,OR] + RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR] + RewriteCond %{QUERY_STRING} \.\./\.\. [NC,OR] + RewriteCond %{QUERY_STRING} (javascript|vbscript|expression|applet|meta|xml|blink|link|iframe|input|embed|script|object|marquee) [NC] + RewriteRule .* - [F,L] + + # Block base64 encoded attacks + RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR] + RewriteCond %{QUERY_STRING} base64_(en|de)code[^(]*\([^)]*\) [NC] + RewriteRule .* - [F,L] + + # Block common exploit scanners and bad bots + RewriteCond %{HTTP_USER_AGENT} (nikto|sqlmap|havij|nessus|masscan|zgrab|python-requests/2\.6|libwww-perl|wget|curl\/7\.[0-4]) [NC] + RewriteRule .* - [F,L] + + +# ── Block access to WordPress paths (scanners look for these) ── + + RewriteRule ^wp-admin - [F,L] + RewriteRule ^wp-login - [F,L] + RewriteRule ^xmlrpc - [F,L] + RewriteRule ^\.env - [F,L] + RewriteRule ^composer\. - [F,L] + + +# ── Security Headers ────────────────────────────────────── + + # Prevent MIME type sniffing + Header always set X-Content-Type-Options "nosniff" + + # Prevent clickjacking + Header always set X-Frame-Options "DENY" + + # XSS protection + Header always set X-XSS-Protection "1; mode=block" + + # Referrer policy + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Permissions policy — disable dangerous browser features + Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()" + + # Content Security Policy — restrict where resources can load from + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://web.squarecdn.com https://sandbox.web.squarecdn.com https://js.squareup.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https: wss:; frame-src 'none'" + + # Strict Transport Security — force HTTPS for 1 year + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + + # Remove server info headers + Header unset Server + Header unset X-Powered-By + + +# ── Canonical HTTPS + non-www redirect ─────────────────── + + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L] + + +# ── Block PHP execution in uploads folder (if it exists) ─ + + RewriteRule ^uploads/.*\.php$ - [F,L] + + +# ── Gzip compression ────────────────────────────────────── + + AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml + + +# ── Browser caching ─────────────────────────────────────── + + ExpiresActive On + ExpiresByType text/html "access plus 1 hour" + ExpiresByType text/css "access plus 1 month" + ExpiresByType application/javascript "access plus 1 month" + ExpiresByType image/svg+xml "access plus 1 month" + ExpiresByType image/png "access plus 1 month" + ExpiresByType image/jpeg "access plus 1 month" + ExpiresByType image/webp "access plus 1 month" + ExpiresByType application/json "access plus 1 day" + diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..f651b0e --- /dev/null +++ b/admin/index.php @@ -0,0 +1,3191 @@ + + + + + + + + + + +TomTomGames Admin + + + + + +
+ + + +
+ + +
+
📊 Dashboard
+ + +
+
Total Users
+
Total Revenue
+
Tokens Sold
+
Pending Purchases
+
Pending Cashouts
+
+
+
⚡ Pending Purchase Approvals
+
+
+
+
⚡ Pending Cashout Requests
+
+
+
+ + +
+
🧾 Token Purchases
+
+ + + + +
+
+
+ + +
+
💸 Cashout Requests
+
+ + + + +
+
+
+ + + +
+ +
+
🎮 Gamer Management
+
+
+ + +
+
+ + + + +
+
+
+
+
+ + + +
+ + +
+
💳 Payment Settings
+
+ Enable or disable each payment method. Disabled methods are hidden from players instantly. Card payments run through Square — your Square account must be active for card to work. +
+
+
+ + + +
+
🎁 Referral Management
+
+ + + + + +
+
+ + +
+ +
+
🔑 Platform Account Requests
+
+ + + +
+
+
+ + +
+
📢 Broadcast Messages
+ + +
+
✉️ Send Broadcast
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+
Sent Broadcasts
+ +
+
+
+ + + +
+ + +
+
💰 Payout Settings
+
+ Configure how you send cashout payments to players. Square Gift Card sends instantly. Manual methods show the player handle so you send from the app and mark done. +
+
+
+ + +
+
💸 Cashout Methods
+
+ 💸 Manage the payout method types available to players when they cash out. Active methods appear in the player's payout method dropdown. +
+ + +
+
➕ Add Cashout Method
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
Active & Inactive Methods
+
Loading...
+
+
+
+
+ + +
+
🕹️ Game Management
+ + +
+
➕ Add New Game
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
Active & Inactive Games
+
Loading...
+
+
+
+
+ + +
+
📋 Full Audit Log 90-day rolling · 20 per page
+ + +
+ + + + + + + +
+ + +
+ + +
+ + +
+
+ + +
+
⏳ Pending Signups
+
+ ⏳ Players who registered but haven't verified their email yet. Approve to create their account immediately, or Delete to remove the request. +
+
+
+ + +
+
Live Chat + + ● Auto-refreshing every 5s + +
+ + +
+ + +
+
✉️ Send Message to Player
+ +
+ + +
+ + + + +
+
Ctrl+Enter to send · Player will see it in their Support chat
+ +
+
+
+ + +
+ 📨 Messages from players appear below. Click any conversation to open it and reply. + +
+
+
+
+
+ + + +
+ + + + +
+ + +
+ TomTomGames Admin v1.0.0 +
+ + diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..55faefa --- /dev/null +++ b/admin/login.php @@ -0,0 +1,54 @@ + + + + + + + +TomTomGames Admin Login + + + + +
+ +
ADMIN ACCESS
+
+
+
+
+ +
+
+ + diff --git a/api/admin.php b/api/admin.php new file mode 100644 index 0000000..3333c82 --- /dev/null +++ b/api/admin.php @@ -0,0 +1,971 @@ + true, 'stats' => [ + 'total_users' => db()->query("SELECT COUNT(*) FROM users")->fetchColumn(), + 'active_users' => db()->query("SELECT COUNT(*) FROM users WHERE status='active'")->fetchColumn(), + 'pending_purchases' => db()->query("SELECT COUNT(*) FROM token_purchases WHERE status='pending'")->fetchColumn(), + 'pending_cashouts' => db()->query("SELECT COUNT(*) FROM cashout_requests WHERE status='pending'")->fetchColumn(), + 'pending_signups' => db()->query("SELECT COUNT(*) FROM pending_registrations WHERE expires_at > NOW()")->fetchColumn(), + 'total_tokens_sold' => db()->query("SELECT COALESCE(SUM(tokens),0) FROM token_purchases WHERE status='completed'")->fetchColumn(), + 'total_revenue' => db()->query("SELECT COALESCE(SUM(amount_cents),0)/100 FROM token_purchases WHERE status='completed'")->fetchColumn(), + ]]); + break; + + // ─── PENDING SIGNUPS ────────────────────────────────────── + case 'pending_signups': + $rows = db()->query("SELECT id,username,alias,email,expires_at,created_at FROM pending_registrations WHERE expires_at > NOW() ORDER BY created_at DESC")->fetchAll(); + echo json_encode(['success'=>true,'pending'=>$rows]); + break; + + case 'delete_pending': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + db()->prepare("DELETE FROM pending_registrations WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + case 'approve_pending': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + // Fetch the pending record + $stmt = db()->prepare("SELECT * FROM pending_registrations WHERE id=?"); + $stmt->execute([$id]); + $pending = $stmt->fetch(); + if (!$pending) { echo json_encode(['success'=>false,'error'=>'Pending signup not found']); exit; } + // Check username/email not already taken + $chkUser = db()->prepare("SELECT id FROM users WHERE username=?"); + $chkUser->execute([$pending['username']]); + if ($chkUser->fetch()) { echo json_encode(['success'=>false,'error'=>'Username already taken']); exit; } + if (!empty($pending['email'])) { + $chkEmail = db()->prepare("SELECT id FROM users WHERE email=?"); + $chkEmail->execute([$pending['email']]); + if ($chkEmail->fetch()) { echo json_encode(['success'=>false,'error'=>'Email already registered']); exit; } + } + // Create the user account — bypass email verification + db()->beginTransaction(); + try { + db()->prepare("INSERT INTO users (username,password,alias,email,email_verified,tokens,is_admin,status) + VALUES (?,?,?,?,1,0,0,'active')") + ->execute([$pending['username'],$pending['password'],$pending['alias'],$pending['email']]); + db()->prepare("DELETE FROM pending_registrations WHERE id=?")->execute([$id]); + db()->commit(); + logActivity('account_approved', (int)db()->lastInsertId(), (int)$_SESSION['user_id'], 'user', 0, 'Account approved for '.$pending['username']); + echo json_encode(['success'=>true,'username'=>$pending['username']]); + } catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'Could not create account']); + } + break; + + // ─── PURCHASES ──────────────────────────────────────────── + case 'purchases': + $status = $_GET['status'] ?? 'pending'; + if ($status === 'all') { + $stmt = db()->query("SELECT tp.*, u.username, u.alias FROM token_purchases tp JOIN users u ON tp.user_id=u.id ORDER BY tp.created_at DESC LIMIT 200"); + } else { + $stmt = db()->prepare("SELECT tp.*, u.username, u.alias FROM token_purchases tp JOIN users u ON tp.user_id=u.id WHERE tp.status=? ORDER BY tp.created_at DESC LIMIT 200"); + $stmt->execute([$status]); + } + echo json_encode(['success' => true, 'purchases' => $stmt->fetchAll()]); + break; + + // ─── RESOLVE PURCHASE (approve manual / reject) ────────── + case 'resolve_purchase': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + $status = $data['status'] ?? ''; + $note = trim($data['note'] ?? ''); + + if (!in_array($status, ['completed','failed'])) { + echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; + } + + // Fetch purchase + $row = db()->prepare("SELECT * FROM token_purchases WHERE id=? AND status='pending'"); + $row->execute([$id]); + $purchase = $row->fetch(); + + if (!$purchase) { + echo json_encode(['success'=>false,'error'=>'Purchase not found or already resolved']); exit; + } + + db()->beginTransaction(); + try { + if ($status === 'completed') { + // Credit tokens to user + logAdminAction('TOKENS_ADJUSTED', $adminId, 'user', isset($targetId)?(int)$targetId:0, 'Manual token adjustment: '.($data['tokens']??0).' tokens', '', ($data['tokens']??''), 'critical'); + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$purchase['tokens'], $purchase['user_id']]); + } + db()->prepare("UPDATE token_purchases SET status=?,admin_note=? WHERE id=?")->execute([$status, $note, $id]); + db()->commit(); + echo json_encode(['success'=>true]); + } catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'DB error']); + } + break; + + // ─── CASHOUTS ───────────────────────────────────────────── + case 'cashouts': + $status = $_GET['status'] ?? 'pending'; + $valid = ['pending','sent','approved','rejected','deleted']; + if (!in_array($status, $valid)) $status = 'pending'; + if ($status === 'pending') { + // Show both pending (player editing) and locked (submitted to admin) + $stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status IN ('pending','locked') ORDER BY cr.status DESC, cr.created_at DESC"); + $stmt->execute(); + } elseif ($status === 'sent') { + $stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status IN ('sent','approved') ORDER BY cr.created_at DESC"); + $stmt->execute(); + } else { + $stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status=? ORDER BY cr.created_at DESC"); + $stmt->execute([$status]); + } + echo json_encode(['success'=>true,'cashouts'=>$stmt->fetchAll()]); + break; + + case 'resolve_cashout': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $id = (int)($data['id'] ?? 0); + $status = $data['status'] ?? ''; + $note = trim($data['note'] ?? ''); + // 'approved' is treated as 'sent' (payment sent to player) + if ($status === 'approved') $status = 'sent'; + if (!in_array($status, ['sent','rejected','deleted'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; } + + $r = db()->prepare("SELECT user_id,tokens FROM cashout_requests WHERE id=? AND status IN ('pending','locked')"); + $r->execute([$id]); + $req = $r->fetch(); + if (!$req) { echo json_encode(['success'=>false,'error'=>'Not found or already resolved']); exit; } + + db()->beginTransaction(); + try { + // Return tokens to player if denied or deleted + if (in_array($status, ['rejected','deleted'])) { + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$req['tokens'],$req['user_id']]); + } + db()->prepare("UPDATE cashout_requests SET status=?,admin_note=?,resolved_at=NOW() WHERE id=?")->execute([$status,$note,$id]); + db()->commit(); + logActivity('cashout_'.$status, $req['user_id'], (int)$_SESSION['user_id'], 'cashout', $id, + 'Cashout '.$status.' by admin. Tokens: '.$req['tokens'].($note?' Note: '.$note:'')); + echo json_encode(['success'=>true,'status'=>$status]); + } catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'DB error']); + } + break; + if (!in_array($status, ['approved','rejected'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; } + + if ($status === 'rejected') { + $r = db()->prepare("SELECT user_id,tokens FROM cashout_requests WHERE id=? AND status='pending'"); + $r->execute([$id]); + $req = $r->fetch(); + if ($req) db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$req['tokens'],$req['user_id']]); + } + db()->prepare("UPDATE cashout_requests SET status=?,admin_note=?,resolved_at=NOW() WHERE id=?")->execute([$status,$note,$id]); + echo json_encode(['success'=>true]); + break; + + // ─── USERS LIST ─────────────────────────────────────────── + case 'users': + $users = db()->query("SELECT id,username,alias,email,email_verified,tokens,is_admin,status,created_at,last_login FROM users ORDER BY created_at DESC")->fetchAll(); + echo json_encode(['success'=>true,'users'=>$users]); + break; + + // ─── SINGLE USER DETAIL ─────────────────────────────────── + case 'user_detail': + $uid = (int)($_GET['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare("SELECT id,username,alias,email,email_verified,tokens,is_admin,status,created_at,last_login FROM users WHERE id=?"); + $stmt->execute([$uid]); + $user = $stmt->fetch(); + if (!$user) { echo json_encode(['success'=>false,'error'=>'User not found']); exit; } + // Rich stats + $s1 = db()->prepare("SELECT COALESCE(SUM(amount_cents),0)/100 FROM token_purchases WHERE user_id=? AND status='completed'"); + $s1->execute([$uid]); + $s2 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='completed'"); $s2->execute([$uid]); + $s3 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='pending'"); $s3->execute([$uid]); + $s4 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='failed'"); $s4->execute([$uid]); + $s5 = db()->prepare("SELECT COUNT(*) FROM cashout_requests WHERE user_id=?"); $s5->execute([$uid]); + $s6 = db()->prepare("SELECT COALESCE(SUM(tokens),0) FROM token_purchases WHERE user_id=? AND status='completed'"); $s6->execute([$uid]); + $stats = [ + 'total_spent' => $s1->fetchColumn(), + 'completed_purchases'=> $s2->fetchColumn(), + 'pending_purchases' => $s3->fetchColumn(), + 'failed_purchases' => $s4->fetchColumn(), + 'total_cashouts' => $s5->fetchColumn(), + 'total_tokens_bought'=> $s6->fetchColumn(), + ]; + echo json_encode(['success'=>true,'user'=>$user,'stats'=>$stats]); + break; + + // ─── USER PURCHASES ─────────────────────────────────────── + case 'user_purchases': + $uid = (int)($_GET['user_id'] ?? 0); + $stmt = db()->prepare("SELECT id,tokens,amount_cents,payment_method,square_payment_id,platform_id,game_alias,player_name,billing_name,billing_address,billing_city,billing_state,billing_zip,billing_email,is_custom,failure_reason,card_brand,card_last4,receipt_url,status,admin_note,created_at FROM token_purchases WHERE user_id=? ORDER BY created_at DESC LIMIT 100"); + $stmt->execute([$uid]); + echo json_encode(['success'=>true,'purchases'=>$stmt->fetchAll()]); + break; + + // ─── USER CASHOUTS ──────────────────────────────────────── + case 'user_cashouts': + $uid = (int)($_GET['user_id'] ?? 0); + $stmt = db()->prepare("SELECT * FROM cashout_requests WHERE user_id=? ORDER BY created_at DESC LIMIT 100"); + $stmt->execute([$uid]); + echo json_encode(['success'=>true,'cashouts'=>$stmt->fetchAll()]); + break; + + // ─── ADJUST TOKENS ──────────────────────────────────────── + case 'adjust_tokens': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $amount = (float)($data['amount'] ?? 0); + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$amount,$uid]); + $bal = db()->prepare("SELECT tokens FROM users WHERE id=?"); $bal->execute([$uid]); + $newBal = $bal->fetchColumn(); + echo json_encode(['success'=>true,'new_balance'=>$newBal]); + break; + + // ─── SET EXACT TOKEN BALANCE ────────────────────────────── + case 'set_tokens': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $bal = (float)($data['balance'] ?? 0); + if ($bal < 0) { echo json_encode(['success'=>false,'error'=>'Balance cannot be negative']); exit; } + db()->prepare("UPDATE users SET tokens=? WHERE id=?")->execute([$bal,$uid]); + echo json_encode(['success'=>true,'new_balance'=>$bal]); + break; + + // ─── EDIT USER ──────────────────────────────────────────── + case 'edit_user': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $username = strtolower(trim($data['username'] ?? '')); + $alias = trim($data['alias'] ?? ''); + $email = strtolower(trim($data['email'] ?? '')); + $password = trim($data['password'] ?? ''); + + if (!$uid || empty($username) || empty($alias)) + { echo json_encode(['success'=>false,'error'=>'Username and alias required']); exit; } + if (!preg_match('/^[a-z0-9_]{3,50}$/', $username)) + { echo json_encode(['success'=>false,'error'=>'Invalid username format']); exit; } + if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) + { echo json_encode(['success'=>false,'error'=>'Invalid email address']); exit; } + + // Check username not taken by another user + $chk = db()->prepare("SELECT id FROM users WHERE username=? AND id!=?"); + $chk->execute([$username,$uid]); + if ($chk->fetch()) { echo json_encode(['success'=>false,'error'=>'Username already taken']); exit; } + + if (!empty($email)) { + $chk2 = db()->prepare("SELECT id FROM users WHERE email=? AND id!=?"); + $chk2->execute([$email,$uid]); + if ($chk2->fetch()) { echo json_encode(['success'=>false,'error'=>'Email already in use']); exit; } + } + + if (!empty($password)) { + if (strlen($password) < 6) { echo json_encode(['success'=>false,'error'=>'Password must be 6+ characters']); exit; } + $hash = password_hash($password, PASSWORD_BCRYPT); + db()->prepare("UPDATE users SET username=?,alias=?,email=?,password=? WHERE id=?")->execute([$username,$alias,$email,$hash,$uid]); + } else { + db()->prepare("UPDATE users SET username=?,alias=?,email=? WHERE id=?")->execute([$username,$alias,$email,$uid]); + } + echo json_encode(['success'=>true]); + break; + + // ─── TOGGLE ADMIN ROLE ─────────────────────────────────── + case 'toggle_admin': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + // Master admin (ID=1) can NEVER lose admin status + if ($uid === MASTER_ADMIN_ID) { + echo json_encode(['success'=>false,'error'=>'Master admin cannot be modified.']); exit; + } + // Cannot remove your own admin + if ($uid === (int)$_SESSION['user_id']) { + echo json_encode(['success'=>false,'error'=>'You cannot change your own admin status.']); exit; + } + // Only master admin can grant/revoke admin + if ((int)$_SESSION['user_id'] !== MASTER_ADMIN_ID) { + echo json_encode(['success'=>false,'error'=>'Only the master admin can change admin roles.']); exit; + } + $stmt = db()->prepare("SELECT is_admin FROM users WHERE id=?"); + $stmt->execute([$uid]); + $current = $stmt->fetchColumn(); + $new_val = $current ? 0 : 1; + if ($new_val) { + db()->prepare("UPDATE users SET is_admin=1, email_verified=1 WHERE id=?")->execute([$uid]); + } else { + db()->prepare("UPDATE users SET is_admin=0 WHERE id=?")->execute([$uid]); + } + // Force the affected user to re-login by invalidating their sessions + // Store a flag in DB that forces re-auth on next request + db()->prepare("UPDATE users SET last_login=last_login WHERE id=?")->execute([$uid]); + logActivity($new_val?'admin_granted':'admin_revoked', $uid, (int)$_SESSION['user_id'], 'user', $uid, 'Admin status changed to '.($new_val?'admin':'player'), '', 'admin', '', '', 'warning'); + echo json_encode(['success'=>true, 'is_admin'=>$new_val, 'needs_relogin'=>true, 'message'=>$new_val ? 'Admin access granted. User must log out and back in.' : 'Admin access removed. User must log out and back in.']); + break; + + // ─── TOGGLE SUSPEND ─────────────────────────────────────── + case 'toggle_user': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + if ($uid === MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Cannot suspend the master admin.']); exit; } + logAdminAction('USER_STATUS_CHANGE', $adminId, 'user', isset($userId)?(int)$userId:0, 'Changed user status to: '.($data['status']??'unknown'), '', ($data['status']??''), 'warning'); + db()->prepare("UPDATE users SET status=IF(status='active','suspended','active') WHERE id=?")->execute([$uid]); + echo json_encode(['success'=>true]); + break; + + // ─── DELETE USER ────────────────────────────────────────── + case 'delete_user': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'Invalid user']); exit; } + // Prevent deleting own account + if ($uid === MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Cannot delete the master admin account.']); exit; } + if ($uid === (int)$_SESSION['user_id']) { echo json_encode(['success'=>false,'error'=>'Cannot delete your own account']); exit; } + db()->beginTransaction(); + try { + db()->prepare("DELETE FROM chat_messages WHERE user_id=?")->execute([$uid]); + db()->prepare("DELETE FROM cashout_requests WHERE user_id=?")->execute([$uid]); + db()->prepare("DELETE FROM token_purchases WHERE user_id=?")->execute([$uid]); + db()->prepare("DELETE FROM users WHERE id=?")->execute([$uid]); + db()->commit(); + echo json_encode(['success'=>true]); + } catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'Delete failed']); + } + break; + + // ─── SEND PASSWORD RESET ────────────────────────────────── + case 'send_password_reset': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $stmt = db()->prepare("SELECT email,alias FROM users WHERE id=?"); + $stmt->execute([$uid]); + $user = $stmt->fetch(); + if (!$user || empty($user['email'])) { echo json_encode(['success'=>false,'error'=>'No email on file']); exit; } + // Generate reset token — reuse pending_registrations pattern + $token = bin2hex(random_bytes(32)); + $exp = date('Y-m-d H:i:s', time() + 3600); // 1 hour + db()->prepare("INSERT INTO pending_registrations (username,password,alias,email,token,expires_at) VALUES ('__reset__','',''.?,?,'__reset__',?) ON DUPLICATE KEY UPDATE token=VALUES(token),expires_at=VALUES(expires_at)")->execute([$user['alias'],$user['email'],$token,$exp]); + // Simple reset email + $resetUrl = rtrim(SITE_URL,'/') . '/reset_password.php?token=' . urlencode($token); + $subject = SITE_NAME . ' — Password Reset Request'; + $body = "Hi {$user['alias']},\n\nA password reset was requested for your account.\n\nClick here to reset: {$resetUrl}\n\nExpires in 1 hour. If you didn't request this, ignore this email.\n\n— " . SITE_NAME; + $headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\nReply-To: " . MAIL_REPLY_TO; + mail($user['email'], $subject, $body, $headers, '-f' . MAIL_FROM); + echo json_encode(['success'=>true]); + break; + + // ─── PLATFORM ACCOUNTS ──────────────────────────────── + case 'platform_accounts_list': + $status = $_GET['status'] ?? 'pending'; + $uid = (int)($_GET['user_id'] ?? 0); + if ($uid) { + $stmt = db()->prepare("SELECT pa.*, COALESCE(p.name,pa.platform_slug) AS platform_name, u.username, u.alias AS user_alias FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug JOIN users u ON pa.user_id=u.id WHERE pa.user_id=? ORDER BY pa.requested_at DESC"); + $stmt->execute([$uid]); + } else { + $valid = ['pending','approved','denied','deleted']; + if (!in_array($status,$valid)) $status='pending'; + $stmt = db()->prepare("SELECT pa.*, COALESCE(p.name,pa.platform_slug) AS platform_name, u.username, u.alias AS user_alias FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug JOIN users u ON pa.user_id=u.id WHERE pa.status=? ORDER BY pa.requested_at DESC"); + $stmt->execute([$status]); + } + echo json_encode(['success'=>true,'accounts'=>$stmt->fetchAll()]); + break; + + case 'platform_account_resolve': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id=$d['id']??0; $status=$d['status']??''; + $uname=substr(trim($d['platform_username']??''),0,100); + $pass=substr(trim($d['platform_password']??''),0,200); + $note=substr(trim($d['admin_note']??''),0,300); + if (!in_array($status,['approved','denied','deleted'])){echo json_encode(['success'=>false,'error'=>'Invalid status']);exit;} + $chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch(); + if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;} + db()->prepare("UPDATE platform_accounts SET status=?,platform_username=?,platform_password=?,admin_note=?,resolved_at=NOW(),admin_id=? WHERE id=?") + ->execute([$status,$uname,$pass,$note,(int)$_SESSION['user_id'],$id]); + if ($status==='approved'&&$uname) { + db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)") + ->execute([$row['user_id'],$row['platform_slug'],$uname]); + } + echo json_encode(['success'=>true]); + break; + + case 'platform_account_update': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d=json_decode(file_get_contents('php://input'),true); + $id=$d['id']??0; + $uname=substr(trim($d['platform_username']??''),0,100); + $pass=substr(trim($d['platform_password']??''),0,200); + $note=substr(trim($d['admin_note']??''),0,300); + $chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch(); + if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;} + db()->prepare("UPDATE platform_accounts SET platform_username=?,platform_password=?,admin_note=? WHERE id=?") + ->execute([$uname,$pass,$note,$id]); + if ($uname){ + db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)") + ->execute([$row['user_id'],$row['platform_slug'],$uname]); + } + echo json_encode(['success'=>true]); + break; + $rows = db()->query(" + SELECT b.*, u.username AS sender_name, + (SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count, + (SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count, + (SELECT COUNT(*) FROM users WHERE is_admin=0 AND status='active') AS total_players + FROM broadcasts b JOIN users u ON b.admin_id=u.id + ORDER BY b.sent_at DESC LIMIT 50 + ")->fetchAll(); + echo json_encode(['success'=>true,'broadcasts'=>$rows]); + break; + + case 'broadcast_list': + try { + $sql = "SELECT b.id, b.subject, b.message, b.target, b.sent_at, + u.username AS sender_name, + (SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count, + (SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count, + (SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0) AS total_players + FROM broadcasts b + JOIN users u ON b.admin_id=u.id + ORDER BY b.sent_at DESC + LIMIT 100"; + $stmt = db()->query($sql); + echo json_encode(['success'=>true,'broadcasts'=>$stmt->fetchAll()]); + } catch(Exception $e) { + echo json_encode(['success'=>false,'error'=>$e->getMessage()]); + } + break; + + case 'broadcast_send': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $subject = substr(trim($d['subject']??''),0,200); + $message = substr(trim($d['message']??''),0,5000); + $target = in_array($d['target']??'',['all','verified','unverified','admins']) ? $d['target'] : 'all'; + if (!$subject||!$message) { echo json_encode(['success'=>false,'error'=>'Subject and message required']); exit; } + db()->prepare("INSERT INTO broadcasts (admin_id,subject,message,target) VALUES (?,?,?,?)") + ->execute([$_SESSION['user_id'],$subject,$message,$target]); + $bid = db()->lastInsertId(); + // Count recipients + $countQ = [ + 'all' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0", + 'verified' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0 AND email_verified=1", + 'unverified' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0 AND email_verified=0", + 'admins' => "SELECT COUNT(*) FROM users WHERE is_admin=1", + ]; + $count = db()->query($countQ[$target])->fetchColumn(); + echo json_encode(['success'=>true,'id'=>$bid,'recipient_count'=>(int)$count]); + break; + + case 'broadcast_delete': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id']??0); + db()->prepare("DELETE FROM broadcasts WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + case 'broadcast_edit': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $subject = substr(trim($d['subject'] ?? ''), 0, 200); + $message = trim($d['message'] ?? ''); + $target = in_array($d['target']??'', ['all','verified','unverified','admins']) ? $d['target'] : 'all'; + if (!$id || !$subject || !$message) { echo json_encode(['success'=>false,'error'=>'Missing fields']); exit; } + db()->prepare("UPDATE broadcasts SET subject=?, message=?, target=? WHERE id=?")->execute([$subject, $message, $target, $id]); + logAdminAction('BROADCAST_EDITED', $adminId, 'broadcast', $id, 'Edited broadcast #'.$id, '', '', 'info'); + echo json_encode(['success'=>true]); + break; + + case 'broadcast_resend': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'Missing ID']); exit; } + $bc = db()->prepare("SELECT * FROM broadcasts WHERE id=?"); + $bc->execute([$id]); + $orig = $bc->fetch(); + if (!$orig) { echo json_encode(['success'=>false,'error'=>'Broadcast not found']); exit; } + // Count recipients + $target = $orig['target']; + $countSql = "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0"; + if ($target === 'verified') $countSql .= " AND email_verified=1"; + if ($target === 'unverified') $countSql .= " AND email_verified=0"; + $recipientCount = (int)db()->query($countSql)->fetchColumn(); + // Delete old reads so everyone sees it again + db()->prepare("DELETE FROM broadcast_reads WHERE broadcast_id=?")->execute([$id]); + // Update sent_at to now + db()->prepare("UPDATE broadcasts SET sent_at=NOW() WHERE id=?")->execute([$id]); + logAdminAction('BROADCAST_RESENT', $adminId, 'broadcast', $id, 'Resent broadcast #'.$id.' to '.$recipientCount.' players', '', '', 'info'); + echo json_encode(['success'=>true,'recipient_count'=>$recipientCount]); + break; + + case 'broadcast_reads': + $bid = (int)($_GET['broadcast_id']??0); + $rows = db()->prepare("SELECT br.read_at, u.username, u.alias FROM broadcast_reads br JOIN users u ON br.user_id=u.id WHERE br.broadcast_id=? ORDER BY br.read_at ASC"); + $rows->execute([$bid]); + echo json_encode(['success'=>true,'reads'=>$rows->fetchAll()]); + break; + + case 'broadcast_replies': + $bid = (int)($_GET['broadcast_id']??0); + $rows = db()->prepare("SELECT br.*, u.username, u.alias, u.is_admin FROM broadcast_replies br JOIN users u ON br.user_id=u.id WHERE br.broadcast_id=? ORDER BY br.created_at ASC"); + $rows->execute([$bid]); + echo json_encode(['success'=>true,'replies'=>$rows->fetchAll()]); + break; + + case 'broadcast_reply': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $bid = (int)($d['broadcast_id']??0); + $msg = substr(trim($d['message']??''),0,1000); + if (!$bid||!$msg) { echo json_encode(['success'=>false,'error'=>'Required fields missing']); exit; } + db()->prepare("INSERT INTO broadcast_replies (broadcast_id,user_id,message) VALUES (?,?,?)") + ->execute([$bid,$_SESSION['user_id'],$msg]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + break; + $rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true,'types'=>$rows]); + break; + + case 'cashout_methods_create': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $slug = preg_replace('/[^a-z0-9_]/','',strtolower(trim($d['slug']??''))); + $label= substr(trim($d['label']??''),0,100); + $icon = substr(trim($d['icon']??'💰'),0,10); + $desc = substr(trim($d['description']??''),0,200); + $sort = (int)($d['sort_order']??99); + $active=(int)(bool)($d['is_active']??1); + if (!$slug||!$label){echo json_encode(['success'=>false,'error'=>'Slug and label required']);exit;} + try { + db()->prepare("INSERT INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$slug,$label,$icon,$desc,$active,$sort]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + } catch(Exception $e){ echo json_encode(['success'=>false,'error'=>'Slug already exists']); } + break; + + case 'cashout_methods_update': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id']??0); + $label= substr(trim($d['label']??''),0,100); + $icon = substr(trim($d['icon']??'💰'),0,10); + $desc = substr(trim($d['description']??''),0,200); + $sort = (int)($d['sort_order']??0); + $active=(int)(bool)($d['is_active']??1); + if (!$id||!$label){echo json_encode(['success'=>false,'error'=>'ID and label required']);exit;} + db()->prepare("UPDATE cashout_method_types SET label=?,icon=?,description=?,is_active=?,sort_order=? WHERE id=?") + ->execute([$label,$icon,$desc,$active,$sort,$id]); + echo json_encode(['success'=>true]); + break; + + case 'cashout_methods_delete': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d=(json_decode(file_get_contents('php://input'),true)); + $id=(int)($d['id']??0); + if (!$id){echo json_encode(['success'=>false,'error'=>'ID required']);exit;} + db()->prepare("DELETE FROM cashout_method_types WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + // ── + case 'platform_account_deny': + if ($_SERVER['REQUEST_METHOD']!=='POST'){echo json_encode(['success'=>false]);exit;} + $d=json_decode(file_get_contents('php://input'),true); + $id=(int)($d['id']??0);$nt=substr(trim($d['admin_note']??''),0,500); + db()->prepare("UPDATE platform_accounts SET status='denied',admin_note=?,admin_id=? WHERE id=?")->execute([$nt,$_SESSION['user_id'],$id]); + echo json_encode(['success'=>true]); + break; + + case 'platform_account_delete': + if ($_SERVER['REQUEST_METHOD']!=='POST'){echo json_encode(['success'=>false]);exit;} + $d=json_decode(file_get_contents('php://input'),true); + $id=(int)($d['id']??0); + db()->prepare("DELETE FROM platform_accounts WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + case 'platform_accounts_user': + $uid=(int)($_GET['user_id']??0); + $stmt=db()->prepare("SELECT pa.*,COALESCE(p.name,pa.platform_name,pa.platform_slug) AS display_name,p.color,p.player_url FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug WHERE pa.user_id=? ORDER BY pa.requested_at DESC"); + $stmt->execute([$uid]); + echo json_encode(['success'=>true,'accounts'=>$stmt->fetchAll()]); + break; + case 'activity_log': + case 'activity_log_v2': + $page = max(1, (int)($_GET['page']??1)); + $limit = 20; + $offset = ($page - 1) * $limit; + $category = trim($_GET['category'] ?? ''); + $severity = trim($_GET['severity'] ?? ''); + $search = trim($_GET['search'] ?? ''); + $date = trim($_GET['date'] ?? ''); + + $where = ["al.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)"]; + $params = []; + + if ($category) { $where[] = "al.category = ?"; $params[] = $category; } + if ($severity) { $where[] = "al.severity = ?"; $params[] = $severity; } + if ($date) { $where[] = "DATE(al.created_at) = ?"; $params[] = $date; } + if ($search) { + $where[] = "(al.action LIKE ? OR al.detail LIKE ? OR u.username LIKE ? OR u.alias LIKE ? OR al.ip LIKE ?)"; + $s = '%'.$search.'%'; + $params = array_merge($params, [$s,$s,$s,$s,$s]); + } + + $whereStr = implode(' AND ', $where); + $baseQuery = "FROM activity_log al + LEFT JOIN users u ON al.user_id = u.id + LEFT JOIN users a ON al.admin_id = a.id + WHERE $whereStr"; + + $countStmt = db()->prepare("SELECT COUNT(*) $baseQuery"); + $countStmt->execute($params); + $total = (int)$countStmt->fetchColumn(); + + $dataStmt = db()->prepare("SELECT al.*, u.username, u.alias, + a.username AS admin_username + $baseQuery + ORDER BY al.created_at DESC + LIMIT $limit OFFSET $offset"); + $dataStmt->execute($params); + $events = $dataStmt->fetchAll(); + + // Stats for the current filter set + $statsParams = $params; + $statsStmt = db()->prepare("SELECT + SUM(al.severity='critical') AS critical, + SUM(al.severity='warning') AS warning, + COUNT(DISTINCT al.ip) AS unique_ips + $baseQuery"); + $statsStmt->execute($statsParams); + $stats = $statsStmt->fetch(); + + echo json_encode(['success'=>true,'events'=>$events,'total'=>$total,'page'=>$page,'stats'=>$stats]); + break; + + case 'activity_log_csv': + $category = trim($_GET['category'] ?? ''); + $severity = trim($_GET['severity'] ?? ''); + $search = trim($_GET['search'] ?? ''); + $date = trim($_GET['date'] ?? ''); + + $where = ["al.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)"]; + $params = []; + if ($category) { $where[] = "al.category = ?"; $params[] = $category; } + if ($severity) { $where[] = "al.severity = ?"; $params[] = $severity; } + if ($date) { $where[] = "DATE(al.created_at) = ?"; $params[] = $date; } + if ($search) { + $where[] = "(al.action LIKE ? OR al.detail LIKE ? OR u.username LIKE ?)"; + $s = '%'.$search.'%'; $params = array_merge($params, [$s,$s,$s]); + } + + $whereStr = implode(' AND ', $where); + $stmt = db()->prepare("SELECT al.*, u.username, u.alias, a.username AS admin_username + FROM activity_log al + LEFT JOIN users u ON al.user_id=u.id + LEFT JOIN users a ON al.admin_id=a.id + WHERE $whereStr ORDER BY al.created_at DESC LIMIT 5000"); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="tomtomgames_audit_' . date('Y-m-d') . '.csv"'); + $out = fopen('php://output', 'w'); + fputcsv($out, ['ID','Timestamp','Category','Severity','Action','Username','Alias','Admin','Detail','Old Value','New Value','IP','User Agent','Page','Session ID']); + foreach ($rows as $r) { + fputcsv($out, [$r['id'],$r['created_at'],$r['category'],$r['severity'],$r['action'], + $r['username']??'',$r['alias']??'',$r['admin_username']??'', + $r['detail']??'',$r['old_value']??'',$r['new_value']??'', + $r['ip']??'',$r['user_agent']??'',$r['page']??'',$r['session_id']??'']); + } + fclose($out); + exit; + break; + + // ─── CASHOUT METHODS: list (admin) ──────────────────── + case 'cashout_methods_list': + $rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true,'types'=>$rows]); + break; + + // ─── PAYMENT SETTINGS: list (admin) ─────────────────── + case 'payment_settings_list': + $rows = db()->query("SELECT * FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true,'methods'=>$rows]); + break; + + case 'payment_settings_update': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $label= substr(trim($d['label']??''), 0, 100); + $handle = substr(trim($d['handle']??''), 0, 200); + $inst = substr(trim($d['instructions']??''), 0, 500); + $enabled = (int)(bool)($d['is_enabled'] ?? 1); + $sort = (int)($d['sort_order'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("UPDATE payment_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?") + ->execute([$label,$handle,$inst,$enabled,$sort,$id]); + echo json_encode(['success'=>true]); + break; + + + // ─── PAYOUT METHODS: get for user ──────────────────────── + case 'payout_methods_get': + $uid = (int)($_GET['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $rows = db()->prepare("SELECT * FROM payout_methods WHERE user_id=? ORDER BY is_default DESC, id ASC"); + $rows->execute([$uid]); + echo json_encode(['success'=>true,'methods'=>$rows->fetchAll()]); + break; + + // ─── GAME ALIASES: get ──────────────────────────────── + case 'game_aliases_get': + $uid = (int)($_GET['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare("SELECT platform_slug, alias FROM game_aliases WHERE user_id=?"); + $stmt->execute([$uid]); + $rows = $stmt->fetchAll(); + $map = []; + foreach ($rows as $r) $map[$r['platform_slug']] = $r['alias']; + echo json_encode(['success'=>true,'aliases'=>$map]); + break; + + // ─── GAME ALIASES: save all ─────────────────────────── + case 'game_aliases_save_all': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $aliases = $data['aliases'] ?? []; + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) + ON DUPLICATE KEY UPDATE alias=VALUES(alias)"); + $del = db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?"); + foreach ($aliases as $slug => $alias) { + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($slug))); + $alias = substr(trim($alias), 0, 100); + if (!$slug) continue; + if ($alias === '') $del->execute([$uid, $slug]); + else $stmt->execute([$uid, $slug, $alias]); + } + echo json_encode(['success'=>true]); + break; + + // ─── PLATFORMS: admin list ──────────────────────────── + case 'platforms_admin': + $rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true,'platforms'=>$rows]); + break; + + // ─── PLATFORMS: create ──────────────────────────────── + case 'platforms_create': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? ''))); + $name = substr(trim($d['name'] ?? ''), 0, 100); + $purl = substr(trim($d['player_url'] ?? ''), 0, 500); + $curl = substr(trim($d['console_url'] ?? ''), 0, 500); + $color= preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color']??'') ? $d['color'] : '#f0c040'; + $sort = (int)($d['sort_order'] ?? 99); + $active=(int)(bool)($d['is_active'] ?? 1); + if (!$slug||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL required']); exit; } + try { + db()->prepare("INSERT INTO platforms (slug,name,player_url,console_url,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?)") + ->execute([$slug,$name,$purl,$curl,$color,$sort,$active]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + } catch (Exception $e) { echo json_encode(['success'=>false,'error'=>'Slug already exists']); } + break; + + // ─── PLATFORMS: update ──────────────────────────────── + case 'platforms_update': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $name = substr(trim($d['name'] ?? ''), 0, 100); + $purl = substr(trim($d['player_url'] ?? ''), 0, 500); + $curl = substr(trim($d['console_url'] ?? ''), 0, 500); + $color= preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color']??'') ? $d['color'] : '#f0c040'; + $sort = (int)($d['sort_order'] ?? 99); + $active=(int)(bool)($d['is_active'] ?? 1); + if (!$id||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'ID, name, and URL required']); exit; } + db()->prepare("UPDATE platforms SET name=?,player_url=?,console_url=?,color=?,sort_order=?,is_active=? WHERE id=?") + ->execute([$name,$purl,$curl,$color,$sort,$active,$id]); + echo json_encode(['success'=>true]); + break; + + // ─── PLATFORMS: delete ──────────────────────────────── + case 'platforms_delete': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + case 'billing_get': + $uid = (int)($_GET['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare("SELECT * FROM saved_billing WHERE user_id=?"); + $stmt->execute([$uid]); + $row = $stmt->fetch(); + // Admin sees masked card info + if ($row) { + $row['card_display'] = $row['card_brand'] && $row['card_last4'] + ? $row['card_brand'] . ' ····' . $row['card_last4'] : null; + // Don't expose raw sq_card_id + unset($row['sq_card_id']); + } + echo json_encode(['success'=>true,'billing'=>$row ?: null]); + break; + + // ─── BILLING: save/update ──────────────────────────────── + case 'billing_save': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare(" + INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip) + VALUES (?,?,?,?,?,?,?,?) + ON DUPLICATE KEY UPDATE + first_name=VALUES(first_name), last_name=VALUES(last_name), + email=VALUES(email), address=VALUES(address), + city=VALUES(city), state=VALUES(state), zip=VALUES(zip) + "); + $stmt->execute([ + $uid, + substr(trim($data['first_name']??''),0,80), + substr(trim($data['last_name'] ??''),0,80), + substr(strtolower(trim($data['email']??'')),0,150), + substr(trim($data['address'] ??''),0,200), + substr(trim($data['city'] ??''),0,80), + strtoupper(substr(trim($data['state']??''),0,2)), + substr(trim($data['zip'] ??''),0,10), + ]); + echo json_encode(['success'=>true]); + break; + + // ─── BILLING: clear card ───────────────────────────────── + case 'billing_clear_card': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + db()->prepare("UPDATE saved_billing SET card_brand=NULL,card_last4=NULL,card_exp_month=NULL,card_exp_year=NULL,sq_card_id=NULL WHERE user_id=?")->execute([$uid]); + echo json_encode(['success'=>true]); + break; + + // ─── BILLING: clear all ────────────────────────────────── + case 'billing_clear_all': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + db()->prepare("DELETE FROM saved_billing WHERE user_id=?")->execute([$uid]); + echo json_encode(['success'=>true]); + break; + + // ─── RESEND VERIFICATION (from admin) ───────────────────── + case 'resend_verification': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = (int)($data['user_id'] ?? 0); + $stmt = db()->prepare("SELECT email,alias FROM users WHERE id=?"); + $stmt->execute([$uid]); + $user = $stmt->fetch(); + if (!$user) { echo json_encode(['success'=>false,'error'=>'User not found']); exit; } + $result = resendVerification($user['email']); + echo json_encode($result); + break; + + // ─── CHAT: inbox list ────────────────────────────────── + case 'chat_inbox': + $rows = db()->query(" + SELECT u.id AS user_id, u.username, u.alias, + cm.message AS last_message, cm.sender AS last_sender, + cm.created_at AS last_time, + (SELECT COUNT(*) FROM chat_messages + WHERE user_id=u.id AND sender='user' AND is_read=0) AS unread_count + FROM users u + INNER JOIN chat_messages cm ON cm.id=( + SELECT id FROM chat_messages WHERE user_id=u.id ORDER BY id DESC LIMIT 1 + ) + ORDER BY cm.created_at DESC + ")->fetchAll(); + echo json_encode(['success'=>true,'inbox'=>$rows]); + break; + + // ─── CHAT: full thread for one user ─────────────────── + case 'chat_thread': + $tid = (int)($_GET['user_id'] ?? 0); + $since = (int)($_GET['since'] ?? 0); + if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + $stmt = db()->prepare("SELECT id,sender,message,is_read,created_at FROM chat_messages WHERE user_id=? AND id>? ORDER BY id ASC LIMIT 300"); + $stmt->execute([$tid, $since]); + // Mark user messages read + db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='user' AND is_read=0")->execute([$tid]); + $uStmt = db()->prepare("SELECT id,username,alias,tokens FROM users WHERE id=?"); + $uStmt->execute([$tid]); + echo json_encode(['success'=>true,'messages'=>$stmt->fetchAll(),'user'=>$uStmt->fetch()]); + break; + + // ─── CHAT: admin reply ──────────────────────────────── + case 'chat_admin_send': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $tid = (int)($data['user_id'] ?? 0); + $msg = trim($data['message'] ?? ''); + if (!$tid || empty($msg)) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; } + $stmt = db()->prepare("INSERT INTO chat_messages (user_id,sender,message) VALUES (?,'admin',?)"); + $stmt->execute([$tid, $msg]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + break; + + // ─── CHAT: clear single user thread ─────────────────── + case 'chat_clear_thread': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $tid = (int)($data['user_id'] ?? 0); + if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + db()->prepare("DELETE FROM chat_messages WHERE user_id=?")->execute([$tid]); + echo json_encode(['success'=>true]); + break; + + // ─── CHAT: clear ALL chats ──────────────────────────── + case 'chat_clear_all': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + db()->exec("DELETE FROM chat_messages"); + echo json_encode(['success'=>true]); + break; + case 'chat_unread': + $count = db()->query("SELECT COUNT(*) FROM chat_messages WHERE sender='user' AND is_read=0")->fetchColumn(); + echo json_encode(['success'=>true,'count'=>(int)$count]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/billing.php b/api/billing.php new file mode 100644 index 0000000..5e6a8e2 --- /dev/null +++ b/api/billing.php @@ -0,0 +1,91 @@ +false,'error'=>'Not authenticated']); exit; } + +$action = $_GET['action'] ?? ''; +$userId = $_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); + +switch ($action) { + + // ── Get saved billing (user sees own; admin passes user_id param) ── + case 'get': + $uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId; + $stmt = db()->prepare("SELECT * FROM saved_billing WHERE user_id=?"); + $stmt->execute([$uid]); + $row = $stmt->fetch(); + if ($row && !$isAdmin) { + // Mask card number for non-admin + $row['card_display'] = $row['card_brand'] && $row['card_last4'] + ? $row['card_brand'] . ' ····' . $row['card_last4'] + : null; + unset($row['sq_card_id']); + } + echo json_encode(['success'=>true, 'billing'=>$row ?: null]); + break; + + // ── Save / update billing info ───────────────────────────── + case 'save': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId; + + $firstName = substr(trim($data['first_name'] ?? ''), 0, 80); + $lastName = substr(trim($data['last_name'] ?? ''), 0, 80); + $email = substr(strtolower(trim($data['email'] ?? '')), 0, 150); + $address = substr(trim($data['address'] ?? ''), 0, 200); + $city = substr(trim($data['city'] ?? ''), 0, 80); + $state = strtoupper(substr(trim($data['state'] ?? ''), 0, 2)); + $zip = substr(trim($data['zip'] ?? ''), 0, 10); + + // Card info — only update if provided + $cardBrand = isset($data['card_brand']) ? substr(trim($data['card_brand']), 0, 30) : null; + $cardLast4 = isset($data['card_last4']) ? substr(trim($data['card_last4']), 0, 4) : null; + $cardExpMonth = isset($data['card_exp_month'])? substr(trim($data['card_exp_month']),0, 2) : null; + $cardExpYear = isset($data['card_exp_year']) ? substr(trim($data['card_exp_year']), 0, 4) : null; + $sqCardId = isset($data['sq_card_id']) ? substr(trim($data['sq_card_id']), 0, 255) : null; + + $stmt = db()->prepare(" + INSERT INTO saved_billing + (user_id, first_name, last_name, email, address, city, state, zip, + card_brand, card_last4, card_exp_month, card_exp_year, sq_card_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + ON DUPLICATE KEY UPDATE + first_name=VALUES(first_name), last_name=VALUES(last_name), + email=VALUES(email), address=VALUES(address), city=VALUES(city), + state=VALUES(state), zip=VALUES(zip), + card_brand=COALESCE(VALUES(card_brand), card_brand), + card_last4=COALESCE(VALUES(card_last4), card_last4), + card_exp_month=COALESCE(VALUES(card_exp_month), card_exp_month), + card_exp_year=COALESCE(VALUES(card_exp_year), card_exp_year), + sq_card_id=COALESCE(VALUES(sq_card_id), sq_card_id) + "); + $stmt->execute([$uid,$firstName,$lastName,$email,$address,$city,$state,$zip, + $cardBrand,$cardLast4,$cardExpMonth,$cardExpYear,$sqCardId]); + echo json_encode(['success'=>true]); + break; + + // ── Clear card info only ─────────────────────────────────── + case 'clear_card': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId; + db()->prepare("UPDATE saved_billing SET card_brand=NULL, card_last4=NULL, card_exp_month=NULL, card_exp_year=NULL, sq_card_id=NULL WHERE user_id=?") + ->execute([$uid]); + echo json_encode(['success'=>true]); + break; + + // ── Clear all billing info ──────────────────────────────── + case 'clear_all': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId; + db()->prepare("DELETE FROM saved_billing WHERE user_id=?")->execute([$uid]); + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/broadcast.php b/api/broadcast.php new file mode 100644 index 0000000..09654e4 --- /dev/null +++ b/api/broadcast.php @@ -0,0 +1,89 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; } + +$action = $_GET['action'] ?? 'list'; +$userId = (int)$_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); + +switch ($action) { + + // ── List broadcasts for this user ───────────────────── + case 'list': + // Get broadcasts targeting this user + $stmt = db()->prepare(" + SELECT b.*, + u.username AS sender_name, + u.alias AS sender_alias, + (SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count, + (SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?) AS is_read, + (SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count + FROM broadcasts b + JOIN users u ON b.admin_id = u.id + WHERE b.target = 'all' + OR (b.target = 'verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1 AND is_admin=0)) + OR (b.target = 'unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0)) + OR (b.target = 'admins' AND ?) + ORDER BY b.sent_at DESC + "); + $stmt->execute([$userId, $userId, $userId, $isAdmin ? 1 : 0]); + echo json_encode(['success'=>true, 'broadcasts'=>$stmt->fetchAll()]); + break; + + // ── Mark as read ────────────────────────────────────── + case 'mark_read': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $bid= (int)($d['broadcast_id'] ?? 0); + if (!$bid) { echo json_encode(['success'=>false]); exit; } + db()->prepare("INSERT IGNORE INTO broadcast_reads (broadcast_id,user_id) VALUES (?,?)")->execute([$bid,$userId]); + echo json_encode(['success'=>true]); + break; + + // ── Get replies for a broadcast ─────────────────────── + case 'replies': + $bid = (int)($_GET['broadcast_id'] ?? 0); + if (!$bid) { echo json_encode(['success'=>false]); exit; } + $stmt = db()->prepare(" + SELECT br.*, u.username, u.alias, u.is_admin + FROM broadcast_replies br + JOIN users u ON br.user_id = u.id + WHERE br.broadcast_id = ? + ORDER BY br.created_at ASC + "); + $stmt->execute([$bid]); + echo json_encode(['success'=>true,'replies'=>$stmt->fetchAll()]); + break; + + // ── Post a reply ────────────────────────────────────── + case 'reply': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $bid = (int)($d['broadcast_id'] ?? 0); + $msg = substr(trim($d['message'] ?? ''), 0, 1000); + if (!$bid || !$msg) { echo json_encode(['success'=>false,'error'=>'broadcast_id and message required']); exit; } + db()->prepare("INSERT INTO broadcast_replies (broadcast_id,user_id,message) VALUES (?,?,?)")->execute([$bid,$userId,$msg]); + db()->prepare("INSERT IGNORE INTO broadcast_reads (broadcast_id,user_id) VALUES (?,?)")->execute([$bid,$userId]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + break; + + // ── Unread count ────────────────────────────────────── + case 'unread_count': + $stmt = db()->prepare(" + SELECT COUNT(*) FROM broadcasts b + WHERE (b.target='all' OR (b.target='verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1 AND is_admin=0)) + OR (b.target='unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0)) + OR (b.target='admins' AND ?)) + AND NOT EXISTS (SELECT 1 FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?) + "); + $stmt->execute([$userId,$userId,$isAdmin?1:0,$userId]); + echo json_encode(['success'=>true,'count'=>(int)$stmt->fetchColumn()]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/cashout.php b/api/cashout.php new file mode 100644 index 0000000..6cc2bd3 --- /dev/null +++ b/api/cashout.php @@ -0,0 +1,148 @@ +false,'error'=>'Not authenticated']); exit; } + +$userId = (int)$_SESSION['user_id']; +$method = $_SERVER['REQUEST_METHOD']; + +// ══════════════════════════════════════════════════════════ +// GET — player's own requests (list, delete, update, lock) +// ══════════════════════════════════════════════════════════ +if ($method === 'GET') { + $action = $_GET['action'] ?? 'list'; + + if ($action === 'list') { + $stmt = db()->prepare(" + SELECT cr.*, + COALESCE(p.name, cr.platform_id) AS platform_name + FROM cashout_requests cr + LEFT JOIN platforms p ON cr.platform_id = p.slug + WHERE cr.user_id = ? + ORDER BY cr.created_at DESC + LIMIT 50 + "); + $stmt->execute([$userId]); + echo json_encode(['success'=>true, 'requests'=>$stmt->fetchAll()]); + exit; + } + + if ($action === 'delete') { + $id = (int)($_GET['id'] ?? 0); + $chk = db()->prepare("SELECT id,tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'"); + $chk->execute([$id, $userId]); + $row = $chk->fetch(); + if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; } + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$row['tokens'], $userId]); + db()->prepare("DELETE FROM cashout_requests WHERE id=?")->execute([$id]); + $nb = db()->prepare("SELECT tokens FROM users WHERE id=?"); + $nb->execute([$userId]); + echo json_encode(['success'=>true,'new_balance'=>(float)$nb->fetchColumn()]); + exit; + } + + if ($action === 'update') { + $id = (int)($_GET['id'] ?? 0); + $tokens = (float)($_GET['tokens'] ?? 0); + $alias = substr(trim($_GET['alias'] ?? ''), 0, 100); + $chk = db()->prepare("SELECT id,tokens AS old_tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'"); + $chk->execute([$id, $userId]); + $row = $chk->fetch(); + if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; } + if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum 1 token']); exit; } + $diff = $tokens - $row['old_tokens']; + if ($diff > 0) { + $balChk = db()->prepare("SELECT tokens FROM users WHERE id=?"); + $balChk->execute([$userId]); + if ($diff > (float)$balChk->fetchColumn()) { echo json_encode(['success'=>false,'error'=>'Insufficient balance']); exit; } + } + db()->beginTransaction(); + db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$diff, $userId]); + db()->prepare("UPDATE cashout_requests SET tokens=?,alias=? WHERE id=?")->execute([$tokens, $alias, $id]); + db()->commit(); + echo json_encode(['success'=>true]); + exit; + } + + if ($action === 'lock') { + $id = (int)($_GET['id'] ?? 0); + $chk = db()->prepare("SELECT id FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'"); + $chk->execute([$id, $userId]); + if (!$chk->fetch()) { echo json_encode(['success'=>false,'error'=>'Request not found']); exit; } + try { + db()->exec("ALTER TABLE cashout_requests MODIFY COLUMN status ENUM('pending','locked','sent','approved','rejected','deleted') DEFAULT 'pending'"); + } catch (Exception $e) {} + db()->prepare("UPDATE cashout_requests SET status='locked' WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + exit; + } + + echo json_encode(['success'=>false,'error'=>'Unknown action']); + exit; +} + +// ══════════════════════════════════════════════════════════ +// POST — submit new cashout request +// ══════════════════════════════════════════════════════════ +if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } + +$data = json_decode(file_get_contents('php://input'), true); +$platformId = trim($data['platform_id'] ?? ''); +$alias = trim($data['alias'] ?? ''); +$tokens = (float)($data['tokens'] ?? 0); +$payoutMethodId = (int)($data['payout_method_id'] ?? 0); +$payoutMethodType = trim($data['payout_method_type'] ?? ''); +$payoutHandle = trim($data['payout_handle'] ?? ''); + +// Validate platform +$platStmt = db()->prepare("SELECT slug FROM platforms WHERE slug=? AND is_active=1 LIMIT 1"); +$platStmt->execute([$platformId]); +if (!$platStmt->fetch()) { + $platforms = json_decode(PLATFORMS, true); + if (empty(array_filter($platforms, fn($p) => $p['id'] === $platformId))) { + echo json_encode(['success'=>false,'error'=>'Invalid platform.']); exit; + } +} + +if (empty($alias)) { echo json_encode(['success'=>false,'error'=>'Platform alias required.']); exit; } +if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum cashout is 1 token.']); exit; } + +// Validate payout method +if ($payoutMethodId) { + $chk = db()->prepare("SELECT method_type,account_handle FROM payout_methods WHERE id=? AND user_id=?"); + $chk->execute([$payoutMethodId, $userId]); + if ($pm = $chk->fetch()) { + $payoutMethodType = $pm['method_type']; + $payoutHandle = $pm['account_handle']; + } +} + +// Check balance +$balStmt = db()->prepare("SELECT tokens FROM users WHERE id=?"); +$balStmt->execute([$userId]); +$balance = (float)$balStmt->fetchColumn(); +if ($tokens > $balance) { echo json_encode(['success'=>false,'error'=>'Insufficient token balance.']); exit; } + +// Deduct & create +db()->beginTransaction(); +try { + db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$tokens, $userId]); + db()->prepare("INSERT INTO cashout_requests (user_id,platform_id,alias,tokens,payout_method_type,payout_handle) VALUES (?,?,?,?,?,?)") + ->execute([$userId, $platformId, $alias, $tokens, $payoutMethodType, $payoutHandle]); + db()->commit(); +} catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'Request failed. Try again.']); exit; +} + +$newBalStmt = db()->prepare("SELECT tokens FROM users WHERE id=?"); +$newBalStmt->execute([$userId]); +$nb = (float)$newBalStmt->fetchColumn(); + +try { logActivity('cashout_request', $userId, null, 'cashout', 0, "Cashout: {$tokens} tokens via {$payoutMethodType}"); } catch(Exception $e){} + +echo json_encode(['success'=>true, 'new_balance'=>$nb]); diff --git a/api/cashout_method_types.php b/api/cashout_method_types.php new file mode 100644 index 0000000..b0cc1b4 --- /dev/null +++ b/api/cashout_method_types.php @@ -0,0 +1,73 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +$action = $_GET['action'] ?? 'list'; +$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']); + +switch ($action) { + + // Public: active types for player dropdown + case 'list': + $rows = db()->query("SELECT slug,label,icon,description FROM cashout_method_types WHERE is_active=1 ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'types'=>$rows]); + break; + + // Admin: all types + case 'admin_list': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'types'=>$rows]); + break; + + // Admin: create + case 'create': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? ''))); + $label= substr(trim($d['label'] ?? ''), 0, 100); + $icon = substr(trim($d['icon'] ?? '💰'), 0, 10); + $desc = substr(trim($d['description'] ?? ''), 0, 200); + $sort = (int)($d['sort_order'] ?? 99); + $active = (int)(bool)($d['is_active'] ?? 1); + if (!$slug || !$label) { echo json_encode(['success'=>false,'error'=>'Slug and label required']); exit; } + try { + db()->prepare("INSERT INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)") + ->execute([$slug,$label,$icon,$desc,$active,$sort]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + } catch (Exception $e) { + echo json_encode(['success'=>false,'error'=>'Slug already exists']); + } + break; + + // Admin: update + case 'update': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $label= substr(trim($d['label'] ?? ''), 0, 100); + $icon = substr(trim($d['icon'] ?? '💰'), 0, 10); + $desc = substr(trim($d['description'] ?? ''), 0, 200); + $sort = (int)($d['sort_order'] ?? 0); + $active = (int)(bool)($d['is_active'] ?? 1); + if (!$id || !$label) { echo json_encode(['success'=>false,'error'=>'ID and label required']); exit; } + db()->prepare("UPDATE cashout_method_types SET label=?,icon=?,description=?,is_active=?,sort_order=? WHERE id=?") + ->execute([$label,$icon,$desc,$active,$sort,$id]); + echo json_encode(['success'=>true]); + break; + + // Admin: delete + case 'delete': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("DELETE FROM cashout_method_types WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/chat.php b/api/chat.php new file mode 100644 index 0000000..daf0bad --- /dev/null +++ b/api/chat.php @@ -0,0 +1,124 @@ +false,'error'=>'Not authenticated']); exit; } + +$action = $_GET['action'] ?? ''; +$userId = $_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); + +switch ($action) { + + // ── User: get own messages ───────────────────────────── + case 'messages': + $since = (int)($_GET['since'] ?? 0); // last message id for polling + $stmt = db()->prepare(" + SELECT id, sender, message, is_read, created_at + FROM chat_messages + WHERE user_id = ? AND id > ? + ORDER BY id ASC + LIMIT 100 + "); + $stmt->execute([$userId, $since]); + $msgs = $stmt->fetchAll(); + + // Mark admin messages as read + db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='admin' AND is_read=0")->execute([$userId]); + + echo json_encode(['success'=>true, 'messages'=>$msgs]); + break; + + // ── User: send message to admin ─────────────────────── + case 'send': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $msg = trim($data['message'] ?? ''); + if (empty($msg) || mb_strlen($msg) > 2000) { echo json_encode(['success'=>false,'error'=>'Invalid message']); exit; } + + $stmt = db()->prepare("INSERT INTO chat_messages (user_id, sender, message) VALUES (?, 'user', ?)"); + $stmt->execute([$userId, $msg]); + echo json_encode(['success'=>true, 'id'=>db()->lastInsertId()]); + break; + + // ── Admin: get inbox list (one row per user, latest msg) ── + case 'admin_inbox': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $rows = db()->query(" + SELECT + u.id AS user_id, + u.username, + u.alias, + cm.message AS last_message, + cm.sender AS last_sender, + cm.created_at AS last_time, + SUM(CASE WHEN cm2.sender='user' AND cm2.is_read=0 THEN 1 ELSE 0 END) AS unread_count + FROM users u + JOIN chat_messages cm ON cm.id = ( + SELECT id FROM chat_messages + WHERE user_id = u.id + ORDER BY id DESC LIMIT 1 + ) + LEFT JOIN chat_messages cm2 ON cm2.user_id = u.id + GROUP BY u.id, u.username, u.alias, cm.message, cm.sender, cm.created_at + ORDER BY cm.created_at DESC + ")->fetchAll(); + echo json_encode(['success'=>true, 'inbox'=>$rows]); + break; + + // ── Admin: get all messages for a specific user ──────── + case 'admin_thread': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $tid = (int)($_GET['user_id'] ?? 0); + $since = (int)($_GET['since'] ?? 0); + if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; } + + $stmt = db()->prepare(" + SELECT id, sender, message, is_read, created_at + FROM chat_messages + WHERE user_id = ? AND id > ? + ORDER BY id ASC LIMIT 200 + "); + $stmt->execute([$tid, $since]); + $msgs = $stmt->fetchAll(); + + // Mark user messages as read + db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='user' AND is_read=0")->execute([$tid]); + + // Get user info + $uStmt = db()->prepare("SELECT id, username, alias, tokens FROM users WHERE id=?"); + $uStmt->execute([$tid]); + $user = $uStmt->fetch(); + + echo json_encode(['success'=>true, 'messages'=>$msgs, 'user'=>$user]); + break; + + // ── Admin: reply to a user ──────────────────────────── + case 'admin_send': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $tid = (int)($data['user_id'] ?? 0); + $msg = trim($data['message'] ?? ''); + if (!$tid || empty($msg) || mb_strlen($msg) > 2000) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; } + + $stmt = db()->prepare("INSERT INTO chat_messages (user_id, sender, message) VALUES (?, 'admin', ?)"); + $stmt->execute([$tid, $msg]); + echo json_encode(['success'=>true, 'id'=>db()->lastInsertId()]); + break; + + // ── Unread count (for badge) ────────────────────────── + case 'unread': + if ($isAdmin) { + $count = db()->query("SELECT COUNT(*) FROM chat_messages WHERE sender='user' AND is_read=0")->fetchColumn(); + } else { + $stmt = db()->prepare("SELECT COUNT(*) FROM chat_messages WHERE user_id=? AND sender='admin' AND is_read=0"); + $stmt->execute([$userId]); + $count = $stmt->fetchColumn(); + } + echo json_encode(['success'=>true, 'count'=>(int)$count]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/game_aliases.php b/api/game_aliases.php new file mode 100644 index 0000000..3832cc4 --- /dev/null +++ b/api/game_aliases.php @@ -0,0 +1,65 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; } + +$action = $_GET['action'] ?? ''; +$userId = $_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); + +switch ($action) { + + // ── Get all aliases for a user ──────────────────────── + case 'get': + $uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId; + $stmt = db()->prepare("SELECT platform_slug, alias FROM game_aliases WHERE user_id=?"); + $stmt->execute([$uid]); + $rows = $stmt->fetchAll(); + $map = []; + foreach ($rows as $r) $map[$r['platform_slug']] = $r['alias']; + echo json_encode(['success'=>true, 'aliases'=>$map]); + break; + + // ── Save a single alias ─────────────────────────────── + case 'save': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId; + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($data['platform_slug'] ?? ''))); + $alias = substr(trim($data['alias'] ?? ''), 0, 100); + if (!$slug) { echo json_encode(['success'=>false,'error'=>'Platform slug required']); exit; } + if ($alias === '') { + // Empty alias = delete it + db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?")->execute([$uid,$slug]); + } else { + db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) + ON DUPLICATE KEY UPDATE alias=VALUES(alias)")->execute([$uid,$slug,$alias]); + } + echo json_encode(['success'=>true]); + break; + + // ── Save all aliases at once (bulk) ─────────────────── + case 'save_all': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $data = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId; + $aliases = $data['aliases'] ?? []; + $stmt = db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) + ON DUPLICATE KEY UPDATE alias=VALUES(alias)"); + $del = db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?"); + foreach ($aliases as $slug => $alias) { + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($slug))); + $alias = substr(trim($alias), 0, 100); + if (!$slug) continue; + if ($alias === '') $del->execute([$uid, $slug]); + else $stmt->execute([$uid, $slug, $alias]); + } + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/login.php b/api/login.php new file mode 100644 index 0000000..e263a49 --- /dev/null +++ b/api/login.php @@ -0,0 +1,37 @@ +false,'error'=>'Server error']); + exit; +} +ob_end_clean(); +header('Content-Type: application/json'); + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +$username = trim($data['username'] ?? ''); +$password = trim($data['password'] ?? ''); + +if (empty($username) || empty($password)) { + echo json_encode(['success'=>false,'error'=>'Username and password required']); exit; +} + +try { + $result = loginUser($username, $password); +} catch (Throwable $e) { + echo json_encode(['success'=>false,'error'=>'Login error. Please try again.']); exit; +} + +if ($result['success'] && isset($result['user'])) { + logPlayerAction('LOGIN_SUCCESS', $result['user']['id'], 'User logged in', 'auth', 'info'); + unset($result['user']['password']); +} +if (!$result['success']) { logSecurityEvent('LOGIN_FAILED', null, 'Failed login attempt for: ' . $username, 'warning'); } +echo json_encode($result); diff --git a/api/logout.php b/api/logout.php new file mode 100644 index 0000000..2afa501 --- /dev/null +++ b/api/logout.php @@ -0,0 +1,5 @@ + true]); diff --git a/api/me.php b/api/me.php new file mode 100644 index 0000000..8b77479 --- /dev/null +++ b/api/me.php @@ -0,0 +1,35 @@ + false, 'error' => 'Server error']); + exit; +} +ob_end_clean(); +header('Content-Type: application/json'); + +if (!isLoggedIn()) { + echo json_encode(['success' => false, 'error' => 'Not authenticated']); + exit; +} + +try { + $user = currentUser(); +} catch (Throwable $e) { + echo json_encode(['success' => false, 'error' => 'DB error']); + exit; +} + +if (!$user) { + echo json_encode(['success' => false, 'error' => 'User not found']); + exit; +} + +// Sync session is_admin with DB value — catches admin elevation/demotion +$_SESSION['is_admin'] = (int)$user['is_admin']; + +unset($user['password']); +echo json_encode(['success' => true, 'user' => $user]); diff --git a/api/my_activity.php b/api/my_activity.php new file mode 100644 index 0000000..b6268d6 --- /dev/null +++ b/api/my_activity.php @@ -0,0 +1,76 @@ +false,'error'=>'Not authenticated']); exit; } + +$userId = (int)$_SESSION['user_id']; +$action = $_GET['action'] ?? 'all'; + +// ── Purchases ────────────────────────────────────────────── +if ($action === 'all' || $action === 'purchases') { + $stmt = db()->prepare(" + SELECT id, tokens, amount_cents, payment_method, platform_id, game_alias, + card_brand, card_last4, status, admin_note, created_at + FROM token_purchases + WHERE user_id=? + ORDER BY created_at DESC + LIMIT 50 + "); + $stmt->execute([$userId]); + $purchases = $stmt->fetchAll(); +} + +// ── Cashouts ─────────────────────────────────────────────── +if ($action === 'all' || $action === 'cashouts') { + $stmt = db()->prepare(" + SELECT cr.*, + COALESCE(p.name, cr.platform_id) AS platform_name + FROM cashout_requests cr + LEFT JOIN platforms p ON cr.platform_id = p.slug + WHERE cr.user_id=? + ORDER BY cr.created_at DESC + LIMIT 50 + "); + $stmt->execute([$userId]); + $cashouts = $stmt->fetchAll(); +} + +// ── Broadcasts/Invites (use broadcasts as announcements) ─── +if ($action === 'all' || $action === 'broadcasts') { + $stmt = db()->prepare(" + SELECT b.id, b.subject, b.message, b.sent_at, + u.username AS sender, + (SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?) AS is_read, + (SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id AND user_id=?) AS replied + FROM broadcasts b + JOIN users u ON b.admin_id=u.id + WHERE b.target='all' + OR (b.target='verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1)) + OR (b.target='unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0)) + OR (b.target='admins' AND 0) + ORDER BY b.sent_at DESC + LIMIT 20 + "); + $stmt->execute([$userId,$userId,$userId,$userId]); + $broadcasts = $stmt->fetchAll(); +} + +if ($action === 'all') { + echo json_encode([ + 'success' => true, + 'purchases' => $purchases, + 'cashouts' => $cashouts, + 'broadcasts' => $broadcasts, + ]); +} elseif ($action === 'purchases') { + echo json_encode(['success'=>true,'purchases'=>$purchases]); +} elseif ($action === 'cashouts') { + echo json_encode(['success'=>true,'cashouts'=>$cashouts]); +} elseif ($action === 'broadcasts') { + echo json_encode(['success'=>true,'broadcasts'=>$broadcasts]); +} else { + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/my_purchases.php b/api/my_purchases.php new file mode 100644 index 0000000..b340f8f --- /dev/null +++ b/api/my_purchases.php @@ -0,0 +1,17 @@ +false,'error'=>'Not authenticated']); exit; } + +$userId = $_SESSION['user_id']; +$stmt = db()->prepare(" + SELECT id, tokens, amount_cents, payment_method, platform_id, game_alias, + card_brand, card_last4, status, created_at + FROM token_purchases + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 20 +"); +$stmt->execute([$userId]); +echo json_encode(['success'=>true, 'purchases'=>$stmt->fetchAll()]); diff --git a/api/payment_settings.php b/api/payment_settings.php new file mode 100644 index 0000000..d4f5267 --- /dev/null +++ b/api/payment_settings.php @@ -0,0 +1,44 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +$action = $_GET['action'] ?? 'list'; +$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']); + +switch ($action) { + + // Public: get all enabled payment methods including card status + case 'list': + // Include card row (is_enabled controls whether card appears at checkout) + $rows = db()->query("SELECT method_key, label, handle, instructions, is_enabled FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'methods'=>$rows]); + break; + + // Admin: get all methods including disabled + case 'admin_list': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $rows = db()->query("SELECT * FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'methods'=>$rows]); + break; + + // Admin: update a single method + case 'update': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $label= substr(trim($d['label']??''), 0, 100); + $handle = substr(trim($d['handle']??''), 0, 200); + $instructions = substr(trim($d['instructions']??''), 0, 500); + $enabled = (int)(bool)($d['is_enabled'] ?? 1); + $sort = (int)($d['sort_order'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("UPDATE payment_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?") + ->execute([$label,$handle,$instructions,$enabled,$sort,$id]); + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/payout_methods.php b/api/payout_methods.php new file mode 100644 index 0000000..95249f7 --- /dev/null +++ b/api/payout_methods.php @@ -0,0 +1,75 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; } + +$action = $_GET['action'] ?? ''; +$userId = (int)$_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); + +switch ($action) { + + case 'list': + $uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId; + $rows = db()->prepare("SELECT * FROM payout_methods WHERE user_id=? ORDER BY is_default DESC, id ASC"); + $rows->execute([$uid]); + echo json_encode(['success'=>true, 'methods'=>$rows->fetchAll()]); + break; + + case 'add': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $uid = $isAdmin && isset($d['user_id']) ? (int)$d['user_id'] : $userId; + $type = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['method_type'] ?? ''))); + $label = substr(trim($d['label'] ?? ''), 0, 100); + $handle= substr(trim($d['account_handle'] ?? ''), 0, 200); + $def = (int)(bool)($d['is_default'] ?? 0); + if (!$type || !$label || !$handle) { echo json_encode(['success'=>false,'error'=>'All fields required']); exit; } + db()->beginTransaction(); + if ($def) db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]); + // If first method, auto-set as default + $count = db()->prepare("SELECT COUNT(*) FROM payout_methods WHERE user_id=?"); $count->execute([$uid]); + if ((int)$count->fetchColumn() === 0) $def = 1; + db()->prepare("INSERT INTO payout_methods (user_id,method_type,label,account_handle,is_default) VALUES (?,?,?,?,?)") + ->execute([$uid,$type,$label,$handle,$def]); + $newId = db()->lastInsertId(); + db()->commit(); + echo json_encode(['success'=>true,'id'=>$newId]); + break; + + case 'set_default': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + // Verify ownership + $chk = db()->prepare("SELECT user_id FROM payout_methods WHERE id=?"); $chk->execute([$id]); + $row = $chk->fetch(); + if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; } + $uid = $row['user_id']; + db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]); + db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + case 'delete': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $chk = db()->prepare("SELECT user_id,is_default FROM payout_methods WHERE id=?"); $chk->execute([$id]); + $row = $chk->fetch(); + if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; } + db()->prepare("DELETE FROM payout_methods WHERE id=?")->execute([$id]); + // If deleted default, set next one as default + if ($row['is_default']) { + $next = db()->prepare("SELECT id FROM payout_methods WHERE user_id=? LIMIT 1"); $next->execute([$row['user_id']]); + if ($n = $next->fetch()) db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$n['id']]); + } + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/payout_process.php b/api/payout_process.php new file mode 100644 index 0000000..53993fb --- /dev/null +++ b/api/payout_process.php @@ -0,0 +1,153 @@ +false,'error'=>'Forbidden']); exit; +} + +$adminId = (int)$_SESSION['user_id']; +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? 'list_settings'; + +// ── GET actions ────────────────────────────────────────── +if ($method === 'GET') { + if ($action === 'list_settings') { + $rows = db()->query("SELECT * FROM admin_payout_settings ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'settings'=>$rows]); + exit; + } + if ($action === 'cashout_detail') { + $id = (int)($_GET['id'] ?? 0); + $stmt = db()->prepare(" + SELECT cr.*, u.username, u.alias AS user_alias, u.email, + pm.method_type AS saved_payout_type, pm.account_handle AS saved_payout_handle, pm.label AS saved_payout_label + FROM cashout_requests cr + JOIN users u ON cr.user_id = u.id + LEFT JOIN payout_methods pm ON pm.user_id = cr.user_id AND pm.is_default = 1 + WHERE cr.id = ? + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + echo json_encode(['success'=>true, 'cashout'=>$row]); + exit; + } + echo json_encode(['success'=>false,'error'=>'Unknown action']); exit; +} + +if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } +$d = json_decode(file_get_contents('php://input'), true); + +// ── Update payout settings ──────────────────────────────── +if ($action === 'update_setting') { + $id = (int)($d['id'] ?? 0); + $handle = substr(trim($d['handle']??''), 0, 200); + $instr = substr(trim($d['instructions']??''), 0, 500); + $enabled = (int)(bool)($d['is_enabled']??1); + $label = substr(trim($d['label']??''), 0, 100); + $sort = (int)($d['sort_order']??0); + db()->prepare("UPDATE admin_payout_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?") + ->execute([$label,$handle,$instr,$enabled,$sort,$id]); + echo json_encode(['success'=>true]); exit; +} + +// ── Process cashout payout ──────────────────────────────── +if ($action === 'process_payout') { + $cashoutId = (int)($d['cashout_id'] ?? 0); + $payoutKey = trim($d['payout_method_key'] ?? ''); + $payoutType = trim($d['payout_type'] ?? 'manual'); // 'manual' or 'square_gift_card' + $note = substr(trim($d['note'] ?? ''), 0, 500); + + // Load cashout + $stmt = db()->prepare("SELECT cr.*, u.username, u.alias, u.email FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.id=? AND cr.status IN ('pending','locked')"); + $stmt->execute([$cashoutId]); + $cashout = $stmt->fetch(); + if (!$cashout) { echo json_encode(['success'=>false,'error'=>'Cashout not found or already processed']); exit; } + + // Amount in cents (1 token = $1) + $amountCents = (int)round($cashout['tokens'] * 100); + + // Load payout setting + $pset = db()->prepare("SELECT * FROM admin_payout_settings WHERE method_key=? AND is_enabled=1"); + $pset->execute([$payoutKey]); + $setting = $pset->fetch(); + + $squareTxnId = null; + $giftCardGan = null; + $giftCardBal = null; + $txnStatus = 'completed'; + + if ($payoutType === 'square_gift_card') { + // ── Square Gift Card Flow ───────────────────────── + try { + // 1. Create gift card + $gcRes = SquareClient::post('/v2/gift-cards', [ + 'idempotency_key' => 'gc-create-' . $cashoutId . '-' . time(), + 'location_id' => SQUARE_LOCATION_ID, + 'gift_card' => ['type' => 'DIGITAL'], + ]); + + if (empty($gcRes['gift_card']['id'])) { + throw new Exception('Failed to create gift card: ' . json_encode($gcRes)); + } + + $gcId = $gcRes['gift_card']['id']; + $gcGan = $gcRes['gift_card']['gan'] ?? ''; + + // 2. Activate gift card with balance + $actRes = SquareClient::post('/v2/gift-card-activities', [ + 'idempotency_key' => 'gc-activate-' . $cashoutId . '-' . time(), + 'gift_card_activity' => [ + 'type' => 'ACTIVATE', + 'location_id' => SQUARE_LOCATION_ID, + 'gift_card_id' => $gcId, + 'activate_activity_details' => [ + 'amount_money' => [ + 'amount' => $amountCents, + 'currency' => 'USD', + ], + ], + ], + ]); + + if (empty($actRes['gift_card_activity']['id'])) { + throw new Exception('Failed to activate gift card: ' . json_encode($actRes)); + } + + $giftCardGan = $gcGan; + $giftCardBal = $amountCents; + $squareTxnId = $actRes['gift_card_activity']['id']; + + } catch (Exception $e) { + echo json_encode(['success'=>false,'error'=>'Square error: ' . $e->getMessage()]); exit; + } + } + + // ── Mark cashout as sent ────────────────────────────── + db()->beginTransaction(); + try { + db()->prepare("UPDATE cashout_requests SET status='sent', admin_note=?, resolved_at=NOW() WHERE id=?") + ->execute([$note, $cashoutId]); + + db()->prepare("INSERT INTO cashout_transactions (cashout_id,admin_id,payout_method,payout_type,amount_cents,square_txn_id,gift_card_gan,gift_card_balance,note,status) + VALUES (?,?,?,?,?,?,?,?,?,'completed')") + ->execute([$cashoutId, $adminId, $payoutKey, $payoutType, $amountCents, $squareTxnId, $giftCardGan, $giftCardBal, $note]); + + db()->commit(); + } catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'DB error: '.$e->getMessage()]); exit; + } + + $response = ['success'=>true, 'status'=>'sent', 'amount_cents'=>$amountCents]; + if ($giftCardGan) { + $response['gift_card_gan'] = $giftCardGan; + $response['gift_card_balance'] = $giftCardBal; + } + echo json_encode($response); exit; +} + +echo json_encode(['success'=>false,'error'=>'Unknown action']); diff --git a/api/platform_accounts.php b/api/platform_accounts.php new file mode 100644 index 0000000..1df2991 --- /dev/null +++ b/api/platform_accounts.php @@ -0,0 +1,92 @@ +false,'error'=>'Not authenticated']); exit; } +$userId = (int)$_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? 'list'; + +if ($method === 'GET') { + if ($action === 'list') { + $uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId; + $stmt = db()->prepare("SELECT pa.*, COALESCE(p.name, pa.platform_slug) AS platform_name, p.color + FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug + WHERE pa.user_id=? ORDER BY pa.requested_at DESC"); + $stmt->execute([$uid]); + $rows = $stmt->fetchAll(); + foreach ($rows as &$row) { + if (!$isAdmin && $row['status'] !== 'approved') $row['platform_password'] = null; + } + echo json_encode(['success'=>true,'accounts'=>$rows]); + } elseif ($action === 'check_onboarding') { + $cnt = db()->prepare("SELECT COUNT(*) FROM platform_accounts WHERE user_id=?"); + $cnt->execute([$userId]); + $hasAny = (int)$cnt->fetchColumn() > 0; + // Check flag — graceful fallback if column doesn't exist + $done = false; + try { + $s = db()->prepare("SELECT platform_onboarding_done FROM users WHERE id=?"); + $s->execute([$userId]); + $r = $s->fetch(); $done = !empty($r['platform_onboarding_done']); + } catch(Exception $e){} + echo json_encode(['success'=>true,'needs_onboarding'=>(!$done && !$hasAny),'has_accounts'=>$hasAny]); + } else { + echo json_encode(['success'=>false,'error'=>'Unknown action']); + } + exit; +} + +if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } +$d = json_decode(file_get_contents('php://input'), true); + +if ($action === 'request') { + $slug = preg_replace('/[^a-z0-9_]/','',strtolower(trim($d['platform_slug']??''))); + if (!$slug) { echo json_encode(['success'=>false,'error'=>'Platform required']); exit; } + try { + db()->prepare("INSERT INTO platform_accounts (user_id,platform_slug) VALUES (?,?)")->execute([$userId,$slug]); + try { db()->prepare("UPDATE users SET platform_onboarding_done=1 WHERE id=?")->execute([$userId]); } catch(Exception $e){} + echo json_encode(['success'=>true]); + } catch(Exception $e) { echo json_encode(['success'=>false,'error'=>'Already requested for this platform']); } + exit; +} +if ($action === 'dismiss_onboarding') { + try { db()->prepare("UPDATE users SET platform_onboarding_done=1 WHERE id=?")->execute([$userId]); } catch(Exception $e){} + echo json_encode(['success'=>true]); + exit; +} +if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } +if ($action === 'resolve') { + $id=$d['id']??0; $status=$d['status']??''; + $uname=substr(trim($d['platform_username']??''),0,100); + $pass=substr(trim($d['platform_password']??''),0,200); + $note=substr(trim($d['admin_note']??''),0,300); + if (!in_array($status,['approved','denied','deleted'])){echo json_encode(['success'=>false,'error'=>'Invalid status']);exit;} + $chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch(); + if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;} + db()->prepare("UPDATE platform_accounts SET status=?,platform_username=?,platform_password=?,admin_note=?,resolved_at=NOW(),admin_id=? WHERE id=?") + ->execute([$status,$uname,$pass,$note,(int)$_SESSION['user_id'],$id]); + if ($status==='approved'&&$uname) { + db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)") + ->execute([$row['user_id'],$row['platform_slug'],$uname]); + } + echo json_encode(['success'=>true]);exit; +} +if ($action === 'update_credentials') { + $id=$d['id']??0; + $uname=substr(trim($d['platform_username']??''),0,100); + $pass=substr(trim($d['platform_password']??''),0,200); + $note=substr(trim($d['admin_note']??''),0,300); + $chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch(); + if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;} + db()->prepare("UPDATE platform_accounts SET platform_username=?,platform_password=?,admin_note=? WHERE id=?") + ->execute([$uname,$pass,$note,$id]); + if ($uname) { + db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)") + ->execute([$row['user_id'],$row['platform_slug'],$uname]); + } + echo json_encode(['success'=>true]);exit; +} +echo json_encode(['success'=>false,'error'=>'Unknown action']); diff --git a/api/platforms.php b/api/platforms.php new file mode 100644 index 0000000..76a8a5d --- /dev/null +++ b/api/platforms.php @@ -0,0 +1,93 @@ +false,'error'=>'Server error']); exit; } +ob_end_clean(); +header('Content-Type: application/json'); + +$action = $_GET['action'] ?? 'list'; +$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']); + +switch ($action) { + + // ── Public: active platforms for player app ─────────── + case 'list': + $stmt = db()->query("SELECT slug,name,player_url,color,icon_path FROM platforms WHERE is_active=1 ORDER BY sort_order ASC, id ASC"); + $rows = $stmt->fetchAll(); + // Normalize to match old CFG format + $out = array_map(fn($r) => [ + 'id' => $r['slug'], + 'name' => $r['name'], + 'url' => $r['player_url'], + 'color' => $r['color'], + ], $rows); + echo json_encode(['success'=>true, 'platforms'=>$out]); + break; + + // ── Admin: full list including console_url and inactive ─ + case 'admin_list': + if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll(); + echo json_encode(['success'=>true, 'platforms'=>$rows]); + break; + + // ── Admin: create platform ──────────────────────────── + case 'create': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? ''))); + $name = substr(trim($d['name'] ?? ''), 0, 100); + $player_url = substr(trim($d['player_url'] ?? ''), 0, 500); + $console_url = substr(trim($d['console_url'] ?? ''), 0, 500); + $color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040'; + $sort_order = (int)($d['sort_order'] ?? 99); + $is_active = isset($d['is_active']) ? (int)(bool)$d['is_active'] : 1; + if (!$slug || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL are required']); exit; } + try { + $stmt = db()->prepare("INSERT INTO platforms (slug,name,player_url,console_url,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?)"); + $stmt->execute([$slug,$name,$player_url,$console_url,$color,$sort_order,$is_active]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + } catch (Exception $e) { + echo json_encode(['success'=>false,'error'=>'Slug already exists or DB error']); + } + break; + + // ── Admin: update platform ──────────────────────────── + case 'update': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + $name = substr(trim($d['name'] ?? ''), 0, 100); + $player_url = substr(trim($d['player_url'] ?? ''), 0, 500); + $console_url = substr(trim($d['console_url'] ?? ''), 0, 500); + $color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040'; + $sort_order = (int)($d['sort_order'] ?? 99); + $is_active = (int)(bool)($d['is_active'] ?? 1); + if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; } + db()->prepare("UPDATE platforms SET name=?,player_url=?,console_url=?,color=?,sort_order=?,is_active=? WHERE id=?") + ->execute([$name,$player_url,$console_url,$color,$sort_order,$is_active,$id]); + echo json_encode(['success'=>true]); + break; + + // ── Admin: delete platform ──────────────────────────── + case 'delete': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + // ── Admin: reorder platforms ────────────────────────── + case 'reorder': + if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $order = $d['order'] ?? []; // array of IDs in desired order + $stmt = db()->prepare("UPDATE platforms SET sort_order=? WHERE id=?"); + foreach ($order as $i => $pid) { $stmt->execute([$i, (int)$pid]); } + echo json_encode(['success'=>true]); + break; + + default: + echo json_encode(['success'=>false,'error'=>'Unknown action']); +} diff --git a/api/purchase.php b/api/purchase.php new file mode 100644 index 0000000..8ae4b39 --- /dev/null +++ b/api/purchase.php @@ -0,0 +1,163 @@ +false,'error'=>'Not authenticated']); exit; } +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } + +$data = json_decode(file_get_contents('php://input'), true); +$sourceId = trim($data['source_id'] ?? ''); +$tokens = (int)($data['tokens'] ?? 0); +$priceCents = (int)($data['price_cents'] ?? 0); +$method = trim($data['method'] ?? 'card'); +$platformId = trim($data['platform_id'] ?? ''); +$gameAlias = trim($data['game_alias'] ?? ''); +$playerName = trim($data['player_name'] ?? ''); +$isCustom = (bool)($data['is_custom'] ?? false); +$billing = $data['billing'] ?? []; +$userId = $_SESSION['user_id']; + +// ─── Validate ───────────────────────────────────────────── +if ($tokens < 1 || $priceCents < 100) { + echo json_encode(['success'=>false,'error'=>'Minimum purchase is $1 (1 token).']); exit; +} + +// Validate preset packages (custom bypasses preset check) +if (!$isCustom) { + $packages = json_decode(TOKEN_PACKAGES, true); + $validPkg = false; + foreach ($packages as $pkg) { + if ($pkg['tokens'] === $tokens && ($pkg['price'] * 100) === $priceCents) { $validPkg = true; break; } + } + if (!$validPkg) { + echo json_encode(['success'=>false,'error'=>'Invalid token package.']); exit; + } +} else { + // Custom: tokens must equal dollars (1:1 ratio), cap at $500 + if ($tokens !== ($priceCents / 100) || $tokens > 500) { + echo json_encode(['success'=>false,'error'=>'Invalid custom amount.']); exit; + } +} + +// Sanitize billing fields +$billingFirst = substr(trim($billing['first_name'] ?? ''), 0, 80); +$billingLast = substr(trim($billing['last_name'] ?? ''), 0, 80); +$billingAddress = substr(trim($billing['address'] ?? ''), 0, 200); +$billingCity = substr(trim($billing['city'] ?? ''), 0, 80); +$billingState = strtoupper(substr(trim($billing['state'] ?? ''), 0, 2)); +$billingZip = substr(trim($billing['zip'] ?? ''), 0, 10); +$billingEmail = substr(strtolower(trim($billing['email'] ?? '')), 0, 150); +$cardholderName = trim("$billingFirst $billingLast"); + +$isManual = in_array($method, ['venmo','chime','cashapp','zelle']); + +// ─── Manual payment ──────────────────────────────────────── +if ($isManual) { + $stmt = db()->prepare(" + INSERT INTO token_purchases + (user_id, tokens, amount_cents, payment_method, platform_id, game_alias, + player_name, billing_name, billing_email, is_custom, status) + VALUES (?,?,?,?,?,?,?,?,?,?,'pending') + "); + $stmt->execute([ + $userId, $tokens, $priceCents, $method, $platformId, $gameAlias, + $playerName, $cardholderName, $billingEmail, $isCustom ? 1 : 0 + ]); + $pid = db()->lastInsertId(); + logActivity('manual_payment_pending', $userId, null, 'purchase', 0, "Manual payment pending: {$paymentMethod} \${$amountDollars}"); +echo json_encode(['success'=>true,'manual'=>true,'purchase_id'=>$pid, + 'message'=>"Request #{$pid} submitted! Tokens credited after payment verification."]); + exit; +} + +// ─── Card payment via Square ─────────────────────────────── +if (empty($sourceId)) { echo json_encode(['success'=>false,'error'=>'Card payment token required.']); exit; } + +$square = new SquarePayment(); +$note = "TomGames {$tokens}tok | {$platformId} | {$gameAlias} | user#{$userId}" . ($isCustom ? ' [CUSTOM]' : ''); + +// Build buyer info for Square +$buyerInfo = []; +if ($cardholderName) $buyerInfo['buyer_email_address'] = $billingEmail ?: null; +$billingAddr = []; +if ($billingAddress) $billingAddr['address_line_1'] = $billingAddress; +if ($billingCity) $billingAddr['locality'] = $billingCity; +if ($billingState) $billingAddr['administrative_district_level_1'] = $billingState; +if ($billingZip) $billingAddr['postal_code'] = $billingZip; +$billingAddr['country'] = 'US'; + +$result = $square->charge($sourceId, $priceCents, $note, $cardholderName, $billingAddr, $billingEmail); + +if (!$result['success']) { + // Log failed attempt + db()->prepare("INSERT INTO token_purchases (user_id,tokens,amount_cents,payment_method,platform_id,game_alias,player_name,billing_name,billing_email,is_custom,status,failure_reason) VALUES (?,?,?,'card',?,?,?,?,?,?,'failed',?)") + ->execute([$userId,$tokens,$priceCents,$platformId,$gameAlias,$playerName,$cardholderName,$billingEmail,$isCustom?1:0,$result['error']]); + echo json_encode(['success'=>false,'error'=>$result['error']]); exit; +} + +// ─── Credit tokens ───────────────────────────────────────── +db()->beginTransaction(); +try { + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$tokens,$userId]); + $cardBrand = $result['card_brand'] ?? null; + $cardLast4 = $result['last_4'] ?? null; + $receiptUrl= $result['receipt_url'] ?? null; + + db()->prepare(" + INSERT INTO token_purchases + (user_id, tokens, amount_cents, payment_method, square_payment_id, + platform_id, game_alias, player_name, billing_name, billing_address, + billing_city, billing_state, billing_zip, billing_email, + is_custom, card_brand, card_last4, receipt_url, status) + VALUES (?,?,?,'card',?,?,?,?,?,?,?,?,?,?,?,?,?,?,'completed') + ")->execute([ + $userId, $tokens, $priceCents, $result['payment_id'], + $platformId, $gameAlias, $playerName, + $cardholderName, $billingAddress, $billingCity, $billingState, $billingZip, $billingEmail, + $isCustom ? 1 : 0, $cardBrand, $cardLast4, $receiptUrl + ]); + + db()->commit(); +} catch (Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'Token credit failed. Payment ID: '.$result['payment_id']]); exit; +} + +// ─── Update saved billing (separate try — must NOT roll back token credit) ── +try { + db()->prepare(" + INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip,card_brand,card_last4) + VALUES (?,?,?,?,?,?,?,?,?,?) + ON DUPLICATE KEY UPDATE + card_brand=VALUES(card_brand), card_last4=VALUES(card_last4), + first_name=COALESCE(NULLIF(VALUES(first_name),''),first_name), + last_name=COALESCE(NULLIF(VALUES(last_name),''),last_name), + email=COALESCE(NULLIF(VALUES(email),''),email), + address=COALESCE(NULLIF(VALUES(address),''),address), + city=COALESCE(NULLIF(VALUES(city),''),city), + state=COALESCE(NULLIF(VALUES(state),''),state), + zip=COALESCE(NULLIF(VALUES(zip),''),zip) + ")->execute([ + $userId, $billingFirst, $billingLast, $billingEmail, + $billingAddress, $billingCity, $billingState, $billingZip, + $cardBrand, $cardLast4 + ]); +} catch (Exception $e) { /* non-critical — tokens already credited */ } + +$bal = db()->prepare("SELECT tokens FROM users WHERE id=?"); +$bal->execute([$userId]); +$newBal = (float)$bal->fetchColumn(); +logActivity('token_purchase', $userId, null, 'purchase', (int)db()->lastInsertId(), + "Bought {$tokens} tokens via {$paymentMethod} for \${$amountDollars}"); +echo json_encode([ + 'success' => true, + 'manual' => false, + 'tokens_added' => (int)$tokens, + 'new_balance' => $newBal, + 'payment_id' => $result['payment_id'], + 'card_brand' => $cardBrand, + 'card_last4' => $cardLast4, + 'receipt_url' => $receiptUrl, +]); diff --git a/api/referrals.php b/api/referrals.php new file mode 100644 index 0000000..b335c9b --- /dev/null +++ b/api/referrals.php @@ -0,0 +1,255 @@ +false,'error'=>'Not authenticated']); exit; } + +$userId = (int)$_SESSION['user_id']; +$isAdmin = !empty($_SESSION['is_admin']); +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? 'status'; + +// ── GET actions ─────────────────────────────────────────── +if ($method === 'GET') { + + if ($action === 'status') { + // Player's referral dashboard data + $user = db()->prepare("SELECT referral_code, referred_by FROM users WHERE id=?"); + $user->execute([$userId]); + $u = $user->fetch(); + + // Auto-generate code if missing + if (empty($u['referral_code'])) { + $code = strtoupper(substr(md5($userId.uniqid()),0,8)); + db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]); + $u['referral_code'] = $code; + } + + // Count verified referrals + $countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'"); + $countStmt->execute([$userId]); + $verified = (int)$countStmt->fetchColumn(); + + // All referrals + $refs = db()->prepare(" + SELECT r.*, u.username, u.alias, u.email_verified, u.created_at AS joined_at + FROM referrals r + JOIN users u ON r.referred_id = u.id + WHERE r.referrer_id = ? + ORDER BY r.created_at DESC + "); + $refs->execute([$userId]); + + // Current tier + $tier = db()->query(" + SELECT * FROM referral_tiers + WHERE is_active=1 AND min_referrals <= $verified + ORDER BY min_referrals DESC LIMIT 1 + ")->fetch(); + + // Next tier + $nextTier = db()->query(" + SELECT * FROM referral_tiers + WHERE is_active=1 AND min_referrals > $verified + ORDER BY min_referrals ASC LIMIT 1 + ")->fetch(); + + // Total tokens earned from referrals + $earned = db()->prepare("SELECT COALESCE(SUM(tokens_awarded),0) FROM referrals WHERE referrer_id=? AND status='verified'"); + $earned->execute([$userId]); + + // Social shares + $shares = db()->prepare("SELECT * FROM referral_social_shares WHERE user_id=? ORDER BY created_at DESC"); + $shares->execute([$userId]); + + echo json_encode([ + 'success' => true, + 'referral_code' => $u['referral_code'] ?? '', + 'referral_url' => (defined('SITE_URL')?SITE_URL:'https://tomtomgames.com') . '/?ref=' . ($u['referral_code'] ?? ''), + 'verified_count' => $verified, + 'total_earned' => (float)$earned->fetchColumn(), + 'current_tier' => $tier, + 'next_tier' => $nextTier, + 'referrals' => $refs->fetchAll(), + 'social_shares' => $shares->fetchAll(), + ]); + exit; + } + + if ($action === 'tiers') { + $rows = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 ORDER BY min_referrals ASC")->fetchAll(); + echo json_encode(['success'=>true,'tiers'=>$rows]); + exit; + } + + if ($action === 'all_tiers' && $isAdmin) { + $rows = db()->query("SELECT * FROM referral_tiers ORDER BY sort_order ASC, min_referrals ASC")->fetchAll(); + echo json_encode(['success'=>true,'tiers'=>$rows]); + exit; + } + + if ($action === 'admin_list' && $isAdmin) { + $status = $_GET['status'] ?? 'pending'; + $stmt = db()->prepare(" + SELECT r.*, + ru.username AS referrer_name, ru.alias AS referrer_alias, + rd.username AS referred_name, rd.alias AS referred_alias, rd.email_verified, + t.name AS tier_name + FROM referrals r + JOIN users ru ON r.referrer_id = ru.id + JOIN users rd ON r.referred_id = rd.id + LEFT JOIN referral_tiers t ON r.tier_id = t.id + WHERE r.status = ? + ORDER BY r.created_at DESC + LIMIT 100 + "); + $stmt->execute([$status]); + echo json_encode(['success'=>true,'referrals'=>$stmt->fetchAll()]); + exit; + } + + if ($action === 'admin_shares' && $isAdmin) { + $status = $_GET['status'] ?? 'pending'; + $stmt = db()->prepare(" + SELECT rs.*, u.username, u.alias + FROM referral_social_shares rs + JOIN users u ON rs.user_id = u.id + WHERE rs.status = ? + ORDER BY rs.created_at DESC + "); + $stmt->execute([$status]); + echo json_encode(['success'=>true,'shares'=>$stmt->fetchAll()]); + exit; + } + + echo json_encode(['success'=>false,'error'=>'Unknown action']); exit; +} + +// ── POST actions ────────────────────────────────────────── +if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } +$d = json_decode(file_get_contents('php://input'), true); + +if ($action === 'submit_share') { + $platform = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['platform'] ?? ''))); + if (!$platform) { echo json_encode(['success'=>false,'error'=>'Platform required']); exit; } + // Get bonus tokens for this platform from tiers config + $bonus = 5; // default + try { + db()->prepare("INSERT INTO referral_social_shares (user_id,platform,bonus_tokens) VALUES (?,?,?)") + ->execute([$userId, $platform, $bonus]); + echo json_encode(['success'=>true]); + } catch(Exception $e) { + echo json_encode(['success'=>false,'error'=>'Already submitted for this platform']); + } + exit; +} + +// ── Admin only below ────────────────────────────────────── +if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } + +if ($action === 'resolve_referral') { + $id = (int)($d['id'] ?? 0); + $status = $d['status'] ?? ''; + $note = substr(trim($d['note'] ?? ''), 0, 300); + if (!in_array($status, ['verified','denied','deleted'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; } + + $chk = db()->prepare("SELECT r.*, t.tokens_per_ref, t.min_referrals, t.bonus_tokens FROM referrals r LEFT JOIN referral_tiers t ON r.tier_id=t.id WHERE r.id=?"); + $chk->execute([$id]); + $ref = $chk->fetch(); + if (!$ref) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; } + + if ($status === 'verified') { + // Determine best tier for referrer + $countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'"); + $countStmt->execute([$ref['referrer_id']]); + $verifiedCount = (int)$countStmt->fetchColumn() + 1; // +1 for this one + + $tier = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 AND min_referrals <= $verifiedCount ORDER BY min_referrals DESC LIMIT 1")->fetch(); + $tokensToAward = $tier ? (float)$tier['tokens_per_ref'] : 5; + + // Check if this hits a bonus milestone + $bonusTokens = 0; + if ($tier && $verifiedCount == (int)$tier['min_referrals']) { + $bonusTokens = (float)$tier['bonus_tokens']; + } + + $totalAward = $tokensToAward + $bonusTokens; + + db()->beginTransaction(); + try { + db()->prepare("UPDATE referrals SET status='verified', tier_id=?, tokens_awarded=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?") + ->execute([$tier['id'] ?? null, $totalAward, (int)$_SESSION['user_id'], $note, $id]); + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$totalAward, $ref['referrer_id']]); + logAdminAction('REFERRAL_VERIFIED', (int)$_SESSION['user_id'], 'referral', $id, 'Verified referral #'.$id.' — awarded '.$totalAward.' tokens to user #'.$ref['referrer_id'], 'pending', 'verified', 'info'); + db()->commit(); + echo json_encode(['success'=>true,'tokens_awarded'=>$totalAward,'bonus'=>$bonusTokens,'tier'=>$tier['name']??'']); + } catch(Exception $e) { + db()->rollBack(); + echo json_encode(['success'=>false,'error'=>'DB error']); + } + } else { + db()->prepare("UPDATE referrals SET status=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?") + ->execute([$status, (int)$_SESSION['user_id'], $note, $id]); + echo json_encode(['success'=>true]); + } + exit; +} + +if ($action === 'resolve_share') { + $id = (int)($d['id'] ?? 0); + $status = $d['status'] ?? ''; + if (!in_array($status, ['approved','denied'])) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; } + $chk = db()->prepare("SELECT * FROM referral_social_shares WHERE id=?"); $chk->execute([$id]); $share = $chk->fetch(); + if (!$share) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; } + db()->prepare("UPDATE referral_social_shares SET status=?,admin_id=?,resolved_at=NOW() WHERE id=?")->execute([$status,(int)$_SESSION['user_id'],$id]); + if ($status === 'approved') { + db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$share['bonus_tokens'],$share['user_id']]); + logAdminAction('SOCIAL_SHARE_APPROVED', (int)$_SESSION['user_id'], 'referral_share', $id, 'Approved social share #'.$id.' — awarded '.$share['bonus_tokens'].' bonus tokens to user #'.$share['user_id'], '', 'approved', 'info'); + } + echo json_encode(['success'=>true]); + exit; +} + +// ── Tier CRUD ───────────────────────────────────────────── +if ($action === 'tier_create') { + $name = substr(trim($d['name']??''),0,100); + $minRefs = (int)($d['min_referrals']??1); + $tokPer = (float)($d['tokens_per_ref']??5); + $bonus = (float)($d['bonus_tokens']??0); + $desc = substr(trim($d['description']??''),0,300); + $sort = (int)($d['sort_order']??99); + $active = (int)(bool)($d['is_active']??1); + if (!$name) { echo json_encode(['success'=>false,'error'=>'Name required']); exit; } + db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)") + ->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + exit; +} + +if ($action === 'tier_update') { + $id = (int)($d['id']??0); + $name = substr(trim($d['name']??''),0,100); + $minRefs = (int)($d['min_referrals']??1); + $tokPer = (float)($d['tokens_per_ref']??5); + $bonus = (float)($d['bonus_tokens']??0); + $desc = substr(trim($d['description']??''),0,300); + $sort = (int)($d['sort_order']??0); + $active = (int)(bool)($d['is_active']??1); + if (!$id||!$name) { echo json_encode(['success'=>false,'error'=>'ID and name required']); exit; } + db()->prepare("UPDATE referral_tiers SET name=?,min_referrals=?,tokens_per_ref=?,bonus_tokens=?,description=?,is_active=?,sort_order=? WHERE id=?") + ->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort,$id]); + echo json_encode(['success'=>true]); + exit; +} + +if ($action === 'tier_delete') { + $id = (int)($d['id']??0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("DELETE FROM referral_tiers WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + exit; +} + +echo json_encode(['success'=>false,'error'=>'Unknown action']); diff --git a/api/register.php b/api/register.php new file mode 100644 index 0000000..6d2bce9 --- /dev/null +++ b/api/register.php @@ -0,0 +1,18 @@ +false,'error'=>'Method not allowed']); exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +$username = trim($data['username'] ?? ''); +$password = trim($data['password'] ?? ''); +$alias = trim($data['alias'] ?? ''); +$email = trim($data['email'] ?? ''); +$referralCode= trim($data['referral_code']?? ''); + +logSecurityEvent('REGISTER_ATTEMPT', null, 'Registration attempt for: ' . $email, 'info'); +$result = initiateRegistration($username, $password, $alias, $email, $referralCode); +echo json_encode($result); diff --git a/api/resend_verify.php b/api/resend_verify.php new file mode 100644 index 0000000..7d1c539 --- /dev/null +++ b/api/resend_verify.php @@ -0,0 +1,16 @@ +false,'error'=>'Method not allowed']); exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +$email = trim($data['email'] ?? ''); + +if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success'=>false,'error'=>'Valid email required']); exit; +} + +echo json_encode(resendVerification($email)); diff --git a/assets/img/egame99.svg b/assets/img/egame99.svg new file mode 100644 index 0000000..acbed0c --- /dev/null +++ b/assets/img/egame99.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + 🎮 + + eGAME 99 + + + + + + diff --git a/assets/img/firekirin.svg b/assets/img/firekirin.svg new file mode 100644 index 0000000..768a024 --- /dev/null +++ b/assets/img/firekirin.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + 🔥 + + KIRIN + + + + + + diff --git a/assets/img/icon-192.png b/assets/img/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3f325e8f905bb8e77e4f05382cad10eb9d6647 GIT binary patch literal 1088 zcmZuxdr(wW7(aJm;R?%2im;0=-hc$8flVPy20XAAWI`)* zau*F9iIK%6KzY=fmCO`HGqJ;rBO42LdCnNI=q?Z;+r;fsN17U^>5p^H@B7a0@%?e; zoJ~nO=;Gw*1ORZ6$B_zje)D+YJhV@J&87fg?#C7@asb0bNNWH=TTNc39FN z`^A%|dhZ#HY)o)tr^lCAL7z}pfA{WAvYq{;3TqR>aBh8dWZgw$ah^~3e$pDOH}Qvy zg6O^@Vw#^Ac4!nZ+M@5$?CB{|DZh;|d?Fx_U}vR6PKL<6bqbz-saDrTS!DzMs9jp9 ze_OMeuDF@Uc*yMxkw<2&7PW)ws$YVw&6}t1Z3tvDssS@-XsQT5K(G#s#o17~7Jti5 z)nEFRC#Ik2%U{Kqa0|`l zKT6*NZRN&yUk6NZ%u+lsRQikug%E6Up00(mmS~p;Fny-_ygN>F?nOaBlZHcEAl)FQ zIZl^rr>c74juZ6F2CAyPcNP@TcJGH;Kq@>iegB2ATRUj(^k@5l#uN>0Ul>EN8eJpM z_t*9u-$StF^J|DW0+`I`9t5@mW(H0p0K5Y>(VQh98hCeJpsX{Ag*;ce#6k7vtF`V_ z9Lg#NG^Y&6K|EyaO0X{}X8)F!ed+(SOWhNV7^+;Bi0 zZivSo7{o%(~Su_E0C-t;xHoM&^Ocxmpz(H@B^(zbUO&LYP`zdw}&AnF=8pVM+1^= zc0>kq7%{#H`nK@~Qp_jb1T~*uvBPB%+p966Y~m0UJsfMPwWFk__QEZo)uWrI)(R!R z0%C3YC=^|vN)_aStd4v?Ur@7NEFlD=mK~>7C}~>fhyx(2R=_~YIXAGI5M(F6T*V^< zKPO)Gg54kRaGAJC&xSzJ&}f%27bQ3D3W^lFp1B`ZviLhCb-VpQ8H}m5m%Bw}+Xz9- z8^$4LEXzO1vMa+Gmpyf~Z|NN<+TCULMk8D|??<@PtU$QkoR9Fwg8+}sp5KRMb)50? zBLv%qUvsd(W-4k$(_}=Xf^YqOnhPlD#iZnYP#RDA1regQmaP($ZaFK&V>J(Sn5j|2 z&9#A5jjF$(5S|t@z2kVwbFsFB=lK_pHE?g0#ek`sO?mg{GD2ZJqfLuhNzaD$)j6rh nK!oF^fK`4-x>9@!J%M%bV`iwjqv&`x`Wb*cCW-8ce&ys}+1|4@ literal 0 HcmV?d00001 diff --git a/assets/img/icon-512.png b/assets/img/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..bf41cc8ffc16c9baee4038b4283a83349f4c8c38 GIT binary patch literal 3856 zcmbVPdsI_b_TD#yw}B80K@g2Rg@{m*ilRK^B?Y8f1(Zi(9r0BS)hcv|$@NtN)TLDs zEg;NTYkd(dMG!P(+QiYRTKiCseXNahQQ6w4FCk0^HLWA@Nmcj7w)IhFXc3VM{j0o@}i2y0dw)*@g3qF3+6mK zlK;ce%FL2&*IKsuZLpu{KCuV>mD=byrn~I1`_H<+Kaj)NUuAX)2q(d!VVlMtFyb#w zg97u&PnAxzg(wMvt5fy1JBC(1S#5l_RQyRG7>!jG6KG{}7@^q1Ty71qUtA@HyOR@+ zRBAODn?s-|Twf=knEw93MdqYpCePqa`TyOEF8@4&Pufh9 zUA;!R$3AaJ@R=CZD%LE>o(D{h>R4wXyXzHC!dk^dyi&s6^a_IkDke3Lbrc1|QL6ik zw{k8K=)n`;VYc!nJsUyKW^L$ry>&wVGpk)6i(JO&$%G(aVe3)FV)Sn zRH5pC*=dj`N>s)Ls>8xqi%tE0JuQ16Ar7XoQfKFzsZ{+|^f6}LIWO{SYKBuxt$dZ3@C{QhJyXN99}tqsl*DmuwdE}bqO+LYqbR0_ z?=`?(nL6=I7b8|?!=Nb9-<-+*wIdihv~u@HVd~ytFS6FOoB!0Ohi-B))KMLTfU4IE zh|Y92xFv_}pCJW_R?d5;#$xa1TTd~9q&9{(B?n3*nqLjMbZo(XxG>pHZS9~M&IA$_ zY3ykSo2h=;Ox{o<bM}1D zduje=7)J{Z9RgK|ee>4GRMxaef?34gT7G(s^7Gw4ls?%G9z!kGZU__DkiiVCD}opG>b^4-l-x=ziSwMb`c5Q^2<#Q!jUeJ*V;Y; z_>-}G!&hMJkr}+`_PdclN4b?1sbZett_yjyW%@=Q?D3Lc$Adaj1G;M4Pj`?xk&K~a zU2A}ndsr_p2izbjU6s}Ap2Ri~MIri6WR6mMJG6=|H)GLlK9p;9C%H(yo3JW9LcEE__1;J#Y>63t%B%IwF9; z6S##Z#HX*}&po143UIU^Lz#$&3&xU+QV{J=z~seVB%>P>9frg9cY*4OGF;_D%Xe6r zkx$`qUo_B`u?jH{USD$(jG7#>-35}4=O9f0kr#}N2RxHl3BRBoVE@uM1bZkpao{Tm zjwR=CU=xBb!sb8@j0vmfnRjzwDS}-T`5YJzpC*93=!)P>)LIOFoZrvrmj-|4hSfNX zggZD4N1d^0sFRn(Nfvt~u@=u8*#NI`-UUF7Yyi~12-!6;kcgANdrU_&5|l5@$!Law zjdSV4fD+!&I3x=}h3m&4p_8E(IrgKP^Wg!3pq6fTN=vv+%^qJq_bV9k`$p4BAb^ zVIQ{@axm?(hS}4RcBuV>?Nb1}Z_nh6Lw{R~Mw>a2Gww6tC?)(eF7x+m9+geuCf``I z!x5yXUcs@j$XbR}s}3~D3DB>F5nve?wSn7fsJ=}JOhbFM1C-WAdD)lvO#qB~xbk-vZY4@@10S;>>g zzut*iG;~qJ=T+!Xus}5vH3jJ#8&E3QZk)04B<>emprz}5(cz)QlF5m&{cw(BE2Ved zosHqIhU<5UxrtZl#Z%lc-eaw8Suye*Ojf!6&bQ+zE=y(ltv9*f5WPBVKuA_O!55}- z+l3t75c;+-RPme5fT{Ei)Njha9RZ#KSkK+jcwHm3quSQj{^}S*iUa9ytRL9GgR*(5 z+fzGM!UR7kiqNkvH^0icOclx78Xk@3T#lK_A0;$6TmM2BT)D2nPE@Rdp}oxJd#3|A z=;5*Q)5KCPy7dfGvJ0;0lPBj-;P(=OX4Bdsm;i#7$O&>9ZcxIa!Y} z+Z{@UmcQ=r=Vtp&`IA4HgPi1>^lw*0ZNd}{)|^H2?_rr5K$t~KSan9CbF&QvZLoKBYsF3MhWg@cIB%eL>hDzWQg3pHLgz2?WhHv;*9_>%gr~Bkh zP!yzgy%DN@;x8m+rd*z%F=~rHuk0+d&TyZaq4y>_)7W{~WOd))4R=G-k!SWXY069( z6es!@6tgumLZPG9;(1D@?y2;~2D*y>yn)89dckN{{WW#W@p-H5qP6&54Ocg;<4Z%a z6W`6u*(@T@FyANVu}%%HhOtzJ%L9|mgX`!0M*I2O#;-(!K0VXaUgS7a?$|+L&lI~9 z9@jL>ibB*ms(IWyp?LJ2a7&oDydsZX!yJH9!Rl|yQrU%Z^Wc_uqLRsGXI0d|sR(ud zP#zmtu@yg1>Zi{iCGpx^{3Av+@8c^b;+rLdZQtlhm~&Kq=0bMo#z}-ZPZf?$|D27^ zgtouP8Hl}ywY978YwwtB)y4&wT*%2mhaFRXXb{#~|Q>FKcMoOj&;Y}(i zemB$6vJav^V1x~oCtRSd*4BP{Wj@dFIW@fg$0Z-a>(^A@a_!G=p6MSh+rC$np~06f NWTwqeZJn#w_8(M0$IJi# literal 0 HcmV?d00001 diff --git a/assets/img/logo-icon.svg b/assets/img/logo-icon.svg new file mode 100644 index 0000000..cd251a4 --- /dev/null +++ b/assets/img/logo-icon.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/milkyway.svg b/assets/img/milkyway.svg new file mode 100644 index 0000000..b89a5fd --- /dev/null +++ b/assets/img/milkyway.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 🌌 + MILKY WAY + + + diff --git a/assets/img/noble777.svg b/assets/img/noble777.svg new file mode 100644 index 0000000..16a61e5 --- /dev/null +++ b/assets/img/noble777.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + 👑 + + 777 + NOBLE + + + + + + diff --git a/assets/img/og-image.svg b/assets/img/og-image.svg new file mode 100644 index 0000000..62fee62 --- /dev/null +++ b/assets/img/og-image.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TomTomGames + + Buy tokens for VBlink777 · Fire Kirin · Milky Way · Ultra Panda & more + + + ⚡ Instant Delivery + + 🔒 SSL Secured + + 💬 24/7 Support + diff --git a/assets/img/pandamaster.svg b/assets/img/pandamaster.svg new file mode 100644 index 0000000..b7ec28a --- /dev/null +++ b/assets/img/pandamaster.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + 🐾 + + MASTER + + diff --git a/assets/img/ultrapanda.svg b/assets/img/ultrapanda.svg new file mode 100644 index 0000000..f8e384f --- /dev/null +++ b/assets/img/ultrapanda.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + 🐼 + + ULTRA + + + + diff --git a/assets/img/vblink777.svg b/assets/img/vblink777.svg new file mode 100644 index 0000000..47e8e61 --- /dev/null +++ b/assets/img/vblink777.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + 🎰 + + 777 + + + + + + diff --git a/bump_version.php b/bump_version.php new file mode 100644 index 0000000..a5ab587 --- /dev/null +++ b/bump_version.php @@ -0,0 +1,52 @@ + 'Forbidden']); + exit; +} + +require_once __DIR__ . '/../includes/db.php'; + +// Get current version +$current = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn() ?: '1.0.0'; +[$major, $minor, $patch] = array_map('intval', explode('.', $current)); + +// Determine new version +if (!empty($_GET['version'])) { + $newVersion = trim($_GET['version']); +} elseif (!empty($_GET['bump'])) { + switch ($_GET['bump']) { + case 'major': $newVersion = ($major+1).'.0.0'; break; + case 'minor': $newVersion = $major.'.'.($minor+1).'.0'; break; + default: $newVersion = $major.'.'.$minor.'.'.($patch+1); break; + } +} else { + // Default: bump patch + $newVersion = $major.'.'.$minor.'.'.($patch+1); +} + +$notes = trim($_GET['notes'] ?? 'Build ' . date('Y-m-d H:i:s')); + +db()->prepare("INSERT INTO app_version (version, notes) VALUES (?, ?)") + ->execute([$newVersion, $notes]); + +header('Content-Type: application/json'); +echo json_encode([ + 'success' => true, + 'previous' => $current, + 'new_version' => $newVersion, + 'notes' => $notes, + 'timestamp' => date('Y-m-d H:i:s'), +]); diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ef9c61649366bd296d35e51c2f75986466bd7f86 GIT binary patch literal 136 zcmZQzU<5(|0R|wcz)-}%z#s<1odJICyj)UTKqjxJhf5HU2C85X;9vui@}K`F0x3&R z7srr_TgeFvOn(#{cxWFvt01w + + + T + \ No newline at end of file diff --git a/fix_broadcast.php b/fix_broadcast.php new file mode 100644 index 0000000..b8b66f1 --- /dev/null +++ b/fix_broadcast.php @@ -0,0 +1,28 @@ +query(\$sql);\n echo json_encode(['success'=>true,'broadcasts'=>\$stmt->fetchAll()]);\n } catch(Exception \$e) {\n echo json_encode(['success'=>false,'error'=>\$e->getMessage()]);\n }\n break;"; + +$c = substr($c, 0, $idx) . $new . substr($c, $end); +file_put_contents($f, $c); + +// Verify it works +require_once '/home/tomtomgames.com/includes/config.php'; +require_once '/home/tomtomgames.com/includes/db.php'; + +try { + $stmt = db()->query("SELECT COUNT(*) FROM broadcasts"); + echo "OK - broadcasts in DB: " . $stmt->fetchColumn() . "\n"; + $stmt2 = db()->query("SELECT b.id, b.subject, b.sent_at, u.username AS sender_name FROM broadcasts b JOIN users u ON b.admin_id=u.id ORDER BY b.sent_at DESC LIMIT 5"); + $rows = $stmt2->fetchAll(); + echo "Query works - rows: " . count($rows) . "\n"; + foreach($rows as $r) echo " #{$r['id']}: {$r['subject']} by {$r['sender_name']}\n"; +} catch(Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} +echo "DONE - delete this file\n"; diff --git a/games/index.php b/games/index.php new file mode 100644 index 0000000..87004dd --- /dev/null +++ b/games/index.php @@ -0,0 +1,232 @@ + + + + + + +Buy Fish Table Game Tokens | VBlink777, Fire Kirin, Milky Way | TomTomGames + + + + + + + + + + + + + + + + +
+ +
+ +
+
+

Buy Game Tokens for Fish Table & Skill Games

+

The fastest, most trusted way to load up on tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.

+ 🪙 Buy Tokens Now +
+ 🔒 SSL Secured + ⚡ Instant Delivery + 💳 Multiple Payment Methods + 💬 24/7 Support + 🎮 7 Game Platforms +
+
+
+ +
+
+

Supported Game Platforms

+

Buy tokens for all major fish table and skill game platforms in one place.

+
+
+

VBlink777

+

One of the most popular fish table games. Buy VBlink777 tokens securely and get them credited fast to your game account.

+ Buy VBlink777 Tokens → +
+
+

Fire Kirin

+

Fire Kirin is a top-rated skill game with exciting fish table gameplay. Purchase Fire Kirin tokens through TomTomGames for instant delivery.

+ Buy Fire Kirin Tokens → +
+
+

Milky Way

+

Milky Way 777 game credits — buy tokens for one of the best online fish table platforms. Secure payment, fast top-up.

+ Buy Milky Way Tokens → +
+
+

Ultra Panda

+

Ultra Panda game tokens for sale. Top up your account instantly through our secure portal and start playing right away.

+ Buy Ultra Panda Tokens → +
+
+

Panda Master

+

Panda Master tokens — buy game credits quickly and safely. Multiple payment methods accepted including card, Venmo, and Cash App.

+ Buy Panda Master Tokens → +
+
+

Noble 777

+

Noble 777 game token purchases made easy. Register, select your package, and have tokens in your account within minutes.

+ Buy Noble 777 Tokens → +
+
+

eGame99

+

eGame99 tokens for sale at the best rates. Fast crediting, 24/7 customer support, and a seamless buying experience.

+ Buy eGame99 Tokens → +
+
+
+
+ +
+
+

How to Buy Tokens in 3 Steps

+

Get tokens credited to your game account in minutes.

+
+
+
1
+

Create Account

+

Register free in under 60 seconds. Verify your email and log in.

+
+
+
2
+

Select Game & Package

+

Choose your game platform, enter your in-game alias, and pick a token package — or enter a custom amount.

+
+
+
3
+

Pay & Play

+

Pay securely by card or manual transfer. Tokens are credited to your game account fast.

+
+
+
+

Accepted Payment Methods

+
+
💳 Credit / Debit Card
+
💙 Venmo
+
💚 Cash App
+
🟢 Chime
+
💜 Zelle
+
+
+
+
+ +
+
+

Frequently Asked Questions

+

Everything you need to know about buying game tokens.

+
+
+

What is TomTomGames?

+

TomTomGames is a token portal that lets you purchase game credits for popular fish table and skill games including VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99. We handle the token purchase securely so you can focus on playing.

+
+
+

How quickly are tokens delivered?

+

Card payments are processed instantly through Square. Manual payments (Venmo, Zelle, Cash App, Chime) are credited within a few minutes after we confirm receipt of your payment.

+
+
+

How much do tokens cost?

+

Tokens are priced at $1 per token. We offer packages starting from 5 tokens ($5) up to 100 tokens ($100), or you can enter a custom amount up to $500. Volume packages are available — contact support for details.

+
+
+

Is my payment information secure?

+

Yes. All card transactions are processed through Square, a fully PCI-compliant payment processor. We never store your full card number. Manual payment methods require no card details at all.

+
+
+

What if I need help?

+

Our support team is available 24/7 through the live chat feature inside the app. You can also send a message through your account and we'll respond within minutes.

+
+
+

Can I cash out my tokens?

+

Yes. You can request a cashout through your account and receive your funds via your preferred payment method. Cashouts are processed by our team promptly.

+
+
+ +
+
+ +
+
+

TomTomGames — Your trusted token portal for fish table and skill games.

+

Home · Games · Support

+

© TomTomGames. All rights reserved. Game tokens are for entertainment purposes on supported platforms only. Please play responsibly.

+
+
+ + + diff --git a/get_location.php b/get_location.php new file mode 100644 index 0000000..cac2f09 --- /dev/null +++ b/get_location.php @@ -0,0 +1,108 @@ + true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $token, + 'Square-Version: 2024-01-18', + 'Content-Type: application/json', + ], + CURLOPT_TIMEOUT => 15, +]); +$resp = curl_exec($ch); +$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($code === 200) { + $data = json_decode($resp, true); + $locations = $data['locations'] ?? []; +} else { + $error = "HTTP $code: " . htmlspecialchars($resp); +} +?> + + + + + +Square Location ID Finder + + + +

🔑 Square Location Finder

+
TomGames — Run once, then delete this file
+ + +
Error fetching locations:
+ +
No locations found on this Square account.
+ +

Found location(s). Copy the ID for your main location:

+ +
+
+
+
+ +
+
+ + · + Currency: · + Country: +
+
+ + + + + +
⚠️ Security: Delete get_location.php from your server after use. It exposes your access token.
+ + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..3d222b7 --- /dev/null +++ b/index.php @@ -0,0 +1,3283 @@ + + + + + + + + + + + + + + +TomTomGames — Buy Game Tokens | VBlink777, Fire Kirin, Milky Way & More + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/install.php b/install.php new file mode 100644 index 0000000..d7f1275 --- /dev/null +++ b/install.php @@ -0,0 +1,206 @@ +Access denied. Add ?key=TomGames2024Admin to URL.'); + +require_once __DIR__ . '/../includes/config.php'; + +$log = []; +function ok($msg) { global $log; $log[] = ['t'=>'ok', 'm'=>$msg]; } +function err($msg) { global $log; $log[] = ['t'=>'err', 'm'=>$msg]; } +function warn($msg) { global $log; $log[] = ['t'=>'warn','m'=>$msg]; } +function info($msg) { global $log; $log[] = ['t'=>'info','m'=>$msg]; } + +try { + $pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4", DB_USER, DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + ok('Connected to '.DB_NAME.' as '.DB_USER.''); +} catch (Exception $e) { + die('
CONNECTION FAILED: '.htmlspecialchars($e->getMessage()).'
'); +} + +// Helper: check if column exists +function colExists(PDO $pdo, string $table, string $col): bool { + $r = $pdo->query("SHOW COLUMNS FROM `$table` LIKE '$col'")->fetch(); + return (bool)$r; +} + +// ── CREATE TABLES ─────────────────────────────────────────── +$tables = [ +'users' => "CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + alias VARCHAR(100) NOT NULL, + email VARCHAR(150) UNIQUE, + email_verified TINYINT(1) DEFAULT 0, + tokens DECIMAL(10,2) DEFAULT 0, + is_admin TINYINT(1) DEFAULT 0, + status ENUM('active','suspended') DEFAULT 'active', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + +'pending_registrations' => "CREATE TABLE IF NOT EXISTS pending_registrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + alias VARCHAR(100) NOT NULL, + email VARCHAR(150) NOT NULL, + token VARCHAR(64) UNIQUE NOT NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + +'token_purchases' => "CREATE TABLE IF NOT EXISTS token_purchases ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + tokens INT NOT NULL, + amount_cents INT NOT NULL, + payment_method VARCHAR(20) DEFAULT 'card', + square_payment_id VARCHAR(255), + platform_id VARCHAR(50), + game_alias VARCHAR(100), + player_name VARCHAR(100), + billing_name VARCHAR(160), + billing_address VARCHAR(200), + billing_city VARCHAR(80), + billing_state VARCHAR(2), + billing_zip VARCHAR(10), + billing_email VARCHAR(150), + is_custom TINYINT(1) DEFAULT 0, + failure_reason TEXT, + card_brand VARCHAR(30), + card_last4 VARCHAR(4), + receipt_url VARCHAR(512), + status ENUM('pending','completed','failed') DEFAULT 'pending', + admin_note TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + +'cashout_requests' => "CREATE TABLE IF NOT EXISTS cashout_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + platform_id VARCHAR(50) NOT NULL, + alias VARCHAR(100) NOT NULL, + tokens DECIMAL(10,2) NOT NULL, + status ENUM('pending','approved','rejected') DEFAULT 'pending', + admin_note TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + +'saved_billing' => "CREATE TABLE IF NOT EXISTS saved_billing ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT UNIQUE NOT NULL, + first_name VARCHAR(80), + last_name VARCHAR(80), + email VARCHAR(150), + address VARCHAR(200), + city VARCHAR(80), + state VARCHAR(2), + zip VARCHAR(10), + card_brand VARCHAR(30), + card_last4 VARCHAR(4), + card_exp_month VARCHAR(2), + card_exp_year VARCHAR(4), + sq_card_id VARCHAR(255), + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + +'chat_messages' => "CREATE TABLE IF NOT EXISTS chat_messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + sender ENUM('user','admin') NOT NULL, + message TEXT NOT NULL, + is_read TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", +]; + +foreach ($tables as $name => $sql) { + try { $pdo->exec($sql); ok("Table $name ✓"); } + catch (Exception $e) { err("Table $name: ".htmlspecialchars($e->getMessage())); } +} + +// ── ADD MISSING COLUMNS (compatible with MySQL 5.6/5.7/8) ── +// Check existence first, then ALTER — works on all MySQL versions +$addCols = [ + // [table, column, definition, after] + ['token_purchases', 'billing_name', "VARCHAR(160)", 'player_name'], + ['token_purchases', 'billing_address', "VARCHAR(200)", 'billing_name'], + ['token_purchases', 'billing_city', "VARCHAR(80)", 'billing_address'], + ['token_purchases', 'billing_state', "VARCHAR(2)", 'billing_city'], + ['token_purchases', 'billing_zip', "VARCHAR(10)", 'billing_state'], + ['token_purchases', 'billing_email', "VARCHAR(150)", 'billing_zip'], + ['token_purchases', 'is_custom', "TINYINT(1) DEFAULT 0", 'billing_email'], + ['token_purchases', 'failure_reason', "TEXT", 'is_custom'], + ['token_purchases', 'card_brand', "VARCHAR(30)", 'failure_reason'], + ['token_purchases', 'card_last4', "VARCHAR(4)", 'card_brand'], + ['token_purchases', 'receipt_url', "VARCHAR(512)", 'card_last4'], + ['token_purchases', 'admin_note', "TEXT", 'status'], + ['users', 'email_verified', "TINYINT(1) DEFAULT 0", 'email'], +]; + +foreach ($addCols as [$tbl, $col, $def, $after]) { + if (colExists($pdo, $tbl, $col)) { + ok("Column $tbl.$col already exists ✓"); + } else { + try { + $pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`"); + ok("Column $tbl.$col added ✓"); + } catch (Exception $e) { + err("Column $tbl.$col: ".htmlspecialchars($e->getMessage())); + } + } +} + +// ── FIX ADMIN email_verified ──────────────────────────────── +try { + $n = $pdo->exec("UPDATE users SET email_verified=1 WHERE is_admin=1"); + ok("Admin accounts email_verified set to 1 ($n updated)"); +} catch (Exception $e) { warn("Admin fix: ".htmlspecialchars($e->getMessage())); } + +// ── SUMMARY ───────────────────────────────────────────────── +$tables_now = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN); +info("All tables: ".implode(', ', $tables_now).""); + +try { + $total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); + $admins = $pdo->query("SELECT COUNT(*) FROM users WHERE is_admin=1")->fetchColumn(); + info("Users: $total total, $admins admin(s)"); +} catch (Exception $e) {} +?> + +TomTomGames DB Install + +

🎮 TomTomGames — DB Install / Repair

+
Database:
+ +
+ + +
⚠ Delete install.php after use — it exposes DB structure.
+ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b6df8ee --- /dev/null +++ b/manifest.json @@ -0,0 +1,43 @@ +{ + "name": "TomTomGames — Game Token Portal", + "short_name": "TomTomGames", + "description": "Buy tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 and eGame99. Fast, secure, mobile-first.", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a12", + "theme_color": "#f0c040", + "orientation": "portrait", + "scope": "/", + "lang": "en-US", + "categories": ["games", "entertainment"], + "icons": [ + { + "src": "/assets/img/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/assets/img/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "shortcuts": [ + { + "name": "Buy Tokens", + "short_name": "Buy", + "description": "Buy game tokens instantly", + "url": "/?action=buy", + "icons": [{ "src": "/assets/img/icon-192.png", "sizes": "192x192" }] + }, + { + "name": "Support", + "short_name": "Help", + "description": "Contact TomTomGames support", + "url": "/?action=chat", + "icons": [{ "src": "/assets/img/icon-192.png", "sizes": "192x192" }] + } + ] +} diff --git a/phpcheck.php b/phpcheck.php new file mode 100644 index 0000000..f97dab1 --- /dev/null +++ b/phpcheck.php @@ -0,0 +1,25 @@ +&1"); + echo $f . ": " . trim($out) . "\n"; +} + +// Also test DB connection directly +try { + require_once __DIR__ . '/../../includes/config.php'; + require_once __DIR__ . '/../../includes/db.php'; + $v = db()->query("SELECT COUNT(*) FROM users")->fetchColumn(); + echo "\nDB OK — users: $v\n"; + $v2 = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn(); + echo "App version: $v2\n"; +} catch (Throwable $e) { + echo "\nDB ERROR: " . $e->getMessage() . "\n"; + echo "File: " . $e->getFile() . " line " . $e->getLine() . "\n"; +} diff --git a/ref_test.php b/ref_test.php new file mode 100644 index 0000000..377e9b0 --- /dev/null +++ b/ref_test.php @@ -0,0 +1,26 @@ +query("SELECT id, username, referral_code FROM users")->fetchAll(); +$out = []; +foreach ($users as $u) { + $out[] = [ + 'id' => $u['id'], + 'username' => $u['username'], + 'referral_code' => $u['referral_code'], + 'referral_url' => 'https://tomtomgames.com/?ref=' . $u['referral_code'], + ]; +} + +// Also check tiers +$tiers = db()->query("SELECT id, name, min_referrals, tokens_per_ref, bonus_tokens, is_active FROM referral_tiers ORDER BY sort_order")->fetchAll(); + +echo json_encode([ + 'users' => $out, + 'tiers' => $tiers, + 'tier_count' => count($tiers), +], JSON_PRETTY_PRINT); diff --git a/referral_setup.php b/referral_setup.php new file mode 100644 index 0000000..b5063b1 --- /dev/null +++ b/referral_setup.php @@ -0,0 +1,96 @@ +exec("CREATE TABLE IF NOT EXISTS referral_tiers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + min_referrals INT NOT NULL DEFAULT 1, + tokens_per_ref DECIMAL(10,2) NOT NULL DEFAULT 10, + bonus_tokens DECIMAL(10,2) NOT NULL DEFAULT 0, + description VARCHAR(300), + is_active TINYINT(1) DEFAULT 1, + sort_order INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); +echo "referral_tiers table OK\n"; + +// 2. Seed tiers +$count = (int)db()->query("SELECT COUNT(*) FROM referral_tiers")->fetchColumn(); +if ($count == 0) { + $seeds = [ + ['Bronze Referrer', 1, 5, 0, 'Earn 5 tokens for each verified referral', 1, 0], + ['Silver Referrer', 5, 8, 25, 'Earn 8 tokens per referral + 25 bonus at 5 referrals', 1, 1], + ['Gold Referrer', 10, 10, 100, 'Earn 10 tokens per referral + 100 bonus at 10 referrals', 1, 2], + ['Elite Referrer', 25, 15, 250, 'Earn 15 tokens per referral + 250 bonus at 25 referrals', 1, 3], + ]; + $st = db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)"); + foreach ($seeds as $s) $st->execute($s); + echo "4 tiers seeded\n"; +} else { + echo "Tiers already exist: $count\n"; +} + +// 3. referrals table +db()->exec("CREATE TABLE IF NOT EXISTS referrals ( + id INT AUTO_INCREMENT PRIMARY KEY, + referrer_id INT NOT NULL, + referred_id INT NOT NULL UNIQUE, + tier_id INT, + status ENUM('pending','verified','denied','deleted') DEFAULT 'pending', + tokens_awarded DECIMAL(10,2) DEFAULT 0, + admin_id INT, + admin_note VARCHAR(300), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); +echo "referrals table OK\n"; + +// 4. referral_social_shares +db()->exec("CREATE TABLE IF NOT EXISTS referral_social_shares ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + platform VARCHAR(50) NOT NULL, + bonus_tokens DECIMAL(10,2) DEFAULT 5, + status ENUM('pending','approved','denied') DEFAULT 'pending', + admin_id INT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); +echo "referral_social_shares table OK\n"; + +// 5. Add referral_code to users +$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field'); +if (!in_array('referral_code', $cols)) { + db()->exec("ALTER TABLE users ADD COLUMN referral_code VARCHAR(20) UNIQUE"); + echo "referral_code column added\n"; +} + +// 6. Add referred_by to users +if (!in_array('referred_by', $cols)) { + db()->exec("ALTER TABLE users ADD COLUMN referred_by INT DEFAULT NULL"); + echo "referred_by column added\n"; +} + +// 7. Add referred_by to pending_registrations +$pcols = array_column(db()->query("SHOW COLUMNS FROM pending_registrations")->fetchAll(), 'Field'); +if (!in_array('referred_by', $pcols)) { + db()->exec("ALTER TABLE pending_registrations ADD COLUMN referred_by INT DEFAULT NULL"); + echo "pending_registrations.referred_by added\n"; +} + +// 8. Generate codes for users missing one +$users = db()->query("SELECT id FROM users WHERE referral_code IS NULL OR referral_code = ''")->fetchAll(); +$upd = db()->prepare("UPDATE users SET referral_code=? WHERE id=?"); +foreach ($users as $u) { + $code = strtoupper(substr(md5($u['id'].uniqid()), 0, 8)); + $upd->execute([$code, $u['id']]); +} +echo count($users) . " users given referral codes\n"; + +// 9. Show sample +$sample = db()->query("SELECT username, referral_code FROM users LIMIT 5")->fetchAll(); +foreach ($sample as $r) echo " " . $r['username'] . ": " . $r['referral_code'] . "\n"; + +echo "\nDONE\n"; diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..297f710 --- /dev/null +++ b/robots.txt @@ -0,0 +1,23 @@ +User-agent: * +Allow: / +Allow: /assets/ +Disallow: /admin/ +Disallow: /api/ +Disallow: /install.php +Disallow: /test.php +Disallow: /test_login.php +Disallow: /get_location.php +Disallow: /?q= + +# Block AI training bots (optional but protects content) +User-agent: GPTBot +Disallow: / + +User-agent: Google-Extended +Disallow: / + +User-agent: CCBot +Disallow: / + +# Sitemap +Sitemap: https://tomtomgames.com/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..661c133 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,21 @@ + + + + + + https://tomtomgames.com/ + 2026-05-19 + daily + 1.0 + + + + + https://tomtomgames.com/games/ + 2026-05-19 + weekly + 0.9 + + + diff --git a/test.php b/test.php new file mode 100644 index 0000000..00f0b67 --- /dev/null +++ b/test.php @@ -0,0 +1,6 @@ + PHP_VERSION, + 'time' => date('Y-m-d H:i:s'), + 'status' => 'ok' +]); diff --git a/test_login.php b/test_login.php new file mode 100644 index 0000000..08d99ca --- /dev/null +++ b/test_login.php @@ -0,0 +1,18 @@ +$e->getMessage()])); } +ob_end_clean(); +header('Content-Type: application/json'); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + $result = loginUser($data['username'] ?? '', $data['password'] ?? ''); + if ($result['success']) unset($result['user']['password']); + echo json_encode($result); + exit; +} + +// GET - show users +$users = db()->query("SELECT id,username,alias,is_admin,email_verified,status FROM users")->fetchAll(); +echo json_encode(['users'=>$users,'session_id'=>session_id()]); diff --git a/test_mail.php b/test_mail.php new file mode 100644 index 0000000..daaa45e --- /dev/null +++ b/test_mail.php @@ -0,0 +1,91 @@ + + + + +Email Test + + + +

TomTomGames — Email Diagnostics

+ +
+Server Info:
+PHP:
+Server:
+Hostname:
+mail() function:
+sendmail_path:
+SMTP: : +
+ + +
+Send Result:
+To:
+mail() returned:
+Error:
+ +
Check your inbox (and spam folder). If nothing arrives in 5 minutes, the server is not sending outbound mail. + +
mail() returned false — server cannot send email. You need to configure SMTP (PHPMailer) or enable sendmail. + +
+ + +
+
+ + + +
+
+ +

Delete test_mail.php after diagnosis is complete.

+ + diff --git a/verify.php b/verify.php new file mode 100644 index 0000000..e2d8d37 --- /dev/null +++ b/verify.php @@ -0,0 +1,66 @@ + + + + + + +<?= SITE_NAME ?> — Email Verified + + + + + + + +
+ + + + 🎉 +
Account Verified!
+

+ Welcome to , !

+ Your account is active and you've been automatically logged in. Let's play! +

+ 🎮 ENTER TOMTOMGAMES +

Redirecting in 4 seconds...

+ + + + +
Verification Failed
+

+ REGISTER AGAIN + BACK TO HOME + +
+ +