v1.0.0 - Initial release: registration, SendGrid email, Square payments, cashout, admin panel

This commit is contained in:
2026-05-10 14:45:49 -05:00
commit c70027f8fc
61 changed files with 11762 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
includes/config.php
public_html/create_admin.php
*.log
.DS_Store
Thumbs.db
+175
View File
@@ -0,0 +1,175 @@
# 🎰 TomGames Platform — Setup Guide
## Files Overview
```
tomgames/
├── includes/
│ ├── config.php ← ⚠️ EDIT THIS FIRST
│ ├── db.php ← Auto-creates tables
│ ├── auth.php ← Login/register helpers
│ └── square.php ← Square payment API
└── public_html/
├── index.php ← Main mobile app
├── .htaccess ← Security rules
├── create_admin.php ← Run once, then DELETE
└── api/
├── login.php
├── logout.php
├── register.php
├── me.php
├── purchase.php
├── cashout.php
└── admin.php
└── admin/
├── index.php ← Admin dashboard
└── login.php ← Admin login
```
---
## STEP 1 — Get Your Square Credentials
1. Go to https://developer.squareup.com
2. Log in with your Square merchant account
3. Click **"My Apps"** → **"Create an App"** (name it TomGames)
4. From the app dashboard, copy:
- **Application ID** (starts with `sq0idp-`)
- **Access Token** (starts with `EAAAl` for production)
- **Location ID** (under Locations tab)
> For testing first, use the **Sandbox** tab — keys start with `sandbox-sq0idp-`
---
## STEP 2 — Create MySQL Database in cPanel
1. Log into cPanel → **MySQL Databases**
2. Create database: `tomgames_db`
3. Create user: `tomgames_user` with a strong password
4. Add user to database with **ALL PRIVILEGES**
5. Note your password — you'll need it in Step 3
---
## STEP 3 — Edit config.php
Open `includes/config.php` and fill in:
```php
define('DB_PASS', 'YOUR_DATABASE_PASSWORD');
define('SQUARE_APP_ID', 'sq0idp-YOUR_APP_ID');
define('SQUARE_ACCESS_TOKEN', 'EAAAl-YOUR_TOKEN');
define('SQUARE_LOCATION_ID', 'YOUR_LOCATION_ID');
define('SQUARE_ENV', 'production'); // or 'sandbox' for testing
define('SITE_URL', 'https://yourdomain.com');
define('ADMIN_EMAIL', 'your@email.com');
```
---
## STEP 4 — Upload Files via FTP
**FTP Details:**
- Host: `fiber18-r.iaasdns.com`
- Username: `tomgames`
- Password: *(your FTP password)*
- Port: `21`
**Upload structure:**
```
Upload includes/ folder → one level ABOVE public_html
Upload public_html/* content → INTO your server's public_html/
```
So your server should look like:
```
/home/tomgames/
├── includes/ ← outside web root (secure!)
└── public_html/
├── index.php
├── .htaccess
├── api/
└── admin/
```
> ⚠️ The `includes/` folder must be OUTSIDE `public_html` so it can't be accessed via browser.
---
## STEP 5 — Create Admin Account
1. In your browser, go to: `https://yourdomain.com/create_admin.php`
2. Enter secret key: `TomGames2024Admin`
3. Enter your desired admin username and password
4. Click **Create Admin**
5.**Immediately delete** `create_admin.php` from your server via FTP!
---
## STEP 6 — Test Everything
1. Visit `https://yourdomain.com` — you should see the login screen
2. Register a test user account
3. Try buying tokens (use Square sandbox first)
4. Submit a cashout request
5. Log into admin at `https://yourdomain.com/admin/` and approve it
---
## Payment Methods
| Method | How it works |
|--------|-------------|
| Credit/Debit Card | Square processes in real-time — tokens added immediately |
| Venmo | Manual — user sends payment, you verify and approve tokens via admin |
| Chime | Manual — same as Venmo |
| Cash App | Manual — same as Venmo |
> For Venmo/Chime/Cash App, users submit the request, you verify the payment in those apps, then go to Admin → Users → Adjust Tokens to credit them.
---
## Admin Panel
URL: `https://yourdomain.com/admin/`
| Feature | Description |
|---------|-------------|
| Dashboard | Stats + pending cashout requests |
| Users | View all users, adjust tokens, suspend accounts |
| Cashouts | Approve or reject cashout requests |
| Purchases | View all purchase history |
---
## Security Checklist
- [ ] Change FTP password after upload
- [ ] Change GitHub password (it was shared in chat)
- [ ] Delete `create_admin.php` from server
- [ ] Set `SQUARE_ENV` to `'production'` when ready
- [ ] Add your domain to Square's allowed domains in the developer dashboard
- [ ] Keep `includes/` folder OUTSIDE of `public_html`
---
## Troubleshooting
**Blank page or PHP errors:**
- Check that `DB_PASS` in config.php is correct
- Verify database name and user match what you created in cPanel
**Square payment not working:**
- Confirm `SQUARE_APP_ID` and `SQUARE_LOCATION_ID` match exactly
- Add your domain to Square's Web Payments SDK allowed domains
- Start with `sandbox` mode for testing
**Can't reach admin panel:**
- Make sure you ran `create_admin.php` and the admin was created
- Go to `/admin/login.php` directly
**FTP upload issues:**
- Make sure `includes/` lands at `/home/tomgames/includes/` (not inside public_html)
- Upload `public_html/` contents directly INTO your server's `public_html/`
+1
View File
@@ -0,0 +1 @@
1.0.1
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# TomTomGames Build & Deploy Script
# Usage: ./build.sh "Commit message describing changes"
#
# Prerequisites (one-time setup):
# git init
# git remote add origin https://github.com/tomtomgames/tomtomgames-app.git
# git config user.email "myronblair@outlook.com"
# git config user.name "TomTomGames"
#
# For GitHub auth, create a PAT at:
# https://github.com/settings/tokens → New classic token → repo scope
# Then: git remote set-url origin https://YOUR_TOKEN@github.com/tomtomgames/tomtomgames-app.git
set -e
MSG="${1:-Auto build $(date '+%Y-%m-%d %H:%M')}"
# ── Determine next version ────────────────────────────────
CURRENT=$(cat VERSION 2>/dev/null || echo "1.0.1")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "──────────────────────────────────────"
echo " Building TomTomGames v$NEW_VERSION"
echo " Message: $MSG"
echo "──────────────────────────────────────"
# ── Write version file ────────────────────────────────────
echo "$NEW_VERSION" > VERSION
echo "✓ VERSION → $NEW_VERSION"
# ── Update version in PHP config ─────────────────────────
# (The DB is the source of truth — bump_version.php handles it on the server)
# ── Build zip ─────────────────────────────────────────────
ZIP_NAME="tomgames_v${NEW_VERSION}.zip"
zip -r "$ZIP_NAME" tomgames/ -x "*.DS_Store" -x "*/.git/*" -q
echo "✓ Built $ZIP_NAME ($(du -sh "$ZIP_NAME" | cut -f1))"
# ── Git commit & push ─────────────────────────────────────
git add -A
git commit -m "v${NEW_VERSION}${MSG}"
git tag -a "v${NEW_VERSION}" -m "v${NEW_VERSION}: ${MSG}"
git push origin main --tags
echo "✓ Pushed to GitHub as v${NEW_VERSION}"
echo ""
echo "═══════════════════════════════════════"
echo " After uploading to server, run:"
echo " https://tomtomgames.com/bump_version.php?key=TTG_bump_2026!&version=$NEW_VERSION&notes=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MSG'))")"
echo "═══════════════════════════════════════"
+8
View File
@@ -0,0 +1,8 @@
{
"require": {
"phpmailer/phpmailer": "^6.9"
},
"config": {
"vendor-dir": "vendor"
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/mailer.php';
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: /'); exit;
}
}
function requireAdmin(): void {
requireLogin();
if (empty($_SESSION['is_admin'])) {
header('Location: /'); exit;
}
}
function currentUser(): ?array {
if (!isLoggedIn()) return null;
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch() ?: null;
}
function loginUser(string $username, string $password): array {
$stmt = db()->prepare("SELECT * FROM users WHERE username = ? AND status = 'active'");
$stmt->execute([strtolower(trim($username))]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password.'];
}
// Block unverified accounts — admins are always exempt
if (!$user['email_verified'] && !$user['is_admin']) {
return [
'success' => false,
'error' => 'Please verify your email address before logging in. Check your inbox for the verification link.',
'unverified' => true,
'email' => $user['email'],
];
}
// Auto-verify admin accounts so they're never locked out
if ($user['is_admin'] && !$user['email_verified']) {
db()->prepare("UPDATE users SET email_verified=1 WHERE id=?")->execute([$user['id']]);
$user['email_verified'] = 1;
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['alias'] = $user['alias'];
$_SESSION['is_admin'] = $user['is_admin'];
db()->prepare("UPDATE users SET last_login=NOW() WHERE id=?")->execute([$user['id']]);
return ['success' => true, 'user' => $user];
}
/**
* Stage a registration: store in pending_registrations, send verification email.
* Does NOT create the user row yet.
*/
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
$username = strtolower(trim($username));
$alias = trim($alias);
$email = strtolower(trim($email));
// Validate
if (strlen($username) < 3 || strlen($username) > 50)
return ['success' => false, 'error' => 'Username must be 350 characters.'];
if (!preg_match('/^[a-z0-9_]+$/', $username))
return ['success' => false, 'error' => 'Username may only contain letters, numbers, and underscores.'];
if (strlen($password) < 6)
return ['success' => false, 'error' => 'Password must be at least 6 characters.'];
if (empty($alias))
return ['success' => false, 'error' => 'Alias is required.'];
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
return ['success' => false, 'error' => 'A valid email address is required to verify your account.'];
// Check username taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE username=?");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already taken.'];
// Check email taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE email=?");
$s->execute([$email]);
if ($s->fetch()) return ['success' => false, 'error' => 'An account with that email already exists.'];
// Check username in pending
$s = db()->prepare("SELECT id FROM pending_registrations WHERE username=? AND expires_at > NOW()");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already reserved. Try again in 24 hours or choose another.'];
// Check email in pending — resend if already pending
$s = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()");
$s->execute([$email]);
$existing = $s->fetch();
if ($existing) {
// Resend verification to same email
sendVerificationEmail($email, $alias, $existing['token']);
return ['success' => true, 'resent' => true, 'email' => $email];
}
// Delete any expired pending rows for this email/username
db()->prepare("DELETE FROM pending_registrations WHERE email=? OR (username=? AND expires_at <= NOW())")->execute([$email, $username]);
// Resolve referral code to user ID
$referrerId = null;
if ($referralCode) {
$refStmt = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
$refStmt->execute([strtoupper(trim($referralCode))]);
$refUser = $refStmt->fetch();
if ($refUser) $referrerId = (int)$refUser['id'];
}
$token = bin2hex(random_bytes(32));
$hash = password_hash($password, PASSWORD_BCRYPT);
$expiresAt = date('Y-m-d H:i:s', time() + VERIFY_TTL);
$stmt = db()->prepare("INSERT INTO pending_registrations (username, password, alias, email, token, referred_by, expires_at) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$username, $hash, $alias, $email, $token, $referrerId, $expiresAt]);
$sent = sendVerificationEmail($email, $alias, $token);
if (!$sent) {
// Email failed but keep registration — user can resend from login screen
error_log('[TomTomGames] Verification email failed for ' . $email);
return ['success' => true, 'email' => $email, 'mail_warning' => true];
}
return ['success' => true, 'email' => $email];
}
/**
* Consume a verification token: create the real user, delete pending row.
*/
function verifyEmailToken(string $token): array {
$token = trim($token);
if (empty($token)) return ['success' => false, 'error' => 'Invalid verification link.'];
$stmt = db()->prepare("SELECT * FROM pending_registrations WHERE token=? AND expires_at > NOW()");
$stmt->execute([$token]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'This verification link is invalid or has expired. Please register again.'];
}
// Check username/email not taken since pending was created
$s = db()->prepare("SELECT id FROM users WHERE username=? OR email=?");
$s->execute([$pending['username'], $pending['email']]);
if ($s->fetch()) {
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
return ['success' => false, 'error' => 'This username or email was already registered. Please log in.'];
}
db()->beginTransaction();
try {
// Create the user
$ins = db()->prepare("INSERT INTO users (username, password, alias, email, email_verified, status) VALUES (?,?,?,?,1,'active')");
$ins->execute([$pending['username'], $pending['password'], $pending['alias'], $pending['email']]);
$userId = db()->lastInsertId();
// Generate unique referral code
$code = strtoupper(substr(md5($userId . uniqid()), 0, 8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]);
// Track referral if referred_by is in pending
if (!empty($pending['referred_by'])) {
$referrerId = (int)$pending['referred_by'];
try {
db()->prepare("INSERT IGNORE INTO referrals (referrer_id, referred_id, status) VALUES (?,?,'pending')")
->execute([$referrerId, $userId]);
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$referrerId, $userId]);
} catch(Exception $e) {}
}
// Delete pending row
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
}
// Auto-login
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $pending['username'];
$_SESSION['alias'] = $pending['alias'];
$_SESSION['is_admin'] = 0;
return ['success' => true, 'username' => $pending['username'], 'alias' => $pending['alias']];
}
/**
* Resend verification email for an unverified account.
*/
function resendVerification(string $email): array {
$email = strtolower(trim($email));
$stmt = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW() ORDER BY id DESC LIMIT 1");
$stmt->execute([$email]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'No pending registration found for that email, or it has expired. Please register again.'];
}
$alias = db()->prepare("SELECT alias FROM pending_registrations WHERE id=?");
$alias->execute([$pending['id']]);
$row = $alias->fetch();
sendVerificationEmail($email, $row['alias'] ?? 'Player', $pending['token']);
return ['success' => true];
}
function logoutUser(): void {
$_SESSION = [];
session_destroy();
}
// ─── Comprehensive Audit Logger ────────────────────────────
function logActivity(
string $action,
?int $userId = null,
?int $adminId = null,
string $entityType= '',
int $entityId = 0,
string $detail = '',
string $ip = '',
string $category = 'general',
string $oldValue = '',
string $newValue = '',
string $severity = 'info'
): void {
try {
// Auto-purge entries older than 90 days (probabilistic — 3% of calls)
if (rand(1, 100) <= 3) {
db()->exec("DELETE FROM activity_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
}
$ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '');
$userAgent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 300);
$page = substr(($_SERVER['REQUEST_URI'] ?? ''), 0, 200);
$sessionId = session_id() ?: '';
db()->prepare("
INSERT INTO activity_log
(user_id, admin_id, action, category, entity_type, entity_id, detail,
old_value, new_value, ip, user_agent, page, session_id, severity)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
")->execute([
$userId ?: null,
$adminId ?: null,
substr($action, 0, 120),
$category,
$entityType,
$entityId ?: null,
substr($detail, 0, 2000),
substr($oldValue, 0, 2000),
substr($newValue, 0, 2000),
$ip,
$userAgent,
$page,
$sessionId,
$severity,
]);
} catch (Exception $e) { /* never fail silently */ }
}
// Convenience wrappers
function logPlayerAction(string $action, int $userId, string $detail='', string $category='player', string $severity='info'): void {
logActivity($action, $userId, null, 'user', $userId, $detail, '', $category, '', '', $severity);
}
function logAdminAction(string $action, int $adminId, string $entityType='', int $entityId=0, string $detail='', string $old='', string $new='', string $severity='info'): void {
logActivity($action, null, $adminId, $entityType, $entityId, $detail, '', 'admin', $old, $new, $severity);
}
function logSecurityEvent(string $action, ?int $userId=null, string $detail='', string $severity='warning'): void {
logActivity($action, $userId, null, 'security', 0, $detail, '', 'security', '', '', $severity);
}
// ─── App Version ───────────────────────────────────────────
function getAppVersion(): string {
try {
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
return $v ?: '1.0.0';
} catch(Exception $e) { return '1.0.0'; }
}
+541
View File
@@ -0,0 +1,541 @@
<?php
require_once __DIR__ . '/config.php';
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
try {
$this->pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4",
DB_USER, DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['success'=>false,'error'=>'Database connection failed.']));
}
}
public static function getInstance(): self {
if (!self::$instance) self::$instance = new self();
return self::$instance;
}
public function getConnection(): PDO { return $this->pdo; }
}
function db(): PDO { return Database::getInstance()->getConnection(); }
// Check if a column exists — MySQL 5.x compatible
function colExists(PDO $pdo, string $table, string $col): bool {
return (bool)$pdo->query("SHOW COLUMNS FROM `$table` LIKE '$col'")->fetch();
}
function initDB(): void {
$pdo = db();
// Each table in its own exec() — PDO only runs one statement per call
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE,
email_verified TINYINT(1) DEFAULT 0,
tokens DECIMAL(10,2) DEFAULT 0,
is_admin TINYINT(1) DEFAULT 0,
status ENUM('active','suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS pending_registrations (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
token VARCHAR(64) UNIQUE NOT NULL,
referred_by INT DEFAULT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS token_purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
tokens INT NOT NULL,
amount_cents INT NOT NULL,
payment_method VARCHAR(20) DEFAULT 'card',
square_payment_id VARCHAR(255),
platform_id VARCHAR(50),
game_alias VARCHAR(100),
player_name VARCHAR(100),
billing_name VARCHAR(160),
billing_address VARCHAR(200),
billing_city VARCHAR(80),
billing_state VARCHAR(2),
billing_zip VARCHAR(10),
billing_email VARCHAR(150),
is_custom TINYINT(1) DEFAULT 0,
failure_reason TEXT,
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
receipt_url VARCHAR(512),
status ENUM('pending','completed','failed') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_id VARCHAR(50) NOT NULL,
alias VARCHAR(100) NOT NULL,
tokens DECIMAL(10,2) NOT NULL,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS platform_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_slug VARCHAR(50) NOT NULL,
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
status ENUM('pending','approved','denied','deleted') DEFAULT 'pending',
platform_username VARCHAR(100),
platform_password VARCHAR(200),
admin_note VARCHAR(300),
resolved_at DATETIME,
admin_id INT,
UNIQUE KEY uq_user_platform (user_id, platform_slug),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS admin_payout_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
method_key VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
method_type ENUM('manual','square_gift_card') DEFAULT 'manual',
is_enabled TINYINT(1) DEFAULT 1,
handle VARCHAR(200),
instructions TEXT,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
cashout_id INT NOT NULL,
admin_id INT NOT NULL,
payout_method VARCHAR(50) NOT NULL,
payout_type ENUM('manual','square_gift_card') DEFAULT 'manual',
amount_cents INT NOT NULL,
square_txn_id VARCHAR(200),
gift_card_gan VARCHAR(100),
gift_card_balance INT,
note TEXT,
status ENUM('pending','completed','failed') DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cashout_id) REFERENCES cashout_requests(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
admin_id INT,
action VARCHAR(120) NOT NULL,
category VARCHAR(40) DEFAULT 'general',
entity_type VARCHAR(40),
entity_id INT,
detail TEXT,
old_value TEXT,
new_value TEXT,
ip VARCHAR(45),
user_agent VARCHAR(300),
page VARCHAR(200),
session_id VARCHAR(64),
severity ENUM('info','warning','critical') DEFAULT 'info',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created (created_at),
INDEX idx_user (user_id),
INDEX idx_admin (admin_id),
INDEX idx_category (category),
INDEX idx_severity (severity)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcasts (
id INT AUTO_INCREMENT PRIMARY KEY,
admin_id INT NOT NULL,
subject VARCHAR(200) NOT NULL,
message TEXT NOT NULL,
target ENUM('all','verified','unverified','admins') DEFAULT 'all',
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (admin_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcast_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
broadcast_id INT NOT NULL,
user_id INT NOT NULL,
read_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_br (broadcast_id, user_id),
FOREIGN KEY (broadcast_id) REFERENCES broadcasts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS broadcast_replies (
id INT AUTO_INCREMENT PRIMARY KEY,
broadcast_id INT NOT NULL,
user_id INT NOT NULL,
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (broadcast_id) REFERENCES broadcasts(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS cashout_method_types (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
icon VARCHAR(10) DEFAULT '💰',
description VARCHAR(200),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS payout_methods (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
method_type VARCHAR(50) NOT NULL,
label VARCHAR(100) NOT NULL,
account_handle VARCHAR(200) NOT NULL,
is_default TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Add payout fields to cashout_requests if not present
try {
$cols = array_column($pdo->query("SHOW COLUMNS FROM cashout_requests")->fetchAll(), 'Field');
if (!in_array('payout_method_type', $cols)) {
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN payout_method_type VARCHAR(50) AFTER alias");
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN payout_handle VARCHAR(200) AFTER payout_method_type");
}
if (!in_array('sent_note', $cols)) {
$pdo->exec("ALTER TABLE cashout_requests ADD COLUMN sent_note TEXT AFTER admin_note");
}
$pdo->exec("ALTER TABLE cashout_requests MODIFY COLUMN status ENUM('pending','approved','sent','rejected','deleted') DEFAULT 'pending'");
} catch (Exception $e) { /* ignore — columns may already exist */ }
$pdo->exec("CREATE TABLE IF NOT EXISTS saved_billing (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT UNIQUE NOT NULL,
first_name VARCHAR(80),
last_name VARCHAR(80),
email VARCHAR(150),
address VARCHAR(200),
city VARCHAR(80),
state VARCHAR(2),
zip VARCHAR(10),
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
card_exp_month VARCHAR(2),
card_exp_year VARCHAR(4),
sq_card_id VARCHAR(255),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS platforms (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
player_url VARCHAR(500) NOT NULL,
console_url VARCHAR(500),
color VARCHAR(20) DEFAULT '#f0c040',
icon_path VARCHAR(200),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS payment_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
method_key VARCHAR(50) UNIQUE NOT NULL,
label VARCHAR(100) NOT NULL,
is_enabled TINYINT(1) DEFAULT 1,
handle VARCHAR(200),
instructions TEXT,
sort_order INT DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
$pdo->exec("CREATE TABLE IF NOT EXISTS game_aliases (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_slug VARCHAR(50) NOT NULL,
alias VARCHAR(100) NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_platform (user_id, platform_slug),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Track whether user has seen the post-verify onboarding
try {
if (!colExists($pdo,'users','onboarding_done')) {
$pdo->exec("ALTER TABLE users ADD COLUMN onboarding_done TINYINT(1) DEFAULT 0");
}
} catch (Exception $e) {}
$pdo->exec("CREATE TABLE IF NOT EXISTS chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
sender ENUM('user','admin') NOT NULL,
message TEXT NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Add missing columns to existing tables — MySQL 5.x compatible (no IF NOT EXISTS)
$addCols = [
['token_purchases', 'billing_name', "VARCHAR(160)", 'player_name'],
['token_purchases', 'billing_address', "VARCHAR(200)", 'billing_name'],
['token_purchases', 'billing_city', "VARCHAR(80)", 'billing_address'],
['token_purchases', 'billing_state', "VARCHAR(2)", 'billing_city'],
['token_purchases', 'billing_zip', "VARCHAR(10)", 'billing_state'],
['token_purchases', 'billing_email', "VARCHAR(150)", 'billing_zip'],
['token_purchases', 'is_custom', "TINYINT(1) DEFAULT 0", 'billing_email'],
['token_purchases', 'failure_reason', "TEXT", 'is_custom'],
['token_purchases', 'card_brand', "VARCHAR(30)", 'failure_reason'],
['token_purchases', 'card_last4', "VARCHAR(4)", 'card_brand'],
['token_purchases', 'receipt_url', "VARCHAR(512)", 'card_last4'],
['token_purchases', 'admin_note', "TEXT", 'status'],
['users', 'email_verified', "TINYINT(1) DEFAULT 0", 'email'],
];
foreach ($addCols as [$tbl, $col, $def, $after]) {
if (!colExists($pdo, $tbl, $col)) {
try {
$pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`");
} catch (Exception $e) { /* ignore — concurrent init */ }
}
}
}
try { initDB(); } catch (Exception $e) { /* already initialised */ }
// Seed admin_payout_settings if empty
try {
if (db()->query("SELECT COUNT(*) FROM admin_payout_settings")->fetchColumn() == 0) {
$seeds = [
['venmo', 'Venmo', 'manual', 1, '@your-venmo', 'Send via Venmo app'],
['cashapp', 'Cash App', 'manual', 1, '$yourcashtag', 'Send via Cash App'],
['zelle', 'Zelle', 'manual', 1, 'your@email', 'Send via Zelle'],
['chime', 'Chime', 'manual', 1, '', 'Send via Chime'],
['square_gift', 'Square Gift Card','square_gift_card',1, '', 'Instant Square gift card — player redeems anywhere Square is accepted'],
];
$st = db()->prepare("INSERT IGNORE INTO admin_payout_settings (method_key,label,method_type,is_enabled,handle,instructions,sort_order) VALUES (?,?,?,?,?,?,?)");
foreach ($seeds as $i => $s) { $st->execute([$s[0],$s[1],$s[2],$s[3],$s[4],$s[5],$i]); }
}
} catch(Exception $e) {}
// ─── REFERRAL TABLES ──────────────────────────────────────
try {
db()->exec("CREATE TABLE IF NOT EXISTS referral_tiers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
min_referrals INT NOT NULL DEFAULT 1,
tokens_per_ref DECIMAL(10,2) NOT NULL DEFAULT 10,
bonus_tokens DECIMAL(10,2) NOT NULL DEFAULT 0,
description VARCHAR(300),
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
db()->exec("CREATE TABLE IF NOT EXISTS referrals (
id INT AUTO_INCREMENT PRIMARY KEY,
referrer_id INT NOT NULL,
referred_id INT NOT NULL UNIQUE,
tier_id INT,
status ENUM('pending','verified','denied','deleted') DEFAULT 'pending',
tokens_awarded DECIMAL(10,2) DEFAULT 0,
admin_id INT,
admin_note VARCHAR(300),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (referrer_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (referred_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
db()->exec("CREATE TABLE IF NOT EXISTS referral_social_shares (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform VARCHAR(50) NOT NULL,
bonus_tokens DECIMAL(10,2) DEFAULT 0,
status ENUM('pending','approved','denied') DEFAULT 'pending',
admin_id INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch(Exception $e) {}
// Seed default referral tiers
try {
if (db()->query("SELECT COUNT(*) FROM referral_tiers")->fetchColumn() == 0) {
$seeds = [
['Bronze Referrer', 1, 5, 0, 'Earn 5 tokens for each verified referral', 1, 0],
['Silver Referrer', 5, 8, 25, 'Earn 8 tokens per referral + 25 bonus at 5 referrals', 1, 1],
['Gold Referrer', 10, 10, 100,'Earn 10 tokens per referral + 100 bonus at 10 referrals',1, 2],
['Elite Referrer', 25, 15, 250,'Earn 15 tokens per referral + 250 bonus at 25 referrals',1, 3],
];
$st = db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)");
foreach ($seeds as $s) $st->execute($s);
}
} catch(Exception $e) {}
// Add referral_code to users if missing
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('referral_code', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN referral_code VARCHAR(20) UNIQUE");
// Generate codes for existing users
$users = db()->query("SELECT id FROM users WHERE referral_code IS NULL")->fetchAll();
$upd = db()->prepare("UPDATE users SET referral_code=? WHERE id=?");
foreach ($users as $u) {
$code = strtoupper(substr(md5($u['id'].uniqid()),0,8));
$upd->execute([$code, $u['id']]);
}
}
} catch(Exception $e) {}
// Add referred_by to users if missing
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('referred_by', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN referred_by INT DEFAULT NULL");
}
} catch(Exception $e) {}
// Add card payment_settings row if missing
try {
$cardCount = db()->query("SELECT COUNT(*) FROM payment_settings WHERE method_key='card'")->fetchColumn();
if ($cardCount == 0) {
db()->exec("INSERT IGNORE INTO payment_settings (method_key,label,handle,instructions,is_enabled,sort_order) VALUES ('card','Credit / Debit Card','','Processed via Square',1,-1)");
}
} catch(Exception $e) {}
// Expand activity_log if columns missing
try {
$alCols = array_column(db()->query("SHOW COLUMNS FROM activity_log")->fetchAll(), 'Field');
$alAdd = [
'category' => "ALTER TABLE activity_log ADD COLUMN category VARCHAR(40) DEFAULT 'general' AFTER action",
'old_value' => "ALTER TABLE activity_log ADD COLUMN old_value TEXT AFTER detail",
'new_value' => "ALTER TABLE activity_log ADD COLUMN new_value TEXT AFTER old_value",
'user_agent' => "ALTER TABLE activity_log ADD COLUMN user_agent VARCHAR(300) AFTER ip",
'page' => "ALTER TABLE activity_log ADD COLUMN page VARCHAR(200) AFTER user_agent",
'session_id' => "ALTER TABLE activity_log ADD COLUMN session_id VARCHAR(64) AFTER page",
'severity' => "ALTER TABLE activity_log ADD COLUMN severity ENUM('info','warning','critical') DEFAULT 'info' AFTER session_id",
];
foreach ($alAdd as $col => $sql) {
if (!in_array($col, $alCols)) db()->exec($sql);
}
// Change detail to TEXT if it's VARCHAR
$detailType = '';
foreach (db()->query("SHOW COLUMNS FROM activity_log")->fetchAll() as $col) {
if ($col['Field'] === 'detail') $detailType = $col['Type'];
}
if (stripos($detailType, 'varchar') !== false) {
db()->exec("ALTER TABLE activity_log MODIFY COLUMN detail TEXT");
}
} catch(Exception $e) {}
// Add platform_onboarding_done to users if not exists
try {
$cols = array_column(db()->query("SHOW COLUMNS FROM users")->fetchAll(), 'Field');
if (!in_array('platform_onboarding_done', $cols)) {
db()->exec("ALTER TABLE users ADD COLUMN platform_onboarding_done TINYINT(1) DEFAULT 0");
}
} catch(Exception $e){}
// App version table
try {
db()->exec("CREATE TABLE IF NOT EXISTS app_version (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(20) NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Seed initial version if empty
if (db()->query("SELECT COUNT(*) FROM app_version")->fetchColumn() == 0) {
db()->exec("INSERT INTO app_version (version, notes) VALUES ('1.0.0', 'Initial release')");
}
} catch(Exception $e) {}
// Seed cashout_method_types if empty
try {
$cmtCount = db()->query("SELECT COUNT(*) FROM cashout_method_types")->fetchColumn();
if ($cmtCount == 0) {
$types = [
['venmo', 'Venmo', '💙', 'Send via Venmo username', 1, 0],
['cashapp', 'Cash App', '💚', 'Send via Cash App $cashtag', 1, 1],
['zelle', 'Zelle', '💜', 'Send via Zelle phone or email', 1, 2],
['chime', 'Chime', '🟢', 'Send via Chime account', 1, 3],
['bank', 'Bank Transfer', '🏦', 'Direct bank transfer', 1, 4],
['other', 'Other', '💰', 'Other payment method', 1, 5],
];
$stmt = db()->prepare("INSERT IGNORE INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)");
foreach ($types as $t) $stmt->execute($t);
}
} catch (Exception $e) { /* ignore */ }
// Seed payment_settings from config if empty
try {
$pmtCount = db()->query("SELECT COUNT(*) FROM payment_settings")->fetchColumn();
if ($pmtCount == 0) {
$methods = [
['card', 'Credit / Debit Card', '', 'Processed via Square', 1, -1],
['venmo', 'Venmo', PAY_VENMO, 'Send payment via Venmo', 1, 0],
['chime', 'Chime', PAY_CHIME, 'Send payment via Chime', 1, 1],
['cashapp', 'Cash App', PAY_CASHAPP, 'Send payment via Cash App',1, 2],
['zelle', 'Zelle', PAY_ZELLE, 'Send payment via Zelle', 1, 3],
];
$stmt = db()->prepare("INSERT IGNORE INTO payment_settings (method_key,label,handle,instructions,is_enabled,sort_order) VALUES (?,?,?,?,1,?)");
foreach ($methods as $m) {
$stmt->execute([$m[0],$m[1],$m[2],$m[3],$m[5]]);
}
}
} catch (Exception $e) { /* ignore */ }
// Seed platforms table from config if empty
try {
$count = db()->query("SELECT COUNT(*) FROM platforms")->fetchColumn();
if ($count == 0) {
$platforms = json_decode(PLATFORMS, true);
$stmt = db()->prepare("INSERT IGNORE INTO platforms (slug,name,player_url,color,sort_order) VALUES (?,?,?,?,?)");
foreach ($platforms as $i => $p) {
$stmt->execute([$p['id'], $p['name'], $p['url'], $p['color'], $i]);
}
}
} catch (Exception $e) { /* ignore */ }
// Always ensure admin accounts are email-verified
try {
if (colExists(db(), 'users', 'email_verified')) {
db()->exec("UPDATE users SET email_verified=1 WHERE is_admin=1 AND email_verified=0");
}
} catch (Exception $e) { /* ignore */ }
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* TomTomGames Mailer — SendGrid HTTP API (cURL)
*/
function sendVerificationEmail(string $toEmail, string $toName, string $token): bool {
$siteName = defined('SITE_NAME') ? SITE_NAME : 'TomTomGames';
$siteUrl = defined('SITE_URL') ? SITE_URL : 'https://tomtomgames.com';
$verifyUrl = $siteUrl . '/verify.php?token=' . urlencode($token);
$subject = "Verify your {$siteName} account";
$text = "Welcome to {$siteName}, {$toName}!\n\n"
. "Verify your email address:\n{$verifyUrl}\n\n"
. "This link expires in 24 hours.\n\n"
. "If you did not create this account, ignore this email.\n\n"
. "— The {$siteName} Team";
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>'
. '<body style="margin:0;padding:0;background:#0a0a12;font-family:Arial,sans-serif">'
. '<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a0a12;padding:32px 16px">'
. '<tr><td align="center"><table width="520" cellpadding="0" cellspacing="0" '
. 'style="background:#1a1a2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;overflow:hidden;max-width:520px;width:100%">'
. '<tr><td style="background:linear-gradient(135deg,#f0c040,#ff6b35);padding:28px 32px;text-align:center">'
. '<span style="font-weight:900;font-size:24px;color:#000">&#127918; ' . htmlspecialchars($siteName) . '</span>'
. '</td></tr>'
. '<tr><td style="padding:36px 32px;color:#e8e8f0">'
. '<h2 style="margin:0 0 16px;font-size:22px;color:#f0c040">Verify your account</h2>'
. '<p style="margin:0 0 12px;font-size:15px;color:#aaaacc;line-height:1.6">Hey <strong style="color:#e8e8f0">'
. htmlspecialchars($toName) . '</strong>,</p>'
. '<p style="margin:0 0 24px;font-size:15px;color:#aaaacc;line-height:1.6">Thanks for signing up! Click below to verify your email and activate your account.</p>'
. '<div style="text-align:center;margin-bottom:28px">'
. '<a href="' . htmlspecialchars($verifyUrl) . '" style="display:inline-block;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:700;font-size:16px;padding:16px 40px;border-radius:10px;text-decoration:none;letter-spacing:.5px">VERIFY MY ACCOUNT</a>'
. '</div>'
. '<p style="font-size:12px;color:#666688;margin-bottom:8px">Or paste this into your browser:</p>'
. '<p style="font-size:12px;color:#00e5ff;word-break:break-all;margin:0 0 24px">' . htmlspecialchars($verifyUrl) . '</p>'
. '<p style="font-size:12px;color:#555577;border-top:1px solid rgba(255,255,255,.06);padding-top:16px;margin:0">'
. 'Link expires in 24 hours. Did not sign up? You can safely ignore this email.</p>'
. '</td></tr>'
. '<tr><td style="background:#111122;padding:16px 32px;text-align:center">'
. '<span style="font-size:11px;color:#444466">&copy; ' . htmlspecialchars($siteName)
. ' &middot; <a href="' . htmlspecialchars($siteUrl) . '" style="color:#f0c040;text-decoration:none">'
. htmlspecialchars($siteUrl) . '</a></span>'
. '</td></tr></table></td></tr></table></body></html>';
return sendgridSend($toEmail, $toName, $subject, $text, $html);
}
function sendgridSend(string $toEmail, string $toName, string $subject, string $textBody, string $htmlBody = ''): bool {
$apiKey = defined('SENDGRID_API_KEY') ? SENDGRID_API_KEY : '';
if (!$apiKey) {
error_log('[TomTomGames mailer] SENDGRID_API_KEY not defined');
return false;
}
$payload = json_encode([
'personalizations' => [['to' => [['email' => $toEmail, 'name' => $toName]]]],
'from' => [
'email' => defined('SMTP_FROM') ? SMTP_FROM : 'noreply@tomtomgames.com',
'name' => defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'TomTomGames',
],
'subject' => $subject,
'content' => array_values(array_filter([
['type' => 'text/plain', 'value' => $textBody],
$htmlBody ? ['type' => 'text/html', 'value' => $htmlBody] : null,
])),
]);
$ch = curl_init('https://api.sendgrid.com/v3/mail/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($httpCode === 202) return true;
error_log('[TomTomGames mailer] SendGrid HTTP ' . $httpCode . ' err=' . $curlErr . ' body=' . $response);
return false;
}
+183
View File
@@ -0,0 +1,183 @@
<?php
/**
* TomTomGames SMTP Mailer
* Sends email via SMTP using PHP's socket functions.
* Compatible with Outlook/Office365, Gmail, and standard SMTP servers.
*/
class SmtpMailer {
private string $host;
private int $port;
private string $user;
private string $pass;
private string $fromEmail;
private string $fromName;
private bool $debug;
private array $log = [];
public function __construct(
string $host,
int $port,
string $user,
string $pass,
string $fromEmail,
string $fromName,
bool $debug = false
) {
$this->host = $host;
$this->port = $port;
$this->user = $user;
$this->pass = $pass;
$this->fromEmail = $fromEmail;
$this->fromName = $fromName;
$this->debug = $debug;
}
public function send(string $toEmail, string $toName, string $subject, string $textBody, string $htmlBody = ''): bool {
$errno = 0; $errstr = '';
$socket = fsockopen("tcp://{$this->host}", $this->port, $errno, $errstr, 15);
if (!$socket) { $this->log[] = "Connect failed: $errstr ($errno)"; return false; }
stream_set_timeout($socket, 15);
try {
// Read greeting
$this->expect($socket, 220, "greeting");
// EHLO
$this->send_cmd($socket, "EHLO " . gethostname());
$ehlo = $this->read_response($socket);
if (substr($ehlo, 0, 3) !== '250') { throw new Exception("EHLO failed: $ehlo"); }
// STARTTLS
$this->send_cmd($socket, "STARTTLS");
$this->expect($socket, 220, "STARTTLS");
// Upgrade to TLS
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new Exception("TLS upgrade failed");
}
// EHLO again after TLS
$this->send_cmd($socket, "EHLO " . gethostname());
$ehlo2 = $this->read_response($socket);
if (substr($ehlo2, 0, 3) !== '250') { throw new Exception("EHLO2 failed: $ehlo2"); }
// AUTH LOGIN
$this->send_cmd($socket, "AUTH LOGIN");
$this->expect($socket, 334, "AUTH LOGIN prompt");
$this->send_cmd($socket, base64_encode($this->user));
$this->expect($socket, 334, "Username prompt");
$this->send_cmd($socket, base64_encode($this->pass));
$this->expect($socket, 235, "AUTH success");
// MAIL FROM
$this->send_cmd($socket, "MAIL FROM:<{$this->fromEmail}>");
$this->expect($socket, 250, "MAIL FROM");
// RCPT TO
$this->send_cmd($socket, "RCPT TO:<{$toEmail}>");
$this->expect($socket, 250, "RCPT TO");
// DATA
$this->send_cmd($socket, "DATA");
$this->expect($socket, 354, "DATA");
// Build message
$boundary = 'boundary_' . md5(uniqid());
$fromHdr = $this->encodeName($this->fromName) . " <{$this->fromEmail}>";
$toHdr = $this->encodeName($toName) . " <{$toEmail}>";
$subjHdr = $this->encodeSubject($subject);
$msgId = '<' . time() . '.' . rand(1000,9999) . '@' . $this->host . '>';
$headers = "From: $fromHdr\r\n";
$headers .= "To: $toHdr\r\n";
$headers .= "Subject: $subjHdr\r\n";
$headers .= "Message-ID: $msgId\r\n";
$headers .= "Date: " . date('r') . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "X-Mailer: TomTomGames/1.0\r\n";
if ($htmlBody) {
$headers .= "Content-Type: multipart/alternative; boundary=\"$boundary\"\r\n";
$body = "--$boundary\r\n";
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= quoted_printable_encode($textBody) . "\r\n";
$body .= "--$boundary\r\n";
$body .= "Content-Type: text/html; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= quoted_printable_encode($htmlBody) . "\r\n";
$body .= "--$boundary--";
} else {
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
$body = quoted_printable_encode($textBody);
}
// Dot-stuff and send
$message = $headers . "\r\n" . $body;
$message = preg_replace('/^\./m', '..', $message);
fwrite($socket, $message . "\r\n.\r\n");
$this->expect($socket, 250, "Message accepted");
// QUIT
$this->send_cmd($socket, "QUIT");
fclose($socket);
return true;
} catch (Exception $e) {
$this->log[] = "Error: " . $e->getMessage();
try { $this->send_cmd($socket, "QUIT"); } catch(Exception $_) {}
fclose($socket);
return false;
}
}
private function send_cmd($socket, string $cmd): void {
if ($this->debug) $this->log[] = ">>> $cmd";
fwrite($socket, $cmd . "\r\n");
}
private function read_response($socket): string {
$response = '';
while ($line = fgets($socket, 512)) {
if ($this->debug) $this->log[] = "<<< " . trim($line);
$response .= $line;
if (isset($line[3]) && $line[3] === ' ') break; // Multi-line ends when 4th char is space
}
return trim($response);
}
private function expect($socket, int $code, string $context): void {
$resp = $this->read_response($socket);
if (substr($resp, 0, 3) !== (string)$code) {
throw new Exception("Expected $code at $context, got: $resp");
}
}
private function encodeName(string $name): string {
if (preg_match('/[^\x20-\x7E]/', $name) || strpbrk($name, '"<>()')) {
return '=?UTF-8?B?' . base64_encode($name) . '?=';
}
return '"' . addslashes($name) . '"';
}
private function encodeSubject(string $subject): string {
if (preg_match('/[^\x20-\x7E]/', $subject)) {
return '=?UTF-8?B?' . base64_encode($subject) . '?=';
}
return $subject;
}
public function getLog(): array { return $this->log; }
}
/**
* Factory — returns a ready-to-use mailer using config constants.
*/
function mailer(): SmtpMailer {
return new SmtpMailer(
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS,
SMTP_FROM, SMTP_FROM_NAME
);
}
+115
View File
@@ -0,0 +1,115 @@
<?php
require_once __DIR__ . '/config.php';
class SquarePayment {
private string $baseUrl;
private string $token;
public function __construct() {
$this->token = SQUARE_ACCESS_TOKEN;
$this->baseUrl = SQUARE_ENV === 'production'
? 'https://connect.squareup.com/v2'
: 'https://connect.squareupsandbox.com/v2';
}
public function charge(
string $sourceId,
int $amountCents,
string $note = '',
string $cardholderName= '',
array $billingAddress= [],
string $buyerEmail = ''
): array {
$body = [
'idempotency_key' => uniqid('tg_', true),
'source_id' => $sourceId,
'amount_money' => ['amount' => $amountCents, 'currency' => 'USD'],
'location_id' => SQUARE_LOCATION_ID,
'note' => $note ?: 'TomGames Token Purchase',
'autocomplete' => true,
];
if ($cardholderName) {
$body['buyer_email_address'] = $buyerEmail ?: null;
}
if (!empty($billingAddress)) {
$body['billing_address'] = array_filter($billingAddress);
}
if ($buyerEmail && filter_var($buyerEmail, FILTER_VALIDATE_EMAIL)) {
$body['buyer_email_address'] = $buyerEmail;
$body['receipt_email'] = $buyerEmail;
}
$ch = curl_init($this->baseUrl . '/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->token,
'Square-Version: 2024-01-18',
],
CURLOPT_POSTFIELDS => json_encode(array_filter($body, fn($v) => $v !== null)),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErr) return ['success'=>false,'error'=>'Connection error. Please try again.'];
$data = json_decode($response, true);
if ($httpCode === 200 && isset($data['payment']['id'])) {
return [
'success' => true,
'payment_id' => $data['payment']['id'],
'status' => $data['payment']['status'],
'receipt_url'=> $data['payment']['receipt_url'] ?? null,
'card_brand' => $data['payment']['card_details']['card']['card_brand'] ?? null,
'last_4' => $data['payment']['card_details']['card']['last_4'] ?? null,
];
}
$errorMsg = $data['errors'][0]['detail'] ?? ($data['errors'][0]['code'] ?? 'Payment failed. Please try again.');
return ['success'=>false,'error'=>$errorMsg];
}
public static function sdkUrl(): string {
return SQUARE_ENV === 'production'
? 'https://web.squarecdn.com/v1/square.js'
: 'https://sandbox.web.squarecdn.com/v1/square.js';
}
// Generic POST for Square APIs (gift cards, etc.)
public static function post(string $path, array $body): array {
$baseUrl = SQUARE_ENV === 'production'
? 'https://connect.squareup.com'
: 'https://connect.squareupsandbox.com';
$url = $baseUrl . $path;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Square-Version: 2024-01-18',
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
],
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
$httpCode= curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new Exception('Square connection error: ' . $err);
$data = json_decode($resp, true);
if (isset($data['errors'])) {
throw new Exception($data['errors'][0]['detail'] ?? 'Square API error');
}
return $data;
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# TomTomGames Mail Queue Processor
# Runs as root via cron every minute
# crontab entry: * * * * * /home/tomgames/public_html/../mail_queue/process_queue.sh >> /tmp/mailqueue.log 2>&1
QUEUE_DIR="$(dirname "$0")"
API_KEY=$(php -r "require '$(dirname "$0")/../includes/config.php'; echo SENDGRID_API_KEY;" 2>/dev/null)
if [ -z "$API_KEY" ]; then
# Fallback: read directly from config
API_KEY=$(grep "SENDGRID_API_KEY" "$(dirname "$0")/../includes/config.php" | grep -o "'SG\.[^']*'" | tr -d "'")
fi
for FILE in "$QUEUE_DIR"/*.json; do
[ -f "$FILE" ] || continue
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing: $FILE"
HTTP_CODE=$(curl -s -o /tmp/sg_response.txt -w "%{http_code}" \
--request POST \
--url https://api.sendgrid.com/v3/mail/send \
--header "Authorization: Bearer $API_KEY" \
--header "Content-Type: application/json" \
--data "@$FILE" \
--max-time 30)
if [ "$HTTP_CODE" = "202" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $FILE (HTTP $HTTP_CODE)"
rm -f "$FILE"
else
RESPONSE=$(cat /tmp/sg_response.txt 2>/dev/null)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] FAILED: $FILE (HTTP $HTTP_CODE) — $RESPONSE"
# Move to failed folder after 3 attempts
ATTEMPTS=$(cat "${FILE}.attempts" 2>/dev/null || echo 0)
ATTEMPTS=$((ATTEMPTS + 1))
echo $ATTEMPTS > "${FILE}.attempts"
if [ "$ATTEMPTS" -ge 3 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Giving up after 3 attempts: $FILE"
mv "$FILE" "${FILE}.failed"
rm -f "${FILE}.attempts"
fi
fi
done
+59
View File
@@ -0,0 +1,59 @@
Options -Indexes
ServerSignature Off
# ── Block sensitive files ────────────────────────────────
<FilesMatch "\.(sql|env|log|sh|md|git)$">
Order allow,deny
Deny from all
</FilesMatch>
# ── Block direct access to includes ──────────────────────
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^includes/ - [F,L]
</IfModule>
# ── Security headers ──────────────────────────────────────
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
# ── Canonical HTTPS redirect ──────────────────────────────
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Remove www (pick one: www or non-www, use non-www)
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
</IfModule>
# ── Gzip compression ──────────────────────────────────────
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml
</IfModule>
# ── Browser caching ───────────────────────────────────────
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 1 hour"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType application/json "access plus 1 day"
</IfModule>
# ── LiteSpeed cache rules ─────────────────────────────────
<IfModule LiteSpeed>
CacheEnable public /assets/
CacheEnable public /manifest.json
CacheEnable public /sitemap.xml
CacheEnable public /robots.txt
</IfModule>
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
if (isLoggedIn() && !empty($_SESSION['is_admin'])) {
header('Location: /admin/index.php'); exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = loginUser($_POST['username'] ?? '', $_POST['password'] ?? '');
if ($result['success'] && !empty($_SESSION['is_admin'])) {
header('Location: /admin/index.php'); exit;
} elseif ($result['success']) {
logoutUser();
$error = 'Access denied. Admin account required.';
} else {
$error = $result['error'];
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow, noarchive">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TomTomGames Admin Login</title>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@700;900&family=Rajdhani:wght@500;600&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0a0a12;color:#e8e8f0;font-family:'Rajdhani',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.box{background:#1a1a2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:40px;width:100%;max-width:380px}
.logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:28px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:4px}
.sub{color:#8888aa;font-size:13px;margin-bottom:28px;letter-spacing:1px}
.form-group{margin-bottom:16px}
label{font-size:12px;font-weight:700;color:#8888aa;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px;display:block}
input{width:100%;background:#181828;border:1px solid rgba(255,255,255,.07);border-radius:8px;padding:13px 15px;color:#e8e8f0;font-family:'Rajdhani',sans-serif;font-size:16px;outline:none;transition:border-color .2s}
input:focus{border-color:#00e5ff}
.btn{width:100%;padding:15px;border:none;border-radius:8px;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;letter-spacing:1px;cursor:pointer;margin-top:8px}
.error{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:#ff4444;padding:12px 14px;border-radius:8px;font-size:14px;font-weight:600;margin-bottom:16px}
</style>
</head>
<body>
<div class="box">
<div class="logo" style="display:flex;align-items:center;gap:10px;justify-content:center"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="36" height="36" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="ll1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="ll2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="gll"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#ll1)" filter="url(#gll)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span>TomTomGames</span></div>
<div class="sub">ADMIN ACCESS</div>
<?php if($error): ?><div class="error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="POST">
<div class="form-group"><label>Username</label><input type="text" name="username" autocomplete="username" autocapitalize="none" required></div>
<div class="form-group"><label>Password</label><input type="password" name="password" autocomplete="current-password" required></div>
<button class="btn" type="submit">LOGIN TO ADMIN</button>
</form>
</div>
</body>
</html>
+966
View File
@@ -0,0 +1,966 @@
<?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
db()->prepare("logAdminAction('TOKENS_ADJUSTED', $adminId, 'user', isset($targetId)?$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 granting admin, also set email_verified=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]);
}
logActivity($new_val?'admin_granted':'admin_revoked', $uid, (int)$_SESSION['user_id'], 'user', $uid, 'Admin status changed to '.($new_val?'admin':'player'));
echo json_encode(['success'=>true, 'is_admin'=>$new_val]);
break;
// ─── TOGGLE SUSPEND ───────────────────────────────────────
case 'toggle_user':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$uid = (int)($data['user_id'] ?? 0);
if ($uid === MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Cannot suspend the master admin.']); exit; }
db()->prepare("logAdminAction('USER_STATUS_CHANGE', $adminId, 'user', isset($userId)?$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_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_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;
// ─── PLATFORM ACCOUNTS ────────────────────────────────
case 'platform_accounts_list':
$status = $_GET['status'] ?? 'pending';
$valid = ['pending','approved','denied','deleted'];
if (!in_array($status,$valid)) $status='pending';
$stmt = db()->prepare("
SELECT pa.*, u.username, u.alias,
COALESCE(p.name, pa.platform_name, pa.platform_slug) AS display_name,
p.color
FROM platform_accounts pa
JOIN users u ON pa.user_id = u.id
LEFT JOIN platforms p ON pa.platform_slug = p.slug
WHERE pa.status = ?
ORDER BY pa.requested_at DESC
");
$stmt->execute([$status]);
echo json_encode(['success'=>true,'accounts'=>$stmt->fetchAll()]);
break;
case 'platform_account_approve':
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);
$u = substr(trim($d['provided_username']??''),0,100);
$pw = substr(trim($d['provided_password']??''),0,200);
$nt = substr(trim($d['admin_note']??''),0,500);
if (!$id||!$u||!$pw){echo json_encode(['success'=>false,'error'=>'ID, username and password required']);exit;}
$r=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$r->execute([$id]);$req=$r->fetch();
if(!$req){echo json_encode(['success'=>false,'error'=>'Not found']);exit;}
db()->prepare("UPDATE platform_accounts SET status='approved',provided_username=?,provided_password=?,admin_note=?,approved_at=NOW(),admin_id=? WHERE id=?")
->execute([$u,$pw,$nt,$_SESSION['user_id'],$id]);
db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")
->execute([$req['user_id'],$req['platform_slug'],$u]);
try{logActivity('platform_account_approved',$req['user_id'],(int)$_SESSION['user_id'],'platform_account',$id,"Approved {$req['platform_slug']}: {$u}");}catch(Exception $e){}
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 = (int)($d['id']??0);
$u = substr(trim($d['provided_username']??''),0,100);
$pw = substr(trim($d['provided_password']??''),0,200);
$nt = substr(trim($d['admin_note']??''),0,500);
if (!$id){echo json_encode(['success'=>false,'error'=>'ID required']);exit;}
db()->prepare("UPDATE platform_accounts SET provided_username=?,provided_password=?,admin_note=?,admin_id=? WHERE id=?")
->execute([$u,$pw,$nt,$_SESSION['user_id'],$id]);
$r=db()->prepare("SELECT user_id,platform_slug FROM platform_accounts WHERE id=?");$r->execute([$id]);$req=$r->fetch();
if($req&&$u){db()->prepare("INSERT INTO game_aliases (user_id,platform_slug,alias) VALUES (?,?,?) ON DUPLICATE KEY UPDATE alias=VALUES(alias)")->execute([$req['user_id'],$req['platform_slug'],$u]);}
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']);
}
+91
View File
@@ -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']);
}
+89
View File
@@ -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']);
}
+148
View File
@@ -0,0 +1,148 @@
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$userId = (int)$_SESSION['user_id'];
$method = $_SERVER['REQUEST_METHOD'];
// ══════════════════════════════════════════════════════════
// GET — player's own requests (list, delete, update, lock)
// ══════════════════════════════════════════════════════════
if ($method === 'GET') {
$action = $_GET['action'] ?? 'list';
if ($action === 'list') {
$stmt = db()->prepare("
SELECT cr.*,
COALESCE(p.name, cr.platform_id) AS platform_name
FROM cashout_requests cr
LEFT JOIN platforms p ON cr.platform_id = p.slug
WHERE cr.user_id = ?
ORDER BY cr.created_at DESC
LIMIT 50
");
$stmt->execute([$userId]);
echo json_encode(['success'=>true, 'requests'=>$stmt->fetchAll()]);
exit;
}
if ($action === 'delete') {
$id = (int)($_GET['id'] ?? 0);
$chk = db()->prepare("SELECT id,tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
$row = $chk->fetch();
if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; }
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$row['tokens'], $userId]);
db()->prepare("DELETE FROM cashout_requests WHERE id=?")->execute([$id]);
$nb = db()->prepare("SELECT tokens FROM users WHERE id=?");
$nb->execute([$userId]);
echo json_encode(['success'=>true,'new_balance'=>(float)$nb->fetchColumn()]);
exit;
}
if ($action === 'update') {
$id = (int)($_GET['id'] ?? 0);
$tokens = (float)($_GET['tokens'] ?? 0);
$alias = substr(trim($_GET['alias'] ?? ''), 0, 100);
$chk = db()->prepare("SELECT id,tokens AS old_tokens FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
$row = $chk->fetch();
if (!$row) { echo json_encode(['success'=>false,'error'=>'Request not found or already locked']); exit; }
if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum 1 token']); exit; }
$diff = $tokens - $row['old_tokens'];
if ($diff > 0) {
$balChk = db()->prepare("SELECT tokens FROM users WHERE id=?");
$balChk->execute([$userId]);
if ($diff > (float)$balChk->fetchColumn()) { echo json_encode(['success'=>false,'error'=>'Insufficient balance']); exit; }
}
db()->beginTransaction();
db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$diff, $userId]);
db()->prepare("UPDATE cashout_requests SET tokens=?,alias=? WHERE id=?")->execute([$tokens, $alias, $id]);
db()->commit();
echo json_encode(['success'=>true]);
exit;
}
if ($action === 'lock') {
$id = (int)($_GET['id'] ?? 0);
$chk = db()->prepare("SELECT id FROM cashout_requests WHERE id=? AND user_id=? AND status='pending'");
$chk->execute([$id, $userId]);
if (!$chk->fetch()) { echo json_encode(['success'=>false,'error'=>'Request not found']); exit; }
try {
db()->exec("ALTER TABLE cashout_requests MODIFY COLUMN status ENUM('pending','locked','sent','approved','rejected','deleted') DEFAULT 'pending'");
} catch (Exception $e) {}
db()->prepare("UPDATE cashout_requests SET status='locked' WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
exit;
}
// ══════════════════════════════════════════════════════════
// POST — submit new cashout request
// ══════════════════════════════════════════════════════════
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$platformId = trim($data['platform_id'] ?? '');
$alias = trim($data['alias'] ?? '');
$tokens = (float)($data['tokens'] ?? 0);
$payoutMethodId = (int)($data['payout_method_id'] ?? 0);
$payoutMethodType = trim($data['payout_method_type'] ?? '');
$payoutHandle = trim($data['payout_handle'] ?? '');
// Validate platform
$platStmt = db()->prepare("SELECT slug FROM platforms WHERE slug=? AND is_active=1 LIMIT 1");
$platStmt->execute([$platformId]);
if (!$platStmt->fetch()) {
$platforms = json_decode(PLATFORMS, true);
if (empty(array_filter($platforms, fn($p) => $p['id'] === $platformId))) {
echo json_encode(['success'=>false,'error'=>'Invalid platform.']); exit;
}
}
if (empty($alias)) { echo json_encode(['success'=>false,'error'=>'Platform alias required.']); exit; }
if ($tokens < 1) { echo json_encode(['success'=>false,'error'=>'Minimum cashout is 1 token.']); exit; }
// Validate payout method
if ($payoutMethodId) {
$chk = db()->prepare("SELECT method_type,account_handle FROM payout_methods WHERE id=? AND user_id=?");
$chk->execute([$payoutMethodId, $userId]);
if ($pm = $chk->fetch()) {
$payoutMethodType = $pm['method_type'];
$payoutHandle = $pm['account_handle'];
}
}
// Check balance
$balStmt = db()->prepare("SELECT tokens FROM users WHERE id=?");
$balStmt->execute([$userId]);
$balance = (float)$balStmt->fetchColumn();
if ($tokens > $balance) { echo json_encode(['success'=>false,'error'=>'Insufficient token balance.']); exit; }
// Deduct & create
db()->beginTransaction();
try {
db()->prepare("UPDATE users SET tokens=tokens-? WHERE id=?")->execute([$tokens, $userId]);
db()->prepare("INSERT INTO cashout_requests (user_id,platform_id,alias,tokens,payout_method_type,payout_handle) VALUES (?,?,?,?,?,?)")
->execute([$userId, $platformId, $alias, $tokens, $payoutMethodType, $payoutHandle]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Request failed. Try again.']); exit;
}
$newBalStmt = db()->prepare("SELECT tokens FROM users WHERE id=?");
$newBalStmt->execute([$userId]);
$nb = (float)$newBalStmt->fetchColumn();
try { logActivity('cashout_request', $userId, null, 'cashout', 0, "Cashout: {$tokens} tokens via {$payoutMethodType}"); } catch(Exception $e){}
echo json_encode(['success'=>true, 'new_balance'=>$nb]);
+73
View File
@@ -0,0 +1,73 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
$action = $_GET['action'] ?? 'list';
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
switch ($action) {
// Public: active types for player dropdown
case 'list':
$rows = db()->query("SELECT slug,label,icon,description FROM cashout_method_types WHERE is_active=1 ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'types'=>$rows]);
break;
// Admin: all types
case 'admin_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("SELECT * FROM cashout_method_types ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'types'=>$rows]);
break;
// Admin: create
case 'create':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$label= substr(trim($d['label'] ?? ''), 0, 100);
$icon = substr(trim($d['icon'] ?? '💰'), 0, 10);
$desc = substr(trim($d['description'] ?? ''), 0, 200);
$sort = (int)($d['sort_order'] ?? 99);
$active = (int)(bool)($d['is_active'] ?? 1);
if (!$slug || !$label) { echo json_encode(['success'=>false,'error'=>'Slug and label required']); exit; }
try {
db()->prepare("INSERT INTO cashout_method_types (slug,label,icon,description,is_active,sort_order) VALUES (?,?,?,?,?,?)")
->execute([$slug,$label,$icon,$desc,$active,$sort]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Slug already exists']);
}
break;
// Admin: update
case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$label= substr(trim($d['label'] ?? ''), 0, 100);
$icon = substr(trim($d['icon'] ?? '💰'), 0, 10);
$desc = substr(trim($d['description'] ?? ''), 0, 200);
$sort = (int)($d['sort_order'] ?? 0);
$active = (int)(bool)($d['is_active'] ?? 1);
if (!$id || !$label) { echo json_encode(['success'=>false,'error'=>'ID and label required']); exit; }
db()->prepare("UPDATE cashout_method_types SET label=?,icon=?,description=?,is_active=?,sort_order=? WHERE id=?")
->execute([$label,$icon,$desc,$active,$sort,$id]);
echo json_encode(['success'=>true]);
break;
// Admin: delete
case 'delete':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM cashout_method_types WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
+124
View File
@@ -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']);
}
+65
View File
@@ -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']);
}
+37
View File
@@ -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);
+5
View File
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json');
logoutUser();
echo json_encode(['success' => true]);
+32
View File
@@ -0,0 +1,32 @@
<?php
ob_start(); // Buffer any accidental output (PHP errors, notices, etc.)
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;
}
unset($user['password']);
echo json_encode(['success' => true, 'user' => $user]);
+76
View File
@@ -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']);
}
+17
View File
@@ -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()]);
+44
View File
@@ -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']);
}
+75
View File
@@ -0,0 +1,75 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
$action = $_GET['action'] ?? '';
$userId = (int)$_SESSION['user_id'];
$isAdmin = !empty($_SESSION['is_admin']);
switch ($action) {
case 'list':
$uid = $isAdmin ? (int)($_GET['user_id'] ?? $userId) : $userId;
$rows = db()->prepare("SELECT * FROM payout_methods WHERE user_id=? ORDER BY is_default DESC, id ASC");
$rows->execute([$uid]);
echo json_encode(['success'=>true, 'methods'=>$rows->fetchAll()]);
break;
case 'add':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$uid = $isAdmin && isset($d['user_id']) ? (int)$d['user_id'] : $userId;
$type = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['method_type'] ?? '')));
$label = substr(trim($d['label'] ?? ''), 0, 100);
$handle= substr(trim($d['account_handle'] ?? ''), 0, 200);
$def = (int)(bool)($d['is_default'] ?? 0);
if (!$type || !$label || !$handle) { echo json_encode(['success'=>false,'error'=>'All fields required']); exit; }
db()->beginTransaction();
if ($def) db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]);
// If first method, auto-set as default
$count = db()->prepare("SELECT COUNT(*) FROM payout_methods WHERE user_id=?"); $count->execute([$uid]);
if ((int)$count->fetchColumn() === 0) $def = 1;
db()->prepare("INSERT INTO payout_methods (user_id,method_type,label,account_handle,is_default) VALUES (?,?,?,?,?)")
->execute([$uid,$type,$label,$handle,$def]);
$newId = db()->lastInsertId();
db()->commit();
echo json_encode(['success'=>true,'id'=>$newId]);
break;
case 'set_default':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
// Verify ownership
$chk = db()->prepare("SELECT user_id FROM payout_methods WHERE id=?"); $chk->execute([$id]);
$row = $chk->fetch();
if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
$uid = $row['user_id'];
db()->prepare("UPDATE payout_methods SET is_default=0 WHERE user_id=?")->execute([$uid]);
db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
case 'delete':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$chk = db()->prepare("SELECT user_id,is_default FROM payout_methods WHERE id=?"); $chk->execute([$id]);
$row = $chk->fetch();
if (!$row || ($row['user_id'] != $userId && !$isAdmin)) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
db()->prepare("DELETE FROM payout_methods WHERE id=?")->execute([$id]);
// If deleted default, set next one as default
if ($row['is_default']) {
$next = db()->prepare("SELECT id FROM payout_methods WHERE user_id=? LIMIT 1"); $next->execute([$row['user_id']]);
if ($n = $next->fetch()) db()->prepare("UPDATE payout_methods SET is_default=1 WHERE id=?")->execute([$n['id']]);
}
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
+153
View File
@@ -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']);
+92
View File
@@ -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']);
+93
View File
@@ -0,0 +1,93 @@
<?php
ob_start();
try { require_once __DIR__ . '/../../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); header('Content-Type: application/json'); echo json_encode(['success'=>false,'error'=>'Server error']); exit; }
ob_end_clean();
header('Content-Type: application/json');
$action = $_GET['action'] ?? 'list';
$isAdmin = isLoggedIn() && !empty($_SESSION['is_admin']);
switch ($action) {
// ── Public: active platforms for player app ───────────
case 'list':
$stmt = db()->query("SELECT slug,name,player_url,color,icon_path FROM platforms WHERE is_active=1 ORDER BY sort_order ASC, id ASC");
$rows = $stmt->fetchAll();
// Normalize to match old CFG format
$out = array_map(fn($r) => [
'id' => $r['slug'],
'name' => $r['name'],
'url' => $r['player_url'],
'color' => $r['color'],
], $rows);
echo json_encode(['success'=>true, 'platforms'=>$out]);
break;
// ── Admin: full list including console_url and inactive ─
case 'admin_list':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll();
echo json_encode(['success'=>true, 'platforms'=>$rows]);
break;
// ── Admin: create platform ────────────────────────────
case 'create':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? '')));
$name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$console_url = substr(trim($d['console_url'] ?? ''), 0, 500);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$sort_order = (int)($d['sort_order'] ?? 99);
$is_active = isset($d['is_active']) ? (int)(bool)$d['is_active'] : 1;
if (!$slug || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL are required']); exit; }
try {
$stmt = db()->prepare("INSERT INTO platforms (slug,name,player_url,console_url,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$slug,$name,$player_url,$console_url,$color,$sort_order,$is_active]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success'=>false,'error'=>'Slug already exists or DB error']);
}
break;
// ── Admin: update platform ────────────────────────────
case 'update':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$name = substr(trim($d['name'] ?? ''), 0, 100);
$player_url = substr(trim($d['player_url'] ?? ''), 0, 500);
$console_url = substr(trim($d['console_url'] ?? ''), 0, 500);
$color = preg_match('/^#[0-9a-fA-F]{3,8}$/', $d['color'] ?? '') ? $d['color'] : '#f0c040';
$sort_order = (int)($d['sort_order'] ?? 99);
$is_active = (int)(bool)($d['is_active'] ?? 1);
if (!$id || !$name || !$player_url) { echo json_encode(['success'=>false,'error'=>'ID, name, and player URL required']); exit; }
db()->prepare("UPDATE platforms SET name=?,player_url=?,console_url=?,color=?,sort_order=?,is_active=? WHERE id=?")
->execute([$name,$player_url,$console_url,$color,$sort_order,$is_active,$id]);
echo json_encode(['success'=>true]);
break;
// ── Admin: delete platform ────────────────────────────
case 'delete':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
break;
// ── Admin: reorder platforms ──────────────────────────
case 'reorder':
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$order = $d['order'] ?? []; // array of IDs in desired order
$stmt = db()->prepare("UPDATE platforms SET sort_order=? WHERE id=?");
foreach ($order as $i => $pid) { $stmt->execute([$i, (int)$pid]); }
echo json_encode(['success'=>true]);
break;
default:
echo json_encode(['success'=>false,'error'=>'Unknown action']);
}
+163
View File
@@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/../../includes/auth.php';
require_once __DIR__ . '/../../includes/square.php';
header('Content-Type: application/json');
if (!isLoggedIn()) { echo json_encode(['success'=>false,'error'=>'Not authenticated']); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$data = json_decode(file_get_contents('php://input'), true);
$sourceId = trim($data['source_id'] ?? '');
$tokens = (int)($data['tokens'] ?? 0);
$priceCents = (int)($data['price_cents'] ?? 0);
$method = trim($data['method'] ?? 'card');
$platformId = trim($data['platform_id'] ?? '');
$gameAlias = trim($data['game_alias'] ?? '');
$playerName = trim($data['player_name'] ?? '');
$isCustom = (bool)($data['is_custom'] ?? false);
$billing = $data['billing'] ?? [];
$userId = $_SESSION['user_id'];
// ─── Validate ─────────────────────────────────────────────
if ($tokens < 1 || $priceCents < 100) {
echo json_encode(['success'=>false,'error'=>'Minimum purchase is $1 (1 token).']); exit;
}
// Validate preset packages (custom bypasses preset check)
if (!$isCustom) {
$packages = json_decode(TOKEN_PACKAGES, true);
$validPkg = false;
foreach ($packages as $pkg) {
if ($pkg['tokens'] === $tokens && ($pkg['price'] * 100) === $priceCents) { $validPkg = true; break; }
}
if (!$validPkg) {
echo json_encode(['success'=>false,'error'=>'Invalid token package.']); exit;
}
} else {
// Custom: tokens must equal dollars (1:1 ratio), cap at $500
if ($tokens !== ($priceCents / 100) || $tokens > 500) {
echo json_encode(['success'=>false,'error'=>'Invalid custom amount.']); exit;
}
}
// Sanitize billing fields
$billingFirst = substr(trim($billing['first_name'] ?? ''), 0, 80);
$billingLast = substr(trim($billing['last_name'] ?? ''), 0, 80);
$billingAddress = substr(trim($billing['address'] ?? ''), 0, 200);
$billingCity = substr(trim($billing['city'] ?? ''), 0, 80);
$billingState = strtoupper(substr(trim($billing['state'] ?? ''), 0, 2));
$billingZip = substr(trim($billing['zip'] ?? ''), 0, 10);
$billingEmail = substr(strtolower(trim($billing['email'] ?? '')), 0, 150);
$cardholderName = trim("$billingFirst $billingLast");
$isManual = in_array($method, ['venmo','chime','cashapp','zelle']);
// ─── Manual payment ────────────────────────────────────────
if ($isManual) {
$stmt = db()->prepare("
INSERT INTO token_purchases
(user_id, tokens, amount_cents, payment_method, platform_id, game_alias,
player_name, billing_name, billing_email, is_custom, status)
VALUES (?,?,?,?,?,?,?,?,?,?,'pending')
");
$stmt->execute([
$userId, $tokens, $priceCents, $method, $platformId, $gameAlias,
$playerName, $cardholderName, $billingEmail, $isCustom ? 1 : 0
]);
$pid = db()->lastInsertId();
logActivity('manual_payment_pending', $userId, null, 'purchase', 0, "Manual payment pending: {$paymentMethod} \${$amountDollars}");
echo json_encode(['success'=>true,'manual'=>true,'purchase_id'=>$pid,
'message'=>"Request #{$pid} submitted! Tokens credited after payment verification."]);
exit;
}
// ─── Card payment via Square ───────────────────────────────
if (empty($sourceId)) { echo json_encode(['success'=>false,'error'=>'Card payment token required.']); exit; }
$square = new SquarePayment();
$note = "TomGames {$tokens}tok | {$platformId} | {$gameAlias} | user#{$userId}" . ($isCustom ? ' [CUSTOM]' : '');
// Build buyer info for Square
$buyerInfo = [];
if ($cardholderName) $buyerInfo['buyer_email_address'] = $billingEmail ?: null;
$billingAddr = [];
if ($billingAddress) $billingAddr['address_line_1'] = $billingAddress;
if ($billingCity) $billingAddr['locality'] = $billingCity;
if ($billingState) $billingAddr['administrative_district_level_1'] = $billingState;
if ($billingZip) $billingAddr['postal_code'] = $billingZip;
$billingAddr['country'] = 'US';
$result = $square->charge($sourceId, $priceCents, $note, $cardholderName, $billingAddr, $billingEmail);
if (!$result['success']) {
// Log failed attempt
db()->prepare("INSERT INTO token_purchases (user_id,tokens,amount_cents,payment_method,platform_id,game_alias,player_name,billing_name,billing_email,is_custom,status,failure_reason) VALUES (?,?,?,'card',?,?,?,?,?,?,'failed',?)")
->execute([$userId,$tokens,$priceCents,$platformId,$gameAlias,$playerName,$cardholderName,$billingEmail,$isCustom?1:0,$result['error']]);
echo json_encode(['success'=>false,'error'=>$result['error']]); exit;
}
// ─── Credit tokens ─────────────────────────────────────────
db()->beginTransaction();
try {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$tokens,$userId]);
$cardBrand = $result['card_brand'] ?? null;
$cardLast4 = $result['last_4'] ?? null;
$receiptUrl= $result['receipt_url'] ?? null;
db()->prepare("
INSERT INTO token_purchases
(user_id, tokens, amount_cents, payment_method, square_payment_id,
platform_id, game_alias, player_name, billing_name, billing_address,
billing_city, billing_state, billing_zip, billing_email,
is_custom, card_brand, card_last4, receipt_url, status)
VALUES (?,?,?,'card',?,?,?,?,?,?,?,?,?,?,?,?,?,?,'completed')
")->execute([
$userId, $tokens, $priceCents, $result['payment_id'],
$platformId, $gameAlias, $playerName,
$cardholderName, $billingAddress, $billingCity, $billingState, $billingZip, $billingEmail,
$isCustom ? 1 : 0, $cardBrand, $cardLast4, $receiptUrl
]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'Token credit failed. Payment ID: '.$result['payment_id']]); exit;
}
// ─── Update saved billing (separate try — must NOT roll back token credit) ──
try {
db()->prepare("
INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip,card_brand,card_last4)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
card_brand=VALUES(card_brand), card_last4=VALUES(card_last4),
first_name=COALESCE(NULLIF(VALUES(first_name),''),first_name),
last_name=COALESCE(NULLIF(VALUES(last_name),''),last_name),
email=COALESCE(NULLIF(VALUES(email),''),email),
address=COALESCE(NULLIF(VALUES(address),''),address),
city=COALESCE(NULLIF(VALUES(city),''),city),
state=COALESCE(NULLIF(VALUES(state),''),state),
zip=COALESCE(NULLIF(VALUES(zip),''),zip)
")->execute([
$userId, $billingFirst, $billingLast, $billingEmail,
$billingAddress, $billingCity, $billingState, $billingZip,
$cardBrand, $cardLast4
]);
} catch (Exception $e) { /* non-critical — tokens already credited */ }
$bal = db()->prepare("SELECT tokens FROM users WHERE id=?");
$bal->execute([$userId]);
$newBal = (float)$bal->fetchColumn();
logActivity('token_purchase', $userId, null, 'purchase', (int)db()->lastInsertId(),
"Bought {$tokens} tokens via {$paymentMethod} for \${$amountDollars}");
echo json_encode([
'success' => true,
'manual' => false,
'tokens_added' => (int)$tokens,
'new_balance' => $newBal,
'payment_id' => $result['payment_id'],
'card_brand' => $cardBrand,
'card_last4' => $cardLast4,
'receipt_url' => $receiptUrl,
]);
+248
View File
@@ -0,0 +1,248 @@
<?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();
// Count verified referrals
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
$countStmt->execute([$userId]);
$verified = (int)$countStmt->fetchColumn();
// All referrals
$refs = db()->prepare("
SELECT r.*, u.username, u.alias, u.email_verified, u.created_at AS joined_at
FROM referrals r
JOIN users u ON r.referred_id = u.id
WHERE r.referrer_id = ?
ORDER BY r.created_at DESC
");
$refs->execute([$userId]);
// Current tier
$tier = db()->query("
SELECT * FROM referral_tiers
WHERE is_active=1 AND min_referrals <= $verified
ORDER BY min_referrals DESC LIMIT 1
")->fetch();
// Next tier
$nextTier = db()->query("
SELECT * FROM referral_tiers
WHERE is_active=1 AND min_referrals > $verified
ORDER BY min_referrals ASC LIMIT 1
")->fetch();
// Total tokens earned from referrals
$earned = db()->prepare("SELECT COALESCE(SUM(tokens_awarded),0) FROM referrals WHERE referrer_id=? AND status='verified'");
$earned->execute([$userId]);
// Social shares
$shares = db()->prepare("SELECT * FROM referral_social_shares WHERE user_id=? ORDER BY created_at DESC");
$shares->execute([$userId]);
echo json_encode([
'success' => true,
'referral_code' => $u['referral_code'] ?? '',
'referral_url' => (defined('SITE_URL')?SITE_URL:'https://tomtomgames.com') . '/?ref=' . ($u['referral_code'] ?? ''),
'verified_count' => $verified,
'total_earned' => (float)$earned->fetchColumn(),
'current_tier' => $tier,
'next_tier' => $nextTier,
'referrals' => $refs->fetchAll(),
'social_shares' => $shares->fetchAll(),
]);
exit;
}
if ($action === 'tiers') {
$rows = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 ORDER BY min_referrals ASC")->fetchAll();
echo json_encode(['success'=>true,'tiers'=>$rows]);
exit;
}
if ($action === 'all_tiers' && $isAdmin) {
$rows = db()->query("SELECT * FROM referral_tiers ORDER BY sort_order ASC, min_referrals ASC")->fetchAll();
echo json_encode(['success'=>true,'tiers'=>$rows]);
exit;
}
if ($action === 'admin_list' && $isAdmin) {
$status = $_GET['status'] ?? 'pending';
$stmt = db()->prepare("
SELECT r.*,
ru.username AS referrer_name, ru.alias AS referrer_alias,
rd.username AS referred_name, rd.alias AS referred_alias, rd.email_verified,
t.name AS tier_name
FROM referrals r
JOIN users ru ON r.referrer_id = ru.id
JOIN users rd ON r.referred_id = rd.id
LEFT JOIN referral_tiers t ON r.tier_id = t.id
WHERE r.status = ?
ORDER BY r.created_at DESC
LIMIT 100
");
$stmt->execute([$status]);
echo json_encode(['success'=>true,'referrals'=>$stmt->fetchAll()]);
exit;
}
if ($action === 'admin_shares' && $isAdmin) {
$status = $_GET['status'] ?? 'pending';
$stmt = db()->prepare("
SELECT rs.*, u.username, u.alias
FROM referral_social_shares rs
JOIN users u ON rs.user_id = u.id
WHERE rs.status = ?
ORDER BY rs.created_at DESC
");
$stmt->execute([$status]);
echo json_encode(['success'=>true,'shares'=>$stmt->fetchAll()]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']); exit;
}
// ── POST actions ──────────────────────────────────────────
if ($method !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$d = json_decode(file_get_contents('php://input'), true);
if ($action === 'submit_share') {
$platform = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['platform'] ?? '')));
if (!$platform) { echo json_encode(['success'=>false,'error'=>'Platform required']); exit; }
// Get bonus tokens for this platform from tiers config
$bonus = 5; // default
try {
db()->prepare("INSERT INTO referral_social_shares (user_id,platform,bonus_tokens) VALUES (?,?,?)")
->execute([$userId, $platform, $bonus]);
echo json_encode(['success'=>true]);
} catch(Exception $e) {
echo json_encode(['success'=>false,'error'=>'Already submitted for this platform']);
}
exit;
}
// ── Admin only below ──────────────────────────────────────
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
if ($action === 'resolve_referral') {
$id = (int)($d['id'] ?? 0);
$status = $d['status'] ?? '';
$note = substr(trim($d['note'] ?? ''), 0, 300);
if (!in_array($status, ['verified','denied','deleted'])) { echo json_encode(['success'=>false,'error'=>'Invalid status']); exit; }
$chk = db()->prepare("SELECT r.*, t.tokens_per_ref, t.min_referrals, t.bonus_tokens FROM referrals r LEFT JOIN referral_tiers t ON r.tier_id=t.id WHERE r.id=?");
$chk->execute([$id]);
$ref = $chk->fetch();
if (!$ref) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
if ($status === 'verified') {
// Determine best tier for referrer
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
$countStmt->execute([$ref['referrer_id']]);
$verifiedCount = (int)$countStmt->fetchColumn() + 1; // +1 for this one
$tier = db()->query("SELECT * FROM referral_tiers WHERE is_active=1 AND min_referrals <= $verifiedCount ORDER BY min_referrals DESC LIMIT 1")->fetch();
$tokensToAward = $tier ? (float)$tier['tokens_per_ref'] : 5;
// Check if this hits a bonus milestone
$bonusTokens = 0;
if ($tier && $verifiedCount == (int)$tier['min_referrals']) {
$bonusTokens = (float)$tier['bonus_tokens'];
}
$totalAward = $tokensToAward + $bonusTokens;
db()->beginTransaction();
try {
db()->prepare("UPDATE referrals SET status='verified', tier_id=?, tokens_awarded=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?")
->execute([$tier['id'] ?? null, $totalAward, (int)$_SESSION['user_id'], $note, $id]);
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$totalAward, $ref['referrer_id']]);
logAdminAction('REFERRAL_VERIFIED', (int)\$_SESSION['user_id'], 'referral', \$id, 'Verified referral #'.\$id.' — awarded '.\$totalAward.' tokens to user #'.\$ref['referrer_id'], 'pending', 'verified', 'info');
db()->commit();
echo json_encode(['success'=>true,'tokens_awarded'=>$totalAward,'bonus'=>$bonusTokens,'tier'=>$tier['name']??'']);
} catch(Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']);
}
} else {
db()->prepare("UPDATE referrals SET status=?, admin_id=?, admin_note=?, resolved_at=NOW() WHERE id=?")
->execute([$status, (int)$_SESSION['user_id'], $note, $id]);
echo json_encode(['success'=>true]);
}
exit;
}
if ($action === 'resolve_share') {
$id = (int)($d['id'] ?? 0);
$status = $d['status'] ?? '';
if (!in_array($status, ['approved','denied'])) { echo json_encode(['success'=>false,'error'=>'Invalid']); exit; }
$chk = db()->prepare("SELECT * FROM referral_social_shares WHERE id=?"); $chk->execute([$id]); $share = $chk->fetch();
if (!$share) { echo json_encode(['success'=>false,'error'=>'Not found']); exit; }
db()->prepare("UPDATE referral_social_shares SET status=?,admin_id=?,resolved_at=NOW() WHERE id=?")->execute([$status,(int)$_SESSION['user_id'],$id]);
if ($status === 'approved') {
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$share['bonus_tokens'],$share['user_id']]);
logAdminAction('SOCIAL_SHARE_APPROVED', (int)\$_SESSION['user_id'], 'referral_share', \$id, 'Approved social share #'.\$id.' — awarded '.\$share['bonus_tokens'].' bonus tokens to user #'.\$share['user_id'], '', 'approved', 'info');
}
echo json_encode(['success'=>true]);
exit;
}
// ── Tier CRUD ─────────────────────────────────────────────
if ($action === 'tier_create') {
$name = substr(trim($d['name']??''),0,100);
$minRefs = (int)($d['min_referrals']??1);
$tokPer = (float)($d['tokens_per_ref']??5);
$bonus = (float)($d['bonus_tokens']??0);
$desc = substr(trim($d['description']??''),0,300);
$sort = (int)($d['sort_order']??99);
$active = (int)(bool)($d['is_active']??1);
if (!$name) { echo json_encode(['success'=>false,'error'=>'Name required']); exit; }
db()->prepare("INSERT INTO referral_tiers (name,min_referrals,tokens_per_ref,bonus_tokens,description,is_active,sort_order) VALUES (?,?,?,?,?,?,?)")
->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort]);
echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]);
exit;
}
if ($action === 'tier_update') {
$id = (int)($d['id']??0);
$name = substr(trim($d['name']??''),0,100);
$minRefs = (int)($d['min_referrals']??1);
$tokPer = (float)($d['tokens_per_ref']??5);
$bonus = (float)($d['bonus_tokens']??0);
$desc = substr(trim($d['description']??''),0,300);
$sort = (int)($d['sort_order']??0);
$active = (int)(bool)($d['is_active']??1);
if (!$id||!$name) { echo json_encode(['success'=>false,'error'=>'ID and name required']); exit; }
db()->prepare("UPDATE referral_tiers SET name=?,min_referrals=?,tokens_per_ref=?,bonus_tokens=?,description=?,is_active=?,sort_order=? WHERE id=?")
->execute([$name,$minRefs,$tokPer,$bonus,$desc,$active,$sort,$id]);
echo json_encode(['success'=>true]);
exit;
}
if ($action === 'tier_delete') {
$id = (int)($d['id']??0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; }
db()->prepare("DELETE FROM referral_tiers WHERE id=?")->execute([$id]);
echo json_encode(['success'=>true]);
exit;
}
echo json_encode(['success'=>false,'error'=>'Unknown action']);
+18
View File
@@ -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);
+16
View File
@@ -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));
+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#001a2e"/>
<stop offset="100%" stop-color="#000810"/>
</radialGradient>
<linearGradient id="cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00e5ff"/>
<stop offset="100%" stop-color="#0066ff"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<!-- Grid lines -->
<line x1="0" y1="40" x2="120" y2="40" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
<line x1="0" y1="80" x2="120" y2="80" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
<line x1="40" y1="0" x2="40" y2="120" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
<line x1="80" y1="0" x2="80" y2="120" stroke="#00e5ff" stroke-width="0.3" opacity="0.15"/>
<!-- Controller icon -->
<text x="60" y="56" font-size="36" text-anchor="middle" filter="url(#glow)" font-family="Arial">🎮</text>
<!-- eGame99 -->
<text x="60" y="78" font-size="13" font-weight="900" text-anchor="middle" fill="url(#cyan)" font-family="Arial Black, sans-serif" letter-spacing="1" filter="url(#glow)">eGAME 99</text>
<!-- Corner accents -->
<path d="M10 10 L25 10 L25 14 L14 14 L14 25 L10 25 Z" fill="url(#cyan)" opacity="0.6"/>
<path d="M110 10 L95 10 L95 14 L106 14 L106 25 L110 25 Z" fill="url(#cyan)" opacity="0.6"/>
<path d="M10 110 L25 110 L25 106 L14 106 L14 95 L10 95 Z" fill="url(#cyan)" opacity="0.6"/>
<path d="M110 110 L95 110 L95 106 L106 106 L106 95 L110 95 Z" fill="url(#cyan)" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="70%" r="60%">
<stop offset="0%" stop-color="#2a0800"/>
<stop offset="100%" stop-color="#0a0200"/>
</radialGradient>
<linearGradient id="fire" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#FF2200"/>
<stop offset="50%" stop-color="#FF7700"/>
<stop offset="100%" stop-color="#FFDD00"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#fire)" stroke-width="1.5" opacity="0.6"/>
<!-- Fire wave bottom -->
<path d="M0 100 Q20 85 40 95 Q60 105 80 90 Q100 75 120 88 L120 120 L0 120 Z" fill="#FF2200" opacity="0.25"/>
<!-- Fire emoji -->
<text x="60" y="58" font-size="40" text-anchor="middle" filter="url(#glow)" font-family="Arial">🔥</text>
<!-- KIRIN text -->
<text x="60" y="82" font-size="13" font-weight="900" text-anchor="middle" fill="url(#fire)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">KIRIN</text>
<!-- Sparks -->
<circle cx="35" cy="35" r="2" fill="#FF7700" opacity="0.7"/>
<circle cx="85" cy="28" r="1.5" fill="#FFDD00" opacity="0.8"/>
<circle cx="25" cy="60" r="1.5" fill="#FF2200" opacity="0.6"/>
<circle cx="95" cy="55" r="2" fill="#FF7700" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+38
View File
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<defs>
<linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f0c040"/>
<stop offset="100%" stop-color="#ff6b35"/>
</linearGradient>
<linearGradient id="g2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00e5ff"/>
<stop offset="100%" stop-color="#7b2fbe"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Controller body -->
<rect x="6" y="16" width="36" height="22" rx="11" fill="url(#g1)" filter="url(#glow)"/>
<!-- D-pad left side -->
<rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.5)"/>
<rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.5)"/>
<!-- Buttons right side -->
<circle cx="32" cy="22" r="2" fill="#e63946" opacity="0.85"/>
<circle cx="36" cy="25" r="2" fill="#2ec4b6" opacity="0.85"/>
<circle cx="32" cy="28" r="2" fill="#7b2fbe" opacity="0.85"/>
<circle cx="28" cy="25" r="2" fill="#f4a261" opacity="0.85"/>
<!-- Center connector / menu button -->
<rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.35)"/>
<!-- Handle grips (left and right) -->
<rect x="8" y="30" width="8" height="6" rx="4" fill="url(#g2)" opacity="0.7"/>
<rect x="32" y="30" width="8" height="6" rx="4" fill="url(#g2)" opacity="0.7"/>
<!-- Top bumper buttons -->
<rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#g1)" opacity="0.8"/>
<rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#g1)" opacity="0.8"/>
<!-- Stars / sparkles -->
<circle cx="24" cy="8" r="1.5" fill="#f0c040" opacity="0.9"/>
<circle cx="38" cy="10" r="1" fill="#00e5ff" opacity="0.8"/>
<circle cx="10" cy="10" r="1" fill="#f0c040" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="40%" cy="40%" r="60%">
<stop offset="0%" stop-color="#1a0a40"/>
<stop offset="60%" stop-color="#0a0520"/>
<stop offset="100%" stop-color="#050210"/>
</radialGradient>
<linearGradient id="purple" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#c77dff"/>
<stop offset="100%" stop-color="#7b2fbe"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<!-- Galactic swirl -->
<ellipse cx="60" cy="60" rx="38" ry="12" fill="none" stroke="url(#purple)" stroke-width="1.5" opacity="0.5" transform="rotate(-30 60 60)"/>
<ellipse cx="60" cy="60" rx="28" ry="8" fill="none" stroke="#c77dff" stroke-width="1" opacity="0.4" transform="rotate(20 60 60)"/>
<!-- Stars -->
<circle cx="30" cy="25" r="1.5" fill="white" opacity="0.9"/>
<circle cx="88" cy="30" r="1" fill="white" opacity="0.7"/>
<circle cx="20" cy="70" r="1" fill="white" opacity="0.6"/>
<circle cx="95" cy="65" r="1.5" fill="white" opacity="0.8"/>
<circle cx="50" cy="15" r="1" fill="white" opacity="0.5"/>
<circle cx="75" cy="100" r="1.5" fill="white" opacity="0.7"/>
<circle cx="40" cy="95" r="1" fill="#c77dff" opacity="0.9"/>
<circle cx="100" cy="45" r="1" fill="#c77dff" opacity="0.6"/>
<!-- Galaxy emoji + text -->
<text x="60" y="56" font-size="34" text-anchor="middle" filter="url(#glow)" font-family="Arial">🌌</text>
<text x="60" y="80" font-size="11" font-weight="700" text-anchor="middle" fill="url(#purple)" font-family="Arial, sans-serif" letter-spacing="1.5">MILKY WAY</text>
<!-- Border glow -->
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#purple)" stroke-width="1" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1a1400"/>
<stop offset="100%" stop-color="#0a0800"/>
</radialGradient>
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFD700"/>
<stop offset="50%" stop-color="#FFF0A0"/>
<stop offset="100%" stop-color="#CC9900"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<!-- Diamond border -->
<rect x="8" y="8" width="104" height="104" rx="18" fill="none" stroke="url(#gold)" stroke-width="1.5" opacity="0.7"/>
<rect x="4" y="4" width="112" height="112" rx="20" fill="none" stroke="#FFD700" stroke-width="0.5" opacity="0.3"/>
<!-- Crown -->
<text x="60" y="54" font-size="38" text-anchor="middle" filter="url(#glow)" font-family="Arial">👑</text>
<!-- 777 NOBLE text -->
<text x="60" y="76" font-size="18" font-weight="900" text-anchor="middle" fill="url(#gold)" font-family="Arial Black, sans-serif" letter-spacing="3" filter="url(#glow)">777</text>
<text x="60" y="92" font-size="10" font-weight="700" text-anchor="middle" fill="#CC9900" font-family="Arial, sans-serif" letter-spacing="3" opacity="0.9">NOBLE</text>
<!-- Corner diamonds -->
<polygon points="20,12 24,16 20,20 16,16" fill="#FFD700" opacity="0.7"/>
<polygon points="100,12 104,16 100,20 96,16" fill="#FFD700" opacity="0.7"/>
<polygon points="20,108 24,104 20,100 16,104" fill="#FFD700" opacity="0.7"/>
<polygon points="100,108 104,104 100,100 96,104" fill="#FFD700" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0a0a12"/>
<stop offset="100%" stop-color="#1a1228"/>
</linearGradient>
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f0c040"/>
<stop offset="100%" stop-color="#ff6b35"/>
</linearGradient>
<linearGradient id="cyan" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00e5ff"/>
<stop offset="100%" stop-color="#7b2fbe"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bg)"/>
<!-- Grid lines -->
<line x1="0" y1="315" x2="1200" y2="315" stroke="rgba(240,192,64,0.05)" stroke-width="1"/>
<line x1="600" y1="0" x2="600" y2="630" stroke="rgba(240,192,64,0.05)" stroke-width="1"/>
<!-- Glow circle -->
<circle cx="600" cy="315" r="280" fill="rgba(240,192,64,0.03)" stroke="rgba(240,192,64,0.08)" stroke-width="1"/>
<!-- Gamepad icon (simplified) -->
<g transform="translate(540,180) scale(2.5)">
<rect x="6" y="16" width="36" height="22" rx="11" fill="url(#gold)" opacity="0.9"/>
<rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.5)"/>
<rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.5)"/>
<circle cx="32" cy="22" r="2.2" fill="#e63946"/>
<circle cx="36" cy="25" r="2.2" fill="#2ec4b6"/>
<circle cx="32" cy="28" r="2.2" fill="#7b2fbe"/>
<circle cx="28" cy="25" r="2.2" fill="#f4a261"/>
<rect x="8" y="30" width="8" height="7" rx="4" fill="url(#cyan)" opacity="0.8"/>
<rect x="32" y="30" width="8" height="7" rx="4" fill="url(#cyan)" opacity="0.8"/>
</g>
<!-- Logo text -->
<text x="600" y="375" font-family="Georgia,serif" font-size="64" font-weight="700" fill="#f0c040" text-anchor="middle">TomTomGames</text>
<!-- Tagline -->
<text x="600" y="425" font-family="Arial,sans-serif" font-size="24" fill="#8888aa" text-anchor="middle">Buy tokens for VBlink777 · Fire Kirin · Milky Way · Ultra Panda &amp; more</text>
<!-- Bottom badges -->
<rect x="350" y="470" width="160" height="40" rx="8" fill="rgba(240,192,64,0.1)" stroke="rgba(240,192,64,0.3)" stroke-width="1"/>
<text x="430" y="496" font-family="Arial,sans-serif" font-size="16" fill="#f0c040" text-anchor="middle" font-weight="700">⚡ Instant Delivery</text>
<rect x="520" y="470" width="160" height="40" rx="8" fill="rgba(0,229,255,0.07)" stroke="rgba(0,229,255,0.2)" stroke-width="1"/>
<text x="600" y="496" font-family="Arial,sans-serif" font-size="16" fill="#00e5ff" text-anchor="middle" font-weight="700">🔒 SSL Secured</text>
<rect x="690" y="470" width="160" height="40" rx="8" fill="rgba(0,230,118,0.07)" stroke="rgba(0,230,118,0.2)" stroke-width="1"/>
<text x="770" y="496" font-family="Arial,sans-serif" font-size="16" fill="#00e676" text-anchor="middle" font-weight="700">💬 24/7 Support</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="30%" r="70%">
<stop offset="0%" stop-color="#0a1628"/>
<stop offset="100%" stop-color="#040810"/>
</radialGradient>
<linearGradient id="blue" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#64b5f6"/>
<stop offset="100%" stop-color="#1565c0"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<!-- Crown at top -->
<path d="M42 28 L50 20 L60 26 L70 20 L78 28 L74 38 L46 38 Z" fill="#FFD700" opacity="0.9" filter="url(#glow)"/>
<circle cx="50" cy="20" r="3" fill="#FFD700"/>
<circle cx="60" cy="16" r="3.5" fill="#FFD700"/>
<circle cx="70" cy="20" r="3" fill="#FFD700"/>
<!-- Panda + paw -->
<text x="60" y="68" font-size="36" text-anchor="middle" filter="url(#glow)" font-family="Arial">🐾</text>
<!-- MASTER text -->
<text x="60" y="90" font-size="11" font-weight="900" text-anchor="middle" fill="url(#blue)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">MASTER</text>
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#blue)" stroke-width="1" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#080814"/>
</radialGradient>
<linearGradient id="orange" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFB347"/>
<stop offset="100%" stop-color="#FF6B00"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#orange)" stroke-width="1.5" opacity="0.5"/>
<!-- Bamboo decorations -->
<rect x="12" y="30" width="5" height="65" rx="2" fill="#2d5a27" opacity="0.5"/>
<rect x="103" y="25" width="5" height="65" rx="2" fill="#2d5a27" opacity="0.5"/>
<rect x="10" y="48" width="9" height="3" rx="1" fill="#2d5a27" opacity="0.5"/>
<rect x="101" y="44" width="9" height="3" rx="1" fill="#2d5a27" opacity="0.5"/>
<!-- Panda emoji -->
<text x="60" y="60" font-size="42" text-anchor="middle" filter="url(#glow)" font-family="Arial">🐼</text>
<!-- ULTRA text -->
<text x="60" y="85" font-size="12" font-weight="900" text-anchor="middle" fill="url(#orange)" font-family="Arial Black, sans-serif" letter-spacing="2" filter="url(#glow)">ULTRA</text>
<!-- Stars -->
<text x="35" y="35" font-size="12" text-anchor="middle" fill="#FFD700" opacity="0.8"></text>
<text x="85" y="35" font-size="12" text-anchor="middle" fill="#FFD700" opacity="0.8"></text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<radialGradient id="bg" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1a0a2e"/>
<stop offset="100%" stop-color="#0d0620"/>
</radialGradient>
<linearGradient id="gold" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFD700"/>
<stop offset="100%" stop-color="#FF6B00"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="120" height="120" rx="24" fill="url(#bg)"/>
<rect x="2" y="2" width="116" height="116" rx="22" fill="none" stroke="url(#gold)" stroke-width="1.5" opacity="0.6"/>
<!-- Slot machine reel symbols -->
<text x="60" y="52" font-size="36" text-anchor="middle" fill="url(#gold)" filter="url(#glow)" font-family="Arial">🎰</text>
<!-- 777 text -->
<text x="60" y="84" font-size="22" font-weight="900" text-anchor="middle" fill="url(#gold)" font-family="Arial Black, sans-serif" filter="url(#glow)" letter-spacing="2">777</text>
<!-- Stars -->
<circle cx="22" cy="22" r="2" fill="#FFD700" opacity="0.8"/>
<circle cx="98" cy="22" r="2" fill="#FFD700" opacity="0.8"/>
<circle cx="22" cy="98" r="2" fill="#FF6B00" opacity="0.8"/>
<circle cx="98" cy="98" r="2" fill="#FF6B00" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* TomTomGames Version Manager
* Run after uploading a new build to update the DB version.
* Usage: https://tomtomgames.com/bump_version.php?key=ADMIN_KEY&version=1.0.2&notes=Your+notes+here
*
* Or auto-bump: ?key=ADMIN_KEY&bump=patch (1.0.1 → 1.0.2)
* ?key=ADMIN_KEY&bump=minor (1.0.1 → 1.1.0)
* ?key=ADMIN_KEY&bump=major (1.0.1 → 2.0.0)
*/
define('BUMP_KEY', 'TTG_bump_2026!'); // Change this to your own secret key
if (($_GET['key'] ?? '') !== BUMP_KEY) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
require_once __DIR__ . '/../includes/db.php';
// Get current version
$current = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn() ?: '1.0.0';
[$major, $minor, $patch] = array_map('intval', explode('.', $current));
// Determine new version
if (!empty($_GET['version'])) {
$newVersion = trim($_GET['version']);
} elseif (!empty($_GET['bump'])) {
switch ($_GET['bump']) {
case 'major': $newVersion = ($major+1).'.0.0'; break;
case 'minor': $newVersion = $major.'.'.($minor+1).'.0'; break;
default: $newVersion = $major.'.'.$minor.'.'.($patch+1); break;
}
} else {
// Default: bump patch
$newVersion = $major.'.'.$minor.'.'.($patch+1);
}
$notes = trim($_GET['notes'] ?? 'Build ' . date('Y-m-d H:i:s'));
db()->prepare("INSERT INTO app_version (version, notes) VALUES (?, ?)")
->execute([$newVersion, $notes]);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'previous' => $current,
'new_version' => $newVersion,
'notes' => $notes,
'timestamp' => date('Y-m-d H:i:s'),
]);
+232
View File
@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
<title>Buy Fish Table Game Tokens | VBlink777, Fire Kirin, Milky Way | TomTomGames</title>
<meta name="description" content="Buy tokens for the best fish table and skill games online — VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, eGame99. Instant delivery, secure payments, 24/7 support. Join TomTomGames today.">
<meta name="keywords" content="buy fish table tokens, VBlink777 tokens buy, Fire Kirin game tokens, Milky Way game credits, Ultra Panda tokens, Panda Master game, Noble777 tokens, eGame99 buy, fish table game portal, online skill games tokens, game tokens fast delivery">
<link rel="canonical" href="https://tomtomgames.com/games/">
<meta property="og:title" content="Buy Fish Table Game Tokens | TomTomGames">
<meta property="og:description" content="The #1 token portal for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 & eGame99. Buy tokens in minutes.">
<meta property="og:url" content="https://tomtomgames.com/games/">
<meta property="og:type" content="website">
<meta property="og:image" content="https://tomtomgames.com/assets/img/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Fish Table & Skill Game Platforms",
"description": "Buy tokens for these top fish table and skill game platforms through TomTomGames",
"url": "https://tomtomgames.com/games/",
"itemListElement": [
{"@type":"ListItem","position":1,"name":"VBlink777","description":"Buy VBlink777 tokens instantly. Top-rated fish table game with fast payouts.","url":"https://tomtomgames.com/#vblink777"},
{"@type":"ListItem","position":2,"name":"Fire Kirin","description":"Fire Kirin game tokens — one of the most popular fish table games online.","url":"https://tomtomgames.com/#firekirin"},
{"@type":"ListItem","position":3,"name":"Milky Way","description":"Milky Way game credits. Secure, instant token delivery for Milky Way 777.","url":"https://tomtomgames.com/#milkyway"},
{"@type":"ListItem","position":4,"name":"Ultra Panda","description":"Ultra Panda tokens — buy game credits fast for one of the top skill games.","url":"https://tomtomgames.com/#ultrapanda"},
{"@type":"ListItem","position":5,"name":"Panda Master","description":"Panda Master game tokens. Instant delivery, multiple payment methods.","url":"https://tomtomgames.com/#pandamaster"},
{"@type":"ListItem","position":6,"name":"Noble 777","description":"Noble777 tokens — buy game credits securely through TomTomGames.","url":"https://tomtomgames.com/#noble777"},
{"@type":"ListItem","position":7,"name":"eGame99","description":"eGame99 token purchases. Fast credits, 24/7 support.","url":"https://tomtomgames.com/#egame99"}
]
}
</script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',Arial,sans-serif;background:#0a0a12;color:#e8e8f0;line-height:1.6}
a{color:#f0c040;text-decoration:none}
a:hover{text-decoration:underline}
.wrap{max-width:960px;margin:0 auto;padding:0 20px}
header{background:linear-gradient(135deg,#0a0a12,#1a1228);border-bottom:1px solid rgba(240,192,64,.2);padding:16px 0}
.logo{font-family:'Georgia',serif;font-size:24px;font-weight:700;color:#f0c040}
.logo span{color:#00e5ff}
nav{display:flex;gap:20px;align-items:center;margin-top:8px;font-size:14px}
.hero{background:linear-gradient(135deg,#1a1228,#0d1a2e);padding:60px 0 50px;text-align:center;border-bottom:1px solid rgba(240,192,64,.15)}
h1{font-size:clamp(26px,5vw,42px);font-weight:800;line-height:1.2;margin-bottom:16px;color:#fff}
h1 em{color:#f0c040;font-style:normal}
.hero p{font-size:18px;color:#aab0c0;max-width:620px;margin:0 auto 28px}
.cta-btn{display:inline-block;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:700;font-size:16px;padding:14px 32px;border-radius:8px;transition:transform .15s}
.cta-btn:hover{transform:translateY(-2px);text-decoration:none}
.trust-bar{display:flex;flex-wrap:wrap;justify-content:center;gap:28px;margin-top:32px;font-size:13px;color:#8888aa}
.trust-bar span{display:flex;align-items:center;gap:6px}
section{padding:52px 0}
h2{font-size:28px;font-weight:700;color:#fff;margin-bottom:8px}
.section-sub{color:#8888aa;margin-bottom:32px;font-size:16px}
.games-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:20px}
.game-card{background:#111827;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:24px;transition:transform .2s,border-color .2s}
.game-card:hover{transform:translateY(-3px);border-color:rgba(240,192,64,.3)}
.game-card h3{font-size:18px;color:#fff;margin-bottom:8px}
.game-card p{font-size:14px;color:#8888aa;margin-bottom:16px}
.game-card .buy-link{font-size:13px;font-weight:700;color:#f0c040;border:1px solid rgba(240,192,64,.3);padding:8px 16px;border-radius:6px;display:inline-block}
.game-card .buy-link:hover{background:rgba(240,192,64,.1);text-decoration:none}
.color-dot{width:12px;height:12px;border-radius:50%;display:inline-block;margin-right:8px;vertical-align:middle}
.steps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
.step{background:#111827;border-radius:12px;padding:24px;text-align:center}
.step-num{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;font-weight:800;font-size:18px;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.step h3{font-size:16px;color:#fff;margin-bottom:6px}
.step p{font-size:13px;color:#8888aa}
.pay-methods{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px}
.pay-badge{background:#111827;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:10px 18px;font-size:14px;font-weight:600;color:#e8e8f0}
.faq{max-width:720px}
.faq-item{border-bottom:1px solid rgba(255,255,255,.06);padding:20px 0}
.faq-item h3{font-size:17px;color:#fff;margin-bottom:8px}
.faq-item p{font-size:14px;color:#aab0c0}
footer{background:#060608;border-top:1px solid rgba(255,255,255,.06);padding:32px 0;font-size:13px;color:#555;text-align:center}
footer a{color:#777}
</style>
</head>
<body>
<header>
<div class="wrap" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
<a href="https://tomtomgames.com/" class="logo">TomTom<span>Games</span></a>
<nav>
<a href="https://tomtomgames.com/">🎮 Play Now</a>
<a href="https://tomtomgames.com/">Create Account</a>
<a href="https://tomtomgames.com/" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.3);padding:7px 16px;border-radius:6px;color:#f0c040;font-weight:700">Buy Tokens </a>
</nav>
</div>
</header>
<section class="hero">
<div class="wrap">
<h1>Buy <em>Game Tokens</em> for Fish Table &amp; Skill Games</h1>
<p>The fastest, most trusted way to load up on tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.</p>
<a href="https://tomtomgames.com/" class="cta-btn">🪙 Buy Tokens Now</a>
<div class="trust-bar">
<span>🔒 SSL Secured</span>
<span> Instant Delivery</span>
<span>💳 Multiple Payment Methods</span>
<span>💬 24/7 Support</span>
<span>🎮 7 Game Platforms</span>
</div>
</div>
</section>
<section style="background:#080812;border-bottom:1px solid rgba(255,255,255,.04)">
<div class="wrap">
<h2>Supported Game Platforms</h2>
<p class="section-sub">Buy tokens for all major fish table and skill game platforms in one place.</p>
<div class="games-grid">
<div class="game-card">
<h3><span class="color-dot" style="background:#FF6B35"></span>VBlink777</h3>
<p>One of the most popular fish table games. Buy VBlink777 tokens securely and get them credited fast to your game account.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy VBlink777 Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#E63946"></span>Fire Kirin</h3>
<p>Fire Kirin is a top-rated skill game with exciting fish table gameplay. Purchase Fire Kirin tokens through TomTomGames for instant delivery.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy Fire Kirin Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#7B2FBE"></span>Milky Way</h3>
<p>Milky Way 777 game credits buy tokens for one of the best online fish table platforms. Secure payment, fast top-up.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy Milky Way Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#F4A261"></span>Ultra Panda</h3>
<p>Ultra Panda game tokens for sale. Top up your account instantly through our secure portal and start playing right away.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy Ultra Panda Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#457B9D"></span>Panda Master</h3>
<p>Panda Master tokens buy game credits quickly and safely. Multiple payment methods accepted including card, Venmo, and Cash App.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy Panda Master Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#FFD700"></span>Noble 777</h3>
<p>Noble 777 game token purchases made easy. Register, select your package, and have tokens in your account within minutes.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy Noble 777 Tokens </a>
</div>
<div class="game-card">
<h3><span class="color-dot" style="background:#2EC4B6"></span>eGame99</h3>
<p>eGame99 tokens for sale at the best rates. Fast crediting, 24/7 customer support, and a seamless buying experience.</p>
<a href="https://tomtomgames.com/" class="buy-link">Buy eGame99 Tokens </a>
</div>
</div>
</div>
</section>
<section>
<div class="wrap">
<h2>How to Buy Tokens in 3 Steps</h2>
<p class="section-sub">Get tokens credited to your game account in minutes.</p>
<div class="steps-grid">
<div class="step">
<div class="step-num">1</div>
<h3>Create Account</h3>
<p>Register free in under 60 seconds. Verify your email and log in.</p>
</div>
<div class="step">
<div class="step-num">2</div>
<h3>Select Game &amp; Package</h3>
<p>Choose your game platform, enter your in-game alias, and pick a token package or enter a custom amount.</p>
</div>
<div class="step">
<div class="step-num">3</div>
<h3>Pay &amp; Play</h3>
<p>Pay securely by card or manual transfer. Tokens are credited to your game account fast.</p>
</div>
</div>
<div style="margin-top:32px">
<h3 style="color:#fff;margin-bottom:12px">Accepted Payment Methods</h3>
<div class="pay-methods">
<div class="pay-badge">💳 Credit / Debit Card</div>
<div class="pay-badge">💙 Venmo</div>
<div class="pay-badge">💚 Cash App</div>
<div class="pay-badge">🟢 Chime</div>
<div class="pay-badge">💜 Zelle</div>
</div>
</div>
</div>
</section>
<section style="background:#080812;border-top:1px solid rgba(255,255,255,.04)">
<div class="wrap">
<h2>Frequently Asked Questions</h2>
<p class="section-sub">Everything you need to know about buying game tokens.</p>
<div class="faq">
<div class="faq-item">
<h3>What is TomTomGames?</h3>
<p>TomTomGames is a token portal that lets you purchase game credits for popular fish table and skill games including VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99. We handle the token purchase securely so you can focus on playing.</p>
</div>
<div class="faq-item">
<h3>How quickly are tokens delivered?</h3>
<p>Card payments are processed instantly through Square. Manual payments (Venmo, Zelle, Cash App, Chime) are credited within a few minutes after we confirm receipt of your payment.</p>
</div>
<div class="faq-item">
<h3>How much do tokens cost?</h3>
<p>Tokens are priced at $1 per token. We offer packages starting from 5 tokens ($5) up to 100 tokens ($100), or you can enter a custom amount up to $500. Volume packages are available contact support for details.</p>
</div>
<div class="faq-item">
<h3>Is my payment information secure?</h3>
<p>Yes. All card transactions are processed through Square, a fully PCI-compliant payment processor. We never store your full card number. Manual payment methods require no card details at all.</p>
</div>
<div class="faq-item">
<h3>What if I need help?</h3>
<p>Our support team is available 24/7 through the live chat feature inside the app. You can also send a message through your account and we'll respond within minutes.</p>
</div>
<div class="faq-item">
<h3>Can I cash out my tokens?</h3>
<p>Yes. You can request a cashout through your account and receive your funds via your preferred payment method. Cashouts are processed by our team promptly.</p>
</div>
</div>
<div style="margin-top:36px;text-align:center">
<a href="https://tomtomgames.com/" class="cta-btn">Get Started — Create Free Account</a>
</div>
</div>
</section>
<footer>
<div class="wrap">
<p><strong style="color:#777">TomTomGames</strong> — Your trusted token portal for fish table and skill games.</p>
<p style="margin-top:8px"><a href="https://tomtomgames.com/">Home</a> · <a href="https://tomtomgames.com/games/">Games</a> · <a href="https://tomtomgames.com/">Support</a></p>
<p style="margin-top:12px">© <?= date('Y') ?> TomTomGames. All rights reserved. Game tokens are for entertainment purposes on supported platforms only. Please play responsibly.</p>
</div>
</footer>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
<?php
/**
* TomGames — Square Location ID Finder
* Upload this file, visit it in your browser ONCE to get your Location ID.
* Then paste the ID into includes/config.php and DELETE this file.
*/
$token = 'EAAAl1ECweOVgNiwhC2SuA56QFjlfRLkYxo4xe4r2fMLvqwLT0IKGUZNNOYy1NXn';
$locations = [];
$error = '';
$ch = curl_init('https://connect.squareup.com/v2/locations');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Square-Version: 2024-01-18',
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code === 200) {
$data = json_decode($resp, true);
$locations = $data['locations'] ?? [];
} else {
$error = "HTTP $code: " . htmlspecialchars($resp);
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Square Location ID Finder</title>
<style>
body{font-family:'Segoe UI',sans-serif;background:#0a0a12;color:#e8e8f0;max-width:600px;margin:40px auto;padding:20px}
h1{font-size:22px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px}
.sub{color:#8888aa;font-size:13px;margin-bottom:28px}
.loc{background:#1a1a2e;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px;margin-bottom:16px}
.loc-name{font-size:18px;font-weight:700;color:#e8e8f0;margin-bottom:6px}
.loc-id-wrap{display:flex;align-items:center;gap:10px;margin:10px 0}
.loc-id{font-family:monospace;font-size:18px;font-weight:700;color:#f0c040;background:#0a0a12;border:1px solid rgba(240,192,64,.4);border-radius:8px;padding:10px 16px;flex:1;letter-spacing:1px}
.copy-btn{padding:10px 16px;background:linear-gradient(135deg,#f0c040,#d4a017);border:none;border-radius:8px;color:#000;font-weight:700;font-size:13px;cursor:pointer}
.copy-btn:hover{opacity:.9}
.loc-meta{font-size:12px;color:#8888aa;margin-top:6px}
.next{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);border-radius:12px;padding:18px;margin-top:24px}
.next h2{color:#00e5ff;font-size:15px;margin-bottom:10px}
.next ol{padding-left:18px;line-height:2;color:#c0c0d8;font-size:13px}
.next code{background:#0a0a12;border:1px solid rgba(255,255,255,.15);border-radius:4px;padding:2px 7px;font-family:monospace;color:#f0c040}
.err{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);border-radius:10px;padding:16px;color:#ff6666}
.warn{background:rgba(255,214,10,.08);border:1px solid rgba(255,214,10,.2);border-radius:8px;padding:12px;margin-top:20px;font-size:12px;color:#ffd60a}
</style>
</head>
<body>
<h1>🔑 Square Location Finder</h1>
<div class="sub">TomGames — Run once, then delete this file</div>
<?php if ($error): ?>
<div class="err"><strong>Error fetching locations:</strong><br><?= $error ?></div>
<?php elseif (empty($locations)): ?>
<div class="err">No locations found on this Square account.</div>
<?php else: ?>
<p style="color:#8888aa;font-size:13px;margin-bottom:16px">Found <?= count($locations) ?> location(s). Copy the ID for your main location:</p>
<?php foreach ($locations as $loc): ?>
<div class="loc">
<div class="loc-name"><?= htmlspecialchars($loc['name']) ?> <?= $loc['status'] === 'ACTIVE' ? '✅' : '⚠️ ' . $loc['status'] ?></div>
<div class="loc-id-wrap">
<div class="loc-id" id="id-<?= $loc['id'] ?>"><?= htmlspecialchars($loc['id']) ?></div>
<button class="copy-btn" onclick="copyId('<?= $loc['id'] ?>', this)">COPY</button>
</div>
<div class="loc-meta">
<?= htmlspecialchars($loc['address']['address_line_1'] ?? '') ?>
<?= htmlspecialchars($loc['address']['city'] ?? '') ?> ·
Currency: <?= htmlspecialchars($loc['currency'] ?? 'USD') ?> ·
Country: <?= htmlspecialchars($loc['country'] ?? '—') ?>
</div>
</div>
<?php endforeach; ?>
<div class="next">
<h2>📋 Next Steps</h2>
<ol>
<li>Copy your Location ID above</li>
<li>Open <code>includes/config.php</code></li>
<li>Replace <code>YOUR_LOCATION_ID</code> with your copied ID</li>
<li>Also update <code>DB_PASS</code> with your MySQL password</li>
<li><strong>Delete this file from your server!</strong></li>
</ol>
</div>
<?php endif; ?>
<div class="warn">⚠️ <strong>Security:</strong> Delete <code>get_location.php</code> from your server after use. It exposes your access token.</div>
<script>
function copyId(id, btn) {
navigator.clipboard.writeText(id).then(() => {
btn.textContent = 'COPIED!';
btn.style.background = '#00e676';
setTimeout(() => { btn.textContent = 'COPY'; btn.style.background = ''; }, 2000);
});
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+206
View File
@@ -0,0 +1,206 @@
<?php
$key = $_GET['key'] ?? '';
if ($key !== 'TomGames2024Admin') die('<h2 style="font-family:sans-serif;color:red">Access denied. Add ?key=TomGames2024Admin to URL.</h2>');
require_once __DIR__ . '/../includes/config.php';
$log = [];
function ok($msg) { global $log; $log[] = ['t'=>'ok', 'm'=>$msg]; }
function err($msg) { global $log; $log[] = ['t'=>'err', 'm'=>$msg]; }
function warn($msg) { global $log; $log[] = ['t'=>'warn','m'=>$msg]; }
function info($msg) { global $log; $log[] = ['t'=>'info','m'=>$msg]; }
try {
$pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4", DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
ok('Connected to <strong>'.DB_NAME.'</strong> as <strong>'.DB_USER.'</strong>');
} catch (Exception $e) {
die('<pre style="color:red">CONNECTION FAILED: '.htmlspecialchars($e->getMessage()).'</pre>');
}
// Helper: check if column exists
function colExists(PDO $pdo, string $table, string $col): bool {
$r = $pdo->query("SHOW COLUMNS FROM `$table` LIKE '$col'")->fetch();
return (bool)$r;
}
// ── CREATE TABLES ───────────────────────────────────────────
$tables = [
'users' => "CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE,
email_verified TINYINT(1) DEFAULT 0,
tokens DECIMAL(10,2) DEFAULT 0,
is_admin TINYINT(1) DEFAULT 0,
status ENUM('active','suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'pending_registrations' => "CREATE TABLE IF NOT EXISTS pending_registrations (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
alias VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL,
token VARCHAR(64) UNIQUE NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'token_purchases' => "CREATE TABLE IF NOT EXISTS token_purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
tokens INT NOT NULL,
amount_cents INT NOT NULL,
payment_method VARCHAR(20) DEFAULT 'card',
square_payment_id VARCHAR(255),
platform_id VARCHAR(50),
game_alias VARCHAR(100),
player_name VARCHAR(100),
billing_name VARCHAR(160),
billing_address VARCHAR(200),
billing_city VARCHAR(80),
billing_state VARCHAR(2),
billing_zip VARCHAR(10),
billing_email VARCHAR(150),
is_custom TINYINT(1) DEFAULT 0,
failure_reason TEXT,
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
receipt_url VARCHAR(512),
status ENUM('pending','completed','failed') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'cashout_requests' => "CREATE TABLE IF NOT EXISTS cashout_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
platform_id VARCHAR(50) NOT NULL,
alias VARCHAR(100) NOT NULL,
tokens DECIMAL(10,2) NOT NULL,
status ENUM('pending','approved','rejected') DEFAULT 'pending',
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'saved_billing' => "CREATE TABLE IF NOT EXISTS saved_billing (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT UNIQUE NOT NULL,
first_name VARCHAR(80),
last_name VARCHAR(80),
email VARCHAR(150),
address VARCHAR(200),
city VARCHAR(80),
state VARCHAR(2),
zip VARCHAR(10),
card_brand VARCHAR(30),
card_last4 VARCHAR(4),
card_exp_month VARCHAR(2),
card_exp_year VARCHAR(4),
sq_card_id VARCHAR(255),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'chat_messages' => "CREATE TABLE IF NOT EXISTS chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
sender ENUM('user','admin') NOT NULL,
message TEXT NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
];
foreach ($tables as $name => $sql) {
try { $pdo->exec($sql); ok("Table <strong>$name</strong> ✓"); }
catch (Exception $e) { err("Table <strong>$name</strong>: ".htmlspecialchars($e->getMessage())); }
}
// ── ADD MISSING COLUMNS (compatible with MySQL 5.6/5.7/8) ──
// Check existence first, then ALTER — works on all MySQL versions
$addCols = [
// [table, column, definition, after]
['token_purchases', 'billing_name', "VARCHAR(160)", 'player_name'],
['token_purchases', 'billing_address', "VARCHAR(200)", 'billing_name'],
['token_purchases', 'billing_city', "VARCHAR(80)", 'billing_address'],
['token_purchases', 'billing_state', "VARCHAR(2)", 'billing_city'],
['token_purchases', 'billing_zip', "VARCHAR(10)", 'billing_state'],
['token_purchases', 'billing_email', "VARCHAR(150)", 'billing_zip'],
['token_purchases', 'is_custom', "TINYINT(1) DEFAULT 0", 'billing_email'],
['token_purchases', 'failure_reason', "TEXT", 'is_custom'],
['token_purchases', 'card_brand', "VARCHAR(30)", 'failure_reason'],
['token_purchases', 'card_last4', "VARCHAR(4)", 'card_brand'],
['token_purchases', 'receipt_url', "VARCHAR(512)", 'card_last4'],
['token_purchases', 'admin_note', "TEXT", 'status'],
['users', 'email_verified', "TINYINT(1) DEFAULT 0", 'email'],
];
foreach ($addCols as [$tbl, $col, $def, $after]) {
if (colExists($pdo, $tbl, $col)) {
ok("Column <strong>$tbl.$col</strong> already exists ✓");
} else {
try {
$pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`");
ok("Column <strong>$tbl.$col</strong> added ✓");
} catch (Exception $e) {
err("Column <strong>$tbl.$col</strong>: ".htmlspecialchars($e->getMessage()));
}
}
}
// ── FIX ADMIN email_verified ────────────────────────────────
try {
$n = $pdo->exec("UPDATE users SET email_verified=1 WHERE is_admin=1");
ok("Admin accounts email_verified set to 1 ($n updated)");
} catch (Exception $e) { warn("Admin fix: ".htmlspecialchars($e->getMessage())); }
// ── SUMMARY ─────────────────────────────────────────────────
$tables_now = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);
info("All tables: <strong>".implode(', ', $tables_now)."</strong>");
try {
$total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$admins = $pdo->query("SELECT COUNT(*) FROM users WHERE is_admin=1")->fetchColumn();
info("Users: <strong>$total total</strong>, $admins admin(s)");
} catch (Exception $e) {}
?>
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>TomTomGames DB Install</title>
<style>
body{font-family:'Segoe UI',sans-serif;background:#0a0a12;color:#e8e8f0;max-width:680px;margin:40px auto;padding:20px}
h1{font-size:20px;margin-bottom:4px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.sub{color:#8888aa;font-size:13px;margin-bottom:24px}
.row{padding:9px 14px;border-radius:7px;margin-bottom:5px;font-size:13px;display:flex;align-items:flex-start;gap:10px}
.ok {background:rgba(0,230,118,.08);border:1px solid rgba(0,230,118,.2)}
.err {background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);color:#ff9999}
.warn{background:rgba(255,214,10,.07);border:1px solid rgba(255,214,10,.2);color:#ffd60a}
.info{background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);color:#aaddff}
.ic{flex-shrink:0;font-weight:700}
.next{background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.2);border-radius:10px;padding:16px;margin-top:24px}
.next h2{color:#f0c040;font-size:14px;margin-bottom:8px}
.next ol{padding-left:16px;line-height:2;color:#ccccdd;font-size:13px}
.del{background:rgba(255,68,68,.07);border:1px solid rgba(255,68,68,.2);border-radius:7px;padding:10px 14px;margin-top:14px;font-size:12px;color:#ff9999}
</style></head><body>
<h1>🎮 TomTomGames — DB Install / Repair</h1>
<div class="sub">Database: <strong><?= DB_NAME ?></strong></div>
<?php foreach ($log as $e): $ic = $e['t']==='ok'?'✓':($e['t']==='err'?'✗':($e['t']==='warn'?'⚠':'')); ?>
<div class="row <?= $e['t'] ?>"><span class="ic"><?= $ic ?></span><span><?= $e['m'] ?></span></div>
<?php endforeach; ?>
<div class="next"><h2>Next Steps</h2><ol>
<li>All green ✓? Your database is fully set up</li>
<li>Visit <strong>/create_admin.php</strong> to create admin account (if needed)</li>
<li>Visit <strong>https://tomtomgames.com</strong> — app should load normally</li>
<li><strong>Delete install.php</strong> from your server now</li>
</ol></div>
<div class="del">⚠ Delete <code>install.php</code> after use — it exposes DB structure.</div>
</body></html>
+43
View File
@@ -0,0 +1,43 @@
{
"name": "TomTomGames — Game Token Portal",
"short_name": "TomTomGames",
"description": "Buy tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777 and eGame99. Fast, secure, mobile-first.",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a12",
"theme_color": "#f0c040",
"orientation": "portrait",
"scope": "/",
"lang": "en-US",
"categories": ["games", "entertainment"],
"icons": [
{
"src": "/assets/img/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/assets/img/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "Buy Tokens",
"short_name": "Buy",
"description": "Buy game tokens instantly",
"url": "/?action=buy",
"icons": [{ "src": "/assets/img/icon-192.png", "sizes": "192x192" }]
},
{
"name": "Support",
"short_name": "Help",
"description": "Contact TomTomGames support",
"url": "/?action=chat",
"icons": [{ "src": "/assets/img/icon-192.png", "sizes": "192x192" }]
}
]
}
+23
View File
@@ -0,0 +1,23 @@
User-agent: *
Allow: /
Allow: /assets/
Disallow: /admin/
Disallow: /api/
Disallow: /install.php
Disallow: /test.php
Disallow: /test_login.php
Disallow: /get_location.php
Disallow: /?q=
# Block AI training bots (optional but protects content)
User-agent: GPTBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: CCBot
Disallow: /
# Sitemap
Sitemap: https://tomtomgames.com/sitemap.xml
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- Homepage / App -->
<url>
<loc>https://tomtomgames.com/</loc>
<lastmod><?= date('Y-m-d') ?></lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<!-- SEO Landing Page -->
<url>
<loc>https://tomtomgames.com/games/</loc>
<lastmod><?= date('Y-m-d') ?></lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>
+6
View File
@@ -0,0 +1,6 @@
<?php
echo json_encode([
'php' => PHP_VERSION,
'time' => date('Y-m-d H:i:s'),
'status' => 'ok'
]);
+18
View File
@@ -0,0 +1,18 @@
<?php
// POST test - simulates exactly what the app does
ob_start();
try { require_once __DIR__ . '/../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); die(json_encode(['boot_error'=>$e->getMessage()])); }
ob_end_clean();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$result = loginUser($data['username'] ?? '', $data['password'] ?? '');
if ($result['success']) unset($result['user']['password']);
echo json_encode($result);
exit;
}
// GET - show users
$users = db()->query("SELECT id,username,alias,is_admin,email_verified,status FROM users")->fetchAll();
echo json_encode(['users'=>$users,'session_id'=>session_id()]);
+91
View File
@@ -0,0 +1,91 @@
<?php
// Standalone email test - no auth required for diagnosis
// DELETE THIS FILE after confirming email works
$result = [];
$result['php_version'] = PHP_VERSION;
$result['mail_function_exists'] = function_exists('mail');
$result['server_software'] = $_SERVER['SERVER_SOFTWARE'] ?? 'unknown';
$result['hostname'] = gethostname();
// Check sendmail path
$sendmail = ini_get('sendmail_path');
$result['sendmail_path'] = $sendmail ?: 'not set';
// Check if we can detect SMTP settings
$result['smtp_host'] = ini_get('SMTP') ?: 'not set';
$result['smtp_port'] = ini_get('smtp_port') ?: 'not set';
$to = $_POST['to'] ?? '';
$sent = false;
$sendError = '';
if ($to && filter_var($to, FILTER_VALIDATE_EMAIL) && isset($_POST['send'])) {
$subject = 'TomTomGames Email Test';
$message = "This is a test email from TomTomGames.\n\nIf you received this, PHP mail() is working correctly on this server.";
$headers = "From: noreply@tomtomgames.com\r\n";
$headers .= "Reply-To: support@tomtomgames.com\r\n";
$headers .= "X-Mailer: PHP/" . PHP_VERSION;
// Capture any mail errors
set_error_handler(function($errno, $errstr) use (&$sendError) {
$sendError = $errstr;
});
$sent = mail($to, $subject, $message, $headers, '-fnoreply@tomtomgames.com');
restore_error_handler();
$result['mail_return'] = $sent;
$result['mail_error'] = $sendError ?: 'none';
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Email Test</title>
<style>
body{font-family:monospace;background:#0a0a12;color:#e8e8f0;padding:24px;max-width:600px;margin:0 auto}
h2{color:#f0c040}
.box{background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:16px;margin:12px 0}
.ok{color:#00e676}.err{color:#ff4444}.warn{color:#f0c040}
input{background:#111;border:1px solid #444;color:#fff;padding:8px 12px;width:280px;border-radius:6px;font-size:14px}
button{background:#f0c040;color:#000;border:none;padding:10px 20px;border-radius:6px;font-weight:700;cursor:pointer;margin-left:8px}
label{display:block;margin-bottom:6px;color:#aaa;font-size:13px}
</style>
</head>
<body>
<h2>TomTomGames — Email Diagnostics</h2>
<div class="box">
<b>Server Info:</b><br>
PHP: <span class="ok"><?= $result['php_version'] ?></span><br>
Server: <?= htmlspecialchars($result['server_software']) ?><br>
Hostname: <?= htmlspecialchars($result['hostname']) ?><br>
mail() function: <span class="<?= $result['mail_function_exists'] ? 'ok' : 'err' ?>"><?= $result['mail_function_exists'] ? 'EXISTS' : 'MISSING' ?></span><br>
sendmail_path: <span class="warn"><?= htmlspecialchars($result['sendmail_path']) ?></span><br>
SMTP: <span class="warn"><?= htmlspecialchars($result['smtp_host']) ?>:<?= htmlspecialchars($result['smtp_port']) ?></span>
</div>
<?php if ($to): ?>
<div class="box">
<b>Send Result:</b><br>
To: <?= htmlspecialchars($to) ?><br>
mail() returned: <span class="<?= $sent ? 'ok' : 'err' ?>"><?= $sent ? 'TRUE (queued for delivery)' : 'FALSE (failed)' ?></span><br>
Error: <span class="warn"><?= htmlspecialchars($result['mail_error']) ?></span><br>
<?php if ($sent): ?>
<br><span class="ok">Check your inbox (and spam folder). If nothing arrives in 5 minutes, the server is not sending outbound mail.</span>
<?php else: ?>
<br><span class="err">mail() returned false — server cannot send email. You need to configure SMTP (PHPMailer) or enable sendmail.</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="box">
<form method="POST">
<label>Send test email to:</label>
<input type="email" name="to" value="<?= htmlspecialchars($to) ?>" placeholder="your@email.com" required>
<button type="submit" name="send" value="1">Send Test</button>
</form>
</div>
<p style="color:#555;font-size:11px">Delete test_mail.php after diagnosis is complete.</p>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<?php
require_once __DIR__ . '/../includes/auth.php';
$token = $_GET['token'] ?? '';
$result = verifyEmailToken($token);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= SITE_NAME ?> — Email Verified</title>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;700;900&family=Rajdhani:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0a0a12;color:#e8e8f0;font-family:'Rajdhani',sans-serif;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:24px}
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,229,255,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,229,255,0.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none}
.card{background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:40px 32px;max-width:420px;width:100%;text-align:center;position:relative;z-index:1}
.icon{font-size:64px;margin-bottom:20px;display:block}
.title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:26px;margin-bottom:10px}
.title.success{background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.title.error{color:#ff4444}
.msg{font-size:15px;color:#aaaacc;line-height:1.7;margin-bottom:28px}
.msg strong{color:#e8e8f0}
.btn{display:block;width:100%;padding:15px;border:none;border-radius:10px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;letter-spacing:1px;cursor:pointer;text-decoration:none;text-align:center;transition:all .2s}
.btn-gold{background:linear-gradient(135deg,#f0c040,#d4a017);color:#000;box-shadow:0 4px 20px rgba(240,192,64,.4)}
.btn-outline{background:transparent;border:1.5px solid rgba(255,255,255,.2);color:#aaaacc;margin-top:10px}
.logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:20px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:28px;letter-spacing:1px}
.countdown{font-size:13px;color:#8888aa;margin-top:16px}
</style>
<?php if ($result['success']): ?>
<script>
// Auto-redirect to app after 4 seconds
setTimeout(() => { window.location.href = '/'; }, 4000);
</script>
<?php endif; ?>
</head>
<body>
<div class="card">
<div class="logo" style="display:flex;align-items:center;gap:10px;justify-content:center"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="36" height="36" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="ll1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="ll2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient><filter id="gll"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#ll1)" filter="url(#gll)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#ll2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#ll1)" opacity=".8"/><circle cx="24" cy="7" r="2" fill="#f0c040" opacity=".9"/><circle cx="39" cy="10" r="1.2" fill="#00e5ff" opacity=".8"/><circle cx="9" cy="10" r="1.2" fill="#f0c040" opacity=".7"/></svg><span><?= SITE_NAME ?></span></div>
<?php if ($result['success']): ?>
<span class="icon">🎉</span>
<div class="title success">Account Verified!</div>
<p class="msg">
Welcome to <?= SITE_NAME ?>, <strong><?= htmlspecialchars($result['alias']) ?></strong>!<br><br>
Your account is active and you've been automatically logged in. Let's play!
</p>
<a href="/" class="btn btn-gold">🎮 ENTER TOMTOMGAMES</a>
<p class="countdown" id="countdown">Redirecting in 4 seconds...</p>
<script>
let s = 4;
const el = document.getElementById('countdown');
setInterval(() => { s--; if(s>0) el.textContent = `Redirecting in ${s} second${s!==1?'s':''}...`; else el.textContent = 'Redirecting...'; }, 1000);
</script>
<?php else: ?>
<span class="icon">❌</span>
<div class="title error">Verification Failed</div>
<p class="msg"><?= htmlspecialchars($result['error']) ?></p>
<a href="/" class="btn btn-gold">REGISTER AGAIN</a>
<a href="/" class="btn btn-outline">BACK TO HOME</a>
<?php endif; ?>
</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
Host not in allowlist
+1
View File
@@ -0,0 +1 @@
Host not in allowlist
+1
View File
@@ -0,0 +1 @@
Host not in allowlist