[orbis] Weekly backup 2026-07-07 — 318 files changed, 48597 insertions(+), 7 deletions(-)

This commit is contained in:
DO Server Backup
2026-07-07 15:58:55 +00:00
parent 5fda5a1536
commit fe18800d18
318 changed files with 48597 additions and 7 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
$action = $_GET['action'] ?? 'list';
if ($action !== 'download') {
header('Content-Type: application/json');
}
requireAdmin();
$backupDir = '/home/tomtomgames.com/backups';
if (!is_dir($backupDir)) @mkdir($backupDir, 0750, true);
switch ($action) {
case 'list':
$files = glob($backupDir . '/ttg_backup_*.zip') ?: [];
rsort($files);
$backups = array_map(fn($f) => [
'name' => basename($f),
'size' => filesize($f),
'created' => date('Y-m-d H:i:s', filemtime($f)),
], $files);
echo json_encode(['success'=>true, 'backups'=>$backups]);
break;
case 'create':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; }
set_time_limit(300);
ignore_user_abort(true);
$date = date('Y-m-d_H-i-s');
$sqlFile = "/tmp/ttg_db_{$date}.sql";
$zipFile = "{$backupDir}/ttg_backup_{$date}.zip";
$siteDir = '/home/tomtomgames.com/public_html';
// Export database
$dbCmd = sprintf(
'/usr/bin/mysqldump -u %s -p%s %s > %s 2>/dev/null',
escapeshellarg(DB_USER), escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME), escapeshellarg($sqlFile)
);
exec($dbCmd, $dbOut, $dbRc);
if ($dbRc !== 0 || !file_exists($sqlFile) || filesize($sqlFile) < 10) {
@unlink($sqlFile);
echo json_encode(['success'=>false,'error'=>'Database export failed']); exit;
}
// Zip site files + db dump into one archive
$zipCmd = sprintf(
'/usr/bin/zip -r %s %s %s -x "*/backups/*" 2>&1',
escapeshellarg($zipFile),
escapeshellarg($siteDir),
escapeshellarg($sqlFile)
);
exec($zipCmd, $zipOut, $zipRc);
@unlink($sqlFile);
if ($zipRc !== 0 || !file_exists($zipFile)) {
@unlink($zipFile);
echo json_encode(['success'=>false,'error'=>'Archive creation failed']); exit;
}
// Prune — keep only the 7 most recent
$all = glob($backupDir . '/ttg_backup_*.zip') ?: [];
rsort($all);
foreach (array_slice($all, 7) as $old) @unlink($old);
echo json_encode([
'success' => true,
'name' => basename($zipFile),
'size' => filesize($zipFile),
'created' => date('Y-m-d H:i:s'),
]);
break;
case 'download':
$name = basename($_GET['file'] ?? '');
if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) {
http_response_code(400); echo 'Invalid filename'; exit;
}
$path = $backupDir . '/' . $name;
if (!file_exists($path)) { http_response_code(404); echo 'Not found'; exit; }
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: no-cache');
readfile($path);
exit;
case 'delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'POST required']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$name = basename($data['name'] ?? '');
if (!preg_match('/^ttg_backup_[\d_-]+\.zip$/', $name)) {
echo json_encode(['success'=>false,'error'=>'Invalid filename']); exit;
}
$path = $backupDir . '/' . $name;
if (file_exists($path)) @unlink($path);
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,91 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? '';
$userId = $_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
// ── Get saved billing (user sees own; admin passes user_id param) ──
case 'get':
$uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId;
$stmt = db()->prepare("SELECT * FROM saved_billing WHERE user_id=?");
$stmt->execute([$uid]);
$row = $stmt->fetch();
if ($row && !$isAdmin) {
// Mask card number for non-admin
$row['card_display'] = $row['card_brand'] && $row['card_last4']
? $row['card_brand'] . ' ····' . $row['card_last4']
: null;
unset($row['sq_card_id']);
}
echo json_encode(['success'=>true, 'billing'=>$row ?: null]);
break;
// ── Save / update billing info ─────────────────────────────
case 'save':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId;
$firstName = substr(trim($data['first_name'] ?? ''), 0, 80);
$lastName = substr(trim($data['last_name'] ?? ''), 0, 80);
$email = substr(strtolower(trim($data['email'] ?? '')), 0, 150);
$address = substr(trim($data['address'] ?? ''), 0, 200);
$city = substr(trim($data['city'] ?? ''), 0, 80);
$state = strtoupper(substr(trim($data['state'] ?? ''), 0, 2));
$zip = substr(trim($data['zip'] ?? ''), 0, 10);
// Card info — only update if provided
$cardBrand = isset($data['card_brand']) ? substr(trim($data['card_brand']), 0, 30) : null;
$cardLast4 = isset($data['card_last4']) ? substr(trim($data['card_last4']), 0, 4) : null;
$cardExpMonth = isset($data['card_exp_month'])? substr(trim($data['card_exp_month']),0, 2) : null;
$cardExpYear = isset($data['card_exp_year']) ? substr(trim($data['card_exp_year']), 0, 4) : null;
$sqCardId = isset($data['sq_card_id']) ? substr(trim($data['sq_card_id']), 0, 255) : null;
$stmt = db()->prepare("
INSERT INTO saved_billing
(user_id, first_name, last_name, email, address, city, state, zip,
card_brand, card_last4, card_exp_month, card_exp_year, sq_card_id)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
first_name=VALUES(first_name), last_name=VALUES(last_name),
email=VALUES(email), address=VALUES(address), city=VALUES(city),
state=VALUES(state), zip=VALUES(zip),
card_brand=COALESCE(VALUES(card_brand), card_brand),
card_last4=COALESCE(VALUES(card_last4), card_last4),
card_exp_month=COALESCE(VALUES(card_exp_month), card_exp_month),
card_exp_year=COALESCE(VALUES(card_exp_year), card_exp_year),
sq_card_id=COALESCE(VALUES(sq_card_id), sq_card_id)
");
$stmt->execute([$uid,$firstName,$lastName,$email,$address,$city,$state,$zip,
$cardBrand,$cardLast4,$cardExpMonth,$cardExpYear,$sqCardId]);
echo json_encode(['success'=>true]);
break;
// ── Clear card info only ───────────────────────────────────
case 'clear_card':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId;
db()->prepare("UPDATE saved_billing SET card_brand=NULL, card_last4=NULL, card_exp_month=NULL, card_exp_year=NULL, sq_card_id=NULL WHERE user_id=?")
->execute([$uid]);
echo json_encode(['success'=>true]);
break;
// ── Clear all billing info ────────────────────────────────
case 'clear_all':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId;
db()->prepare("DELETE FROM saved_billing WHERE user_id=?")->execute([$uid]);
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,89 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? 'list';
$userId = (int)$_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
// ── List broadcasts for this user ─────────────────────
case 'list':
// Get broadcasts targeting this user
$stmt = db()->prepare("
SELECT b.*,
u.username AS sender_name,
u.alias AS sender_alias,
(SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?) AS is_read,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count
FROM broadcasts b
JOIN users u ON b.admin_id = u.id
WHERE b.target = 'all'
OR (b.target = 'verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1 AND is_admin=0))
OR (b.target = 'unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0))
OR (b.target = 'admins' AND ?)
ORDER BY b.sent_at DESC
");
$stmt->execute([$userId, $userId, $userId, $isAdmin ? 1 : 0]);
echo json_encode(['success'=>true, 'broadcasts'=>$stmt->fetchAll()]);
break;
// ── Mark as read ──────────────────────────────────────
case 'mark_read':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$bid= (int)($d['broadcast_id'] ?? 0);
if (!$bid) { echo json_encode(['success'=>false]); exit; }
db()->prepare("INSERT IGNORE INTO broadcast_reads (broadcast_id,user_id) VALUES (?,?)")->execute([$bid,$userId]);
echo json_encode(['success'=>true]);
break;
// ── Get replies for a broadcast ───────────────────────
case 'replies':
$bid = (int)($_GET['broadcast_id'] ?? 0);
if (!$bid) { echo json_encode(['success'=>false]); exit; }
$stmt = db()->prepare("
SELECT br.*, u.username, u.alias, u.is_admin
FROM broadcast_replies br
JOIN users u ON br.user_id = u.id
WHERE br.broadcast_id = ?
ORDER BY br.created_at ASC
");
$stmt->execute([$bid]);
echo json_encode(['success'=>true,'replies'=>$stmt->fetchAll()]);
break;
// ── Post a reply ──────────────────────────────────────
case 'reply':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$bid = (int)($d['broadcast_id'] ?? 0);
$msg = substr(trim($d['message'] ?? ''), 0, 1000);
if (!$bid || !$msg) { echo json_encode(['success'=>false,'error'=>'broadcast_id and message required']); exit; }
db()->prepare("INSERT INTO broadcast_replies (broadcast_id,user_id,message) VALUES (?,?,?)")->execute([$bid,$userId,$msg]);
db()->prepare("INSERT IGNORE INTO broadcast_reads (broadcast_id,user_id) VALUES (?,?)")->execute([$bid,$userId]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
break;
// ── Unread count ──────────────────────────────────────
case 'unread_count':
$stmt = db()->prepare("
SELECT COUNT(*) FROM broadcasts b
WHERE (b.target='all' OR (b.target='verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1 AND is_admin=0))
OR (b.target='unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0))
OR (b.target='admins' AND ?))
AND NOT EXISTS (SELECT 1 FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?)
");
$stmt->execute([$userId,$userId,$isAdmin?1:0,$userId]);
echo json_encode(['success'=>true,'count'=>(int)$stmt->fetchColumn()]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,158 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = (int)$_SESSION['user_id'];
$method = $_SERVER['REQUEST_METHOD'];
// ══════════════════════════════════════════════════════════
// GET — player's own requests (list, delete, update, lock)
// ══════════════════════════════════════════════════════════
if ($method === 'GET') {
$action = $_GET['action'] ?? 'list';
if ($action === 'list') {
$stmt = db()->prepare("
SELECT cr.*,
COALESCE(p.name, cr.platform_id) AS platform_name
FROM cashout_requests cr
LEFT JOIN platforms p ON cr.platform_id = p.slug
WHERE cr.user_id = ?
ORDER BY cr.created_at DESC
LIMIT 50
");
$stmt->execute([$userId]);
echo json_encode(['success'=>true, 'requests'=>$stmt->fetchAll()]);
exit;
}
if ($action === 'delete') {
$id = (int)($_GET['id'] ?? 0);
$chk = db()->prepare("SELECT id,tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
$row = $chk->fetch();
if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; }
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$row['tokens'], $userId]);
db()->prepare("DELETE FROM cashout_requests WHERE id=?")->execute([$id]);
$nb = db()->prepare("SELECT tokens FROM users WHERE id=?");
$nb->execute([$userId]);
echo json_encode(['success'=>true,'new_balance'=>(float)$nb->fetchColumn()]);
exit;
}
if ($action === 'update') {
$id = (int)($_GET['id'] ?? 0);
$tokens = (float)($_GET['tokens'] ?? 0);
$alias = substr(trim($_GET['alias'] ?? ''), 0, 100);
$chk = db()->prepare("SELECT id,tokens AS old_tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
$row = $chk->fetch();
if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; }
if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum 1 token']); exit; }
$diff = $tokens - $row['old_tokens'];
if ($diff > 0) {
$balChk = db()->prepare("SELECT tokens FROM users WHERE id=?");
$balChk->execute([$userId]);
if ($diff > (float)$balChk->fetchColumn()) { echo json_encode(['success'=>false,'error'=>'Insufficient balance']); exit; }
}
db()->beginTransaction();
db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$diff, $userId]);
db()->prepare("UPDATE cashout_requests SET tokens=?,alias=? WHERE id=?")->execute([$tokens, $alias, $id]);
db()->commit();
echo json_encode(['success'=>true]);
exit;
}
if ($action === 'lock') {
$id = (int)($_GET['id'] ?? 0);
$chk = db()->prepare("SELECT id FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
if (!$chk->fetch()) { echo json_encode(['success'=>false,'error'=>'Request not found']); exit; }
try {
db()->exec("ALTER TABLE cashout_requests MODIFY COLUMN status ENUM('pending','locked','sent','approved','rejected','deleted') DEFAULT 'pending'");
} catch (Exception $e) {}
db()->prepare("UPDATE cashout_requests SET status='locked' WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
exit;
}
// ══════════════════════════════════════════════════════════
// POST — submit new cashout request
// ══════════════════════════════════════════════════════════
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$platformId = trim($data['platform_id'] ?? '');
$alias = trim($data['alias'] ?? '');
$tokens = (float)($data['tokens'] ?? 0);
$payoutMethodId = (int)($data['payout_method_id'] ?? 0);
$payoutMethodType = trim($data['payout_method_type'] ?? '');
$payoutHandle = trim($data['payout_handle'] ?? '');
// Validate platform
$platStmt = db()->prepare("SELECT slug FROM platforms WHERE slug=? AND is_active=1 LIMIT 1");
$platStmt->execute([$platformId]);
if (!$platStmt->fetch()) {
$platforms = json_decode(PLATFORMS, true);
if (empty(array_filter($platforms, fn($p) => $p['id'] === $platformId))) {
echo json_encode(['success'=>false,'error'=>'Invalid platform.']); exit;
}
}
if (empty($alias)) { echo json_encode(['success'=>false,'error'=>'Platform alias required.']); exit; }
if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum cashout is 1 token.']); exit; }
// Validate payout method
if ($payoutMethodId) {
$chk = db()->prepare("SELECT method_type,account_handle FROM payout_methods WHERE id=? AND user_id=?");
$chk->execute([$payoutMethodId, $userId]);
if ($pm = $chk->fetch()) {
$payoutMethodType = $pm['method_type'];
$payoutHandle = $pm['account_handle'];
}
}
// Reject if the payout method type is not currently admin-enabled
if ($payoutMethodType) {
$enaStmt = db()->prepare("SELECT is_enabled FROM admin_payout_settings WHERE method_key=?");
$enaStmt->execute([$payoutMethodType]);
$enaRow = $enaStmt->fetch();
if (!$enaRow || !$enaRow['is_enabled']) {
echo json_encode(['success'=>false,'error'=>'This payout method is no longer available. Please select a different method or add a new one.']); exit;
}
}
// Check balance
$balStmt = db()->prepare("SELECT tokens FROM users WHERE id=?");
$balStmt->execute([$userId]);
$balance = (float)$balStmt->fetchColumn();
if ($tokens > $balance) { echo json_encode(['success'=>false,'error'=>'Insufficient token balance.']); exit; }
// Deduct & create
db()->beginTransaction();
try {
db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$tokens, $userId]);
db()->prepare("INSERT INTO cashout_requests (user_id,platform_id,alias,tokens,payout_method_type,payout_handle) VALUES (?,?,?,?,?,?)")
->execute([$userId, $platformId, $alias, $tokens, $payoutMethodType, $payoutHandle]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Request failed. Try again.']); exit;
}
$newBalStmt = db()->prepare("SELECT tokens FROM users WHERE id=?");
$newBalStmt->execute([$userId]);
$nb = (float)$newBalStmt->fetchColumn();
try { logActivity('cashout_request', $userId, null, 'cashout', 0, "Cashout: {$tokens} tokens via {$payoutMethodType}"); } catch(Exception $e){}
echo json_encode(['success'=>true, 'new_balance'=>$nb]);
@@ -0,0 +1,79 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
$action = $_GET['action'] ?? 'list';
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
switch ($action) {
// Public: active types that admin has enabled for payout processing
case 'list':
$rows = db()->query("
SELECT cmt.slug, cmt.label, cmt.icon, cmt.description
FROM cashout_method_types cmt
INNER JOIN admin_payout_settings aps ON aps.method_key = cmt.slug
WHERE cmt.is_active = 1 AND aps.is_enabled = 1
ORDER BY cmt.sort_order ASC, cmt.id ASC
")->fetchAll();
echo json_encode(['success'=>true, 'types'=>$rows]);
break;
// Admin: all types
case 'admin_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'types'=>$rows]);
break;
// Admin: create
case 'create':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$label= substr(trim($d['label'] ?? ''), 0, 100);
$icon = substr(trim($d['icon'] ?? '💰'), 0, 10);
$desc = substr(trim($d['description'] ?? ''), 0, 200);
$sort = (int)($d['sort_order'] ?? 99);
$active = (int)(bool)($d['is_active'] ?? 1);
if (!$slug || !$label) { echo json_encode(['success'=>false,'error'=>'Slug and label required']); exit; }
try {
db()->prepare("INSERT INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)")
->execute([$slug,$label,$icon,$desc,$active,$sort]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Slug already exists']);
}
break;
// Admin: update
case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$label= substr(trim($d['label'] ?? ''), 0, 100);
$icon = substr(trim($d['icon'] ?? '💰'), 0, 10);
$desc = substr(trim($d['description'] ?? ''), 0, 200);
$sort = (int)($d['sort_order'] ?? 0);
$active = (int)(bool)($d['is_active'] ?? 1);
if (!$id || !$label) { echo json_encode(['success'=>false,'error'=>'ID and label required']); exit; }
db()->prepare("UPDATE cashout_method_types SET label=?,icon=?,description=?,is_active=?,sort_order=? WHERE id=?")
->execute([$label,$icon,$desc,$active,$sort,$id]);
echo json_encode(['success'=>true]);
break;
// Admin: delete
case 'delete':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM cashout_method_types WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,124 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? '';
$userId = $_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
// ── User: get own messages ─────────────────────────────
case 'messages':
$since = (int)($_GET['since'] ?? 0); // last message id for polling
$stmt = db()->prepare("
SELECT id, sender, message, is_read, created_at
FROM chat_messages
WHERE user_id = ? AND id > ?
ORDER BY id ASC
LIMIT 100
");
$stmt->execute([$userId, $since]);
$msgs = $stmt->fetchAll();
// Mark admin messages as read
db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='admin' AND is_read=0")->execute([$userId]);
echo json_encode(['success'=>true, 'messages'=>$msgs]);
break;
// ── User: send message to admin ───────────────────────
case 'send':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$msg = trim($data['message'] ?? '');
if (empty($msg) || mb_strlen($msg) > 2000) { echo json_encode(['success'=>false,'error'=>'Invalid message']); exit; }
$stmt = db()->prepare("INSERT INTO chat_messages (user_id, sender, message) VALUES (?, 'user', ?)");
$stmt->execute([$userId, $msg]);
echo json_encode(['success'=>true, 'id'=>db()->lastInsertId()]);
break;
// ── Admin: get inbox list (one row per user, latest msg) ──
case 'admin_inbox':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("
SELECT
u.id AS user_id,
u.username,
u.alias,
cm.message AS last_message,
cm.sender AS last_sender,
cm.created_at AS last_time,
SUM(CASE WHEN cm2.sender='user' AND cm2.is_read=0 THEN 1 ELSE 0 END) AS unread_count
FROM users u
JOIN chat_messages cm ON cm.id = (
SELECT id FROM chat_messages
WHERE user_id = u.id
ORDER BY id DESC LIMIT 1
)
LEFT JOIN chat_messages cm2 ON cm2.user_id = u.id
GROUP BY u.id, u.username, u.alias, cm.message, cm.sender, cm.created_at
ORDER BY cm.created_at DESC
")->fetchAll();
echo json_encode(['success'=>true, 'inbox'=>$rows]);
break;
// ── Admin: get all messages for a specific user ────────
case 'admin_thread':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$tid = (int)($_GET['user_id'] ?? 0);
$since = (int)($_GET['since'] ?? 0);
if (!$tid) { echo json_encode(['success'=>false,'error'=>'user_id required']); exit; }
$stmt = db()->prepare("
SELECT id, sender, message, is_read, created_at
FROM chat_messages
WHERE user_id = ? AND id > ?
ORDER BY id ASC LIMIT 200
");
$stmt->execute([$tid, $since]);
$msgs = $stmt->fetchAll();
// Mark user messages as read
db()->prepare("UPDATE chat_messages SET is_read=1 WHERE user_id=? AND sender='user' AND is_read=0")->execute([$tid]);
// Get user info
$uStmt = db()->prepare("SELECT id, username, alias, tokens FROM users WHERE id=?");
$uStmt->execute([$tid]);
$user = $uStmt->fetch();
echo json_encode(['success'=>true, 'messages'=>$msgs, 'user'=>$user]);
break;
// ── Admin: reply to a user ────────────────────────────
case 'admin_send':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$tid = (int)($data['user_id'] ?? 0);
$msg = trim($data['message'] ?? '');
if (!$tid || empty($msg) || mb_strlen($msg) > 2000) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; }
$stmt = db()->prepare("INSERT INTO chat_messages (user_id, sender, message) VALUES (?, 'admin', ?)");
$stmt->execute([$tid, $msg]);
echo json_encode(['success'=>true, 'id'=>db()->lastInsertId()]);
break;
// ── Unread count (for badge) ──────────────────────────
case 'unread':
if ($isAdmin) {
$count = db()->query("SELECT COUNT(*) FROM chat_messages WHERE sender='user' AND is_read=0")->fetchColumn();
} else {
$stmt = db()->prepare("SELECT COUNT(*) FROM chat_messages WHERE user_id=? AND sender='admin' AND is_read=0");
$stmt->execute([$userId]);
$count = $stmt->fetchColumn();
}
echo json_encode(['success'=>true, 'count'=>(int)$count]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,65 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? '';
$userId = $_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
// ── Get all aliases for a user ────────────────────────
case 'get':
$uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId;
$stmt = db()->prepare("SELECT platform_slug, alias FROM game_aliases WHERE user_id=?");
$stmt->execute([$uid]);
$rows = $stmt->fetchAll();
$map = [];
foreach ($rows as $r) $map[$r['platform_slug']] = $r['alias'];
echo json_encode(['success'=>true, 'aliases'=>$map]);
break;
// ── Save a single alias ───────────────────────────────
case 'save':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId;
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($data['platform_slug'] ?? '')));
$alias = substr(trim($data['alias'] ?? ''), 0, 100);
if (!$slug) { echo json_encode(['success'=>false,'error'=>'Platform slug required']); exit; }
if ($alias === '') {
// Empty alias = delete it
db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?")->execute([$uid,$slug]);
} else {
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?)
ON DUPLICATE KEY UPDATE alias=VALUES(alias)")->execute([$uid,$slug,$alias]);
}
echo json_encode(['success'=>true]);
break;
// ── Save all aliases at once (bulk) ───────────────────
case 'save_all':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($data['user_id']) ? (int)$data['user_id'] : $userId;
$aliases = $data['aliases'] ?? [];
$stmt = db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?)
ON DUPLICATE KEY UPDATE alias=VALUES(alias)");
$del = db()->prepare("DELETE FROM game_aliases WHERE user_id=? AND platform_slug=?");
foreach ($aliases as $slug => $alias) {
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($slug)));
$alias = substr(trim($alias), 0, 100);
if (!$slug) continue;
if ($alias === '') $del->execute([$uid, $slug]);
else $stmt->execute([$uid, $slug, $alias]);
}
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,37 @@
<?php
ob_start();
try {
require_once __DIR__ . '/../../includes/auth.php';
} catch (Throwable $e) {
ob_end_clean();
header('Content-Type: application/json');
echo json_encode(['success'=>false,'error'=>'Server error']);
exit;
}
ob_end_clean();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
if (empty($username) || empty($password)) {
echo json_encode(['success'=>false,'error'=>'Username and password required']); exit;
}
try {
$result = loginUser($username, $password);
} catch (Throwable $e) {
echo json_encode(['success'=>false,'error'=>'Login error. Please try again.']); exit;
}
if ($result['success'] && isset($result['user'])) {
logPlayerAction('LOGIN_SUCCESS', $result['user']['id'], 'User logged in', 'auth', 'info');
unset($result['user']['password']);
}
if (!$result['success']) { logSecurityEvent('LOGIN_FAILED', null, 'Failed login attempt for: ' . $username, 'warning'); }
echo json_encode($result);
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
logoutUser();
echo json_encode(['success' => true]);
@@ -0,0 +1,35 @@
<?php
ob_start();
try {
require_once __DIR__ . '/../../includes/auth.php';
} catch (Throwable $e) {
ob_end_clean();
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Server error']);
exit;
}
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) {
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
exit;
}
try {
$user = currentUser();
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => 'DB error']);
exit;
}
if (!$user) {
echo json_encode(['success' => false, 'error' => 'User not found']);
exit;
}
// Sync session is_admin with DB value — catches admin elevation/demotion
$_SESSION['is_admin'] = (int)$user['is_admin'];
unset($user['password']);
echo json_encode(['success' => true, 'user' => $user]);
@@ -0,0 +1,76 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = (int)$_SESSION['user_id'];
$action = $_GET['action'] ?? 'all';
// ── Purchases ──────────────────────────────────────────────
if ($action === 'all' || $action === 'purchases') {
$stmt = db()->prepare("
SELECT id, tokens, amount_cents, payment_method, platform_id, game_alias,
card_brand, card_last4, status, admin_note, created_at
FROM token_purchases
WHERE user_id=?
ORDER BY created_at DESC
LIMIT 50
");
$stmt->execute([$userId]);
$purchases = $stmt->fetchAll();
}
// ── Cashouts ───────────────────────────────────────────────
if ($action === 'all' || $action === 'cashouts') {
$stmt = db()->prepare("
SELECT cr.*,
COALESCE(p.name, cr.platform_id) AS platform_name
FROM cashout_requests cr
LEFT JOIN platforms p ON cr.platform_id = p.slug
WHERE cr.user_id=?
ORDER BY cr.created_at DESC
LIMIT 50
");
$stmt->execute([$userId]);
$cashouts = $stmt->fetchAll();
}
// ── Broadcasts/Invites (use broadcasts as announcements) ───
if ($action === 'all' || $action === 'broadcasts') {
$stmt = db()->prepare("
SELECT b.id, b.subject, b.message, b.sent_at,
u.username AS sender,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id AND user_id=?) AS is_read,
(SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id AND user_id=?) AS replied
FROM broadcasts b
JOIN users u ON b.admin_id=u.id
WHERE b.target='all'
OR (b.target='verified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=1))
OR (b.target='unverified' AND EXISTS(SELECT 1 FROM users WHERE id=? AND email_verified=0))
OR (b.target='admins' AND 0)
ORDER BY b.sent_at DESC
LIMIT 20
");
$stmt->execute([$userId,$userId,$userId,$userId]);
$broadcasts = $stmt->fetchAll();
}
if ($action === 'all') {
echo json_encode([
'success' => true,
'purchases' => $purchases,
'cashouts' => $cashouts,
'broadcasts' => $broadcasts,
]);
} elseif ($action === 'purchases') {
echo json_encode(['success'=>true,'purchases'=>$purchases]);
} elseif ($action === 'cashouts') {
echo json_encode(['success'=>true,'cashouts'=>$cashouts]);
} elseif ($action === 'broadcasts') {
echo json_encode(['success'=>true,'broadcasts'=>$broadcasts]);
} else {
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = $_SESSION['user_id'];
$stmt = db()->prepare("
SELECT id, tokens, amount_cents, payment_method, platform_id, game_alias,
card_brand, card_last4, status, created_at
FROM token_purchases
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 20
");
$stmt->execute([$userId]);
echo json_encode(['success'=>true, 'purchases'=>$stmt->fetchAll()]);
@@ -0,0 +1,44 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
$action = $_GET['action'] ?? 'list';
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
switch ($action) {
// Public: get all enabled payment methods including card status
case 'list':
// Include card row (is_enabled controls whether card appears at checkout)
$rows = db()->query("SELECT method_key, label, handle, instructions, is_enabled FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'methods'=>$rows]);
break;
// Admin: get all methods including disabled
case 'admin_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("SELECT * FROM payment_settings ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'methods'=>$rows]);
break;
// Admin: update a single method
case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$label= substr(trim($d['label']??''), 0, 100);
$handle = substr(trim($d['handle']??''), 0, 200);
$instructions = substr(trim($d['instructions']??''), 0, 500);
$enabled = (int)(bool)($d['is_enabled'] ?? 1);
$sort = (int)($d['sort_order'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("UPDATE payment_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?")
->execute([$label,$handle,$instructions,$enabled,$sort,$id]);
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,82 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? '';
$userId = (int)$_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
case 'list':
$uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId;
$rows = db()->prepare("
SELECT pm.*,
COALESCE(aps.is_enabled, 0) AS admin_enabled
FROM payout_methods pm
LEFT JOIN admin_payout_settings aps ON aps.method_key = pm.method_type
WHERE pm.user_id = ?
ORDER BY pm.is_default DESC, pm.id ASC
");
$rows->execute([$uid]);
echo json_encode(['success'=>true, 'methods'=>$rows->fetchAll()]);
break;
case 'add':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($d['user_id']) ? (int)$d['user_id'] : $userId;
$type = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['method_type'] ?? '')));
$label = substr(trim($d['label'] ?? ''), 0, 100);
$handle= substr(trim($d['account_handle'] ?? ''), 0, 200);
$def = (int)(bool)($d['is_default'] ?? 0);
if (!$type || !$label || !$handle) { echo json_encode(['success'=>false,'error'=>'All fields required']); exit; }
db()->beginTransaction();
if ($def) db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]);
// If first method, auto-set as default
$count = db()->prepare("SELECT COUNT(*) FROM payout_methods WHERE user_id=?"); $count->execute([$uid]);
if ((int)$count->fetchColumn() === 0) $def = 1;
db()->prepare("INSERT INTO payout_methods (user_id,method_type,label,account_handle,is_default) VALUES (?,?,?,?,?)")
->execute([$uid,$type,$label,$handle,$def]);
$newId = db()->lastInsertId();
db()->commit();
echo json_encode(['success'=>true,'id'=>$newId]);
break;
case 'set_default':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
// Verify ownership
$chk = db()->prepare("SELECT user_id FROM payout_methods WHERE id=?"); $chk->execute([$id]);
$row = $chk->fetch();
if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
$uid = $row['user_id'];
db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]);
db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$chk = db()->prepare("SELECT user_id,is_default FROM payout_methods WHERE id=?"); $chk->execute([$id]);
$row = $chk->fetch();
if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
db()->prepare("DELETE FROM payout_methods WHERE id=?")->execute([$id]);
// If deleted default, set next one as default
if ($row['is_default']) {
$next = db()->prepare("SELECT id FROM payout_methods WHERE user_id=? LIMIT 1"); $next->execute([$row['user_id']]);
if ($n = $next->fetch()) db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$n['id']]);
}
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,153 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
require_once __DIR__ . '/../../includes/square.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn() || empty($_SESSION['is_admin'])) {
echo json_encode(['success'=>false,'error'=>'Forbidden']); exit;
}
$adminId = (int)$_SESSION['user_id'];
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'list_settings';
// ── GET actions ──────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'list_settings') {
$rows = db()->query("SELECT * FROM admin_payout_settings ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'settings'=>$rows]);
exit;
}
if ($action === 'cashout_detail') {
$id = (int)($_GET['id'] ?? 0);
$stmt = db()->prepare("
SELECT cr.*, u.username, u.alias AS user_alias, u.email,
pm.method_type AS saved_payout_type, pm.account_handle AS saved_payout_handle, pm.label AS saved_payout_label
FROM cashout_requests cr
JOIN users u ON cr.user_id = u.id
LEFT JOIN payout_methods pm ON pm.user_id = cr.user_id AND pm.is_default = 1
WHERE cr.id = ?
");
$stmt->execute([$id]);
$row = $stmt->fetch();
echo json_encode(['success'=>true, 'cashout'=>$row]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']); exit;
}
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
// ── Update payout settings ────────────────────────────────
if ($action === 'update_setting') {
$id = (int)($d['id'] ?? 0);
$handle = substr(trim($d['handle']??''), 0, 200);
$instr = substr(trim($d['instructions']??''), 0, 500);
$enabled = (int)(bool)($d['is_enabled']??1);
$label = substr(trim($d['label']??''), 0, 100);
$sort = (int)($d['sort_order']??0);
db()->prepare("UPDATE admin_payout_settings SET label=?,handle=?,instructions=?,is_enabled=?,sort_order=? WHERE id=?")
->execute([$label,$handle,$instr,$enabled,$sort,$id]);
echo json_encode(['success'=>true]); exit;
}
// ── Process cashout payout ────────────────────────────────
if ($action === 'process_payout') {
$cashoutId = (int)($d['cashout_id'] ?? 0);
$payoutKey = trim($d['payout_method_key'] ?? '');
$payoutType = trim($d['payout_type'] ?? 'manual'); // 'manual' or 'square_gift_card'
$note = substr(trim($d['note'] ?? ''), 0, 500);
// Load cashout
$stmt = db()->prepare("SELECT cr.*, u.username, u.alias, u.email FROM cashout_requests cr JOIN users u ON cr.user_id=u.id WHERE cr.id=? AND cr.status IN ('pending','locked')");
$stmt->execute([$cashoutId]);
$cashout = $stmt->fetch();
if (!$cashout) { echo json_encode(['success'=>false,'error'=>'Cashout not found or already processed']); exit; }
// Amount in cents (1 token = $1)
$amountCents = (int)round($cashout['tokens'] * 100);
// Load payout setting
$pset = db()->prepare("SELECT * FROM admin_payout_settings WHERE method_key=? AND is_enabled=1");
$pset->execute([$payoutKey]);
$setting = $pset->fetch();
$squareTxnId = null;
$giftCardGan = null;
$giftCardBal = null;
$txnStatus = 'completed';
if ($payoutType === 'square_gift_card') {
// ── Square Gift Card Flow ─────────────────────────
try {
// 1. Create gift card
$gcRes = SquareClient::post('/v2/gift-cards', [
'idempotency_key' => 'gc-create-' . $cashoutId . '-' . time(),
'location_id' => SQUARE_LOCATION_ID,
'gift_card' => ['type' => 'DIGITAL'],
]);
if (empty($gcRes['gift_card']['id'])) {
throw new Exception('Failed to create gift card: ' . json_encode($gcRes));
}
$gcId = $gcRes['gift_card']['id'];
$gcGan = $gcRes['gift_card']['gan'] ?? '';
// 2. Activate gift card with balance
$actRes = SquareClient::post('/v2/gift-card-activities', [
'idempotency_key' => 'gc-activate-' . $cashoutId . '-' . time(),
'gift_card_activity' => [
'type' => 'ACTIVATE',
'location_id' => SQUARE_LOCATION_ID,
'gift_card_id' => $gcId,
'activate_activity_details' => [
'amount_money' => [
'amount' => $amountCents,
'currency' => 'USD',
],
],
],
]);
if (empty($actRes['gift_card_activity']['id'])) {
throw new Exception('Failed to activate gift card: ' . json_encode($actRes));
}
$giftCardGan = $gcGan;
$giftCardBal = $amountCents;
$squareTxnId = $actRes['gift_card_activity']['id'];
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Square error: ' . $e->getMessage()]); exit;
}
}
// ── Mark cashout as sent ──────────────────────────────
db()->beginTransaction();
try {
db()->prepare("UPDATE cashout_requests SET status='sent', admin_note=?, resolved_at=NOW() WHERE id=?")
->execute([$note, $cashoutId]);
db()->prepare("INSERT INTO cashout_transactions (cashout_id,admin_id,payout_method,payout_type,amount_cents,square_txn_id,gift_card_gan,gift_card_balance,note,status)
VALUES (?,?,?,?,?,?,?,?,?,'completed')")
->execute([$cashoutId, $adminId, $payoutKey, $payoutType, $amountCents, $squareTxnId, $giftCardGan, $giftCardBal, $note]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error: '.$e->getMessage()]); exit;
}
$response = ['success'=>true, 'status'=>'sent', 'amount_cents'=>$amountCents];
if ($giftCardGan) {
$response['gift_card_gan'] = $giftCardGan;
$response['gift_card_balance'] = $giftCardBal;
}
echo json_encode($response); exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
@@ -0,0 +1,92 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = (int)$_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'list';
if ($method === 'GET') {
if ($action === 'list') {
$uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId;
$stmt = db()->prepare("SELECT pa.*, COALESCE(p.name, pa.platform_slug) AS platform_name, p.color
FROM platform_accounts pa LEFT JOIN platforms p ON pa.platform_slug=p.slug
WHERE pa.user_id=? ORDER BY pa.requested_at DESC");
$stmt->execute([$uid]);
$rows = $stmt->fetchAll();
foreach ($rows as &$row) {
if (!$isAdmin && $row['status'] !== 'approved') $row['platform_password'] = null;
}
echo json_encode(['success'=>true,'accounts'=>$rows]);
} elseif ($action === 'check_onboarding') {
$cnt = db()->prepare("SELECT COUNT(*) FROM platform_accounts WHERE user_id=?");
$cnt->execute([$userId]);
$hasAny = (int)$cnt->fetchColumn() > 0;
// Check flag — graceful fallback if column doesn't exist
$done = false;
try {
$s = db()->prepare("SELECT platform_onboarding_done FROM users WHERE id=?");
$s->execute([$userId]);
$r = $s->fetch(); $done = !empty($r['platform_onboarding_done']);
} catch(Exception $e){}
echo json_encode(['success'=>true,'needs_onboarding'=>(!$done && !$hasAny),'has_accounts'=>$hasAny]);
} else {
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
exit;
}
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
if ($action === 'request') {
$slug = preg_replace('/[^a-z0-9_]/','',strtolower(trim($d['platform_slug']??'')));
if (!$slug) { echo json_encode(['success'=>false,'error'=>'Platform required']); exit; }
try {
db()->prepare("INSERT INTO platform_accounts (user_id,platform_slug) VALUES (?,?)")->execute([$userId,$slug]);
try { db()->prepare("UPDATE users SET platform_onboarding_done=1 WHERE id=?")->execute([$userId]); } catch(Exception $e){}
echo json_encode(['success'=>true]);
} catch(Exception $e) { echo json_encode(['success'=>false,'error'=>'Already requested for this platform']); }
exit;
}
if ($action === 'dismiss_onboarding') {
try { db()->prepare("UPDATE users SET platform_onboarding_done=1 WHERE id=?")->execute([$userId]); } catch(Exception $e){}
echo json_encode(['success'=>true]);
exit;
}
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
if ($action === 'resolve') {
$id=$d['id']??0; $status=$d['status']??'';
$uname=substr(trim($d['platform_username']??''),0,100);
$pass=substr(trim($d['platform_password']??''),0,200);
$note=substr(trim($d['admin_note']??''),0,300);
if (!in_array($status,['approved','denied','deleted'])){echo json_encode(['success'=>false,'error'=>'Invalid status']);exit;}
$chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch();
if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;}
db()->prepare("UPDATE platform_accounts SET status=?,platform_username=?,platform_password=?,admin_note=?,resolved_at=NOW(),admin_id=? WHERE id=?")
->execute([$status,$uname,$pass,$note,(int)$_SESSION['user_id'],$id]);
if ($status==='approved'&&$uname) {
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")
->execute([$row['user_id'],$row['platform_slug'],$uname]);
}
echo json_encode(['success'=>true]);exit;
}
if ($action === 'update_credentials') {
$id=$d['id']??0;
$uname=substr(trim($d['platform_username']??''),0,100);
$pass=substr(trim($d['platform_password']??''),0,200);
$note=substr(trim($d['admin_note']??''),0,300);
$chk=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$chk->execute([$id]);$row=$chk->fetch();
if (!$row){echo json_encode(['success'=>false,'error'=>'Not found']);exit;}
db()->prepare("UPDATE platform_accounts SET platform_username=?,platform_password=?,admin_note=? WHERE id=?")
->execute([$uname,$pass,$note,$id]);
if ($uname) {
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")
->execute([$row['user_id'],$row['platform_slug'],$uname]);
}
echo json_encode(['success'=>true]);exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
@@ -0,0 +1,185 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
$action = $_GET['action'] ?? 'list';
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
$isMasterAdmin = $isAdmin && (int)($_SESSION['user_id'] ?? 0) === MASTER_ADMIN_ID;
switch ($action) {
// ── Public: active platforms for player app ───────────
case 'list':
$stmt = db()->query("SELECT slug,name,player_url,url_alias_param,color,icon_path FROM platforms WHERE is_active=1 AND is_deleted=0 ORDER BY sort_order ASC, id ASC");
$rows = $stmt->fetchAll();
$out = array_map(fn($r) => [
'id' => $r['slug'],
'name' => $r['name'],
'url' => $r['player_url'],
'alias_param' => $r['url_alias_param'] ?? '',
'color' => $r['color'],
], $rows);
echo json_encode(['success'=>true, 'platforms'=>$out]);
break;
// ── Admin: full list including agent fields and inactive ─
case 'admin_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$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;
// ── Admin: create platform ────────────────────────────
case 'create':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$url_alias_param = preg_replace('/[^a-zA-Z0-9_\-]/', '', trim($d['url_alias_param'] ?? ''));
$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);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$sort_order = (int)($d['sort_order'] ?? 99);
$is_active = isset($d['is_active']) ? (int)(bool)$d['is_active'] : 1;
if (!$slug || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL are required']); exit; }
try {
$stmt = db()->prepare("INSERT INTO platforms (slug,name,player_url,url_alias_param,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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$slug,$name,$player_url,$url_alias_param,$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]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Slug already exists or DB error']);
}
break;
// ── Admin: update platform ────────────────────────────
case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$url_alias_param = preg_replace('/[^a-zA-Z0-9_\-]/', '', trim($d['url_alias_param'] ?? ''));
$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);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$sort_order = (int)($d['sort_order'] ?? 99);
$is_active = (int)(bool)($d['is_active'] ?? 1);
if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; }
if ($isMasterAdmin) {
// Master admin: update all fields including agent info
db()->prepare("UPDATE platforms SET name=?,player_url=?,url_alias_param=?,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=? WHERE id=?")
->execute([$name,$player_url,$url_alias_param,$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,$id]);
} else {
// Regular admin: update non-sensitive fields including alias param
db()->prepare("UPDATE platforms SET name=?,player_url=?,url_alias_param=?,color=?,sort_order=?,is_active=? WHERE id=?")
->execute([$name,$player_url,$url_alias_param,$color,$sort_order,$is_active,$id]);
}
echo json_encode(['success'=>true]);
break;
// ── Admin: delete platform ────────────────────────────
case 'delete':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
// ── Admin: reorder platforms ──────────────────────────
case 'reorder':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$order = $d['order'] ?? []; // array of IDs in desired order
$stmt = db()->prepare("UPDATE platforms SET sort_order=? WHERE id=?");
foreach ($order as $i => $pid) { $stmt->execute([$i, (int)$pid]); }
echo json_encode(['success'=>true]);
break;
// ── Admin: list credits for a platform ───────────────
case 'credits_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$pid = (int)($_GET['platform_id'] ?? 0);
if (!$pid) { echo json_encode(['success'=>false,'error'=>'platform_id required']); exit; }
$rows = db()->prepare("SELECT * FROM platform_credits WHERE platform_id=? ORDER BY credit_date DESC, id DESC");
$rows->execute([$pid]);
$credits = $rows->fetchAll();
$total = db()->prepare("SELECT COALESCE(SUM(CASE WHEN type='debit' THEN -credits_purchased ELSE credits_purchased END),0) FROM platform_credits WHERE platform_id=?");
$total->execute([$pid]);
echo json_encode(['success'=>true,'credits'=>$credits,'total'=>(float)$total->fetchColumn()]);
break;
// ── Admin: add credit entry ───────────────────────────
case 'credits_create':
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$pid = (int)($d['platform_id'] ?? 0);
$credits = (float)($d['credits_purchased'] ?? 0);
$date = $d['credit_date'] ?? date('Y-m-d');
$method = substr(trim($d['payment_method'] ?? ''), 0, 100);
$notes = trim($d['notes'] ?? '');
if (!$pid || $credits <= 0 || !$date) { echo json_encode(['success'=>false,'error'=>'platform_id, credits_purchased, and credit_date are required']); exit; }
$stmt = db()->prepare("INSERT INTO platform_credits (platform_id,credits_purchased,credit_date,payment_method,notes) VALUES (?,?,?,?,?)");
$stmt->execute([$pid,$credits,$date,$method,$notes]);
$newId = db()->lastInsertId();
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
$total->execute([$pid]);
echo json_encode(['success'=>true,'id'=>$newId,'total'=>(float)$total->fetchColumn()]);
break;
// ── Admin: update credit entry ────────────────────────
case 'credits_update':
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$credits = (float)($d['credits_purchased'] ?? 0);
$date = $d['credit_date'] ?? date('Y-m-d');
$method = substr(trim($d['payment_method'] ?? ''), 0, 100);
$notes = trim($d['notes'] ?? '');
if (!$id || $credits <= 0 || !$date) { echo json_encode(['success'=>false,'error'=>'id, credits_purchased, and credit_date are required']); exit; }
db()->prepare("UPDATE platform_credits SET credits_purchased=?,credit_date=?,payment_method=?,notes=? WHERE id=?")
->execute([$credits,$date,$method,$notes,$id]);
$row = db()->prepare("SELECT platform_id FROM platform_credits WHERE id=?");
$row->execute([$id]);
$pid = (int)($row->fetchColumn() ?: 0);
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
$total->execute([$pid]);
echo json_encode(['success'=>true,'total'=>(float)$total->fetchColumn()]);
break;
// ── Admin: delete credit entry ────────────────────────
case 'credits_delete':
if (!$isMasterAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
$row = db()->prepare("SELECT platform_id FROM platform_credits WHERE id=?");
$row->execute([$id]);
$pid = (int)($row->fetchColumn() ?: 0);
db()->prepare("DELETE FROM platform_credits WHERE id=?")->execute([$id]);
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
$total->execute([$pid]);
echo json_encode(['success'=>true,'total'=>(float)$total->fetchColumn()]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
@@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
require_once __DIR__ . '/../../includes/square.php';
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$sourceId = trim($data['source_id'] ?? '');
$tokens = (int)($data['tokens'] ?? 0);
$priceCents = (int)($data['price_cents'] ?? 0);
$method = trim($data['method'] ?? 'card');
$platformId = trim($data['platform_id'] ?? '');
$gameAlias = trim($data['game_alias'] ?? '');
$playerName = trim($data['player_name'] ?? '');
$isCustom = (bool)($data['is_custom'] ?? false);
$billing = $data['billing'] ?? [];
$userId = $_SESSION['user_id'];
// ─── Validate ─────────────────────────────────────────────
if ($tokens < 1 || $priceCents < 100) {
echo json_encode(['success'=>false,'error'=>'Minimum purchase is $1 (1 token).']); exit;
}
// Validate preset packages (custom bypasses preset check)
if (!$isCustom) {
$packages = json_decode(TOKEN_PACKAGES, true);
$validPkg = false;
foreach ($packages as $pkg) {
if ($pkg['tokens'] === $tokens && ($pkg['price'] * 100) === $priceCents) { $validPkg = true; break; }
}
if (!$validPkg) {
echo json_encode(['success'=>false,'error'=>'Invalid token package.']); exit;
}
} else {
// Custom: tokens must equal dollars (1:1 ratio), cap at $500
if ($tokens !== ($priceCents / 100) || $tokens > 500) {
echo json_encode(['success'=>false,'error'=>'Invalid custom amount.']); exit;
}
}
// Sanitize billing fields
$billingFirst = substr(trim($billing['first_name'] ?? ''), 0, 80);
$billingLast = substr(trim($billing['last_name'] ?? ''), 0, 80);
$billingAddress = substr(trim($billing['address'] ?? ''), 0, 200);
$billingCity = substr(trim($billing['city'] ?? ''), 0, 80);
$billingState = strtoupper(substr(trim($billing['state'] ?? ''), 0, 2));
$billingZip = substr(trim($billing['zip'] ?? ''), 0, 10);
$billingEmail = substr(strtolower(trim($billing['email'] ?? '')), 0, 150);
$cardholderName = trim("$billingFirst $billingLast");
$isManual = in_array($method, ['venmo','chime','cashapp','zelle']);
// ─── Manual payment ────────────────────────────────────────
if ($isManual) {
$stmt = db()->prepare("
INSERT INTO token_purchases
(user_id, tokens, amount_cents, payment_method, platform_id, game_alias,
player_name, billing_name, billing_email, is_custom, status)
VALUES (?,?,?,?,?,?,?,?,?,?,'pending')
");
$stmt->execute([
$userId, $tokens, $priceCents, $method, $platformId, $gameAlias,
$playerName, $cardholderName, $billingEmail, $isCustom ? 1 : 0
]);
$pid = db()->lastInsertId();
logActivity('manual_payment_pending', $userId, null, 'purchase', 0, "Manual payment pending: {$method} \$" . number_format($priceCents / 100, 2));
echo json_encode(['success'=>true,'manual'=>true,'purchase_id'=>$pid,
'message'=>"Request #{$pid} submitted! Tokens credited after payment verification."]);
exit;
}
// ─── Card payment via Square ───────────────────────────────
if (empty($sourceId)) { echo json_encode(['success'=>false,'error'=>'Card payment token required.']); exit; }
$square = new SquarePayment();
$note = "TomGames {$tokens}tok | {$platformId} | {$gameAlias} | user#{$userId}" . ($isCustom ? ' [CUSTOM]' : '');
// Build buyer info for Square
$buyerInfo = [];
if ($cardholderName) $buyerInfo['buyer_email_address'] = $billingEmail ?: null;
$billingAddr = [];
if ($billingAddress) $billingAddr['address_line_1'] = $billingAddress;
if ($billingCity) $billingAddr['locality'] = $billingCity;
if ($billingState) $billingAddr['administrative_district_level_1'] = $billingState;
if ($billingZip) $billingAddr['postal_code'] = $billingZip;
$billingAddr['country'] = 'US';
$result = $square->charge($sourceId, $priceCents, $note, $cardholderName, $billingAddr, $billingEmail);
if (!$result['success']) {
// Log failed attempt
db()->prepare("INSERT INTO token_purchases (user_id,tokens,amount_cents,payment_method,platform_id,game_alias,player_name,billing_name,billing_email,is_custom,status,failure_reason) VALUES (?,?,?,'card',?,?,?,?,?,?,'failed',?)")
->execute([$userId,$tokens,$priceCents,$platformId,$gameAlias,$playerName,$cardholderName,$billingEmail,$isCustom?1:0,$result['error']]);
echo json_encode(['success'=>false,'error'=>$result['error']]); exit;
}
// ─── Credit tokens ─────────────────────────────────────────
db()->beginTransaction();
try {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$tokens,$userId]);
$cardBrand = $result['card_brand'] ?? null;
$cardLast4 = $result['last_4'] ?? null;
$receiptUrl= $result['receipt_url'] ?? null;
db()->prepare("
INSERT INTO token_purchases
(user_id, tokens, amount_cents, payment_method, square_payment_id,
platform_id, game_alias, player_name, billing_name, billing_address,
billing_city, billing_state, billing_zip, billing_email,
is_custom, card_brand, card_last4, receipt_url, status)
VALUES (?,?,?,'card',?,?,?,?,?,?,?,?,?,?,?,?,?,?,'completed')
")->execute([
$userId, $tokens, $priceCents, $result['payment_id'],
$platformId, $gameAlias, $playerName,
$cardholderName, $billingAddress, $billingCity, $billingState, $billingZip, $billingEmail,
$isCustom ? 1 : 0, $cardBrand, $cardLast4, $receiptUrl
]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Token credit failed. Payment ID: '.$result['payment_id']]); exit;
}
// ─── Update saved billing (separate try — must NOT roll back token credit) ──
try {
db()->prepare("
INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip,card_brand,card_last4)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
card_brand=VALUES(card_brand), card_last4=VALUES(card_last4),
first_name=COALESCE(NULLIF(VALUES(first_name),''),first_name),
last_name=COALESCE(NULLIF(VALUES(last_name),''),last_name),
email=COALESCE(NULLIF(VALUES(email),''),email),
address=COALESCE(NULLIF(VALUES(address),''),address),
city=COALESCE(NULLIF(VALUES(city),''),city),
state=COALESCE(NULLIF(VALUES(state),''),state),
zip=COALESCE(NULLIF(VALUES(zip),''),zip)
")->execute([
$userId, $billingFirst, $billingLast, $billingEmail,
$billingAddress, $billingCity, $billingState, $billingZip,
$cardBrand, $cardLast4
]);
} catch (Exception $e) { /* non-critical — tokens already credited */ }
$bal = db()->prepare("SELECT tokens FROM users WHERE id=?");
$bal->execute([$userId]);
$newBal = (float)$bal->fetchColumn();
logActivity('token_purchase', $userId, null, 'purchase', (int)db()->lastInsertId(),
"Bought {$tokens} tokens via {$method} for \$" . number_format($priceCents / 100, 2));
echo json_encode([
'success' => true,
'manual' => false,
'tokens_added' => (int)$tokens,
'new_balance' => $newBal,
'payment_id' => $result['payment_id'],
'card_brand' => $cardBrand,
'card_last4' => $cardLast4,
'receipt_url' => $receiptUrl,
]);
@@ -0,0 +1,362 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = (int)$_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'status';
// ── GET actions ───────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'status') {
// Player's referral dashboard data
$user = db()->prepare("SELECT referral_code, referred_by FROM users WHERE id=?");
$user->execute([$userId]);
$u = $user->fetch();
// Auto-generate code if missing
if (empty($u['referral_code'])) {
$code = strtoupper(substr(md5($userId.uniqid()),0,8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]);
$u['referral_code'] = $code;
}
// Count verified referrals
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
$countStmt->execute([$userId]);
$verified = (int)$countStmt->fetchColumn();
// All referrals
$refs = db()->prepare("
SELECT r.*, u.username, u.alias, u.email_verified, u.created_at AS joined_at
FROM referrals r
JOIN users u ON r.referred_id = u.id
WHERE r.referrer_id = ?
ORDER BY r.created_at DESC
");
$refs->execute([$userId]);
// Current tier
$tier = db()->query("
SELECT * FROM referral_tiers
WHERE is_active=1 AND min_referrals <= $verified
ORDER BY min_referrals DESC LIMIT 1
")->fetch();
// Next tier
$nextTier = db()->query("
SELECT * FROM referral_tiers
WHERE is_active=1 AND min_referrals > $verified
ORDER BY min_referrals ASC LIMIT 1
")->fetch();
// Total tokens earned from referrals
$earned = db()->prepare("SELECT COALESCE(SUM(tokens_awarded),0) FROM referrals WHERE referrer_id=? AND status='verified'");
$earned->execute([$userId]);
// Social shares
$shares = db()->prepare("SELECT * FROM referral_social_shares WHERE user_id=? ORDER BY created_at DESC");
$shares->execute([$userId]);
echo json_encode([
'success' => true,
'referral_code' => $u['referral_code'] ?? '',
'referral_url' => (defined('SITE_URL')?SITE_URL:'https://tomtomgames.com') . '/?ref=' . ($u['referral_code'] ?? ''),
'verified_count' => $verified,
'total_earned' => (float)$earned->fetchColumn(),
'current_tier' => $tier,
'next_tier' => $nextTier,
'referrals' => $refs->fetchAll(),
'social_shares' => $shares->fetchAll(),
]);
exit;
}
if ($action === 'tiers') {
$rows = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 ORDER BY min_referrals ASC")->fetchAll();
echo json_encode(['success'=>true,'tiers'=>$rows]);
exit;
}
if ($action === 'all_tiers' && $isAdmin) {
$rows = db()->query("SELECT * FROM referral_tiers ORDER BY sort_order ASC, min_referrals ASC")->fetchAll();
echo json_encode(['success'=>true,'tiers'=>$rows]);
exit;
}
if ($action === 'admin_list' && $isAdmin) {
$status = $_GET['status'] ?? 'pending';
$stmt = db()->prepare("
SELECT r.*,
ru.username AS referrer_name, ru.alias AS referrer_alias,
rd.username AS referred_name, rd.alias AS referred_alias, rd.email_verified,
t.name AS tier_name
FROM referrals r
JOIN users ru ON r.referrer_id = ru.id
JOIN users rd ON r.referred_id = rd.id
LEFT JOIN referral_tiers t ON r.tier_id = t.id
WHERE r.status = ?
ORDER BY r.created_at DESC
LIMIT 100
");
$stmt->execute([$status]);
echo json_encode(['success'=>true,'referrals'=>$stmt->fetchAll()]);
exit;
}
if ($action === 'admin_shares' && $isAdmin) {
$status = $_GET['status'] ?? 'pending';
$stmt = db()->prepare("
SELECT rs.*, u.username, u.alias
FROM referral_social_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.status = ?
ORDER BY rs.created_at DESC
");
$stmt->execute([$status]);
echo json_encode(['success'=>true,'shares'=>$stmt->fetchAll()]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']); exit;
}
// ── Helpers ───────────────────────────────────────────────
function inferPlatformFromUrl(string $url): string {
$host = strtolower(parse_url($url, PHP_URL_HOST) ?? '');
if (str_contains($host, 'reddit.com')) return 'reddit';
if (str_contains($host, 'twitter.com') || str_contains($host, 'x.com')) return 'twitter';
if (str_contains($host, 'facebook.com')) return 'facebook';
if (str_contains($host, 'instagram.com')) return 'instagram';
if (str_contains($host, 'tiktok.com')) return 'tiktok';
if (str_contains($host, 'youtube.com')) return 'youtube';
if (str_contains($host, 'discord.com')) return 'discord';
if (str_contains($host, 'twitch.tv')) return 'twitch';
return 'other';
}
function scrapeForReferralCode(string $url, string $code): array {
// SSRF guard — only http/https, no private/reserved IPs
$parsed = parse_url($url);
if (!$parsed || !in_array(strtolower($parsed['scheme'] ?? ''), ['http','https'])) {
return ['verified'=>false,'reason'=>'invalid_url'];
}
$host = $parsed['host'] ?? '';
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return ['verified'=>false,'reason'=>'invalid_url'];
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Accept: text/html,application/xhtml+xml,*/*;q=0.8'],
CURLOPT_BUFFERSIZE => 131072, // read up to 128KB
]);
$html = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErr || !$html) return ['verified'=>false,'reason'=>'unreachable'];
if ($httpCode === 401 || $httpCode === 403) return ['verified'=>false,'reason'=>'login_required'];
if ($httpCode >= 400) return ['verified'=>false,'reason'=>'unreachable'];
// Truncate to 512KB to avoid scanning huge pages
$content = substr($html, 0, 524288);
if (stripos($content, $code) !== false) return ['verified'=>true, 'reason'=>'code_found'];
if (stripos($content, 'tomtomgames.com') !== false) return ['verified'=>false,'reason'=>'site_found_no_code'];
return ['verified'=>false,'reason'=>'not_found'];
}
// ── POST actions ──────────────────────────────────────────
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
if ($action === 'submit_share') {
$url = trim($d['url'] ?? '');
$platform = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['platform'] ?? '')));
if (!$url) { echo json_encode(['success'=>false,'error'=>'Please paste the URL of your post']); exit; }
if (!filter_var($url, FILTER_VALIDATE_URL)) { echo json_encode(['success'=>false,'error'=>'That doesn\'t look like a valid URL']); exit; }
if (!$platform) $platform = inferPlatformFromUrl($url);
// Get referrer's code for scraping
$codeRow = db()->prepare("SELECT referral_code FROM users WHERE id=?");
$codeRow->execute([$userId]);
$referralCode = (string)$codeRow->fetchColumn();
// Reject duplicate URLs from same user
$dup = db()->prepare("SELECT id FROM referral_social_shares WHERE user_id=? AND share_url=?");
$dup->execute([$userId, $url]);
if ($dup->fetchColumn()) { echo json_encode(['success'=>false,'error'=>'You already submitted this URL']); exit; }
$bonus = 5;
$scrape = scrapeForReferralCode($url, $referralCode);
$status = $scrape['verified'] ? 'approved' : 'pending';
$reasonMessages = [
'code_found' => null, // auto-verified, no extra message needed
'login_required' => 'This post requires a login to view — our team will review it manually.',
'unreachable' => "We couldn't reach that URL — our team will review it manually.",
'not_found' => "Your referral code wasn't found on that page — our team will review it manually.",
'site_found_no_code'=> 'TomTomGames was mentioned but your referral code wasn\'t found — our team will review it manually.',
'invalid_url' => 'That URL doesn\'t look valid — please check and try again.',
];
if ($scrape['reason'] === 'invalid_url') {
echo json_encode(['success'=>false,'error'=>$reasonMessages['invalid_url']]);
exit;
}
try {
db()->beginTransaction();
db()->prepare("INSERT INTO referral_social_shares (user_id,platform,bonus_tokens,share_url,auto_verified,verify_result,status) VALUES (?,?,?,?,?,?,?)")
->execute([$userId, $platform, $bonus, $url, $scrape['verified']?1:0, $scrape['reason'], $status]);
$shareId = (int)db()->lastInsertId();
if ($scrape['verified']) {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$bonus, $userId]);
logAdminAction('SOCIAL_SHARE_AUTO_VERIFIED', (int)$_SESSION['user_id'], 'referral_share', $shareId,
'Auto-verified share on '.$platform.' — awarded '.$bonus.' tokens. URL: '.$url, '', 'approved', 'info');
}
db()->commit();
$resp = ['success'=>true,'auto_verified'=>$scrape['verified'],'platform'=>$platform];
if ($scrape['verified']) {
$resp['tokens'] = $bonus;
$resp['message'] = '🎉 Verified! +'.$bonus.' tokens awarded automatically.';
} else {
$resp['pending'] = true;
$resp['message'] = $reasonMessages[$scrape['reason']] ?? 'Submitted for manual review.';
}
echo json_encode($resp);
} catch(Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Failed to submit share']);
}
exit;
}
// ── Admin only below ──────────────────────────────────────
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
if ($action === 'resolve_referral') {
$id = (int)($d['id'] ?? 0);
$status = $d['status'] ?? '';
$note = substr(trim($d['note'] ?? ''), 0, 300);
if (!in_array($status, ['verified','denied','deleted'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; }
$chk = db()->prepare("SELECT r.*, t.tokens_per_ref, t.min_referrals, t.bonus_tokens FROM referrals r LEFT JOIN referral_tiers t ON r.tier_id=t.id WHERE r.id=?");
$chk->execute([$id]);
$ref = $chk->fetch();
if (!$ref) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
if ($status === 'verified') {
// Determine best tier for referrer
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
$countStmt->execute([$ref['referrer_id']]);
$verifiedCount = (int)$countStmt->fetchColumn() + 1; // +1 for this one
$tier = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 AND min_referrals <= $verifiedCount ORDER BY min_referrals DESC LIMIT 1")->fetch();
$tokensToAward = $tier ? (float)$tier['tokens_per_ref'] : 5;
// Check if this hits a bonus milestone
$bonusTokens = 0;
if ($tier && $verifiedCount == (int)$tier['min_referrals']) {
$bonusTokens = (float)$tier['bonus_tokens'];
}
$totalAward = $tokensToAward + $bonusTokens;
db()->beginTransaction();
try {
db()->prepare("UPDATE referrals SET status='verified', tier_id=?, tokens_awarded=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?")
->execute([$tier['id'] ?? null, $totalAward, (int)$_SESSION['user_id'], $note, $id]);
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$totalAward, $ref['referrer_id']]);
logAdminAction('REFERRAL_VERIFIED', (int)$_SESSION['user_id'], 'referral', $id, 'Verified referral #'.$id.' — awarded '.$totalAward.' tokens to user #'.$ref['referrer_id'], 'pending', 'verified', 'info');
db()->commit();
echo json_encode(['success'=>true,'tokens_awarded'=>$totalAward,'bonus'=>$bonusTokens,'tier'=>$tier['name']??'']);
} catch(Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']);
}
} else {
db()->prepare("UPDATE referrals SET status=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?")
->execute([$status, (int)$_SESSION['user_id'], $note, $id]);
echo json_encode(['success'=>true]);
}
exit;
}
if ($action === 'resolve_share') {
$id = (int)($d['id'] ?? 0);
$status = $d['status'] ?? '';
if (!in_array($status, ['approved','denied'])) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; }
$chk = db()->prepare("SELECT * FROM referral_social_shares WHERE id=?"); $chk->execute([$id]); $share = $chk->fetch();
if (!$share) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
db()->prepare("UPDATE referral_social_shares SET status=?,admin_id=?,resolved_at=NOW() WHERE id=?")->execute([$status,(int)$_SESSION['user_id'],$id]);
if ($status === 'approved') {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$share['bonus_tokens'],$share['user_id']]);
logAdminAction('SOCIAL_SHARE_APPROVED', (int)$_SESSION['user_id'], 'referral_share', $id, 'Approved social share #'.$id.' — awarded '.$share['bonus_tokens'].' bonus tokens to user #'.$share['user_id'], '', 'approved', 'info');
}
echo json_encode(['success'=>true]);
exit;
}
// ── Tier CRUD ─────────────────────────────────────────────
if ($action === 'tier_create') {
$name = substr(trim($d['name']??''),0,100);
$minRefs = (int)($d['min_referrals']??1);
$tokPer = (float)($d['tokens_per_ref']??5);
$bonus = (float)($d['bonus_tokens']??0);
$desc = substr(trim($d['description']??''),0,300);
$sort = (int)($d['sort_order']??99);
$active = (int)(bool)($d['is_active']??1);
if (!$name) { echo json_encode(['success'=>false,'error'=>'Name required']); exit; }
db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)")
->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
exit;
}
if ($action === 'tier_update') {
$id = (int)($d['id']??0);
$name = substr(trim($d['name']??''),0,100);
$minRefs = (int)($d['min_referrals']??1);
$tokPer = (float)($d['tokens_per_ref']??5);
$bonus = (float)($d['bonus_tokens']??0);
$desc = substr(trim($d['description']??''),0,300);
$sort = (int)($d['sort_order']??0);
$active = (int)(bool)($d['is_active']??1);
if (!$id||!$name) { echo json_encode(['success'=>false,'error'=>'ID and name required']); exit; }
db()->prepare("UPDATE referral_tiers SET name=?,min_referrals=?,tokens_per_ref=?,bonus_tokens=?,description=?,is_active=?,sort_order=? WHERE id=?")
->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort,$id]);
echo json_encode(['success'=>true]);
exit;
}
if ($action === 'tier_delete') {
$id = (int)($d['id']??0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM referral_tiers WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
@@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$alias = trim($data['alias'] ?? '');
$email = trim($data['email'] ?? '');
$referralCode= trim($data['referral_code']?? '');
logSecurityEvent('REGISTER_ATTEMPT', null, 'Registration attempt for: ' . $email, 'info');
$result = initiateRegistration($username, $password, $alias, $email, $referralCode);
echo json_encode($result);
@@ -0,0 +1,16 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$email = trim($data['email'] ?? '');
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success'=>false,'error'=>'Valid email required']); exit;
}
echo json_encode(resendVerification($email));