From c70027f8fcb91b5d541300b06ce0af0b049efed9 Mon Sep 17 00:00:00 2001 From: TomTomGames Date: Sun, 10 May 2026 14:45:49 -0500 Subject: [PATCH] v1.0.0 - Initial release: registration, SendGrid email, Square payments, cashout, admin panel --- .gitignore | 5 + README.md | 175 + VERSION | 1 + build.sh | 53 + composer.json | 8 + includes/auth.php | 293 ++ includes/db.php | 541 +++ includes/mailer.php | 92 + includes/smtp.php | 183 + includes/square.php | 115 + mail_queue/process_queue.sh | 43 + public_html/.htaccess | 59 + public_html/admin/index.php | 3113 +++++++++++++++++ public_html/admin/login.php | 54 + public_html/api/admin.php | 966 ++++++ public_html/api/billing.php | 91 + public_html/api/broadcast.php | 89 + public_html/api/cashout.php | 148 + public_html/api/cashout_method_types.php | 73 + public_html/api/chat.php | 124 + public_html/api/game_aliases.php | 65 + public_html/api/login.php | 37 + public_html/api/logout.php | 5 + public_html/api/me.php | 32 + public_html/api/my_activity.php | 76 + public_html/api/my_purchases.php | 17 + public_html/api/payment_settings.php | 44 + public_html/api/payout_methods.php | 75 + public_html/api/payout_process.php | 153 + public_html/api/platform_accounts.php | 92 + public_html/api/platforms.php | 93 + public_html/api/purchase.php | 163 + public_html/api/referrals.php | 248 ++ public_html/api/register.php | 18 + public_html/api/resend_verify.php | 16 + public_html/assets/img/egame99.svg | 31 + public_html/assets/img/firekirin.svg | 30 + public_html/assets/img/icon-192.png | Bin 0 -> 1088 bytes public_html/assets/img/icon-512.png | Bin 0 -> 3856 bytes public_html/assets/img/logo-icon.svg | 38 + public_html/assets/img/milkyway.svg | 35 + public_html/assets/img/noble777.svg | 31 + public_html/assets/img/og-image.svg | 46 + public_html/assets/img/pandamaster.svg | 27 + public_html/assets/img/ultrapanda.svg | 30 + public_html/assets/img/vblink777.svg | 27 + public_html/bump_version.php | 52 + public_html/games/index.php | 232 ++ public_html/get_location.php | 108 + public_html/index.php | 3238 ++++++++++++++++++ public_html/install.php | 206 ++ public_html/manifest.json | 43 + public_html/robots.txt | 23 + public_html/sitemap.xml | 21 + public_html/test.php | 6 + public_html/test_login.php | 18 + public_html/test_mail.php | 91 + public_html/verify.php | 66 + vendor/phpmailer/phpmailer/src/Exception.php | 1 + vendor/phpmailer/phpmailer/src/PHPMailer.php | 1 + vendor/phpmailer/phpmailer/src/SMTP.php | 1 + 61 files changed, 11762 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 VERSION create mode 100644 build.sh create mode 100644 composer.json create mode 100644 includes/auth.php create mode 100644 includes/db.php create mode 100644 includes/mailer.php create mode 100644 includes/smtp.php create mode 100644 includes/square.php create mode 100644 mail_queue/process_queue.sh create mode 100644 public_html/.htaccess create mode 100644 public_html/admin/index.php create mode 100644 public_html/admin/login.php create mode 100644 public_html/api/admin.php create mode 100644 public_html/api/billing.php create mode 100644 public_html/api/broadcast.php create mode 100644 public_html/api/cashout.php create mode 100644 public_html/api/cashout_method_types.php create mode 100644 public_html/api/chat.php create mode 100644 public_html/api/game_aliases.php create mode 100644 public_html/api/login.php create mode 100644 public_html/api/logout.php create mode 100644 public_html/api/me.php create mode 100644 public_html/api/my_activity.php create mode 100644 public_html/api/my_purchases.php create mode 100644 public_html/api/payment_settings.php create mode 100644 public_html/api/payout_methods.php create mode 100644 public_html/api/payout_process.php create mode 100644 public_html/api/platform_accounts.php create mode 100644 public_html/api/platforms.php create mode 100644 public_html/api/purchase.php create mode 100644 public_html/api/referrals.php create mode 100644 public_html/api/register.php create mode 100644 public_html/api/resend_verify.php create mode 100644 public_html/assets/img/egame99.svg create mode 100644 public_html/assets/img/firekirin.svg create mode 100644 public_html/assets/img/icon-192.png create mode 100644 public_html/assets/img/icon-512.png create mode 100644 public_html/assets/img/logo-icon.svg create mode 100644 public_html/assets/img/milkyway.svg create mode 100644 public_html/assets/img/noble777.svg create mode 100644 public_html/assets/img/og-image.svg create mode 100644 public_html/assets/img/pandamaster.svg create mode 100644 public_html/assets/img/ultrapanda.svg create mode 100644 public_html/assets/img/vblink777.svg create mode 100644 public_html/bump_version.php create mode 100644 public_html/games/index.php create mode 100644 public_html/get_location.php create mode 100644 public_html/index.php create mode 100644 public_html/install.php create mode 100644 public_html/manifest.json create mode 100644 public_html/robots.txt create mode 100644 public_html/sitemap.xml create mode 100644 public_html/test.php create mode 100644 public_html/test_login.php create mode 100644 public_html/test_mail.php create mode 100644 public_html/verify.php create mode 100644 vendor/phpmailer/phpmailer/src/Exception.php create mode 100644 vendor/phpmailer/phpmailer/src/PHPMailer.php create mode 100644 vendor/phpmailer/phpmailer/src/SMTP.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52e3ca9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +includes/config.php +public_html/create_admin.php +*.log +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..583dca6 --- /dev/null +++ b/README.md @@ -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/` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..7dea76e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.1 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..723fb94 --- /dev/null +++ b/build.sh @@ -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¬es=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MSG'))")" +echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b2e7638 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "phpmailer/phpmailer": "^6.9" + }, + "config": { + "vendor-dir": "vendor" + } +} diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..19466aa --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,293 @@ +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 3โ€“50 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'; } +} diff --git a/includes/db.php b/includes/db.php new file mode 100644 index 0000000..732e4e1 --- /dev/null +++ b/includes/db.php @@ -0,0 +1,541 @@ +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 */ } diff --git a/includes/mailer.php b/includes/mailer.php new file mode 100644 index 0000000..0f81e6f --- /dev/null +++ b/includes/mailer.php @@ -0,0 +1,92 @@ +' + . '' + . '' + . '
' + . '' + . '' + . '
' + . '🎮 ' . htmlspecialchars($siteName) . '' + . '
' + . '

Verify your account

' + . '

Hey ' + . htmlspecialchars($toName) . ',

' + . '

Thanks for signing up! Click below to verify your email and activate your account.

' + . '
' + . 'VERIFY MY ACCOUNT' + . '
' + . '

Or paste this into your browser:

' + . '

' . htmlspecialchars($verifyUrl) . '

' + . '

' + . 'Link expires in 24 hours. Did not sign up? You can safely ignore this email.

' + . '
' + . '© ' . htmlspecialchars($siteName) + . ' · ' + . htmlspecialchars($siteUrl) . '' + . '
'; + + 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; +} diff --git a/includes/smtp.php b/includes/smtp.php new file mode 100644 index 0000000..85ad2b2 --- /dev/null +++ b/includes/smtp.php @@ -0,0 +1,183 @@ +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 + ); +} diff --git a/includes/square.php b/includes/square.php new file mode 100644 index 0000000..a6011f5 --- /dev/null +++ b/includes/square.php @@ -0,0 +1,115 @@ +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; + } +} diff --git a/mail_queue/process_queue.sh b/mail_queue/process_queue.sh new file mode 100644 index 0000000..a2f6675 --- /dev/null +++ b/mail_queue/process_queue.sh @@ -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 diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100644 index 0000000..5c85de7 --- /dev/null +++ b/public_html/.htaccess @@ -0,0 +1,59 @@ +Options -Indexes +ServerSignature Off + +# โ”€โ”€ Block sensitive files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + Order allow,deny + Deny from all + + +# โ”€โ”€ Block direct access to includes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + RewriteEngine On + RewriteRule ^includes/ - [F,L] + + +# โ”€โ”€ Security headers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + 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=()" + + +# โ”€โ”€ Canonical HTTPS redirect โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + 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] + + +# โ”€โ”€ Gzip compression โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml + + +# โ”€โ”€ Browser caching โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + 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" + + +# โ”€โ”€ LiteSpeed cache rules โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + CacheEnable public /assets/ + CacheEnable public /manifest.json + CacheEnable public /sitemap.xml + CacheEnable public /robots.txt + diff --git a/public_html/admin/index.php b/public_html/admin/index.php new file mode 100644 index 0000000..1597c6a --- /dev/null +++ b/public_html/admin/index.php @@ -0,0 +1,3113 @@ + + + + + + + + +TomTomGames Admin + + + + + +
+ + + +
+ + +
+
๐Ÿ“Š Dashboard
+ + +
+
Total Users
โ€”
+
Total Revenue
โ€”
+
Tokens Sold
โ€”
+
Pending Purchases
โ€”
+
Pending Cashouts
โ€”
+
+
+
โšก Pending Purchase Approvals
+
+
+
+
โšก Pending Cashout Requests
+
+
+
+ + +
+
๐Ÿงพ Token Purchases
+
+ + + + +
+
+
+ + +
+
๐Ÿ’ธ Cashout Requests
+
+ + + + +
+
+
+ + + +
+ +
+
๐ŸŽฎ Gamer Management
+
+
+ + +
+
+ + + + +
+
+
+
+
+ + + +
+ + +
+
๐Ÿ’ณ Payment Settings
+
+ Enable or disable each payment method. Disabled methods are hidden from players instantly. Card payments run through Square โ€” your Square account must be active for card to work. +
+
+
+ + + +
+
๐ŸŽ Referral Management
+
+ + + + + +
+
+ + +
+ +
+
๐Ÿ”‘ Platform Account Requests
+
+ + + +
+
+
+ + +
+
๐Ÿ“ข Broadcast Messages
+ + +
+
โœ‰๏ธ Send Broadcast
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + +
+
+
Sent Broadcasts
+ +
+
+
+ + + +
+ + +
+
๐Ÿ’ฐ Payout Settings
+
+ Configure how you send cashout payments to players. Square Gift Card sends instantly. Manual methods show the player handle so you send from the app and mark done. +
+
+
+ + +
+
๐Ÿ’ธ Cashout Methods
+
+ ๐Ÿ’ธ Manage the payout method types available to players when they cash out. Active methods appear in the player's payout method dropdown. +
+ + +
+
โž• Add Cashout Method
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
Active & Inactive Methods
+
Loading...
+
+
+
+
+ + +
+
๐Ÿ•น๏ธ Game Management
+ + +
+
โž• Add New Game
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
Active & Inactive Games
+
Loading...
+
+
+
+
+ + +
+
๐Ÿ“‹ Full Audit Log 90-day rolling ยท 20 per page
+ + +
+ + + + + + + +
+ + +
+ + +
+ + +
+
+ + +
+
โณ Pending Signups
+
+ โณ Players who registered but haven't verified their email yet. Approve to create their account immediately, or Delete to remove the request. +
+
+
+ + +
+
Live Chat + + โ— Auto-refreshing every 5s + +
+ + +
+ + +
+
โœ‰๏ธ Send Message to Player
+ +
+ + +
+ + + + +
+
Ctrl+Enter to send ยท Player will see it in their Support chat
+ +
+
+
+ + +
+ ๐Ÿ“จ Messages from players appear below. Click any conversation to open it and reply. + +
+
+
+
+
+ + + +
+ + + + +
+ + +
+ TomTomGames Admin v1.0.0 +
+ + diff --git a/public_html/admin/login.php b/public_html/admin/login.php new file mode 100644 index 0000000..55faefa --- /dev/null +++ b/public_html/admin/login.php @@ -0,0 +1,54 @@ + + + + + + + +TomTomGames Admin Login + + + + +
+ +
ADMIN ACCESS
+
+
+
+
+ +
+
+ + diff --git a/public_html/api/admin.php b/public_html/api/admin.php new file mode 100644 index 0000000..65d8294 --- /dev/null +++ b/public_html/api/admin.php @@ -0,0 +1,966 @@ + 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']); +} diff --git a/public_html/api/billing.php b/public_html/api/billing.php new file mode 100644 index 0000000..5e6a8e2 --- /dev/null +++ b/public_html/api/billing.php @@ -0,0 +1,91 @@ +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']); +} diff --git a/public_html/api/broadcast.php b/public_html/api/broadcast.php new file mode 100644 index 0000000..09654e4 --- /dev/null +++ b/public_html/api/broadcast.php @@ -0,0 +1,89 @@ +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']); +} diff --git a/public_html/api/cashout.php b/public_html/api/cashout.php new file mode 100644 index 0000000..6cc2bd3 --- /dev/null +++ b/public_html/api/cashout.php @@ -0,0 +1,148 @@ +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]); diff --git a/public_html/api/cashout_method_types.php b/public_html/api/cashout_method_types.php new file mode 100644 index 0000000..b0cc1b4 --- /dev/null +++ b/public_html/api/cashout_method_types.php @@ -0,0 +1,73 @@ +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']); +} diff --git a/public_html/api/chat.php b/public_html/api/chat.php new file mode 100644 index 0000000..daf0bad --- /dev/null +++ b/public_html/api/chat.php @@ -0,0 +1,124 @@ +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']); +} diff --git a/public_html/api/game_aliases.php b/public_html/api/game_aliases.php new file mode 100644 index 0000000..3832cc4 --- /dev/null +++ b/public_html/api/game_aliases.php @@ -0,0 +1,65 @@ +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']); +} diff --git a/public_html/api/login.php b/public_html/api/login.php new file mode 100644 index 0000000..e263a49 --- /dev/null +++ b/public_html/api/login.php @@ -0,0 +1,37 @@ +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); diff --git a/public_html/api/logout.php b/public_html/api/logout.php new file mode 100644 index 0000000..2afa501 --- /dev/null +++ b/public_html/api/logout.php @@ -0,0 +1,5 @@ + true]); diff --git a/public_html/api/me.php b/public_html/api/me.php new file mode 100644 index 0000000..3a23025 --- /dev/null +++ b/public_html/api/me.php @@ -0,0 +1,32 @@ + 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]); diff --git a/public_html/api/my_activity.php b/public_html/api/my_activity.php new file mode 100644 index 0000000..b6268d6 --- /dev/null +++ b/public_html/api/my_activity.php @@ -0,0 +1,76 @@ +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']); +} diff --git a/public_html/api/my_purchases.php b/public_html/api/my_purchases.php new file mode 100644 index 0000000..b340f8f --- /dev/null +++ b/public_html/api/my_purchases.php @@ -0,0 +1,17 @@ +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()]); diff --git a/public_html/api/payment_settings.php b/public_html/api/payment_settings.php new file mode 100644 index 0000000..d4f5267 --- /dev/null +++ b/public_html/api/payment_settings.php @@ -0,0 +1,44 @@ +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']); +} diff --git a/public_html/api/payout_methods.php b/public_html/api/payout_methods.php new file mode 100644 index 0000000..95249f7 --- /dev/null +++ b/public_html/api/payout_methods.php @@ -0,0 +1,75 @@ +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']); +} diff --git a/public_html/api/payout_process.php b/public_html/api/payout_process.php new file mode 100644 index 0000000..53993fb --- /dev/null +++ b/public_html/api/payout_process.php @@ -0,0 +1,153 @@ +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']); diff --git a/public_html/api/platform_accounts.php b/public_html/api/platform_accounts.php new file mode 100644 index 0000000..1df2991 --- /dev/null +++ b/public_html/api/platform_accounts.php @@ -0,0 +1,92 @@ +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']); diff --git a/public_html/api/platforms.php b/public_html/api/platforms.php new file mode 100644 index 0000000..76a8a5d --- /dev/null +++ b/public_html/api/platforms.php @@ -0,0 +1,93 @@ +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']); +} diff --git a/public_html/api/purchase.php b/public_html/api/purchase.php new file mode 100644 index 0000000..8ae4b39 --- /dev/null +++ b/public_html/api/purchase.php @@ -0,0 +1,163 @@ +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, +]); diff --git a/public_html/api/referrals.php b/public_html/api/referrals.php new file mode 100644 index 0000000..682952a --- /dev/null +++ b/public_html/api/referrals.php @@ -0,0 +1,248 @@ +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']); diff --git a/public_html/api/register.php b/public_html/api/register.php new file mode 100644 index 0000000..6d2bce9 --- /dev/null +++ b/public_html/api/register.php @@ -0,0 +1,18 @@ +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); diff --git a/public_html/api/resend_verify.php b/public_html/api/resend_verify.php new file mode 100644 index 0000000..7d1c539 --- /dev/null +++ b/public_html/api/resend_verify.php @@ -0,0 +1,16 @@ +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)); diff --git a/public_html/assets/img/egame99.svg b/public_html/assets/img/egame99.svg new file mode 100644 index 0000000..acbed0c --- /dev/null +++ b/public_html/assets/img/egame99.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + ๐ŸŽฎ + + eGAME 99 + + + + + + diff --git a/public_html/assets/img/firekirin.svg b/public_html/assets/img/firekirin.svg new file mode 100644 index 0000000..768a024 --- /dev/null +++ b/public_html/assets/img/firekirin.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + ๐Ÿ”ฅ + + KIRIN + + + + + + diff --git a/public_html/assets/img/icon-192.png b/public_html/assets/img/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3f325e8f905bb8e77e4f05382cad10eb9d6647 GIT binary patch literal 1088 zcmZuxdr(wW7(aJm;R?%2im;0=-hc$8flVPy20XAAWI`)* zau*F9iIK%6KzY=fmCO`HGqJ;rBO42LdCnNI=q?Z;+r;fsN17U^>5p^H@B7a0@%?e; zoJ~nO=;Gw*1ORZ6$B_zje)D+YJhV@J&87fg?#C7@asb0bNNWH=TTNc39FN z`^A%|dhZ#HY)o)tr^lCAL7z}pfA{WAvYq{;3TqR>aBh8dWZgw$ah^~3e$pDOH}Qvy zg6O^@Vw#^Ac4!nZ+M@5$?CB{|DZh;|d?Fx_U}vR6PKL<6bqbz-saDrTS!DzMs9jp9 ze_OMeuDF@Uc*yMxkw<2&7PW)ws$YVw&6}t1Z3tvDssS@-XsQT5K(G#s#o17~7Jti5 z)nEFRC#Ik2%U{Kqa0|`l zKT6*NZRN&yUk6NZ%u+lsRQikug%E6Up00(mmS~p;Fny-_ygN>F?nOaBlZHcEAl)FQ zIZl^rr>c74juZ6F2CAyPcNP@TcJGH;Kq@>iegB2ATRUj(^k@5l#uN>0Ul>EN8eJpM z_t*9u-$StF^J|DW0+`I`9t5@mW(H0p0K5Y>(VQh98hCeJpsX{Ag*;ce#6k7vtF`V_ z9Lg#NG^Y&6K|EyaO0X{}X8)F!ed+(SOWhNV7^+;Bi0 zZivSo7{o%(~Su_E0C-t;xHoM&^Ocxmpz(H@B^(zbUO&LYP`zdw}&AnF=8pVM+1^= zc0>kq7%{#H`nK@~Qp_jb1T~*uvBPB%+p966Y~m0UJsfMPwWFk__QEZo)uWrI)(R!R z0%C3YC=^|vN)_aStd4v?Ur@7NEFlD=mK~>7C}~>fhyx(2R=_~YIXAGI5M(F6T*V^< zKPO)Gg54kRaGAJC&xSzJ&}f%27bQ3D3W^lFp1B`ZviLhCb-VpQ8H}m5m%Bw}+Xz9- z8^$4LEXzO1vMa+Gmpyf~Z|NN<+TCULMk8D|??<@PtU$QkoR9Fwg8+}sp5KRMb)50? zBLv%qUvsd(W-4k$(_}=Xf^YqOnhPlD#iZnYP#RDA1regQmaP($ZaFK&V>J(Sn5j|2 z&9#A5jjF$(5S|t@z2kVwbFsFB=lK_pHE?g0#ek`sO?mg{GD2ZJqfLuhNzaD$)j6rh nK!oF^fK`4-x>9@!J%M%bV`iwjqv&`x`Wb*cCW-8ce&ys}+1|4@ literal 0 HcmV?d00001 diff --git a/public_html/assets/img/icon-512.png b/public_html/assets/img/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..bf41cc8ffc16c9baee4038b4283a83349f4c8c38 GIT binary patch literal 3856 zcmbVPdsI_b_TD#yw}B80K@g2Rg@{m*ilRK^B?Y8f1(Zi(9r0BS)hcv|$@NtN)TLDs zEg;NTYkd(dMG!P(+QiYRTKiCseXNahQQ6w4FCk0^HLWA@Nmcj7w)IhFXc3VM{j0o@}i2y0dw)*@g3qF3+6mK zlK;ce%FL2&*IKsuZLpu{KCuV>mD=byrn~I1`_H<+Kaj)NUuAX)2q(d!VVlMtFyb#w zg97u&PnAxzg(wMvt5fy1JBC(1S#5l_RQyRG7>!jG6KG{}7@^q1Ty71qUtA@HyOR@+ zRBAODn?s-|Twf=knEw93MdqYpCePqa`TyOEF8@4&Pufh9 zUA;!R$3AaJ@R=CZD%LE>o(D{h>R4wXyXzHC!dk^dyi&s6^a_IkDke3Lbrc1|QL6ik zw{k8K=)n`;VYc!nJsUyKW^L$ry>&wVGpk)6i(JO&$%G(aVe3)FV)Sn zRH5pC*=dj`N>s)Ls>8xqi%tE0JuQ16Ar7XoQfKFzsZ{+|^f6}LIWO{SYKBuxt$dZ3@C{QhJyXN99}tqsl*DmuwdE}bqO+LYqbR0_ z?=`?(nL6=I7b8|?!=Nb9-<-+*wIdihv~u@HVd~ytFS6FOoB!0Ohi-B))KMLTfU4IE zh|Y92xFv_}pCJW_R?d5;#$xa1TTd~9q&9{(B?n3*nqLjMbZo(XxG>pHZS9~M&IA$_ zY3ykSo2h=;Ox{o<bM}1D zduje=7)J{Z9RgK|ee>4GRMxaef?34gT7G(s^7Gw4ls?%G9z!kGZU__DkiiVCD}opG>b^4-l-x=ziSwMb`c5Q^2<#Q!jUeJ*V;Y; z_>-}G!&hMJkr}+`_PdclN4b?1sbZett_yjyW%@=Q?D3Lc$Adaj1G;M4Pj`?xk&K~a zU2A}ndsr_p2izbjU6s}Ap2Ri~MIri6WR6mMJG6=|H)GLlK9p;9C%H(yo3JW9LcEE__1;J#Y>63t%B%IwF9; z6S##Z#HX*}&po143UIU^Lz#$&3&xU+QV{J=z~seVB%>P>9frg9cY*4OGF;_D%Xe6r zkx$`qUo_B`u?jH{USD$(jG7#>-35}4=O9f0kr#}N2RxHl3BRBoVE@uM1bZkpao{Tm zjwR=CU=xBb!sb8@j0vmfnRjzwDS}-T`5YJzpC*93=!)P>)LIOFoZrvrmj-|4hSfNX zggZD4N1d^0sFRn(Nfvt~u@=u8*#NI`-UUF7Yyi~12-!6;kcgANdrU_&5|l5@$!Law zjdSV4fD+!&I3x=}h3m&4p_8E(IrgKP^Wg!3pq6fTN=vv+%^qJq_bV9k`$p4BAb^ zVIQ{@axm?(hS}4RcBuV>?Nb1}Z_nh6Lw{R~Mw>a2Gww6tC?)(eF7x+m9+geuCf``I z!x5yXUcs@j$XbR}s}3~D3DB>F5nve?wSn7fsJ=}JOhbFM1C-WAdD)lvO#qB~xbk-vZY4@@10S;>>g zzut*iG;~qJ=T+!Xus}5vH3jJ#8&E3QZk)04B<>emprz}5(cz)QlF5m&{cw(BE2Ved zosHqIhU<5UxrtZl#Z%lc-eaw8Suye*Ojf!6&bQ+zE=y(ltv9*f5WPBVKuA_O!55}- z+l3t75c;+-RPme5fT{Ei)Njha9RZ#KSkK+jcwHm3quSQj{^}S*iUa9ytRL9GgR*(5 z+fzGM!UR7kiqNkvH^0icOclx78Xk@3T#lK_A0;$6TmM2BT)D2nPE@Rdp}oxJd#3|A z=;5*Q)5KCPy7dfGvJ0;0lPBj-;P(=OX4Bdsm;i#7$O&>9ZcxIa!Y} z+Z{@UmcQ=r=Vtp&`IA4HgPi1>^lw*0ZNd}{)|^H2?_rr5K$t~KSan9CbF&QvZLoKBYsF3MhWg@cIB%eL>hDzWQg3pHLgz2?WhHv;*9_>%gr~Bkh zP!yzgy%DN@;x8m+rd*z%F=~rHuk0+d&TyZaq4y>_)7W{~WOd))4R=G-k!SWXY069( z6es!@6tgumLZPG9;(1D@?y2;~2D*y>yn)89dckN{{WW#W@p-H5qP6&54Ocg;<4Z%a z6W`6u*(@T@FyANVu}%%HhOtzJ%L9|mgX`!0M*I2O#;-(!K0VXaUgS7a?$|+L&lI~9 z9@jL>ibB*ms(IWyp?LJ2a7&oDydsZX!yJH9!Rl|yQrU%Z^Wc_uqLRsGXI0d|sR(ud zP#zmtu@yg1>Zi{iCGpx^{3Av+@8c^b;+rLdZQtlhm~&Kq=0bMo#z}-ZPZf?$|D27^ zgtouP8Hl}ywY978YwwtB)y4&wT*%2mhaFRXXb{#~|Q>FKcMoOj&;Y}(i zemB$6vJav^V1x~oCtRSd*4BP{Wj@dFIW@fg$0Z-a>(^A@a_!G=p6MSh+rC$np~06f NWTwqeZJn#w_8(M0$IJi# literal 0 HcmV?d00001 diff --git a/public_html/assets/img/logo-icon.svg b/public_html/assets/img/logo-icon.svg new file mode 100644 index 0000000..cd251a4 --- /dev/null +++ b/public_html/assets/img/logo-icon.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public_html/assets/img/milkyway.svg b/public_html/assets/img/milkyway.svg new file mode 100644 index 0000000..b89a5fd --- /dev/null +++ b/public_html/assets/img/milkyway.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ๐ŸŒŒ + MILKY WAY + + + diff --git a/public_html/assets/img/noble777.svg b/public_html/assets/img/noble777.svg new file mode 100644 index 0000000..16a61e5 --- /dev/null +++ b/public_html/assets/img/noble777.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + ๐Ÿ‘‘ + + 777 + NOBLE + + + + + + diff --git a/public_html/assets/img/og-image.svg b/public_html/assets/img/og-image.svg new file mode 100644 index 0000000..62fee62 --- /dev/null +++ b/public_html/assets/img/og-image.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TomTomGames + + Buy tokens for VBlink777 ยท Fire Kirin ยท Milky Way ยท Ultra Panda & more + + + โšก Instant Delivery + + ๐Ÿ”’ SSL Secured + + ๐Ÿ’ฌ 24/7 Support + diff --git a/public_html/assets/img/pandamaster.svg b/public_html/assets/img/pandamaster.svg new file mode 100644 index 0000000..b7ec28a --- /dev/null +++ b/public_html/assets/img/pandamaster.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + ๐Ÿพ + + MASTER + + diff --git a/public_html/assets/img/ultrapanda.svg b/public_html/assets/img/ultrapanda.svg new file mode 100644 index 0000000..f8e384f --- /dev/null +++ b/public_html/assets/img/ultrapanda.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + ๐Ÿผ + + ULTRA + + โ˜… + โ˜… + diff --git a/public_html/assets/img/vblink777.svg b/public_html/assets/img/vblink777.svg new file mode 100644 index 0000000..47e8e61 --- /dev/null +++ b/public_html/assets/img/vblink777.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + ๐ŸŽฐ + + 777 + + + + + + diff --git a/public_html/bump_version.php b/public_html/bump_version.php new file mode 100644 index 0000000..a5ab587 --- /dev/null +++ b/public_html/bump_version.php @@ -0,0 +1,52 @@ + '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'), +]); diff --git a/public_html/games/index.php b/public_html/games/index.php new file mode 100644 index 0000000..87004dd --- /dev/null +++ b/public_html/games/index.php @@ -0,0 +1,232 @@ + + + + + + +Buy Fish Table Game Tokens | VBlink777, Fire Kirin, Milky Way | TomTomGames + + + + + + + + + + + + + + + + +
+ +
+ +
+
+

Buy Game Tokens for Fish Table & Skill Games

+

The fastest, most trusted way to load up on tokens for VBlink777, Fire Kirin, Milky Way, Ultra Panda, Panda Master, Noble777, and eGame99.

+ ๐Ÿช™ Buy Tokens Now +
+ ๐Ÿ”’ SSL Secured + โšก Instant Delivery + ๐Ÿ’ณ Multiple Payment Methods + ๐Ÿ’ฌ 24/7 Support + ๐ŸŽฎ 7 Game Platforms +
+
+
+ +
+
+

Supported Game Platforms

+

Buy tokens for all major fish table and skill game platforms in one place.

+
+
+

VBlink777

+

One of the most popular fish table games. Buy VBlink777 tokens securely and get them credited fast to your game account.

+ Buy VBlink777 Tokens โ†’ +
+
+

Fire Kirin

+

Fire Kirin is a top-rated skill game with exciting fish table gameplay. Purchase Fire Kirin tokens through TomTomGames for instant delivery.

+ Buy Fire Kirin Tokens โ†’ +
+
+

Milky Way

+

Milky Way 777 game credits โ€” buy tokens for one of the best online fish table platforms. Secure payment, fast top-up.

+ Buy Milky Way Tokens โ†’ +
+
+

Ultra Panda

+

Ultra Panda game tokens for sale. Top up your account instantly through our secure portal and start playing right away.

+ Buy Ultra Panda Tokens โ†’ +
+
+

Panda Master

+

Panda Master tokens โ€” buy game credits quickly and safely. Multiple payment methods accepted including card, Venmo, and Cash App.

+ Buy Panda Master Tokens โ†’ +
+
+

Noble 777

+

Noble 777 game token purchases made easy. Register, select your package, and have tokens in your account within minutes.

+ Buy Noble 777 Tokens โ†’ +
+
+

eGame99

+

eGame99 tokens for sale at the best rates. Fast crediting, 24/7 customer support, and a seamless buying experience.

+ Buy eGame99 Tokens โ†’ +
+
+
+
+ +
+
+

How to Buy Tokens in 3 Steps

+

Get tokens credited to your game account in minutes.

+
+
+
1
+

Create Account

+

Register free in under 60 seconds. Verify your email and log in.

+
+
+
2
+

Select Game & Package

+

Choose your game platform, enter your in-game alias, and pick a token package โ€” or enter a custom amount.

+
+
+
3
+

Pay & Play

+

Pay securely by card or manual transfer. Tokens are credited to your game account fast.

+
+
+
+

Accepted Payment Methods

+
+
๐Ÿ’ณ Credit / Debit Card
+
๐Ÿ’™ Venmo
+
๐Ÿ’š Cash App
+
๐ŸŸข Chime
+
๐Ÿ’œ Zelle
+
+
+
+
+ +
+
+

Frequently Asked Questions

+

Everything you need to know about buying game tokens.

+
+
+

What is TomTomGames?

+

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.

+
+
+

How quickly are tokens delivered?

+

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.

+
+
+

How much do tokens cost?

+

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.

+
+
+

Is my payment information secure?

+

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.

+
+
+

What if I need help?

+

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.

+
+
+

Can I cash out my tokens?

+

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.

+
+
+ +
+
+ +
+
+

TomTomGames โ€” Your trusted token portal for fish table and skill games.

+

Home ยท Games ยท Support

+

ยฉ TomTomGames. All rights reserved. Game tokens are for entertainment purposes on supported platforms only. Please play responsibly.

+
+
+ + + diff --git a/public_html/get_location.php b/public_html/get_location.php new file mode 100644 index 0000000..cac2f09 --- /dev/null +++ b/public_html/get_location.php @@ -0,0 +1,108 @@ + 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); +} +?> + + + + + +Square Location ID Finder + + + +

๐Ÿ”‘ Square Location Finder

+
TomGames โ€” Run once, then delete this file
+ + +
Error fetching locations:
+ +
No locations found on this Square account.
+ +

Found location(s). Copy the ID for your main location:

+ +
+
+
+
+ +
+
+ + ยท + Currency: ยท + Country: +
+
+ + + + + +
โš ๏ธ Security: Delete get_location.php from your server after use. It exposes your access token.
+ + + + diff --git a/public_html/index.php b/public_html/index.php new file mode 100644 index 0000000..751291c --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,3238 @@ + + + + + + + + + + + + +TomTomGames โ€” Buy Game Tokens | VBlink777, Fire Kirin, Milky Way & More + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + diff --git a/public_html/install.php b/public_html/install.php new file mode 100644 index 0000000..d7f1275 --- /dev/null +++ b/public_html/install.php @@ -0,0 +1,206 @@ +Access denied. Add ?key=TomGames2024Admin to URL.'); + +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 '.DB_NAME.' as '.DB_USER.''); +} catch (Exception $e) { + die('
CONNECTION FAILED: '.htmlspecialchars($e->getMessage()).'
'); +} + +// 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 $name โœ“"); } + catch (Exception $e) { err("Table $name: ".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 $tbl.$col already exists โœ“"); + } else { + try { + $pdo->exec("ALTER TABLE `$tbl` ADD COLUMN `$col` $def AFTER `$after`"); + ok("Column $tbl.$col added โœ“"); + } catch (Exception $e) { + err("Column $tbl.$col: ".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: ".implode(', ', $tables_now).""); + +try { + $total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); + $admins = $pdo->query("SELECT COUNT(*) FROM users WHERE is_admin=1")->fetchColumn(); + info("Users: $total total, $admins admin(s)"); +} catch (Exception $e) {} +?> + +TomTomGames DB Install + +

๐ŸŽฎ TomTomGames โ€” DB Install / Repair

+
Database:
+ +
+ + +
โš  Delete install.php after use โ€” it exposes DB structure.
+ diff --git a/public_html/manifest.json b/public_html/manifest.json new file mode 100644 index 0000000..b6df8ee --- /dev/null +++ b/public_html/manifest.json @@ -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" }] + } + ] +} diff --git a/public_html/robots.txt b/public_html/robots.txt new file mode 100644 index 0000000..297f710 --- /dev/null +++ b/public_html/robots.txt @@ -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 diff --git a/public_html/sitemap.xml b/public_html/sitemap.xml new file mode 100644 index 0000000..12c7d8b --- /dev/null +++ b/public_html/sitemap.xml @@ -0,0 +1,21 @@ + + + + + + https://tomtomgames.com/ + + daily + 1.0 + + + + + https://tomtomgames.com/games/ + + weekly + 0.9 + + + diff --git a/public_html/test.php b/public_html/test.php new file mode 100644 index 0000000..00f0b67 --- /dev/null +++ b/public_html/test.php @@ -0,0 +1,6 @@ + PHP_VERSION, + 'time' => date('Y-m-d H:i:s'), + 'status' => 'ok' +]); diff --git a/public_html/test_login.php b/public_html/test_login.php new file mode 100644 index 0000000..08d99ca --- /dev/null +++ b/public_html/test_login.php @@ -0,0 +1,18 @@ +$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()]); diff --git a/public_html/test_mail.php b/public_html/test_mail.php new file mode 100644 index 0000000..daaa45e --- /dev/null +++ b/public_html/test_mail.php @@ -0,0 +1,91 @@ + + + + +Email Test + + + +

TomTomGames โ€” Email Diagnostics

+ +
+Server Info:
+PHP:
+Server:
+Hostname:
+mail() function:
+sendmail_path:
+SMTP: : +
+ + +
+Send Result:
+To:
+mail() returned:
+Error:
+ +
Check your inbox (and spam folder). If nothing arrives in 5 minutes, the server is not sending outbound mail. + +
mail() returned false โ€” server cannot send email. You need to configure SMTP (PHPMailer) or enable sendmail. + +
+ + +
+
+ + + +
+
+ +

Delete test_mail.php after diagnosis is complete.

+ + diff --git a/public_html/verify.php b/public_html/verify.php new file mode 100644 index 0000000..e2d8d37 --- /dev/null +++ b/public_html/verify.php @@ -0,0 +1,66 @@ + + + + + + +<?= SITE_NAME ?> โ€” Email Verified + + + + + + + +
+ + + + ๐ŸŽ‰ +
Account Verified!
+

+ Welcome to , !

+ Your account is active and you've been automatically logged in. Let's play! +

+ ๐ŸŽฎ ENTER TOMTOMGAMES +

Redirecting in 4 seconds...

+ + + + โŒ +
Verification Failed
+

+ REGISTER AGAIN + BACK TO HOME + +
+ + diff --git a/vendor/phpmailer/phpmailer/src/Exception.php b/vendor/phpmailer/phpmailer/src/Exception.php new file mode 100644 index 0000000..dd8043e --- /dev/null +++ b/vendor/phpmailer/phpmailer/src/Exception.php @@ -0,0 +1 @@ +Host not in allowlist \ No newline at end of file diff --git a/vendor/phpmailer/phpmailer/src/PHPMailer.php b/vendor/phpmailer/phpmailer/src/PHPMailer.php new file mode 100644 index 0000000..dd8043e --- /dev/null +++ b/vendor/phpmailer/phpmailer/src/PHPMailer.php @@ -0,0 +1 @@ +Host not in allowlist \ No newline at end of file diff --git a/vendor/phpmailer/phpmailer/src/SMTP.php b/vendor/phpmailer/phpmailer/src/SMTP.php new file mode 100644 index 0000000..dd8043e --- /dev/null +++ b/vendor/phpmailer/phpmailer/src/SMTP.php @@ -0,0 +1 @@ +Host not in allowlist \ No newline at end of file