Files
tomtomgames-app/public_html/api/admin.php
T
2026-05-15 16:46:08 -05:00

972 lines
56 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()")->fetchColumn(),
'total_tokens_sold' => db()->query("SELECT COALESCE(SUM(tokens),0) FROM token_purchases WHERE status='completed'")->fetchColumn(),
'total_revenue' => db()->query("SELECT COALESCE(SUM(amount_cents),0)/100 FROM token_purchases WHERE status='completed'")->fetchColumn(),
]]);
break;
// ─── PENDING SIGNUPS ──────────────────────────────────────
case 'pending_signups':
$rows = db()->query("SELECT id,username,alias,email,expires_at,created_at FROM pending_registrations WHERE expires_at > NOW() ORDER BY created_at DESC")->fetchAll();
echo json_encode(['success'=>true,'pending'=>$rows]);
break;
case 'delete_pending':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
db()->prepare("DELETE FROM pending_registrations WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'approve_pending':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
// Fetch the pending record
$stmt = db()->prepare("SELECT * FROM pending_registrations WHERE id=?");
$stmt->execute([$id]);
$pending = $stmt->fetch();
if (!$pending) { echo json_encode(['success'=>false,'error'=>'Pending signup not found']); exit; }
// Check username/email not already taken
$chkUser = db()->prepare("SELECT id FROM users WHERE username=?");
$chkUser->execute([$pending['username']]);
if ($chkUser->fetch()) { echo json_encode(['success'=>false,'error'=>'Username already taken']); exit; }
if (!empty($pending['email'])) {
$chkEmail = db()->prepare("SELECT id FROM users WHERE email=?");
$chkEmail->execute([$pending['email']]);
if ($chkEmail->fetch()) { echo json_encode(['success'=>false,'error'=>'Email already registered']); exit; }
}
// Create the user account — bypass email verification
db()->beginTransaction();
try {
db()->prepare("INSERT INTO users (username,password,alias,email,email_verified,tokens,is_admin,status)
VALUES (?,?,?,?,1,0,0,'active')")
->execute([$pending['username'],$pending['password'],$pending['alias'],$pending['email']]);
db()->prepare("DELETE FROM pending_registrations WHERE id=?")->execute([$id]);
db()->commit();
logActivity('account_approved', (int)db()->lastInsertId(), (int)$_SESSION['user_id'], 'user', 0, 'Account approved for '.$pending['username']);
echo json_encode(['success'=>true,'username'=>$pending['username']]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Could not create account']);
}
break;
// ─── PURCHASES ────────────────────────────────────────────
case 'purchases':
$status = $_GET['status'] ?? 'pending';
if ($status === 'all') {
$stmt = db()->query("SELECT tp.*, u.username, u.alias FROM token_purchases tp JOIN users u ON tp.user_id=u.id ORDER BY tp.created_at DESC LIMIT 200");
} else {
$stmt = db()->prepare("SELECT tp.*, u.username, u.alias FROM token_purchases tp JOIN users u ON tp.user_id=u.id WHERE tp.status=? ORDER BY tp.created_at DESC LIMIT 200");
$stmt->execute([$status]);
}
echo json_encode(['success' => true, 'purchases' => $stmt->fetchAll()]);
break;
// ─── RESOLVE PURCHASE (approve manual / reject) ──────────
case 'resolve_purchase':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
$status = $data['status'] ?? '';
$note = trim($data['note'] ?? '');
if (!in_array($status, ['completed','failed'])) {
echo json_encode(['success'=>false,'error'=>'Invalid status']); exit;
}
// Fetch purchase
$row = db()->prepare("SELECT * FROM token_purchases WHERE id=? AND status='pending'");
$row->execute([$id]);
$purchase = $row->fetch();
if (!$purchase) {
echo json_encode(['success'=>false,'error'=>'Purchase not found or already resolved']); exit;
}
db()->beginTransaction();
try {
if ($status === 'completed') {
// Credit tokens to user
logAdminAction('TOKENS_ADJUSTED', $adminId, 'user', isset($targetId)?(int)$targetId:0, 'Manual token adjustment: '.($data['tokens']??0).' tokens', '', ($data['tokens']??''), 'critical');
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$purchase['tokens'], $purchase['user_id']]);
}
db()->prepare("UPDATE token_purchases SET status=?,admin_note=? WHERE id=?")->execute([$status, $note, $id]);
db()->commit();
echo json_encode(['success'=>true]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']);
}
break;
// ─── CASHOUTS ─────────────────────────────────────────────
case 'cashouts':
$status = $_GET['status'] ?? 'pending';
$valid = ['pending','sent','approved','rejected','deleted'];
if (!in_array($status, $valid)) $status = 'pending';
if ($status === 'pending') {
// Show both pending (player editing) and locked (submitted to admin)
$stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status IN ('pending','locked') ORDER BY cr.status DESC, cr.created_at DESC");
$stmt->execute();
} elseif ($status === 'sent') {
$stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status IN ('sent','approved') ORDER BY cr.created_at DESC");
$stmt->execute();
} else {
$stmt = db()->prepare("SELECT cr.*, u.username, u.alias AS user_alias FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.status=? ORDER BY cr.created_at DESC");
$stmt->execute([$status]);
}
echo json_encode(['success'=>true,'cashouts'=>$stmt->fetchAll()]);
break;
case 'resolve_cashout':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$id = (int)($data['id'] ?? 0);
$status = $data['status'] ?? '';
$note = trim($data['note'] ?? '');
// 'approved' is treated as 'sent' (payment sent to player)
if ($status === 'approved') $status = 'sent';
if (!in_array($status, ['sent','rejected','deleted'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; }
$r = db()->prepare("SELECT user_id,tokens FROM cashout_requests WHERE id=? AND status IN ('pending','locked')");
$r->execute([$id]);
$req = $r->fetch();
if (!$req) { echo json_encode(['success'=>false,'error'=>'Not found or already resolved']); exit; }
db()->beginTransaction();
try {
// Return tokens to player if denied or deleted
if (in_array($status, ['rejected','deleted'])) {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$req['tokens'],$req['user_id']]);
}
db()->prepare("UPDATE cashout_requests SET status=?,admin_note=?,resolved_at=NOW() WHERE id=?")->execute([$status,$note,$id]);
db()->commit();
logActivity('cashout_'.$status, $req['user_id'], (int)$_SESSION['user_id'], 'cashout', $id,
'Cashout '.$status.' by admin. Tokens: '.$req['tokens'].($note?' Note: '.$note:''));
echo json_encode(['success'=>true,'status'=>$status]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']);
}
break;
if (!in_array($status, ['approved','rejected'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; }
if ($status === 'rejected') {
$r = db()->prepare("SELECT user_id,tokens FROM cashout_requests WHERE id=? AND status='pending'");
$r->execute([$id]);
$req = $r->fetch();
if ($req) db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$req['tokens'],$req['user_id']]);
}
db()->prepare("UPDATE cashout_requests SET status=?,admin_note=?,resolved_at=NOW() WHERE id=?")->execute([$status,$note,$id]);
echo json_encode(['success'=>true]);
break;
// ─── USERS LIST ───────────────────────────────────────────
case 'users':
$users = db()->query("SELECT id,username,alias,email,email_verified,tokens,is_admin,status,created_at,last_login FROM users ORDER BY created_at DESC")->fetchAll();
echo json_encode(['success'=>true,'users'=>$users]);
break;
// ─── SINGLE USER DETAIL ───────────────────────────────────
case 'user_detail':
$uid = (int)($_GET['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("SELECT id,username,alias,email,email_verified,tokens,is_admin,status,created_at,last_login FROM users WHERE id=?");
$stmt->execute([$uid]);
$user = $stmt->fetch();
if (!$user) { echo json_encode(['success'=>false,'error'=>'User not found']); exit; }
// Rich stats
$s1 = db()->prepare("SELECT COALESCE(SUM(amount_cents),0)/100 FROM token_purchases WHERE user_id=? AND status='completed'");
$s1->execute([$uid]);
$s2 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='completed'"); $s2->execute([$uid]);
$s3 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='pending'"); $s3->execute([$uid]);
$s4 = db()->prepare("SELECT COUNT(*) FROM token_purchases WHERE user_id=? AND status='failed'"); $s4->execute([$uid]);
$s5 = db()->prepare("SELECT COUNT(*) FROM cashout_requests WHERE user_id=?"); $s5->execute([$uid]);
$s6 = db()->prepare("SELECT COALESCE(SUM(tokens),0) FROM token_purchases WHERE user_id=? AND status='completed'"); $s6->execute([$uid]);
$stats = [
'total_spent' => $s1->fetchColumn(),
'completed_purchases'=> $s2->fetchColumn(),
'pending_purchases' => $s3->fetchColumn(),
'failed_purchases' => $s4->fetchColumn(),
'total_cashouts' => $s5->fetchColumn(),
'total_tokens_bought'=> $s6->fetchColumn(),
];
echo json_encode(['success'=>true,'user'=>$user,'stats'=>$stats]);
break;
// ─── USER PURCHASES ───────────────────────────────────────
case 'user_purchases':
$uid = (int)($_GET['user_id'] ?? 0);
$stmt = db()->prepare("SELECT id,tokens,amount_cents,payment_method,square_payment_id,platform_id,game_alias,player_name,billing_name,billing_address,billing_city,billing_state,billing_zip,billing_email,is_custom,failure_reason,card_brand,card_last4,receipt_url,status,admin_note,created_at FROM token_purchases WHERE user_id=? ORDER BY created_at DESC LIMIT 100");
$stmt->execute([$uid]);
echo json_encode(['success'=>true,'purchases'=>$stmt->fetchAll()]);
break;
// ─── USER CASHOUTS ────────────────────────────────────────
case 'user_cashouts':
$uid = (int)($_GET['user_id'] ?? 0);
$stmt = db()->prepare("SELECT * FROM cashout_requests WHERE user_id=? ORDER BY created_at DESC LIMIT 100");
$stmt->execute([$uid]);
echo json_encode(['success'=>true,'cashouts'=>$stmt->fetchAll()]);
break;
// ─── ADJUST TOKENS ────────────────────────────────────────
case 'adjust_tokens':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$amount = (float)($data['amount'] ?? 0);
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$amount,$uid]);
$bal = db()->prepare("SELECT tokens FROM users WHERE id=?"); $bal->execute([$uid]);
$newBal = $bal->fetchColumn();
echo json_encode(['success'=>true,'new_balance'=>$newBal]);
break;
// ─── SET EXACT TOKEN BALANCE ──────────────────────────────
case 'set_tokens':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$bal = (float)($data['balance'] ?? 0);
if ($bal < 0) { echo json_encode(['success'=>false,'error'=>'Balance cannot be negative']); exit; }
db()->prepare("UPDATE users SET tokens=? WHERE id=?")->execute([$bal,$uid]);
echo json_encode(['success'=>true,'new_balance'=>$bal]);
break;
// ─── EDIT USER ────────────────────────────────────────────
case 'edit_user':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$username = strtolower(trim($data['username'] ?? ''));
$alias = trim($data['alias'] ?? '');
$email = strtolower(trim($data['email'] ?? ''));
$password = trim($data['password'] ?? '');
if (!$uid || empty($username) || empty($alias))
{ echo json_encode(['success'=>false,'error'=>'Username and alias required']); exit; }
if (!preg_match('/^[a-z0-9_]{3,50}$/', $username))
{ echo json_encode(['success'=>false,'error'=>'Invalid username format']); exit; }
if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL))
{ echo json_encode(['success'=>false,'error'=>'Invalid email address']); exit; }
// Check username not taken by another user
$chk = db()->prepare("SELECT id FROM users WHERE username=? AND id!=?");
$chk->execute([$username,$uid]);
if ($chk->fetch()) { echo json_encode(['success'=>false,'error'=>'Username already taken']); exit; }
if (!empty($email)) {
$chk2 = db()->prepare("SELECT id FROM users WHERE email=? AND id!=?");
$chk2->execute([$email,$uid]);
if ($chk2->fetch()) { echo json_encode(['success'=>false,'error'=>'Email already in use']); exit; }
}
if (!empty($password)) {
if (strlen($password) < 6) { echo json_encode(['success'=>false,'error'=>'Password must be 6+ characters']); exit; }
$hash = password_hash($password, PASSWORD_BCRYPT);
db()->prepare("UPDATE users SET username=?,alias=?,email=?,password=? WHERE id=?")->execute([$username,$alias,$email,$hash,$uid]);
} else {
db()->prepare("UPDATE users SET username=?,alias=?,email=? WHERE id=?")->execute([$username,$alias,$email,$uid]);
}
echo json_encode(['success'=>true]);
break;
// ─── TOGGLE ADMIN ROLE ───────────────────────────────────
case 'toggle_admin':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
// Master admin (ID=1) can NEVER lose admin status
if ($uid === MASTER_ADMIN_ID) {
echo json_encode(['success'=>false,'error'=>'Master admin cannot be modified.']); exit;
}
// Cannot remove your own admin
if ($uid === (int)$_SESSION['user_id']) {
echo json_encode(['success'=>false,'error'=>'You cannot change your own admin status.']); exit;
}
// Only master admin can grant/revoke admin
if ((int)$_SESSION['user_id'] !== MASTER_ADMIN_ID) {
echo json_encode(['success'=>false,'error'=>'Only the master admin can change admin roles.']); exit;
}
$stmt = db()->prepare("SELECT is_admin FROM users WHERE id=?");
$stmt->execute([$uid]);
$current = $stmt->fetchColumn();
$new_val = $current ? 0 : 1;
if ($new_val) {
db()->prepare("UPDATE users SET is_admin=1, email_verified=1 WHERE id=?")->execute([$uid]);
} else {
db()->prepare("UPDATE users SET is_admin=0 WHERE id=?")->execute([$uid]);
}
// Force the affected user to re-login by invalidating their sessions
// Store a flag in DB that forces re-auth on next request
db()->prepare("UPDATE users SET last_login=last_login WHERE id=?")->execute([$uid]);
logActivity($new_val?'admin_granted':'admin_revoked', $uid, (int)$_SESSION['user_id'], 'user', $uid, 'Admin status changed to '.($new_val?'admin':'player'), '', 'admin', '', '', 'warning');
echo json_encode(['success'=>true, 'is_admin'=>$new_val, 'needs_relogin'=>true, 'message'=>$new_val ? 'Admin access granted. User must log out and back in.' : 'Admin access removed. User must log out and back in.']);
break;
// ─── TOGGLE SUSPEND ───────────────────────────────────────
case 'toggle_user':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
if ($uid === MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Cannot suspend the master admin.']); exit; }
logAdminAction('USER_STATUS_CHANGE', $adminId, 'user', isset($userId)?(int)$userId:0, 'Changed user status to: '.($data['status']??'unknown'), '', ($data['status']??''), 'warning');
db()->prepare("UPDATE users SET status=IF(status='active','suspended','active') WHERE id=?")->execute([$uid]);
echo json_encode(['success'=>true]);
break;
// ─── DELETE USER ──────────────────────────────────────────
case 'delete_user':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'Invalid user']); exit; }
// Prevent deleting own account
if ($uid === MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Cannot delete the master admin account.']); exit; }
if ($uid === (int)$_SESSION['user_id']) { echo json_encode(['success'=>false,'error'=>'Cannot delete your own account']); exit; }
db()->beginTransaction();
try {
db()->prepare("DELETE FROM chat_messages WHERE user_id=?")->execute([$uid]);
db()->prepare("DELETE FROM cashout_requests WHERE user_id=?")->execute([$uid]);
db()->prepare("DELETE FROM token_purchases WHERE user_id=?")->execute([$uid]);
db()->prepare("DELETE FROM users WHERE id=?")->execute([$uid]);
db()->commit();
echo json_encode(['success'=>true]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Delete failed']);
}
break;
// ─── SEND PASSWORD RESET ──────────────────────────────────
case 'send_password_reset':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$stmt = db()->prepare("SELECT email,alias FROM users WHERE id=?");
$stmt->execute([$uid]);
$user = $stmt->fetch();
if (!$user || empty($user['email'])) { echo json_encode(['success'=>false,'error'=>'No email on file']); exit; }
// Generate reset token — reuse pending_registrations pattern
$token = bin2hex(random_bytes(32));
$exp = date('Y-m-d H:i:s', time() + 3600); // 1 hour
db()->prepare("INSERT INTO pending_registrations (username,password,alias,email,token,expires_at) VALUES ('__reset__','',''.?,?,'__reset__',?) ON DUPLICATE KEY UPDATE token=VALUES(token),expires_at=VALUES(expires_at)")->execute([$user['alias'],$user['email'],$token,$exp]);
// Simple reset email
$resetUrl = rtrim(SITE_URL,'/') . '/reset_password.php?token=' . urlencode($token);
$subject = SITE_NAME . ' — Password Reset Request';
$body = "Hi {$user['alias']},\n\nA password reset was requested for your account.\n\nClick here to reset: {$resetUrl}\n\nExpires in 1 hour. If you didn't request this, ignore this email.\n\n— " . SITE_NAME;
$headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\nReply-To: " . MAIL_REPLY_TO;
mail($user['email'], $subject, $body, $headers, '-f' . MAIL_FROM);
echo json_encode(['success'=>true]);
break;
// ─── PLATFORM ACCOUNTS ────────────────────────────────
case 'platform_accounts_list':
$status = $_GET['status'] ?? 'pending';
$uid = (int)($_GET['user_id'] ?? 0);
if ($uid) {
$stmt = db()->prepare("SELECT pa.*, COALESCE(p.name,pa.platform_slug) AS platform_name, u.username, u.alias AS user_alias FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug JOIN users u ON pa.user_id=u.id WHERE pa.user_id=? ORDER BY pa.requested_at DESC");
$stmt->execute([$uid]);
} else {
$valid = ['pending','approved','denied','deleted'];
if (!in_array($status,$valid)) $status='pending';
$stmt = db()->prepare("SELECT pa.*, COALESCE(p.name,pa.platform_slug) AS platform_name, u.username, u.alias AS user_alias FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug JOIN users u ON pa.user_id=u.id WHERE pa.status=? ORDER BY pa.requested_at DESC");
$stmt->execute([$status]);
}
echo json_encode(['success'=>true,'accounts'=>$stmt->fetchAll()]);
break;
case 'platform_account_resolve':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id=$d['id']??0; $status=$d['status']??'';
$uname=substr(trim($d['platform_username']??''),0,100);
$pass=substr(trim($d['platform_password']??''),0,200);
$note=substr(trim($d['admin_note']??''),0,300);
if (!in_array($status,['approved','denied','deleted'])){echo json_encode(['success'=>false,'error'=>'Invalid status']);exit;}
$chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch();
if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;}
db()->prepare("UPDATE platform_accounts SET status=?,platform_username=?,platform_password=?,admin_note=?,resolved_at=NOW(),admin_id=? WHERE id=?")
->execute([$status,$uname,$pass,$note,(int)$_SESSION['user_id'],$id]);
if ($status==='approved'&&$uname) {
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")
->execute([$row['user_id'],$row['platform_slug'],$uname]);
}
echo json_encode(['success'=>true]);
break;
case 'platform_account_update':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d=json_decode(file_get_contents('php://input'),true);
$id=$d['id']??0;
$uname=substr(trim($d['platform_username']??''),0,100);
$pass=substr(trim($d['platform_password']??''),0,200);
$note=substr(trim($d['admin_note']??''),0,300);
$chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch();
if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;}
db()->prepare("UPDATE platform_accounts SET platform_username=?,platform_password=?,admin_note=? WHERE id=?")
->execute([$uname,$pass,$note,$id]);
if ($uname){
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")
->execute([$row['user_id'],$row['platform_slug'],$uname]);
}
echo json_encode(['success'=>true]);
break;
$rows = db()->query("
SELECT b.*, u.username AS sender_name,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count,
(SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count,
(SELECT COUNT(*) FROM users WHERE is_admin=0 AND status='active') AS total_players
FROM broadcasts b JOIN users u ON b.admin_id=u.id
ORDER BY b.sent_at DESC LIMIT 50
")->fetchAll();
echo json_encode(['success'=>true,'broadcasts'=>$rows]);
break;
case 'broadcast_list':
try {
$sql = "SELECT b.id, b.subject, b.message, b.target, b.sent_at,
u.username AS sender_name,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count,
(SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count,
(SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0) AS total_players
FROM broadcasts b
JOIN users u ON b.admin_id=u.id
ORDER BY b.sent_at DESC
LIMIT 100";
$stmt = db()->query($sql);
echo json_encode(['success'=>true,'broadcasts'=>$stmt->fetchAll()]);
} catch(Exception $e) {
echo json_encode(['success'=>false,'error'=>$e->getMessage()]);
}
break;
case 'broadcast_send':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$subject = substr(trim($d['subject']??''),0,200);
$message = substr(trim($d['message']??''),0,5000);
$target = in_array($d['target']??'',['all','verified','unverified','admins']) ? $d['target'] : 'all';
if (!$subject||!$message) { echo json_encode(['success'=>false,'error'=>'Subject and message required']); exit; }
db()->prepare("INSERT INTO broadcasts (admin_id,subject,message,target) VALUES (?,?,?,?)")
->execute([$_SESSION['user_id'],$subject,$message,$target]);
$bid = db()->lastInsertId();
// Count recipients
$countQ = [
'all' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0",
'verified' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0 AND email_verified=1",
'unverified' => "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0 AND email_verified=0",
'admins' => "SELECT COUNT(*) FROM users WHERE is_admin=1",
];
$count = db()->query($countQ[$target])->fetchColumn();
echo json_encode(['success'=>true,'id'=>$bid,'recipient_count'=>(int)$count]);
break;
case 'broadcast_delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id']??0);
db()->prepare("DELETE FROM broadcasts WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'broadcast_edit':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$subject = substr(trim($d['subject'] ?? ''), 0, 200);
$message = trim($d['message'] ?? '');
$target = in_array($d['target']??'', ['all','verified','unverified','admins']) ? $d['target'] : 'all';
if (!$id || !$subject || !$message) { echo json_encode(['success'=>false,'error'=>'Missing fields']); exit; }
db()->prepare("UPDATE broadcasts SET subject=?, message=?, target=? WHERE id=?")->execute([$subject, $message, $target, $id]);
logAdminAction('BROADCAST_EDITED', $adminId, 'broadcast', $id, 'Edited broadcast #'.$id, '', '', 'info');
echo json_encode(['success'=>true]);
break;
case 'broadcast_resend':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'Missing ID']); exit; }
$bc = db()->prepare("SELECT * FROM broadcasts WHERE id=?");
$bc->execute([$id]);
$orig = $bc->fetch();
if (!$orig) { echo json_encode(['success'=>false,'error'=>'Broadcast not found']); exit; }
// Count recipients
$target = $orig['target'];
$countSql = "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0";
if ($target === 'verified') $countSql .= " AND email_verified=1";
if ($target === 'unverified') $countSql .= " AND email_verified=0";
$recipientCount = (int)db()->query($countSql)->fetchColumn();
// Delete old reads so everyone sees it again
db()->prepare("DELETE FROM broadcast_reads WHERE broadcast_id=?")->execute([$id]);
// Update sent_at to now
db()->prepare("UPDATE broadcasts SET sent_at=NOW() WHERE id=?")->execute([$id]);
logAdminAction('BROADCAST_RESENT', $adminId, 'broadcast', $id, 'Resent broadcast #'.$id.' to '.$recipientCount.' players', '', '', 'info');
echo json_encode(['success'=>true,'recipient_count'=>$recipientCount]);
break;
case 'broadcast_reads':
$bid = (int)($_GET['broadcast_id']??0);
$rows = db()->prepare("SELECT br.read_at, u.username, u.alias FROM broadcast_reads br JOIN users u ON br.user_id=u.id WHERE br.broadcast_id=? ORDER BY br.read_at ASC");
$rows->execute([$bid]);
echo json_encode(['success'=>true,'reads'=>$rows->fetchAll()]);
break;
case 'broadcast_replies':
$bid = (int)($_GET['broadcast_id']??0);
$rows = db()->prepare("SELECT br.*, u.username, u.alias, u.is_admin FROM broadcast_replies br JOIN users u ON br.user_id=u.id WHERE br.broadcast_id=? ORDER BY br.created_at ASC");
$rows->execute([$bid]);
echo json_encode(['success'=>true,'replies'=>$rows->fetchAll()]);
break;
case 'broadcast_reply':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$bid = (int)($d['broadcast_id']??0);
$msg = substr(trim($d['message']??''),0,1000);
if (!$bid||!$msg) { echo json_encode(['success'=>false,'error'=>'Required fields missing']); exit; }
db()->prepare("INSERT INTO broadcast_replies (broadcast_id,user_id,message) VALUES (?,?,?)")
->execute([$bid,$_SESSION['user_id'],$msg]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
break;
$rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true,'types'=>$rows]);
break;
case 'cashout_methods_create':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/','',strtolower(trim($d['slug']??'')));
$label= substr(trim($d['label']??''),0,100);
$icon = substr(trim($d['icon']??'💰'),0,10);
$desc = substr(trim($d['description']??''),0,200);
$sort = (int)($d['sort_order']??99);
$active=(int)(bool)($d['is_active']??1);
if (!$slug||!$label){echo json_encode(['success'=>false,'error'=>'Slug and label required']);exit;}
try {
db()->prepare("INSERT INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)")
->execute([$slug,$label,$icon,$desc,$active,$sort]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch(Exception $e){ echo json_encode(['success'=>false,'error'=>'Slug already exists']); }
break;
case 'cashout_methods_update':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id']??0);
$label= substr(trim($d['label']??''),0,100);
$icon = substr(trim($d['icon']??'💰'),0,10);
$desc = substr(trim($d['description']??''),0,200);
$sort = (int)($d['sort_order']??0);
$active=(int)(bool)($d['is_active']??1);
if (!$id||!$label){echo json_encode(['success'=>false,'error'=>'ID and label required']);exit;}
db()->prepare("UPDATE cashout_method_types SET label=?,icon=?,description=?,is_active=?,sort_order=? WHERE id=?")
->execute([$label,$icon,$desc,$active,$sort,$id]);
echo json_encode(['success'=>true]);
break;
case 'cashout_methods_delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d=(json_decode(file_get_contents('php://input'),true));
$id=(int)($d['id']??0);
if (!$id){echo json_encode(['success'=>false,'error'=>'ID required']);exit;}
db()->prepare("DELETE FROM cashout_method_types WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
// ──
case 'platform_account_deny':
if ($_SERVER['REQUEST_METHOD']!=='POST'){echo json_encode(['success'=>false]);exit;}
$d=json_decode(file_get_contents('php://input'),true);
$id=(int)($d['id']??0);$nt=substr(trim($d['admin_note']??''),0,500);
db()->prepare("UPDATE platform_accounts SET status='denied',admin_note=?,admin_id=? WHERE id=?")->execute([$nt,$_SESSION['user_id'],$id]);
echo json_encode(['success'=>true]);
break;
case 'platform_account_delete':
if ($_SERVER['REQUEST_METHOD']!=='POST'){echo json_encode(['success'=>false]);exit;}
$d=json_decode(file_get_contents('php://input'),true);
$id=(int)($d['id']??0);
db()->prepare("DELETE FROM platform_accounts WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'platform_accounts_user':
$uid=(int)($_GET['user_id']??0);
$stmt=db()->prepare("SELECT pa.*,COALESCE(p.name,pa.platform_name,pa.platform_slug) AS display_name,p.color,p.player_url FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug WHERE pa.user_id=? ORDER BY pa.requested_at DESC");
$stmt->execute([$uid]);
echo json_encode(['success'=>true,'accounts'=>$stmt->fetchAll()]);
break;
case 'activity_log':
case 'activity_log_v2':
$page = max(1, (int)($_GET['page']??1));
$limit = 20;
$offset = ($page - 1) * $limit;
$category = trim($_GET['category'] ?? '');
$severity = trim($_GET['severity'] ?? '');
$search = trim($_GET['search'] ?? '');
$date = trim($_GET['date'] ?? '');
$where = ["al.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)"];
$params = [];
if ($category) { $where[] = "al.category = ?"; $params[] = $category; }
if ($severity) { $where[] = "al.severity = ?"; $params[] = $severity; }
if ($date) { $where[] = "DATE(al.created_at) = ?"; $params[] = $date; }
if ($search) {
$where[] = "(al.action LIKE ? OR al.detail LIKE ? OR u.username LIKE ? OR u.alias LIKE ? OR al.ip LIKE ?)";
$s = '%'.$search.'%';
$params = array_merge($params, [$s,$s,$s,$s,$s]);
}
$whereStr = implode(' AND ', $where);
$baseQuery = "FROM activity_log al
LEFT JOIN users u ON al.user_id = u.id
LEFT JOIN users a ON al.admin_id = a.id
WHERE $whereStr";
$countStmt = db()->prepare("SELECT COUNT(*) $baseQuery");
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
$dataStmt = db()->prepare("SELECT al.*, u.username, u.alias,
a.username AS admin_username
$baseQuery
ORDER BY al.created_at DESC
LIMIT $limit OFFSET $offset");
$dataStmt->execute($params);
$events = $dataStmt->fetchAll();
// Stats for the current filter set
$statsParams = $params;
$statsStmt = db()->prepare("SELECT
SUM(al.severity='critical') AS critical,
SUM(al.severity='warning') AS warning,
COUNT(DISTINCT al.ip) AS unique_ips
$baseQuery");
$statsStmt->execute($statsParams);
$stats = $statsStmt->fetch();
echo json_encode(['success'=>true,'events'=>$events,'total'=>$total,'page'=>$page,'stats'=>$stats]);
break;
case 'activity_log_csv':
$category = trim($_GET['category'] ?? '');
$severity = trim($_GET['severity'] ?? '');
$search = trim($_GET['search'] ?? '');
$date = trim($_GET['date'] ?? '');
$where = ["al.created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)"];
$params = [];
if ($category) { $where[] = "al.category = ?"; $params[] = $category; }
if ($severity) { $where[] = "al.severity = ?"; $params[] = $severity; }
if ($date) { $where[] = "DATE(al.created_at) = ?"; $params[] = $date; }
if ($search) {
$where[] = "(al.action LIKE ? OR al.detail LIKE ? OR u.username LIKE ?)";
$s = '%'.$search.'%'; $params = array_merge($params, [$s,$s,$s]);
}
$whereStr = implode(' AND ', $where);
$stmt = db()->prepare("SELECT al.*, u.username, u.alias, a.username AS admin_username
FROM activity_log al
LEFT JOIN users u ON al.user_id=u.id
LEFT JOIN users a ON al.admin_id=a.id
WHERE $whereStr ORDER BY al.created_at DESC LIMIT 5000");
$stmt->execute($params);
$rows = $stmt->fetchAll();
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="tomtomgames_audit_' . date('Y-m-d') . '.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','Timestamp','Category','Severity','Action','Username','Alias','Admin','Detail','Old Value','New Value','IP','User Agent','Page','Session ID']);
foreach ($rows as $r) {
fputcsv($out, [$r['id'],$r['created_at'],$r['category'],$r['severity'],$r['action'],
$r['username']??'',$r['alias']??'',$r['admin_username']??'',
$r['detail']??'',$r['old_value']??'',$r['new_value']??'',
$r['ip']??'',$r['user_agent']??'',$r['page']??'',$r['session_id']??'']);
}
fclose($out);
exit;
break;
// ─── CASHOUT METHODS: list (admin) ────────────────────
case 'cashout_methods_list':
$rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true,'types'=>$rows]);
break;
// ─── PAYMENT SETTINGS: list (admin) ───────────────────
case 'payment_settings_list':
$rows = db()->query("SELECT * FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true,'methods'=>$rows]);
break;
case 'payment_settings_update':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$label= substr(trim($d['label']??''), 0, 100);
$handle = substr(trim($d['handle']??''), 0, 200);
$inst = substr(trim($d['instructions']??''), 0, 500);
$enabled = (int)(bool)($d['is_enabled'] ?? 1);
$sort = (int)($d['sort_order'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("UPDATE payment_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?")
->execute([$label,$handle,$inst,$enabled,$sort,$id]);
echo json_encode(['success'=>true]);
break;
// ─── PAYOUT METHODS: get for user ────────────────────────
case 'payout_methods_get':
$uid = (int)($_GET['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$rows = db()->prepare("SELECT * FROM payout_methods WHERE user_id=? ORDER BY is_default DESC, id ASC");
$rows->execute([$uid]);
echo json_encode(['success'=>true,'methods'=>$rows->fetchAll()]);
break;
// ─── GAME ALIASES: get ────────────────────────────────
case 'game_aliases_get':
$uid = (int)($_GET['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("SELECT platform_slug, alias FROM game_aliases WHERE user_id=?");
$stmt->execute([$uid]);
$rows = $stmt->fetchAll();
$map = [];
foreach ($rows as $r) $map[$r['platform_slug']] = $r['alias'];
echo json_encode(['success'=>true,'aliases'=>$map]);
break;
// ─── GAME ALIASES: save all ───────────────────────────
case 'game_aliases_save_all':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$aliases = $data['aliases'] ?? [];
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?)
ON DUPLICATE KEY UPDATE alias=VALUES(alias)");
$del = db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?");
foreach ($aliases as $slug => $alias) {
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($slug)));
$alias = substr(trim($alias), 0, 100);
if (!$slug) continue;
if ($alias === '') $del->execute([$uid, $slug]);
else $stmt->execute([$uid, $slug, $alias]);
}
echo json_encode(['success'=>true]);
break;
// ─── PLATFORMS: admin list ────────────────────────────
case 'platforms_admin':
$rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true,'platforms'=>$rows]);
break;
// ─── PLATFORMS: create ────────────────────────────────
case 'platforms_create':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$name = substr(trim($d['name'] ?? ''), 0, 100);
$purl = substr(trim($d['player_url'] ?? ''), 0, 500);
$curl = substr(trim($d['console_url'] ?? ''), 0, 500);
$color= preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color']??'') ? $d['color'] : '#f0c040';
$sort = (int)($d['sort_order'] ?? 99);
$active=(int)(bool)($d['is_active'] ?? 1);
if (!$slug||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL required']); exit; }
try {
db()->prepare("INSERT INTO platforms (slug,name,player_url,console_url,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?)")
->execute([$slug,$name,$purl,$curl,$color,$sort,$active]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) { echo json_encode(['success'=>false,'error'=>'Slug already exists']); }
break;
// ─── PLATFORMS: update ────────────────────────────────
case 'platforms_update':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$name = substr(trim($d['name'] ?? ''), 0, 100);
$purl = substr(trim($d['player_url'] ?? ''), 0, 500);
$curl = substr(trim($d['console_url'] ?? ''), 0, 500);
$color= preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color']??'') ? $d['color'] : '#f0c040';
$sort = (int)($d['sort_order'] ?? 99);
$active=(int)(bool)($d['is_active'] ?? 1);
if (!$id||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'ID, name, and URL required']); exit; }
db()->prepare("UPDATE platforms SET name=?,player_url=?,console_url=?,color=?,sort_order=?,is_active=? WHERE id=?")
->execute([$name,$purl,$curl,$color,$sort,$active,$id]);
echo json_encode(['success'=>true]);
break;
// ─── PLATFORMS: delete ────────────────────────────────
case 'platforms_delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'billing_get':
$uid = (int)($_GET['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("SELECT * FROM saved_billing WHERE user_id=?");
$stmt->execute([$uid]);
$row = $stmt->fetch();
// Admin sees masked card info
if ($row) {
$row['card_display'] = $row['card_brand'] && $row['card_last4']
? $row['card_brand'] . ' ····' . $row['card_last4'] : null;
// Don't expose raw sq_card_id
unset($row['sq_card_id']);
}
echo json_encode(['success'=>true,'billing'=>$row ?: null]);
break;
// ─── BILLING: save/update ────────────────────────────────
case 'billing_save':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
if (!$uid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("
INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip)
VALUES (?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
first_name=VALUES(first_name), last_name=VALUES(last_name),
email=VALUES(email), address=VALUES(address),
city=VALUES(city), state=VALUES(state), zip=VALUES(zip)
");
$stmt->execute([
$uid,
substr(trim($data['first_name']??''),0,80),
substr(trim($data['last_name'] ??''),0,80),
substr(strtolower(trim($data['email']??'')),0,150),
substr(trim($data['address'] ??''),0,200),
substr(trim($data['city'] ??''),0,80),
strtoupper(substr(trim($data['state']??''),0,2)),
substr(trim($data['zip'] ??''),0,10),
]);
echo json_encode(['success'=>true]);
break;
// ─── BILLING: clear card ─────────────────────────────────
case 'billing_clear_card':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
db()->prepare("UPDATE saved_billing SET card_brand=NULL,card_last4=NULL,card_exp_month=NULL,card_exp_year=NULL,sq_card_id=NULL WHERE user_id=?")->execute([$uid]);
echo json_encode(['success'=>true]);
break;
// ─── BILLING: clear all ──────────────────────────────────
case 'billing_clear_all':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
db()->prepare("DELETE FROM saved_billing WHERE user_id=?")->execute([$uid]);
echo json_encode(['success'=>true]);
break;
// ─── RESEND VERIFICATION (from admin) ─────────────────────
case 'resend_verification':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
$stmt = db()->prepare("SELECT email,alias FROM users WHERE id=?");
$stmt->execute([$uid]);
$user = $stmt->fetch();
if (!$user) { echo json_encode(['success'=>false,'error'=>'User not found']); exit; }
$result = resendVerification($user['email']);
echo json_encode($result);
break;
// ─── CHAT: inbox list ──────────────────────────────────
case 'chat_inbox':
$rows = db()->query("
SELECT u.id AS user_id, u.username, u.alias,
cm.message AS last_message, cm.sender AS last_sender,
cm.created_at AS last_time,
(SELECT COUNT(*) FROM chat_messages
WHERE user_id=u.id AND sender='user' AND is_read=0) AS unread_count
FROM users u
INNER JOIN chat_messages cm ON cm.id=(
SELECT id FROM chat_messages WHERE user_id=u.id ORDER BY id DESC LIMIT 1
)
ORDER BY cm.created_at DESC
")->fetchAll();
echo json_encode(['success'=>true,'inbox'=>$rows]);
break;
// ─── CHAT: full thread for one user ───────────────────
case 'chat_thread':
$tid = (int)($_GET['user_id'] ?? 0);
$since = (int)($_GET['since'] ?? 0);
if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("SELECT id,sender,message,is_read,created_at FROM chat_messages WHERE user_id=? AND id>? ORDER BY id ASC LIMIT 300");
$stmt->execute([$tid, $since]);
// Mark user messages read
db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='user' AND is_read=0")->execute([$tid]);
$uStmt = db()->prepare("SELECT id,username,alias,tokens FROM users WHERE id=?");
$uStmt->execute([$tid]);
echo json_encode(['success'=>true,'messages'=>$stmt->fetchAll(),'user'=>$uStmt->fetch()]);
break;
// ─── CHAT: admin reply ────────────────────────────────
case 'chat_admin_send':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$tid = (int)($data['user_id'] ?? 0);
$msg = trim($data['message'] ?? '');
if (!$tid || empty($msg)) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; }
$stmt = db()->prepare("INSERT INTO chat_messages (user_id,sender,message) VALUES (?,'admin',?)");
$stmt->execute([$tid, $msg]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
break;
// ─── CHAT: clear single user thread ───────────────────
case 'chat_clear_thread':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$tid = (int)($data['user_id'] ?? 0);
if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
db()->prepare("DELETE FROM chat_messages WHERE user_id=?")->execute([$tid]);
echo json_encode(['success'=>true]);
break;
// ─── CHAT: clear ALL chats ────────────────────────────
case 'chat_clear_all':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
db()->exec("DELETE FROM chat_messages");
echo json_encode(['success'=>true]);
break;
case 'chat_unread':
$count = db()->query("SELECT COUNT(*) FROM chat_messages WHERE sender='user' AND is_read=0")->fetchColumn();
echo json_encode(['success'=>true,'count'=>(int)$count]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}