Files
tomtomgames/api/admin.php
T
myron 8d27290831 Fix 6 code review findings: auth, mysqldump stderr, dead code, audit logs
- backup.php: replace manual admin check with requireAdmin(); suppress
  mysqldump password warning (2>&1 → 2>/dev/null) to prevent corrupt dumps
- ttg-backup.sh: same mysqldump stderr fix
- admin.php toggle_user: fix undefined $adminId/$userId in logAdminAction
  call — use $_SESSION['user_id'] and $uid instead
- admin.php chat_clear_all: wrap in try/catch and add logAdminAction audit
- admin.php: delete unreachable broadcast query block after break statement
- admin/index.php: fix cashouts_total formatted as currency — use parseInt
  (tokens are whole numbers, not dollars)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:02:07 +00:00

1059 lines
63 KiB
PHP

<?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() AND username != '__reset__'")->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() AND username != '__reset__' 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;
// ─── PLATFORM STATS ──────────────────────────────────────
case 'platform_stats':
$rows = db()->query("
SELECT p.id, p.name, p.slug, p.color,
COALESCE(SUM(CASE WHEN pc.type='debit' THEN -pc.credits_purchased ELSE pc.credits_purchased END),0) AS credits_balance,
(SELECT COALESCE(SUM(tp.amount_cents),0)/100 FROM token_purchases tp WHERE tp.platform_id=p.slug AND tp.status='completed') AS purchases_total,
(SELECT COALESCE(SUM(cr.tokens),0) FROM cashout_requests cr WHERE cr.platform_id=p.slug AND cr.status IN ('sent','approved')) AS cashouts_total
FROM platforms p
LEFT JOIN platform_credits pc ON pc.platform_id=p.id
WHERE p.is_deleted=0 AND p.is_active=1
GROUP BY p.id, p.name, p.slug, p.color
ORDER BY p.sort_order, p.id
")->fetchAll();
echo json_encode(['success'=>true,'platforms'=>$rows]);
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') {
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();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']); exit;
}
// Insert debit entry into platform_credits when approved
if ($status === 'completed' && !empty($purchase['platform_id'])) {
$platRow = db()->prepare("SELECT id FROM platforms WHERE slug=?");
$platRow->execute([$purchase['platform_id']]);
$platNumId = (int)$platRow->fetchColumn();
if ($platNumId) {
$userRow = db()->prepare("SELECT username FROM users WHERE id=?");
$userRow->execute([$purchase['user_id']]);
$username = $userRow->fetchColumn() ?: 'User#'.$purchase['user_id'];
$amtDollars = number_format($purchase['amount_cents'] / 100, 2);
$playerLabel = trim($purchase['player_name'] ?: $purchase['game_alias'] ?: $username);
$debitNotes = "Purchase #{$id} · {$playerLabel} ({$username}) · {$purchase['tokens']} tokens · \${$amtDollars} via {$purchase['payment_method']}";
db()->prepare("INSERT INTO platform_credits (platform_id, credits_purchased, credit_date, payment_method, notes, type, purchase_ref_id) VALUES (?,?,CURDATE(),?,?,?,?)")
->execute([$platNumId, $purchase['tokens'], $purchase['payment_method'], $debitNotes, 'debit', $id]);
}
}
echo json_encode(['success'=>true]);
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; }
db()->prepare("UPDATE users SET status=IF(status='active','suspended','active') WHERE id=?")->execute([$uid]);
logAdminAction('USER_STATUS_CHANGE', (int)$_SESSION['user_id'], 'user', $uid, 'Changed user status', '', ($data['status']??''), 'warning');
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__','',?,?,?,?) 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;
$sent = sendPasswordResetEmail($user['email'], $user['alias'], $resetUrl);
echo json_encode($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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;
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 (active + inactive, no archived) ──
case 'platforms_admin':
$rows = db()->query("SELECT * FROM platforms WHERE is_deleted=0 ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true,'platforms'=>$rows]);
break;
// ─── PLATFORMS: archived list ─────────────────────────
case 'platforms_archived':
$rows = db()->query("SELECT * FROM platforms WHERE is_deleted=1 ORDER BY deleted_at DESC")->fetchAll();
echo json_encode(['success'=>true,'platforms'=>$rows]);
break;
// ─── PLATFORMS: restore archived ──────────────────────
case 'platforms_restore':
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("UPDATE platforms SET is_deleted=0, deleted_at=NULL, updated_at=NOW() WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
// ─── PLATFORMS: permanent delete (archived only) ──────
case 'platforms_purge':
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=? AND is_deleted=1")->execute([$id]);
echo json_encode(['success'=>true]);
break;
// ─── PLATFORMS: create ────────────────────────────────
case 'platforms_create':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
if ((int)($_SESSION['user_id'] ?? 0) !== MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Only master admin can add games']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$isMasterAdmin = 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);
$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);
$agent_link = $isMasterAdmin ? substr(trim($d['agent_link'] ?? ''), 0, 500) : '';
$agent_login = $isMasterAdmin ? substr(trim($d['agent_login'] ?? ''), 0, 200) : '';
$agent_password = $isMasterAdmin ? substr(trim($d['agent_password'] ?? ''), 0, 200) : '';
$games_link = $isMasterAdmin ? substr(trim($d['games_link'] ?? ''), 0, 500) : '';
$agent_guide = $isMasterAdmin ? trim($d['agent_guide'] ?? '') : '';
$sub_agent_login = $isMasterAdmin ? substr(trim($d['sub_agent_login'] ?? ''), 0, 200) : '';
$sub_agent_password = $isMasterAdmin ? substr(trim($d['sub_agent_password'] ?? ''), 0, 200) : '';
$cashier_login = $isMasterAdmin ? substr(trim($d['cashier_login'] ?? ''), 0, 200) : '';
$cashier_password = $isMasterAdmin ? substr(trim($d['cashier_password'] ?? ''), 0, 200) : '';
if (!$slug||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL required']); exit; }
// Check if slug belongs to an archived platform — reactivate it instead of inserting
$existing = db()->prepare("SELECT id FROM platforms WHERE slug=? AND is_deleted=1 LIMIT 1");
$existing->execute([$slug]);
$archivedId = $existing->fetchColumn();
if ($archivedId) {
db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=?,is_deleted=0,deleted_at=NULL,updated_at=NOW() WHERE id=?")
->execute([$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active,$archivedId]);
echo json_encode(['success'=>true,'id'=>$archivedId,'restored'=>true]);
} else {
try {
db()->prepare("INSERT INTO platforms (slug,name,player_url,agent_link,agent_login,agent_password,games_link,agent_guide,sub_agent_login,sub_agent_password,cashier_login,cashier_password,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
->execute([$slug,$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) { echo json_encode(['success'=>false,'error'=>'Slug already in use by an active game']); }
}
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);
$isMasterAdmin = (int)($_SESSION['user_id'] ?? 0) === MASTER_ADMIN_ID;
$id = (int)($d['id'] ?? 0);
$name = substr(trim($d['name'] ?? ''), 0, 100);
$purl = substr(trim($d['player_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; }
if ($isMasterAdmin) {
$agent_link = substr(trim($d['agent_link'] ?? ''), 0, 500);
$agent_login = substr(trim($d['agent_login'] ?? ''), 0, 200);
$agent_password = substr(trim($d['agent_password'] ?? ''), 0, 200);
$games_link = substr(trim($d['games_link'] ?? ''), 0, 500);
$agent_guide = trim($d['agent_guide'] ?? '');
$sub_agent_login = substr(trim($d['sub_agent_login'] ?? ''), 0, 200);
$sub_agent_password = substr(trim($d['sub_agent_password'] ?? ''), 0, 200);
$cashier_login = substr(trim($d['cashier_login'] ?? ''), 0, 200);
$cashier_password = substr(trim($d['cashier_password'] ?? ''), 0, 200);
db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=?,updated_at=NOW() WHERE id=?")
->execute([$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active,$id]);
} else {
db()->prepare("UPDATE platforms SET name=?,player_url=?,color=?,sort_order=?,is_active=?,updated_at=NOW() WHERE id=?")
->execute([$name,$purl,$color,$sort,$active,$id]);
}
echo json_encode(['success'=>true]);
break;
// ─── PLATFORMS: soft-delete (archive) ────────────────
case 'platforms_delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
if ((int)($_SESSION['user_id'] ?? 0) !== MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Only master admin can archive games']); 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("UPDATE platforms SET is_deleted=1, deleted_at=NOW(), updated_at=NOW() 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; }
try {
db()->exec("DELETE FROM chat_messages");
logAdminAction('CHAT_CLEAR_ALL', (int)$_SESSION['user_id'], 'chat', 0, 'Cleared all chat messages', '', '', 'warning');
echo json_encode(['success'=>true]);
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Failed to clear chat']);
}
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']);
}