Files
tomtomgames/api/admin.php
T
myron d8202427ae Add platform credit overview to dashboard
New section below pending purchases/cashouts: one square card per
active platform showing net credit balance, completed purchase count,
and sent cashout count. Loads on page load alongside other dashboard
data. Credits turn yellow below 100 and red at/below 0 with a warning.
Clicking a card jumps to Game Management and opens that platform's
credit modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 22:03:38 +00:00

1065 lines
65 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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$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 COUNT(*) FROM token_purchases tp WHERE tp.platform_id=p.slug AND tp.status='completed') AS purchases,
(SELECT COUNT(*) FROM cashout_requests cr WHERE cr.platform_id=p.slug AND cr.status IN ('sent','approved')) AS cashouts
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
} 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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
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']);
}