Initial commit
@@ -0,0 +1,5 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
*.swp
|
||||
|
||||
uploads/
|
||||
@@ -0,0 +1,117 @@
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# TomTomGames Security Configuration
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
Options -Indexes -Includes
|
||||
ServerSignature Off
|
||||
|
||||
# ── Block all sensitive file types ───────────────────────
|
||||
<FilesMatch "\.(sql|env|log|sh|md|git|bak|backup|old|orig|tmp|swp|cfg|ini|conf|yaml|yml|json.bak)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# ── Block direct access to sensitive PHP files ───────────
|
||||
<FilesMatch "^(phpcheck|test|test_mail|test_login|sgtest|install|config|db|auth|mailer|square|smtp)\.php$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# ── Block access to includes and vendor folders ──────────
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule ^includes/ - [F,L]
|
||||
RewriteRule ^vendor/ - [F,L]
|
||||
RewriteRule ^mail_queue/ - [F,L]
|
||||
RewriteRule ^\.git/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Block common attack vectors ──────────────────────────
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# ── Block access to WordPress paths (scanners look for these) ──
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^wp-admin - [F,L]
|
||||
RewriteRule ^wp-login - [F,L]
|
||||
RewriteRule ^xmlrpc - [F,L]
|
||||
RewriteRule ^\.env - [F,L]
|
||||
RewriteRule ^composer\. - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Security Headers ──────────────────────────────────────
|
||||
<IfModule mod_headers.c>
|
||||
# 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
|
||||
</IfModule>
|
||||
|
||||
# ── Canonical HTTPS + non-www redirect ───────────────────
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# ── Block PHP execution in uploads folder (if it exists) ─
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^uploads/.*\.php$ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Gzip compression ──────────────────────────────────────
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml
|
||||
</IfModule>
|
||||
|
||||
# ── Browser caching ───────────────────────────────────────
|
||||
<IfModule mod_expires.c>
|
||||
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"
|
||||
</IfModule>
|
||||
@@ -0,0 +1,117 @@
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# TomTomGames Security Configuration
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
Options -Indexes -Includes
|
||||
ServerSignature Off
|
||||
|
||||
# ── Block all sensitive file types ───────────────────────
|
||||
<FilesMatch "\.(sql|env|log|sh|md|git|bak|backup|old|orig|tmp|swp|cfg|ini|conf|yaml|yml|json.bak)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# ── Block direct access to sensitive PHP files ───────────
|
||||
<FilesMatch "^(phpcheck|test|test_mail|test_login|sgtest|install|config|db|auth|mailer|square|smtp)\.php$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# ── Block access to includes and vendor folders ──────────
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule ^includes/ - [F,L]
|
||||
RewriteRule ^vendor/ - [F,L]
|
||||
RewriteRule ^mail_queue/ - [F,L]
|
||||
RewriteRule ^\.git/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Block common attack vectors ──────────────────────────
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# ── Block access to WordPress paths (scanners look for these) ──
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^wp-admin - [F,L]
|
||||
RewriteRule ^wp-login - [F,L]
|
||||
RewriteRule ^xmlrpc - [F,L]
|
||||
RewriteRule ^\.env - [F,L]
|
||||
RewriteRule ^composer\. - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Security Headers ──────────────────────────────────────
|
||||
<IfModule mod_headers.c>
|
||||
# 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
|
||||
</IfModule>
|
||||
|
||||
# ── Canonical HTTPS + non-www redirect ───────────────────
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# ── Block PHP execution in uploads folder (if it exists) ─
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteRule ^uploads/.*\.php$ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ── Gzip compression ──────────────────────────────────────
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml
|
||||
</IfModule>
|
||||
|
||||
# ── Browser caching ───────────────────────────────────────
|
||||
<IfModule mod_expires.c>
|
||||
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"
|
||||
</IfModule>
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
if (isLoggedIn() && !empty($_SESSION['is_admin'])) {
|
||||
header('Location: /admin/index.php'); exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$result = loginUser($_POST['username'] ?? '', $_POST['password'] ?? '');
|
||||
if ($result['success'] && !empty($_SESSION['is_admin'])) {
|
||||
header('Location: /admin/index.php'); exit;
|
||||
} elseif ($result['success']) {
|
||||
logoutUser();
|
||||
$error = 'Access denied. Admin account required.';
|
||||
} else {
|
||||
$error = $result['error'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TomTomGames Admin Login</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@700;900&family=Rajdhani:wght@500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0a0a12;color:#e8e8f0;font-family:'Rajdhani',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.box{background:#1a1a2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:40px;width:100%;max-width:380px}
|
||||
.logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:4px}
|
||||
.sub{color:#8888aa;font-size:13px;margin-bottom:28px;letter-spacing:1px}
|
||||
.form-group{margin-bottom:16px}
|
||||
label{font-size:12px;font-weight:700;color:#8888aa;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px;display:block}
|
||||
input{width:100%;background:#181828;border:1px solid rgba(255,255,255,.07);border-radius:8px;padding:13px 15px;color:#e8e8f0;font-family:'Rajdhani',sans-serif;font-size:16px;outline:none;transition:border-color .2s}
|
||||
input:focus{border-color:#00e5ff}
|
||||
.btn{width:100%;padding:15px;border:none;border-radius:8px;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;letter-spacing:1px;cursor:pointer;margin-top:8px}
|
||||
.error{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:#ff4444;padding:12px 14px;border-radius:8px;font-size:14px;font-weight:600;margin-bottom:16px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="logo" style="display:flex;align-items:center;gap:10px;justify-content:center"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="36" height="36" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="ll1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="ll2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="gll"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#ll1)" filter="url(#gll)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span>TomTomGames</span></div>
|
||||
<div class="sub">ADMIN ACCESS</div>
|
||||
<?php if($error): ?><div class="error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="form-group"><label>Username</label><input type="text" name="username" autocomplete="username" autocapitalize="none" required></div>
|
||||
<div class="form-group"><label>Password</label><input type="password" name="password" autocomplete="current-password" required></div>
|
||||
<button class="btn" type="submit">LOGIN TO ADMIN</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,971 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
header('Content-Type: application/json');
|
||||
requireAdmin();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// ─── STATS ────────────────────────────────────────────────
|
||||
case 'stats':
|
||||
echo json_encode(['success' => 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']);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
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 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']);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
ob_end_clean();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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]);
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
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) {
|
||||
|
||||
// ── 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']);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try {
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
} catch (Throwable $e) {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success'=>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);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
header('Content-Type: application/json');
|
||||
logoutUser();
|
||||
echo json_encode(['success' => true]);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try {
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
} catch (Throwable $e) {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => 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]);
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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()]);
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../includes/square.php';
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn() || empty($_SESSION['is_admin'])) {
|
||||
echo json_encode(['success'=>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']);
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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']);
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>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']);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../includes/square.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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,
|
||||
]);
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isLoggedIn()) { echo json_encode(['success'=>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']);
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
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'] ?? '');
|
||||
$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);
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
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);
|
||||
$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));
|
||||
@@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#001a2e"/>
|
||||
<stop offset="100%" stop-color="#000810"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="cyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00e5ff"/>
|
||||
<stop offset="100%" stop-color="#0066ff"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<!-- Grid lines -->
|
||||
<line x1="0" y1="40" x2="120" y2="40" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
|
||||
<line x1="0" y1="80" x2="120" y2="80" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
|
||||
<line x1="40" y1="0" x2="40" y2="120" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
|
||||
<line x1="80" y1="0" x2="80" y2="120" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
|
||||
<!-- Controller icon -->
|
||||
<text x="60" y="56" font-size="36" text-anchor="middle" filter="url(#glow)" font-family="Arial">🎮</text>
|
||||
<!-- eGame99 -->
|
||||
<text x="60" y="78" font-size="13" font-weight="900" text-anchor="middle" fill="url(#cyan)" font-family="Arial Black, sans-serif" letter-spacing="1" filter="url(#glow)">eGAME 99</text>
|
||||
<!-- Corner accents -->
|
||||
<path d="M10 10 L25 10 L25 14 L14 14 L14 25 L10 25 Z" fill="url(#cyan)" opacity="0.6"/>
|
||||
<path d="M110 10 L95 10 L95 14 L106 14 L106 25 L110 25 Z" fill="url(#cyan)" opacity="0.6"/>
|
||||
<path d="M10 110 L25 110 L25 106 L14 106 L14 95 L10 95 Z" fill="url(#cyan)" opacity="0.6"/>
|
||||
<path d="M110 110 L95 110 L95 106 L106 106 L106 95 L110 95 Z" fill="url(#cyan)" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="70%" r="60%">
|
||||
<stop offset="0%" stop-color="#2a0800"/>
|
||||
<stop offset="100%" stop-color="#0a0200"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="fire" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#FF2200"/>
|
||||
<stop offset="50%" stop-color="#FF7700"/>
|
||||
<stop offset="100%" stop-color="#FFDD00"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2.5" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#fire)" stroke-width="1.5" opacity="0.6"/>
|
||||
<!-- Fire wave bottom -->
|
||||
<path d="M0 100 Q20 85 40 95 Q60 105 80 90 Q100 75 120 88 L120 120 L0 120 Z" fill="#FF2200" opacity="0.25"/>
|
||||
<!-- Fire emoji -->
|
||||
<text x="60" y="58" font-size="40" text-anchor="middle" filter="url(#glow)" font-family="Arial">🔥</text>
|
||||
<!-- KIRIN text -->
|
||||
<text x="60" y="82" font-size="13" font-weight="900" text-anchor="middle" fill="url(#fire)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">KIRIN</text>
|
||||
<!-- Sparks -->
|
||||
<circle cx="35" cy="35" r="2" fill="#FF7700" opacity="0.7"/>
|
||||
<circle cx="85" cy="28" r="1.5" fill="#FFDD00" opacity="0.8"/>
|
||||
<circle cx="25" cy="60" r="1.5" fill="#FF2200" opacity="0.6"/>
|
||||
<circle cx="95" cy="55" r="2" fill="#FF7700" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,38 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<defs>
|
||||
<linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f0c040"/>
|
||||
<stop offset="100%" stop-color="#ff6b35"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00e5ff"/>
|
||||
<stop offset="100%" stop-color="#7b2fbe"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Controller body -->
|
||||
<rect x="6" y="16" width="36" height="22" rx="11" fill="url(#g1)" filter="url(#glow)"/>
|
||||
<!-- D-pad left side -->
|
||||
<rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.5)"/>
|
||||
<rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.5)"/>
|
||||
<!-- Buttons right side -->
|
||||
<circle cx="32" cy="22" r="2" fill="#e63946" opacity="0.85"/>
|
||||
<circle cx="36" cy="25" r="2" fill="#2ec4b6" opacity="0.85"/>
|
||||
<circle cx="32" cy="28" r="2" fill="#7b2fbe" opacity="0.85"/>
|
||||
<circle cx="28" cy="25" r="2" fill="#f4a261" opacity="0.85"/>
|
||||
<!-- Center connector / menu button -->
|
||||
<rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.35)"/>
|
||||
<!-- Handle grips (left and right) -->
|
||||
<rect x="8" y="30" width="8" height="6" rx="4" fill="url(#g2)" opacity="0.7"/>
|
||||
<rect x="32" y="30" width="8" height="6" rx="4" fill="url(#g2)" opacity="0.7"/>
|
||||
<!-- Top bumper buttons -->
|
||||
<rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#g1)" opacity="0.8"/>
|
||||
<rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#g1)" opacity="0.8"/>
|
||||
<!-- Stars / sparkles -->
|
||||
<circle cx="24" cy="8" r="1.5" fill="#f0c040" opacity="0.9"/>
|
||||
<circle cx="38" cy="10" r="1" fill="#00e5ff" opacity="0.8"/>
|
||||
<circle cx="10" cy="10" r="1" fill="#f0c040" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="40%" cy="40%" r="60%">
|
||||
<stop offset="0%" stop-color="#1a0a40"/>
|
||||
<stop offset="60%" stop-color="#0a0520"/>
|
||||
<stop offset="100%" stop-color="#050210"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="purple" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#c77dff"/>
|
||||
<stop offset="100%" stop-color="#7b2fbe"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<!-- Galactic swirl -->
|
||||
<ellipse cx="60" cy="60" rx="38" ry="12" fill="none" stroke="url(#purple)" stroke-width="1.5" opacity="0.5" transform="rotate(-30 60 60)"/>
|
||||
<ellipse cx="60" cy="60" rx="28" ry="8" fill="none" stroke="#c77dff" stroke-width="1" opacity="0.4" transform="rotate(20 60 60)"/>
|
||||
<!-- Stars -->
|
||||
<circle cx="30" cy="25" r="1.5" fill="white" opacity="0.9"/>
|
||||
<circle cx="88" cy="30" r="1" fill="white" opacity="0.7"/>
|
||||
<circle cx="20" cy="70" r="1" fill="white" opacity="0.6"/>
|
||||
<circle cx="95" cy="65" r="1.5" fill="white" opacity="0.8"/>
|
||||
<circle cx="50" cy="15" r="1" fill="white" opacity="0.5"/>
|
||||
<circle cx="75" cy="100" r="1.5" fill="white" opacity="0.7"/>
|
||||
<circle cx="40" cy="95" r="1" fill="#c77dff" opacity="0.9"/>
|
||||
<circle cx="100" cy="45" r="1" fill="#c77dff" opacity="0.6"/>
|
||||
<!-- Galaxy emoji + text -->
|
||||
<text x="60" y="56" font-size="34" text-anchor="middle" filter="url(#glow)" font-family="Arial">🌌</text>
|
||||
<text x="60" y="80" font-size="11" font-weight="700" text-anchor="middle" fill="url(#purple)" font-family="Arial, sans-serif" letter-spacing="1.5">MILKY WAY</text>
|
||||
<!-- Border glow -->
|
||||
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#purple)" stroke-width="1" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#1a1400"/>
|
||||
<stop offset="100%" stop-color="#0a0800"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFD700"/>
|
||||
<stop offset="50%" stop-color="#FFF0A0"/>
|
||||
<stop offset="100%" stop-color="#CC9900"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<!-- Diamond border -->
|
||||
<rect x="8" y="8" width="104" height="104" rx="18" fill="none" stroke="url(#gold)" stroke-width="1.5" opacity="0.7"/>
|
||||
<rect x="4" y="4" width="112" height="112" rx="20" fill="none" stroke="#FFD700" stroke-width="0.5" opacity="0.3"/>
|
||||
<!-- Crown -->
|
||||
<text x="60" y="54" font-size="38" text-anchor="middle" filter="url(#glow)" font-family="Arial">👑</text>
|
||||
<!-- 777 NOBLE text -->
|
||||
<text x="60" y="76" font-size="18" font-weight="900" text-anchor="middle" fill="url(#gold)" font-family="Arial Black, sans-serif" letter-spacing="3" filter="url(#glow)">777</text>
|
||||
<text x="60" y="92" font-size="10" font-weight="700" text-anchor="middle" fill="#CC9900" font-family="Arial, sans-serif" letter-spacing="3" opacity="0.9">NOBLE</text>
|
||||
<!-- Corner diamonds -->
|
||||
<polygon points="20,12 24,16 20,20 16,16" fill="#FFD700" opacity="0.7"/>
|
||||
<polygon points="100,12 104,16 100,20 96,16" fill="#FFD700" opacity="0.7"/>
|
||||
<polygon points="20,108 24,104 20,100 16,104" fill="#FFD700" opacity="0.7"/>
|
||||
<polygon points="100,108 104,104 100,100 96,104" fill="#FFD700" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0a0a12"/>
|
||||
<stop offset="100%" stop-color="#1a1228"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f0c040"/>
|
||||
<stop offset="100%" stop-color="#ff6b35"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#00e5ff"/>
|
||||
<stop offset="100%" stop-color="#7b2fbe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||
<!-- Grid lines -->
|
||||
<line x1="0" y1="315" x2="1200" y2="315" stroke="rgba(240,192,64,0.05)" stroke-width="1"/>
|
||||
<line x1="600" y1="0" x2="600" y2="630" stroke="rgba(240,192,64,0.05)" stroke-width="1"/>
|
||||
<!-- Glow circle -->
|
||||
<circle cx="600" cy="315" r="280" fill="rgba(240,192,64,0.03)" stroke="rgba(240,192,64,0.08)" stroke-width="1"/>
|
||||
<!-- Gamepad icon (simplified) -->
|
||||
<g transform="translate(540,180) scale(2.5)">
|
||||
<rect x="6" y="16" width="36" height="22" rx="11" fill="url(#gold)" opacity="0.9"/>
|
||||
<rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.5)"/>
|
||||
<rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.5)"/>
|
||||
<circle cx="32" cy="22" r="2.2" fill="#e63946"/>
|
||||
<circle cx="36" cy="25" r="2.2" fill="#2ec4b6"/>
|
||||
<circle cx="32" cy="28" r="2.2" fill="#7b2fbe"/>
|
||||
<circle cx="28" cy="25" r="2.2" fill="#f4a261"/>
|
||||
<rect x="8" y="30" width="8" height="7" rx="4" fill="url(#cyan)" opacity="0.8"/>
|
||||
<rect x="32" y="30" width="8" height="7" rx="4" fill="url(#cyan)" opacity="0.8"/>
|
||||
</g>
|
||||
<!-- Logo text -->
|
||||
<text x="600" y="375" font-family="Georgia,serif" font-size="64" font-weight="700" fill="#f0c040" text-anchor="middle">TomTomGames</text>
|
||||
<!-- Tagline -->
|
||||
<text x="600" y="425" font-family="Arial,sans-serif" font-size="24" fill="#8888aa" text-anchor="middle">Buy tokens for VBlink777 · Fire Kirin · Milky Way · Ultra Panda & more</text>
|
||||
<!-- Bottom badges -->
|
||||
<rect x="350" y="470" width="160" height="40" rx="8" fill="rgba(240,192,64,0.1)" stroke="rgba(240,192,64,0.3)" stroke-width="1"/>
|
||||
<text x="430" y="496" font-family="Arial,sans-serif" font-size="16" fill="#f0c040" text-anchor="middle" font-weight="700">⚡ Instant Delivery</text>
|
||||
<rect x="520" y="470" width="160" height="40" rx="8" fill="rgba(0,229,255,0.07)" stroke="rgba(0,229,255,0.2)" stroke-width="1"/>
|
||||
<text x="600" y="496" font-family="Arial,sans-serif" font-size="16" fill="#00e5ff" text-anchor="middle" font-weight="700">🔒 SSL Secured</text>
|
||||
<rect x="690" y="470" width="160" height="40" rx="8" fill="rgba(0,230,118,0.07)" stroke="rgba(0,230,118,0.2)" stroke-width="1"/>
|
||||
<text x="770" y="496" font-family="Arial,sans-serif" font-size="16" fill="#00e676" text-anchor="middle" font-weight="700">💬 24/7 Support</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="30%" r="70%">
|
||||
<stop offset="0%" stop-color="#0a1628"/>
|
||||
<stop offset="100%" stop-color="#040810"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="blue" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#64b5f6"/>
|
||||
<stop offset="100%" stop-color="#1565c0"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<!-- Crown at top -->
|
||||
<path d="M42 28 L50 20 L60 26 L70 20 L78 28 L74 38 L46 38 Z" fill="#FFD700" opacity="0.9" filter="url(#glow)"/>
|
||||
<circle cx="50" cy="20" r="3" fill="#FFD700"/>
|
||||
<circle cx="60" cy="16" r="3.5" fill="#FFD700"/>
|
||||
<circle cx="70" cy="20" r="3" fill="#FFD700"/>
|
||||
<!-- Panda + paw -->
|
||||
<text x="60" y="68" font-size="36" text-anchor="middle" filter="url(#glow)" font-family="Arial">🐾</text>
|
||||
<!-- MASTER text -->
|
||||
<text x="60" y="90" font-size="11" font-weight="900" text-anchor="middle" fill="url(#blue)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">MASTER</text>
|
||||
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#blue)" stroke-width="1" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#1a1a2e"/>
|
||||
<stop offset="100%" stop-color="#080814"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="orange" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFB347"/>
|
||||
<stop offset="100%" stop-color="#FF6B00"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#orange)" stroke-width="1.5" opacity="0.5"/>
|
||||
<!-- Bamboo decorations -->
|
||||
<rect x="12" y="30" width="5" height="65" rx="2" fill="#2d5a27" opacity="0.5"/>
|
||||
<rect x="103" y="25" width="5" height="65" rx="2" fill="#2d5a27" opacity="0.5"/>
|
||||
<rect x="10" y="48" width="9" height="3" rx="1" fill="#2d5a27" opacity="0.5"/>
|
||||
<rect x="101" y="44" width="9" height="3" rx="1" fill="#2d5a27" opacity="0.5"/>
|
||||
<!-- Panda emoji -->
|
||||
<text x="60" y="60" font-size="42" text-anchor="middle" filter="url(#glow)" font-family="Arial">🐼</text>
|
||||
<!-- ULTRA text -->
|
||||
<text x="60" y="85" font-size="12" font-weight="900" text-anchor="middle" fill="url(#orange)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">ULTRA</text>
|
||||
<!-- Stars -->
|
||||
<text x="35" y="35" font-size="12" text-anchor="middle" fill="#FFD700" opacity="0.8">★</text>
|
||||
<text x="85" y="35" font-size="12" text-anchor="middle" fill="#FFD700" opacity="0.8">★</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#1a0a2e"/>
|
||||
<stop offset="100%" stop-color="#0d0620"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFD700"/>
|
||||
<stop offset="100%" stop-color="#FF6B00"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
|
||||
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#gold)" stroke-width="1.5" opacity="0.6"/>
|
||||
<!-- Slot machine reel symbols -->
|
||||
<text x="60" y="52" font-size="36" text-anchor="middle" fill="url(#gold)" filter="url(#glow)" font-family="Arial">🎰</text>
|
||||
<!-- 777 text -->
|
||||
<text x="60" y="84" font-size="22" font-weight="900" text-anchor="middle" fill="url(#gold)" font-family="Arial Black, sans-serif" filter="url(#glow)" letter-spacing="2">777</text>
|
||||
<!-- Stars -->
|
||||
<circle cx="22" cy="22" r="2" fill="#FFD700" opacity="0.8"/>
|
||||
<circle cx="98" cy="22" r="2" fill="#FFD700" opacity="0.8"/>
|
||||
<circle cx="22" cy="98" r="2" fill="#FF6B00" opacity="0.8"/>
|
||||
<circle cx="98" cy="98" r="2" fill="#FF6B00" opacity="0.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* TomTomGames Version Manager
|
||||
* Run after uploading a new build to update the DB version.
|
||||
* Usage: https://tomtomgames.com/bump_version.php?key=ADMIN_KEY&version=1.0.2¬es=Your+notes+here
|
||||
*
|
||||
* Or auto-bump: ?key=ADMIN_KEY&bump=patch (1.0.1 → 1.0.2)
|
||||
* ?key=ADMIN_KEY&bump=minor (1.0.1 → 1.1.0)
|
||||
* ?key=ADMIN_KEY&bump=major (1.0.1 → 2.0.0)
|
||||
*/
|
||||
|
||||
define('BUMP_KEY', 'TTG_bump_2026!'); // Change this to your own secret key
|
||||
|
||||
if (($_GET['key'] ?? '') !== BUMP_KEY) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => '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'),
|
||||
]);
|
||||
|
After Width: | Height: | Size: 136 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#0a0a12"/>
|
||||
<circle cx="16" cy="16" r="13" fill="#f0c040"/>
|
||||
<text x="16" y="22" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="18" fill="#000">T</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// Fix broadcast_list in admin.php on server - delete after running
|
||||
$f = '/home/tomtomgames.com/public_html/api/admin.php';
|
||||
$c = file_get_contents($f);
|
||||
|
||||
$idx = strpos($c, "case 'broadcast_list':");
|
||||
$end = strpos($c, " break;", $idx) + 14;
|
||||
|
||||
$new = "case 'broadcast_list':\n try {\n \$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\";\n \$stmt = db()->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";
|
||||
@@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
|
||||
<title>Buy Fish Table Game Tokens | VBlink777, Fire Kirin, Milky Way | TomTomGames</title>
|
||||
<meta name="description" content="Buy tokens for the best fish table and skill games online — VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, eGame99. Instant delivery, secure payments, 24/7 support. Join TomTomGames today.">
|
||||
<meta name="keywords" content="buy fish table tokens, VBlink777 tokens buy, Fire Kirin game tokens, Milky Way game credits, Ultra Panda tokens, Panda Master game, Noble777 tokens, eGame99 buy, fish table game portal, online skill games tokens, game tokens fast delivery">
|
||||
<link rel="canonical" href="https://tomtomgames.com/games/">
|
||||
|
||||
<meta property="og:title" content="Buy Fish Table Game Tokens | TomTomGames">
|
||||
<meta property="og:description" content="The #1 token portal for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 & eGame99. Buy tokens in minutes.">
|
||||
<meta property="og:url" content="https://tomtomgames.com/games/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://tomtomgames.com/assets/img/og-image.png">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"name": "Fish Table & Skill Game Platforms",
|
||||
"description": "Buy tokens for these top fish table and skill game platforms through TomTomGames",
|
||||
"url": "https://tomtomgames.com/games/",
|
||||
"itemListElement": [
|
||||
{"@type":"ListItem","position":1,"name":"VBlink777","description":"Buy VBlink777 tokens instantly. Top-rated fish table game with fast payouts.","url":"https://tomtomgames.com/#vblink777"},
|
||||
{"@type":"ListItem","position":2,"name":"Fire Kirin","description":"Fire Kirin game tokens — one of the most popular fish table games online.","url":"https://tomtomgames.com/#firekirin"},
|
||||
{"@type":"ListItem","position":3,"name":"Milky Way","description":"Milky Way game credits. Secure, instant token delivery for Milky Way 777.","url":"https://tomtomgames.com/#milkyway"},
|
||||
{"@type":"ListItem","position":4,"name":"Ultra Panda","description":"Ultra Panda tokens — buy game credits fast for one of the top skill games.","url":"https://tomtomgames.com/#ultrapanda"},
|
||||
{"@type":"ListItem","position":5,"name":"Panda Master","description":"Panda Master game tokens. Instant delivery, multiple payment methods.","url":"https://tomtomgames.com/#pandamaster"},
|
||||
{"@type":"ListItem","position":6,"name":"Noble 777","description":"Noble777 tokens — buy game credits securely through TomTomGames.","url":"https://tomtomgames.com/#noble777"},
|
||||
{"@type":"ListItem","position":7,"name":"eGame99","description":"eGame99 token purchases. Fast credits, 24/7 support.","url":"https://tomtomgames.com/#egame99"}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Segoe UI',Arial,sans-serif;background:#0a0a12;color:#e8e8f0;line-height:1.6}
|
||||
a{color:#f0c040;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.wrap{max-width:960px;margin:0 auto;padding:0 20px}
|
||||
header{background:linear-gradient(135deg,#0a0a12,#1a1228);border-bottom:1px solid rgba(240,192,64,.2);padding:16px 0}
|
||||
.logo{font-family:'Georgia',serif;font-size:24px;font-weight:700;color:#f0c040}
|
||||
.logo span{color:#00e5ff}
|
||||
nav{display:flex;gap:20px;align-items:center;margin-top:8px;font-size:14px}
|
||||
.hero{background:linear-gradient(135deg,#1a1228,#0d1a2e);padding:60px 0 50px;text-align:center;border-bottom:1px solid rgba(240,192,64,.15)}
|
||||
h1{font-size:clamp(26px,5vw,42px);font-weight:800;line-height:1.2;margin-bottom:16px;color:#fff}
|
||||
h1 em{color:#f0c040;font-style:normal}
|
||||
.hero p{font-size:18px;color:#aab0c0;max-width:620px;margin:0 auto 28px}
|
||||
.cta-btn{display:inline-block;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:700;font-size:16px;padding:14px 32px;border-radius:8px;transition:transform .15s}
|
||||
.cta-btn:hover{transform:translateY(-2px);text-decoration:none}
|
||||
.trust-bar{display:flex;flex-wrap:wrap;justify-content:center;gap:28px;margin-top:32px;font-size:13px;color:#8888aa}
|
||||
.trust-bar span{display:flex;align-items:center;gap:6px}
|
||||
section{padding:52px 0}
|
||||
h2{font-size:28px;font-weight:700;color:#fff;margin-bottom:8px}
|
||||
.section-sub{color:#8888aa;margin-bottom:32px;font-size:16px}
|
||||
.games-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:20px}
|
||||
.game-card{background:#111827;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:24px;transition:transform .2s,border-color .2s}
|
||||
.game-card:hover{transform:translateY(-3px);border-color:rgba(240,192,64,.3)}
|
||||
.game-card h3{font-size:18px;color:#fff;margin-bottom:8px}
|
||||
.game-card p{font-size:14px;color:#8888aa;margin-bottom:16px}
|
||||
.game-card .buy-link{font-size:13px;font-weight:700;color:#f0c040;border:1px solid rgba(240,192,64,.3);padding:8px 16px;border-radius:6px;display:inline-block}
|
||||
.game-card .buy-link:hover{background:rgba(240,192,64,.1);text-decoration:none}
|
||||
.color-dot{width:12px;height:12px;border-radius:50%;display:inline-block;margin-right:8px;vertical-align:middle}
|
||||
.steps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
|
||||
.step{background:#111827;border-radius:12px;padding:24px;text-align:center}
|
||||
.step-num{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:800;font-size:18px;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
|
||||
.step h3{font-size:16px;color:#fff;margin-bottom:6px}
|
||||
.step p{font-size:13px;color:#8888aa}
|
||||
.pay-methods{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px}
|
||||
.pay-badge{background:#111827;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:10px 18px;font-size:14px;font-weight:600;color:#e8e8f0}
|
||||
.faq{max-width:720px}
|
||||
.faq-item{border-bottom:1px solid rgba(255,255,255,.06);padding:20px 0}
|
||||
.faq-item h3{font-size:17px;color:#fff;margin-bottom:8px}
|
||||
.faq-item p{font-size:14px;color:#aab0c0}
|
||||
footer{background:#060608;border-top:1px solid rgba(255,255,255,.06);padding:32px 0;font-size:13px;color:#555;text-align:center}
|
||||
footer a{color:#777}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="wrap" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
|
||||
<a href="https://tomtomgames.com/" class="logo">TomTom<span>Games</span></a>
|
||||
<nav>
|
||||
<a href="https://tomtomgames.com/">🎮 Play Now</a>
|
||||
<a href="https://tomtomgames.com/">Create Account</a>
|
||||
<a href="https://tomtomgames.com/" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);padding:7px 16px;border-radius:6px;color:#f0c040;font-weight:700">Buy Tokens →</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<h1>Buy <em>Game Tokens</em> for Fish Table & Skill Games</h1>
|
||||
<p>The fastest, most trusted way to load up on tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.</p>
|
||||
<a href="https://tomtomgames.com/" class="cta-btn">🪙 Buy Tokens Now</a>
|
||||
<div class="trust-bar">
|
||||
<span>🔒 SSL Secured</span>
|
||||
<span>⚡ Instant Delivery</span>
|
||||
<span>💳 Multiple Payment Methods</span>
|
||||
<span>💬 24/7 Support</span>
|
||||
<span>🎮 7 Game Platforms</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background:#080812;border-bottom:1px solid rgba(255,255,255,.04)">
|
||||
<div class="wrap">
|
||||
<h2>Supported Game Platforms</h2>
|
||||
<p class="section-sub">Buy tokens for all major fish table and skill game platforms in one place.</p>
|
||||
<div class="games-grid">
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#FF6B35"></span>VBlink777</h3>
|
||||
<p>One of the most popular fish table games. Buy VBlink777 tokens securely and get them credited fast to your game account.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy VBlink777 Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#E63946"></span>Fire Kirin</h3>
|
||||
<p>Fire Kirin is a top-rated skill game with exciting fish table gameplay. Purchase Fire Kirin tokens through TomTomGames for instant delivery.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy Fire Kirin Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#7B2FBE"></span>Milky Way</h3>
|
||||
<p>Milky Way 777 game credits — buy tokens for one of the best online fish table platforms. Secure payment, fast top-up.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy Milky Way Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#F4A261"></span>Ultra Panda</h3>
|
||||
<p>Ultra Panda game tokens for sale. Top up your account instantly through our secure portal and start playing right away.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy Ultra Panda Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#457B9D"></span>Panda Master</h3>
|
||||
<p>Panda Master tokens — buy game credits quickly and safely. Multiple payment methods accepted including card, Venmo, and Cash App.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy Panda Master Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#FFD700"></span>Noble 777</h3>
|
||||
<p>Noble 777 game token purchases made easy. Register, select your package, and have tokens in your account within minutes.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy Noble 777 Tokens →</a>
|
||||
</div>
|
||||
<div class="game-card">
|
||||
<h3><span class="color-dot" style="background:#2EC4B6"></span>eGame99</h3>
|
||||
<p>eGame99 tokens for sale at the best rates. Fast crediting, 24/7 customer support, and a seamless buying experience.</p>
|
||||
<a href="https://tomtomgames.com/" class="buy-link">Buy eGame99 Tokens →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="wrap">
|
||||
<h2>How to Buy Tokens in 3 Steps</h2>
|
||||
<p class="section-sub">Get tokens credited to your game account in minutes.</p>
|
||||
<div class="steps-grid">
|
||||
<div class="step">
|
||||
<div class="step-num">1</div>
|
||||
<h3>Create Account</h3>
|
||||
<p>Register free in under 60 seconds. Verify your email and log in.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">2</div>
|
||||
<h3>Select Game & Package</h3>
|
||||
<p>Choose your game platform, enter your in-game alias, and pick a token package — or enter a custom amount.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-num">3</div>
|
||||
<h3>Pay & Play</h3>
|
||||
<p>Pay securely by card or manual transfer. Tokens are credited to your game account fast.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:32px">
|
||||
<h3 style="color:#fff;margin-bottom:12px">Accepted Payment Methods</h3>
|
||||
<div class="pay-methods">
|
||||
<div class="pay-badge">💳 Credit / Debit Card</div>
|
||||
<div class="pay-badge">💙 Venmo</div>
|
||||
<div class="pay-badge">💚 Cash App</div>
|
||||
<div class="pay-badge">🟢 Chime</div>
|
||||
<div class="pay-badge">💜 Zelle</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background:#080812;border-top:1px solid rgba(255,255,255,.04)">
|
||||
<div class="wrap">
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<p class="section-sub">Everything you need to know about buying game tokens.</p>
|
||||
<div class="faq">
|
||||
<div class="faq-item">
|
||||
<h3>What is TomTomGames?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>How quickly are tokens delivered?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>How much do tokens cost?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>Is my payment information secure?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>What if I need help?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>Can I cash out my tokens?</h3>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:36px;text-align:center">
|
||||
<a href="https://tomtomgames.com/" class="cta-btn">Get Started — Create Free Account</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<p><strong style="color:#777">TomTomGames</strong> — Your trusted token portal for fish table and skill games.</p>
|
||||
<p style="margin-top:8px"><a href="https://tomtomgames.com/">Home</a> · <a href="https://tomtomgames.com/games/">Games</a> · <a href="https://tomtomgames.com/">Support</a></p>
|
||||
<p style="margin-top:12px">© <?= date('Y') ?> TomTomGames. All rights reserved. Game tokens are for entertainment purposes on supported platforms only. Please play responsibly.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* TomGames — Square Location ID Finder
|
||||
* Upload this file, visit it in your browser ONCE to get your Location ID.
|
||||
* Then paste the ID into includes/config.php and DELETE this file.
|
||||
*/
|
||||
|
||||
$token = 'EAAAl1ECweOVgNiwhC2SuA56QFjlfRLkYxo4xe4r2fMLvqwLT0IKGUZNNOYy1NXn';
|
||||
$locations = [];
|
||||
$error = '';
|
||||
|
||||
$ch = curl_init('https://connect.squareup.com/v2/locations');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => 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);
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Square Location ID Finder</title>
|
||||
<style>
|
||||
body{font-family:'Segoe UI',sans-serif;background:#0a0a12;color:#e8e8f0;max-width:600px;margin:40px auto;padding:20px}
|
||||
h1{font-size:22px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px}
|
||||
.sub{color:#8888aa;font-size:13px;margin-bottom:28px}
|
||||
.loc{background:#1a1a2e;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px;margin-bottom:16px}
|
||||
.loc-name{font-size:18px;font-weight:700;color:#e8e8f0;margin-bottom:6px}
|
||||
.loc-id-wrap{display:flex;align-items:center;gap:10px;margin:10px 0}
|
||||
.loc-id{font-family:monospace;font-size:18px;font-weight:700;color:#f0c040;background:#0a0a12;border:1px solid rgba(240,192,64,.4);border-radius:8px;padding:10px 16px;flex:1;letter-spacing:1px}
|
||||
.copy-btn{padding:10px 16px;background:linear-gradient(135deg,#f0c040,#d4a017);border:none;border-radius:8px;color:#000;font-weight:700;font-size:13px;cursor:pointer}
|
||||
.copy-btn:hover{opacity:.9}
|
||||
.loc-meta{font-size:12px;color:#8888aa;margin-top:6px}
|
||||
.next{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);border-radius:12px;padding:18px;margin-top:24px}
|
||||
.next h2{color:#00e5ff;font-size:15px;margin-bottom:10px}
|
||||
.next ol{padding-left:18px;line-height:2;color:#c0c0d8;font-size:13px}
|
||||
.next code{background:#0a0a12;border:1px solid rgba(255,255,255,.15);border-radius:4px;padding:2px 7px;font-family:monospace;color:#f0c040}
|
||||
.err{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);border-radius:10px;padding:16px;color:#ff6666}
|
||||
.warn{background:rgba(255,214,10,.08);border:1px solid rgba(255,214,10,.2);border-radius:8px;padding:12px;margin-top:20px;font-size:12px;color:#ffd60a}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔑 Square Location Finder</h1>
|
||||
<div class="sub">TomGames — Run once, then delete this file</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="err"><strong>Error fetching locations:</strong><br><?= $error ?></div>
|
||||
<?php elseif (empty($locations)): ?>
|
||||
<div class="err">No locations found on this Square account.</div>
|
||||
<?php else: ?>
|
||||
<p style="color:#8888aa;font-size:13px;margin-bottom:16px">Found <?= count($locations) ?> location(s). Copy the ID for your main location:</p>
|
||||
<?php foreach ($locations as $loc): ?>
|
||||
<div class="loc">
|
||||
<div class="loc-name"><?= htmlspecialchars($loc['name']) ?> <?= $loc['status'] === 'ACTIVE' ? '✅' : '⚠️ ' . $loc['status'] ?></div>
|
||||
<div class="loc-id-wrap">
|
||||
<div class="loc-id" id="id-<?= $loc['id'] ?>"><?= htmlspecialchars($loc['id']) ?></div>
|
||||
<button class="copy-btn" onclick="copyId('<?= $loc['id'] ?>', this)">COPY</button>
|
||||
</div>
|
||||
<div class="loc-meta">
|
||||
<?= htmlspecialchars($loc['address']['address_line_1'] ?? '') ?>
|
||||
<?= htmlspecialchars($loc['address']['city'] ?? '') ?> ·
|
||||
Currency: <?= htmlspecialchars($loc['currency'] ?? 'USD') ?> ·
|
||||
Country: <?= htmlspecialchars($loc['country'] ?? '—') ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="next">
|
||||
<h2>📋 Next Steps</h2>
|
||||
<ol>
|
||||
<li>Copy your Location ID above</li>
|
||||
<li>Open <code>includes/config.php</code></li>
|
||||
<li>Replace <code>YOUR_LOCATION_ID</code> with your copied ID</li>
|
||||
<li>Also update <code>DB_PASS</code> with your MySQL password</li>
|
||||
<li><strong>Delete this file from your server!</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="warn">⚠️ <strong>Security:</strong> Delete <code>get_location.php</code> from your server after use. It exposes your access token.</div>
|
||||
|
||||
<script>
|
||||
function copyId(id, btn) {
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
btn.textContent = 'COPIED!';
|
||||
btn.style.background = '#00e676';
|
||||
setTimeout(() => { btn.textContent = 'COPY'; btn.style.background = ''; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
$key = $_GET['key'] ?? '';
|
||||
if ($key !== 'TomGames2024Admin') die('<h2 style="font-family:sans-serif;color:red">Access denied. Add ?key=TomGames2024Admin to URL.</h2>');
|
||||
|
||||
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 <strong>'.DB_NAME.'</strong> as <strong>'.DB_USER.'</strong>');
|
||||
} catch (Exception $e) {
|
||||
die('<pre style="color:red">CONNECTION FAILED: '.htmlspecialchars($e->getMessage()).'</pre>');
|
||||
}
|
||||
|
||||
// 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 <strong>$name</strong> ✓"); }
|
||||
catch (Exception $e) { err("Table <strong>$name</strong>: ".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 <strong>$tbl.$col</strong> already exists ✓");
|
||||
} else {
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`");
|
||||
ok("Column <strong>$tbl.$col</strong> added ✓");
|
||||
} catch (Exception $e) {
|
||||
err("Column <strong>$tbl.$col</strong>: ".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: <strong>".implode(', ', $tables_now)."</strong>");
|
||||
|
||||
try {
|
||||
$total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
||||
$admins = $pdo->query("SELECT COUNT(*) FROM users WHERE is_admin=1")->fetchColumn();
|
||||
info("Users: <strong>$total total</strong>, $admins admin(s)");
|
||||
} catch (Exception $e) {}
|
||||
?>
|
||||
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>TomTomGames DB Install</title>
|
||||
<style>
|
||||
body{font-family:'Segoe UI',sans-serif;background:#0a0a12;color:#e8e8f0;max-width:680px;margin:40px auto;padding:20px}
|
||||
h1{font-size:20px;margin-bottom:4px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.sub{color:#8888aa;font-size:13px;margin-bottom:24px}
|
||||
.row{padding:9px 14px;border-radius:7px;margin-bottom:5px;font-size:13px;display:flex;align-items:flex-start;gap:10px}
|
||||
.ok {background:rgba(0,230,118,.08);border:1px solid rgba(0,230,118,.2)}
|
||||
.err {background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:#ff9999}
|
||||
.warn{background:rgba(255,214,10,.07);border:1px solid rgba(255,214,10,.2);color:#ffd60a}
|
||||
.info{background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);color:#aaddff}
|
||||
.ic{flex-shrink:0;font-weight:700}
|
||||
.next{background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.2);border-radius:10px;padding:16px;margin-top:24px}
|
||||
.next h2{color:#f0c040;font-size:14px;margin-bottom:8px}
|
||||
.next ol{padding-left:16px;line-height:2;color:#ccccdd;font-size:13px}
|
||||
.del{background:rgba(255,68,68,.07);border:1px solid rgba(255,68,68,.2);border-radius:7px;padding:10px 14px;margin-top:14px;font-size:12px;color:#ff9999}
|
||||
</style></head><body>
|
||||
<h1>🎮 TomTomGames — DB Install / Repair</h1>
|
||||
<div class="sub">Database: <strong><?= DB_NAME ?></strong></div>
|
||||
<?php foreach ($log as $e): $ic = $e['t']==='ok'?'✓':($e['t']==='err'?'✗':($e['t']==='warn'?'⚠':'ℹ')); ?>
|
||||
<div class="row <?= $e['t'] ?>"><span class="ic"><?= $ic ?></span><span><?= $e['m'] ?></span></div>
|
||||
<?php endforeach; ?>
|
||||
<div class="next"><h2>Next Steps</h2><ol>
|
||||
<li>All green ✓? Your database is fully set up</li>
|
||||
<li>Visit <strong>/create_admin.php</strong> to create admin account (if needed)</li>
|
||||
<li>Visit <strong>https://tomtomgames.com</strong> — app should load normally</li>
|
||||
<li><strong>Delete install.php</strong> from your server now</li>
|
||||
</ol></div>
|
||||
<div class="del">⚠ Delete <code>install.php</code> after use — it exposes DB structure.</div>
|
||||
</body></html>
|
||||
@@ -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" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// Admin only diagnostic - delete after use
|
||||
$files = [
|
||||
'../../includes/db.php',
|
||||
'../../includes/auth.php',
|
||||
'../api/admin.php',
|
||||
];
|
||||
foreach ($files as $f) {
|
||||
$full = __DIR__ . '/' . $f;
|
||||
$out = shell_exec("php -l " . escapeshellarg($full) . " 2>&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";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Test what the referrals API would return for each user
|
||||
$users = db()->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);
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
require_once '/home/tomtomgames.com/includes/config.php';
|
||||
require_once '/home/tomtomgames.com/includes/db.php';
|
||||
|
||||
// 1. referral_tiers
|
||||
db()->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";
|
||||
@@ -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
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<!-- Homepage / App -->
|
||||
<url>
|
||||
<loc>https://tomtomgames.com/</loc>
|
||||
<lastmod>2026-05-19</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
<!-- SEO Landing Page -->
|
||||
<url>
|
||||
<loc>https://tomtomgames.com/games/</loc>
|
||||
<lastmod>2026-05-19</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
|
||||
</urlset>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
echo json_encode([
|
||||
'php' => PHP_VERSION,
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
'status' => 'ok'
|
||||
]);
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// POST test - simulates exactly what the app does
|
||||
ob_start();
|
||||
try { require_once __DIR__ . '/../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); die(json_encode(['boot_error'=>$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()]);
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
// Standalone email test - no auth required for diagnosis
|
||||
// DELETE THIS FILE after confirming email works
|
||||
|
||||
$result = [];
|
||||
$result['php_version'] = PHP_VERSION;
|
||||
$result['mail_function_exists'] = function_exists('mail');
|
||||
$result['server_software'] = $_SERVER['SERVER_SOFTWARE'] ?? 'unknown';
|
||||
$result['hostname'] = gethostname();
|
||||
|
||||
// Check sendmail path
|
||||
$sendmail = ini_get('sendmail_path');
|
||||
$result['sendmail_path'] = $sendmail ?: 'not set';
|
||||
|
||||
// Check if we can detect SMTP settings
|
||||
$result['smtp_host'] = ini_get('SMTP') ?: 'not set';
|
||||
$result['smtp_port'] = ini_get('smtp_port') ?: 'not set';
|
||||
|
||||
$to = $_POST['to'] ?? '';
|
||||
$sent = false;
|
||||
$sendError = '';
|
||||
|
||||
if ($to && filter_var($to, FILTER_VALIDATE_EMAIL) && isset($_POST['send'])) {
|
||||
$subject = 'TomTomGames Email Test';
|
||||
$message = "This is a test email from TomTomGames.\n\nIf you received this, PHP mail() is working correctly on this server.";
|
||||
$headers = "From: noreply@tomtomgames.com\r\n";
|
||||
$headers .= "Reply-To: support@tomtomgames.com\r\n";
|
||||
$headers .= "X-Mailer: PHP/" . PHP_VERSION;
|
||||
|
||||
// Capture any mail errors
|
||||
set_error_handler(function($errno, $errstr) use (&$sendError) {
|
||||
$sendError = $errstr;
|
||||
});
|
||||
$sent = mail($to, $subject, $message, $headers, '-fnoreply@tomtomgames.com');
|
||||
restore_error_handler();
|
||||
$result['mail_return'] = $sent;
|
||||
$result['mail_error'] = $sendError ?: 'none';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Email Test</title>
|
||||
<style>
|
||||
body{font-family:monospace;background:#0a0a12;color:#e8e8f0;padding:24px;max-width:600px;margin:0 auto}
|
||||
h2{color:#f0c040}
|
||||
.box{background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:16px;margin:12px 0}
|
||||
.ok{color:#00e676}.err{color:#ff4444}.warn{color:#f0c040}
|
||||
input{background:#111;border:1px solid #444;color:#fff;padding:8px 12px;width:280px;border-radius:6px;font-size:14px}
|
||||
button{background:#f0c040;color:#000;border:none;padding:10px 20px;border-radius:6px;font-weight:700;cursor:pointer;margin-left:8px}
|
||||
label{display:block;margin-bottom:6px;color:#aaa;font-size:13px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>TomTomGames — Email Diagnostics</h2>
|
||||
|
||||
<div class="box">
|
||||
<b>Server Info:</b><br>
|
||||
PHP: <span class="ok"><?= $result['php_version'] ?></span><br>
|
||||
Server: <?= htmlspecialchars($result['server_software']) ?><br>
|
||||
Hostname: <?= htmlspecialchars($result['hostname']) ?><br>
|
||||
mail() function: <span class="<?= $result['mail_function_exists'] ? 'ok' : 'err' ?>"><?= $result['mail_function_exists'] ? 'EXISTS' : 'MISSING' ?></span><br>
|
||||
sendmail_path: <span class="warn"><?= htmlspecialchars($result['sendmail_path']) ?></span><br>
|
||||
SMTP: <span class="warn"><?= htmlspecialchars($result['smtp_host']) ?>:<?= htmlspecialchars($result['smtp_port']) ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($to): ?>
|
||||
<div class="box">
|
||||
<b>Send Result:</b><br>
|
||||
To: <?= htmlspecialchars($to) ?><br>
|
||||
mail() returned: <span class="<?= $sent ? 'ok' : 'err' ?>"><?= $sent ? 'TRUE (queued for delivery)' : 'FALSE (failed)' ?></span><br>
|
||||
Error: <span class="warn"><?= htmlspecialchars($result['mail_error']) ?></span><br>
|
||||
<?php if ($sent): ?>
|
||||
<br><span class="ok">Check your inbox (and spam folder). If nothing arrives in 5 minutes, the server is not sending outbound mail.</span>
|
||||
<?php else: ?>
|
||||
<br><span class="err">mail() returned false — server cannot send email. You need to configure SMTP (PHPMailer) or enable sendmail.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box">
|
||||
<form method="POST">
|
||||
<label>Send test email to:</label>
|
||||
<input type="email" name="to" value="<?= htmlspecialchars($to) ?>" placeholder="your@email.com" required>
|
||||
<button type="submit" name="send" value="1">Send Test</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p style="color:#555;font-size:11px">Delete test_mail.php after diagnosis is complete.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
$result = verifyEmailToken($token);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= SITE_NAME ?> — Email Verified</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;700;900&family=Rajdhani:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0a0a12;color:#e8e8f0;font-family:'Rajdhani',sans-serif;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,229,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none}
|
||||
.card{background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:40px 32px;max-width:420px;width:100%;text-align:center;position:relative;z-index:1}
|
||||
.icon{font-size:64px;margin-bottom:20px;display:block}
|
||||
.title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:26px;margin-bottom:10px}
|
||||
.title.success{background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.title.error{color:#ff4444}
|
||||
.msg{font-size:15px;color:#aaaacc;line-height:1.7;margin-bottom:28px}
|
||||
.msg strong{color:#e8e8f0}
|
||||
.btn{display:block;width:100%;padding:15px;border:none;border-radius:10px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;letter-spacing:1px;cursor:pointer;text-decoration:none;text-align:center;transition:all .2s}
|
||||
.btn-gold{background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;box-shadow:0 4px 20px rgba(240,192,64,.4)}
|
||||
.btn-outline{background:transparent;border:1.5px solid rgba(255,255,255,.2);color:#aaaacc;margin-top:10px}
|
||||
.logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:20px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:28px;letter-spacing:1px}
|
||||
.countdown{font-size:13px;color:#8888aa;margin-top:16px}
|
||||
</style>
|
||||
<?php if ($result['success']): ?>
|
||||
<script>
|
||||
// Auto-redirect to app after 4 seconds
|
||||
setTimeout(() => { window.location.href = '/'; }, 4000);
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="logo" style="display:flex;align-items:center;gap:10px;justify-content:center"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="36" height="36" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="ll1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="ll2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="gll"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#ll1)" filter="url(#gll)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span><?= SITE_NAME ?></span></div>
|
||||
|
||||
<?php if ($result['success']): ?>
|
||||
<span class="icon">🎉</span>
|
||||
<div class="title success">Account Verified!</div>
|
||||
<p class="msg">
|
||||
Welcome to <?= SITE_NAME ?>, <strong><?= htmlspecialchars($result['alias']) ?></strong>!<br><br>
|
||||
Your account is active and you've been automatically logged in. Let's play!
|
||||
</p>
|
||||
<a href="/" class="btn btn-gold">🎮 ENTER TOMTOMGAMES</a>
|
||||
<p class="countdown" id="countdown">Redirecting in 4 seconds...</p>
|
||||
<script>
|
||||
let s = 4;
|
||||
const el = document.getElementById('countdown');
|
||||
setInterval(() => { s--; if(s>0) el.textContent = `Redirecting in ${s} second${s!==1?'s':''}...`; else el.textContent = 'Redirecting...'; }, 1000);
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
<span class="icon">❌</span>
|
||||
<div class="title error">Verification Failed</div>
|
||||
<p class="msg"><?= htmlspecialchars($result['error']) ?></p>
|
||||
<a href="/" class="btn btn-gold">REGISTER AGAIN</a>
|
||||
<a href="/" class="btn btn-outline">BACK TO HOME</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||