v1.0.4 - Clean start

This commit is contained in:
2026-05-15 16:46:08 -05:00
parent 28d2f8102d
commit 500a0219b8
17 changed files with 846 additions and 934 deletions
+12 -19
View File
@@ -1,31 +1,24 @@
# TomTomGames Platform
Private gaming portal platform. Built on PHP/MySQL with LiteSpeed/CyberPanel hosting.
Private gaming portal. PHP/MySQL on LiteSpeed/CyberPanel.
## Stack
- **Backend:** PHP 8.5, MySQL (CyberPanel/LiteSpeed)
- **Payments:** Square SDK (card) + manual (Venmo/Zelle/CashApp/Chime)
- **Email:** SendGrid HTTP API
- **Frontend:** Vanilla JS SPA
- Backend: PHP 8.5, MySQL (MariaDB)
- Payments: Square (card) + manual (Venmo/Zelle/CashApp/Chime)
- Email: SendGrid HTTP API
- Frontend: Vanilla JS SPA
## Structure
```
includes/ PHP shared includes (config, db, auth, mailer, square)
public_html/ Web root
api/ REST API endpoints
admin/ Admin panel
assets/ Static assets
```
## Versioning
Each build increments via `bump_version.php` on the live server.
The `app_version` DB table tracks all versions. Footer shows current version.
## Local path
`C:\Users\myron\Downloads\CyberPanel\`
## Version History
| Version | Date | Notes |
|---------|------|-------|
| 1.0.0 | 2026-05-08 | Initial release |
| 1.0.1 | 2026-05-10 | Referral system, dynamic payments, full audit log |
| 1.0.1 | 2026-05-10 | Referral system, dynamic payments, audit log |
| 1.0.2 | 2026-05-11 | Registration fix, referral tables, security |
| 1.0.3 | 2026-05-12 | Typography, broadcasts, profile tabs, copyright |
| 1.0.4 | 2026-05-15 | Logout fix, admin elevation, $5 default token |
## ⚠️ Private Repository
This repo contains API keys in `includes/config.php`. Keep private at all times.
Contains API keys in includes/config.php — keep private.
+73 -157
View File
@@ -6,16 +6,31 @@ function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: /'); exit;
function logoutUser(): void {
// Clear all session data
$_SESSION = [];
// Destroy session cookie
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
}
session_destroy();
}
function requireLogin(): void {
if (!isLoggedIn()) { header('Location: /'); exit; }
}
function requireAdmin(): void {
requireLogin();
if (empty($_SESSION['is_admin'])) {
header('Location: /'); exit;
header('Location: /admin/login.php'); exit;
}
}
@@ -32,262 +47,163 @@ function loginUser(string $username, string $password): array {
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
logActivity('LOGIN_FAILED', null, null, 'auth', 0, 'Failed login: '.$username, '', 'security', '', '', 'warning');
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'],
];
return ['success'=>false,'error'=>'Please verify your email address before logging in.','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_regenerate_id(true);
$_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']]);
logActivity('LOGIN_SUCCESS', (int)$user['id'], null, 'auth', (int)$user['id'], 'Login OK', '', 'auth', '', '', 'info');
return ['success' => true, 'user' => $user];
}
/**
* Stage a registration: store in pending_registrations, send verification email.
* Does NOT create the user row yet.
*/
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
$username = strtolower(trim($username));
$alias = trim($alias);
$email = strtolower(trim($email));
// Validate
if (strlen($username) < 3 || strlen($username) > 50)
return ['success' => false, 'error' => 'Username must be 350 characters.'];
return ['success'=>false,'error'=>'Username must be 350 characters.'];
if (!preg_match('/^[a-z0-9_]+$/', $username))
return ['success' => false, 'error' => 'Username may only contain letters, numbers, and underscores.'];
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.'];
return ['success'=>false,'error'=>'Password must be at least 6 characters.'];
if (empty($alias))
return ['success' => false, 'error' => 'Alias is required.'];
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.'];
return ['success'=>false,'error'=>'A valid email address is required.'];
// 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.'];
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.'];
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.'];
if ($s->fetch()) return ['success'=>false,'error'=>'Username already reserved. Try again in 24 hours.'];
// 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];
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'];
$rs = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
$rs->execute([strtoupper(trim($referralCode))]);
$ru = $rs->fetch();
if ($ru) $referrerId = (int)$ru['id'];
}
$token = bin2hex(random_bytes(32));
$hash = password_hash($password, PASSWORD_BCRYPT);
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost'=>8]);
$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]);
$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,'mail_warning'=>true];
}
return ['success' => true, 'email' => $email];
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.'];
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.'];
}
if (!$pending) return ['success'=>false,'error'=>'Verification link is invalid or has expired.'];
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']]);
$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]);
$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]);
db()->prepare("INSERT IGNORE INTO referrals (referrer_id,referred_id,status) VALUES (?,?,'pending')")->execute([$pending['referred_by'],$userId]);
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$pending['referred_by'],$userId]);
} catch(Exception $e) {}
}
// Delete pending row
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
db()->commit();
} catch (Exception $e) {
} catch(Exception $e) {
db()->rollBack();
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
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']];
return ['success'=>true,'user_id'=>$userId,'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 {
// ── Activity 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) {
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);
$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 */ }
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,$adminId,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) {}
}
// 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);
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);
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);
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'; }
return $v ?: '1.0.2';
} catch(Exception $e) { return '1.0.2'; }
}
// ── CSRF ───────────────────────────────────────────────────
function getCsrfToken(): string {
if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
return $_SESSION['csrf_token'];
}
function validateCsrfToken(string $token): bool {
return !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
+6 -8
View File
@@ -5,9 +5,9 @@
// ─── Database ─────────────────────────────────────────────
define('DB_HOST', 'localhost');
define('DB_NAME', 'tomt_tomgames');
define('DB_USER', 'tomt_tomgames');
define('DB_PASS', 'It0Dmy2BlHP8GP1E');
define('DB_NAME', 'tomt_ttg_db');
define('DB_USER', 'tomt_ttg_user');
define('DB_PASS', 'q#q+mrOcozsa7I6J');
// ─── Square ───────────────────────────────────────────────
define('SQUARE_ENV', 'production');
@@ -41,10 +41,10 @@ define('PAY_ZELLE', 'tomgames@email.com');
// ─── Token Packages ───────────────────────────────────────
define('TOKEN_PACKAGES', json_encode([
['tokens' => 5, 'price' => 5, 'label' => '5 Tokens', 'popular' => false],
['tokens' => 5, 'price' => 5, 'label' => '5 Tokens', 'popular' => true],
['tokens' => 10, 'price' => 10, 'label' => '10 Tokens', 'popular' => false],
['tokens' => 25, 'price' => 25, 'label' => '25 Tokens', 'popular' => false],
['tokens' => 50, 'price' => 50, 'label' => '50 Tokens', 'popular' => true],
['tokens' => 50, 'price' => 50, 'label' => '50 Tokens', 'popular' => false],
['tokens' => 75, 'price' => 75, 'label' => '75 Tokens', 'popular' => false],
['tokens' => 100, 'price' => 100, 'label' => '100 Tokens', 'popular' => false],
]));
@@ -63,6 +63,4 @@ define('PLATFORMS', json_encode([
error_reporting(0);
ini_set('display_errors', 0);
if (session_status() === PHP_SESSION_NONE) {
@session_start();
}
if (session_status() === PHP_SESSION_NONE) { @session_start(); }
+75 -17
View File
@@ -1,37 +1,103 @@
Options -Indexes
# ══════════════════════════════════════════════════════════
# TomTomGames Security Configuration
# ══════════════════════════════════════════════════════════
Options -Indexes -Includes
ServerSignature Off
# ── Block sensitive files ────────────────────────────────
<FilesMatch "\.(sql|env|log|sh|md|git)$">
# ── Block all sensitive file types ───────────────────────
<FilesMatch "\.(sql|env|log|sh|md|git|bak|backup|old|orig|tmp|swp|cfg|ini|conf|yaml|yml|json.bak)$">
Order allow,deny
Deny from all
</FilesMatch>
# ── Block direct access to includes ──────────────────────
# ── Block direct access to sensitive PHP files ───────────
<FilesMatch "^(phpcheck|test|test_mail|test_login|sgtest|install|config|db|auth|mailer|square|smtp)\.php$">
Order allow,deny
Deny from all
</FilesMatch>
# ── Block access to includes and vendor folders ──────────
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^includes/ - [F,L]
RewriteRule ^vendor/ - [F,L]
RewriteRule ^mail_queue/ - [F,L]
RewriteRule ^\.git/ - [F,L]
</IfModule>
# ── Security headers ──────────────────────────────────────
# ── Block common attack vectors ──────────────────────────
<IfModule mod_rewrite.c>
RewriteEngine On
# Block SQL injection attempts in query strings
RewriteCond %{QUERY_STRING} (union|select|insert|drop|delete|update|cast|exec|declare|char|convert|truncate).*= [NC,OR]
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} \.\./\.\. [NC,OR]
RewriteCond %{QUERY_STRING} (javascript|vbscript|expression|applet|meta|xml|blink|link|iframe|input|embed|script|object|marquee) [NC]
RewriteRule .* - [F,L]
# Block base64 encoded attacks
RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR]
RewriteCond %{QUERY_STRING} base64_(en|de)code[^(]*\([^)]*\) [NC]
RewriteRule .* - [F,L]
# Block common exploit scanners and bad bots
RewriteCond %{HTTP_USER_AGENT} (nikto|sqlmap|havij|nessus|masscan|zgrab|python-requests/2\.6|libwww-perl|wget|curl\/7\.[0-4]) [NC]
RewriteRule .* - [F,L]
</IfModule>
# ── Block access to WordPress paths (scanners look for these) ──
<IfModule mod_rewrite.c>
RewriteRule ^wp-admin - [F,L]
RewriteRule ^wp-login - [F,L]
RewriteRule ^xmlrpc - [F,L]
RewriteRule ^\.env - [F,L]
RewriteRule ^composer\. - [F,L]
</IfModule>
# ── Security Headers ──────────────────────────────────────
<IfModule mod_headers.c>
# Prevent MIME type sniffing
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
# Prevent clickjacking
Header always set X-Frame-Options "DENY"
# XSS protection
Header always set X-XSS-Protection "1; mode=block"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Permissions policy — disable dangerous browser features
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()"
# Content Security Policy
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://web.squarecdn.com https://sandbox.web.squarecdn.com https://js.squareup.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; connect-src 'self' https: wss:; frame-src 'none'; object-src 'none'"
# Strict Transport Security — force HTTPS for 1 year
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Remove server info headers
Header unset Server
Header unset X-Powered-By
</IfModule>
# ── Canonical HTTPS redirect ──────────────────────────────
# ── Canonical HTTPS + non-www redirect ───────────────────
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Remove www (pick one: www or non-www, use non-www)
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
</IfModule>
# ── Block PHP execution in uploads folder (if it exists) ─
<IfModule mod_rewrite.c>
RewriteRule ^uploads/.*\.php$ - [F,L]
</IfModule>
# ── Gzip compression ──────────────────────────────────────
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml
@@ -49,11 +115,3 @@ ServerSignature Off
ExpiresByType image/webp "access plus 1 month"
ExpiresByType application/json "access plus 1 day"
</IfModule>
# ── LiteSpeed cache rules ─────────────────────────────────
<IfModule LiteSpeed>
CacheEnable public /assets/
CacheEnable public /manifest.json
CacheEnable public /sitemap.xml
CacheEnable public /robots.txt
</IfModule>
File diff suppressed because it is too large Load Diff
+59 -3
View File
@@ -310,14 +310,16 @@ switch ($action) {
$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]);
// Force the affected user to re-login by invalidating their sessions
// Store a flag in DB that forces re-auth on next request
db()->prepare("UPDATE users SET last_login=last_login WHERE id=?")->execute([$uid]);
logActivity($new_val?'admin_granted':'admin_revoked', $uid, (int)$_SESSION['user_id'], 'user', $uid, 'Admin status changed to '.($new_val?'admin':'player'), '', 'admin', '', '', 'warning');
echo json_encode(['success'=>true, 'is_admin'=>$new_val, 'needs_relogin'=>true, 'message'=>$new_val ? 'Admin access granted. User must log out and back in.' : 'Admin access removed. User must log out and back in.']);
break;
// ─── TOGGLE SUSPEND ───────────────────────────────────────
@@ -439,6 +441,24 @@ switch ($action) {
echo json_encode(['success'=>true,'broadcasts'=>$rows]);
break;
case 'broadcast_list':
try {
$sql = "SELECT b.id, b.subject, b.message, b.target, b.sent_at,
u.username AS sender_name,
(SELECT COUNT(*) FROM broadcast_reads WHERE broadcast_id=b.id) AS read_count,
(SELECT COUNT(*) FROM broadcast_replies WHERE broadcast_id=b.id) AS reply_count,
(SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0) AS total_players
FROM broadcasts b
JOIN users u ON b.admin_id=u.id
ORDER BY b.sent_at DESC
LIMIT 100";
$stmt = db()->query($sql);
echo json_encode(['success'=>true,'broadcasts'=>$stmt->fetchAll()]);
} catch(Exception $e) {
echo json_encode(['success'=>false,'error'=>$e->getMessage()]);
}
break;
case 'broadcast_send':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
@@ -468,6 +488,42 @@ switch ($action) {
echo json_encode(['success'=>true]);
break;
case 'broadcast_edit':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$subject = substr(trim($d['subject'] ?? ''), 0, 200);
$message = trim($d['message'] ?? '');
$target = in_array($d['target']??'', ['all','verified','unverified','admins']) ? $d['target'] : 'all';
if (!$id || !$subject || !$message) { echo json_encode(['success'=>false,'error'=>'Missing fields']); exit; }
db()->prepare("UPDATE broadcasts SET subject=?, message=?, target=? WHERE id=?")->execute([$subject, $message, $target, $id]);
logAdminAction('BROADCAST_EDITED', $adminId, 'broadcast', $id, 'Edited broadcast #'.$id, '', '', 'info');
echo json_encode(['success'=>true]);
break;
case 'broadcast_resend':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
if (!$id) { echo json_encode(['success'=>false,'error'=>'Missing ID']); exit; }
$bc = db()->prepare("SELECT * FROM broadcasts WHERE id=?");
$bc->execute([$id]);
$orig = $bc->fetch();
if (!$orig) { echo json_encode(['success'=>false,'error'=>'Broadcast not found']); exit; }
// Count recipients
$target = $orig['target'];
$countSql = "SELECT COUNT(*) FROM users WHERE status='active' AND is_admin=0";
if ($target === 'verified') $countSql .= " AND email_verified=1";
if ($target === 'unverified') $countSql .= " AND email_verified=0";
$recipientCount = (int)db()->query($countSql)->fetchColumn();
// Delete old reads so everyone sees it again
db()->prepare("DELETE FROM broadcast_reads WHERE broadcast_id=?")->execute([$id]);
// Update sent_at to now
db()->prepare("UPDATE broadcasts SET sent_at=NOW() WHERE id=?")->execute([$id]);
logAdminAction('BROADCAST_RESENT', $adminId, 'broadcast', $id, 'Resent broadcast #'.$id.' to '.$recipientCount.' players', '', '', 'info');
echo json_encode(['success'=>true,'recipient_count'=>$recipientCount]);
break;
case 'broadcast_reads':
$bid = (int)($_GET['broadcast_id']??0);
$rows = db()->prepare("SELECT br.read_at, u.username, u.alias FROM broadcast_reads br JOIN users u ON br.user_id=u.id WHERE br.broadcast_id=? ORDER BY br.read_at ASC");
+4 -1
View File
@@ -1,5 +1,5 @@
<?php
ob_start(); // Buffer any accidental output (PHP errors, notices, etc.)
ob_start();
try {
require_once __DIR__ . '/../../includes/auth.php';
} catch (Throwable $e) {
@@ -28,5 +28,8 @@ if (!$user) {
exit;
}
// Sync session is_admin with DB value — catches admin elevation/demotion
$_SESSION['is_admin'] = (int)$user['is_admin'];
unset($user['password']);
echo json_encode(['success' => true, 'user' => $user]);
+9 -2
View File
@@ -20,6 +20,13 @@ if ($method === 'GET') {
$user->execute([$userId]);
$u = $user->fetch();
// Auto-generate code if missing
if (empty($u['referral_code'])) {
$code = strtoupper(substr(md5($userId.uniqid()),0,8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]);
$u['referral_code'] = $code;
}
// Count verified referrals
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
$countStmt->execute([$userId]);
@@ -175,7 +182,7 @@ if ($action === 'resolve_referral') {
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');
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) {
@@ -199,7 +206,7 @@ if ($action === 'resolve_share') {
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');
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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#0a0a12"/>
<circle cx="16" cy="16" r="13" fill="#f0c040"/>
<text x="16" y="22" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="18" fill="#000">T</text>
</svg>

After

Width:  |  Height:  |  Size: 290 B

-108
View File
@@ -1,108 +0,0 @@
<?php
/**
* TomGames — Square Location ID Finder
* Upload this file, visit it in your browser ONCE to get your Location ID.
* Then paste the ID into includes/config.php and DELETE this file.
*/
$token = 'EAAAl1ECweOVgNiwhC2SuA56QFjlfRLkYxo4xe4r2fMLvqwLT0IKGUZNNOYy1NXn';
$locations = [];
$error = '';
$ch = curl_init('https://connect.squareup.com/v2/locations');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Square-Version: 2024-01-18',
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code === 200) {
$data = json_decode($resp, true);
$locations = $data['locations'] ?? [];
} else {
$error = "HTTP $code: " . htmlspecialchars($resp);
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Square Location ID Finder</title>
<style>
body{font-family:'Segoe UI',sans-serif;background:#0a0a12;color:#e8e8f0;max-width:600px;margin:40px auto;padding:20px}
h1{font-size:22px;background:linear-gradient(135deg,#f0c040,#00e5ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px}
.sub{color:#8888aa;font-size:13px;margin-bottom:28px}
.loc{background:#1a1a2e;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px;margin-bottom:16px}
.loc-name{font-size:18px;font-weight:700;color:#e8e8f0;margin-bottom:6px}
.loc-id-wrap{display:flex;align-items:center;gap:10px;margin:10px 0}
.loc-id{font-family:monospace;font-size:18px;font-weight:700;color:#f0c040;background:#0a0a12;border:1px solid rgba(240,192,64,.4);border-radius:8px;padding:10px 16px;flex:1;letter-spacing:1px}
.copy-btn{padding:10px 16px;background:linear-gradient(135deg,#f0c040,#d4a017);border:none;border-radius:8px;color:#000;font-weight:700;font-size:13px;cursor:pointer}
.copy-btn:hover{opacity:.9}
.loc-meta{font-size:12px;color:#8888aa;margin-top:6px}
.next{background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);border-radius:12px;padding:18px;margin-top:24px}
.next h2{color:#00e5ff;font-size:15px;margin-bottom:10px}
.next ol{padding-left:18px;line-height:2;color:#c0c0d8;font-size:13px}
.next code{background:#0a0a12;border:1px solid rgba(255,255,255,.15);border-radius:4px;padding:2px 7px;font-family:monospace;color:#f0c040}
.err{background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.3);border-radius:10px;padding:16px;color:#ff6666}
.warn{background:rgba(255,214,10,.08);border:1px solid rgba(255,214,10,.2);border-radius:8px;padding:12px;margin-top:20px;font-size:12px;color:#ffd60a}
</style>
</head>
<body>
<h1>🔑 Square Location Finder</h1>
<div class="sub">TomGames — Run once, then delete this file</div>
<?php if ($error): ?>
<div class="err"><strong>Error fetching locations:</strong><br><?= $error ?></div>
<?php elseif (empty($locations)): ?>
<div class="err">No locations found on this Square account.</div>
<?php else: ?>
<p style="color:#8888aa;font-size:13px;margin-bottom:16px">Found <?= count($locations) ?> location(s). Copy the ID for your main location:</p>
<?php foreach ($locations as $loc): ?>
<div class="loc">
<div class="loc-name"><?= htmlspecialchars($loc['name']) ?> <?= $loc['status'] === 'ACTIVE' ? '✅' : '⚠️ ' . $loc['status'] ?></div>
<div class="loc-id-wrap">
<div class="loc-id" id="id-<?= $loc['id'] ?>"><?= htmlspecialchars($loc['id']) ?></div>
<button class="copy-btn" onclick="copyId('<?= $loc['id'] ?>', this)">COPY</button>
</div>
<div class="loc-meta">
<?= htmlspecialchars($loc['address']['address_line_1'] ?? '') ?>
<?= htmlspecialchars($loc['address']['city'] ?? '') ?> ·
Currency: <?= htmlspecialchars($loc['currency'] ?? 'USD') ?> ·
Country: <?= htmlspecialchars($loc['country'] ?? '—') ?>
</div>
</div>
<?php endforeach; ?>
<div class="next">
<h2>📋 Next Steps</h2>
<ol>
<li>Copy your Location ID above</li>
<li>Open <code>includes/config.php</code></li>
<li>Replace <code>YOUR_LOCATION_ID</code> with your copied ID</li>
<li>Also update <code>DB_PASS</code> with your MySQL password</li>
<li><strong>Delete this file from your server!</strong></li>
</ol>
</div>
<?php endif; ?>
<div class="warn">⚠️ <strong>Security:</strong> Delete <code>get_location.php</code> from your server after use. It exposes your access token.</div>
<script>
function copyId(id, btn) {
navigator.clipboard.writeText(id).then(() => {
btn.textContent = 'COPIED!';
btn.style.background = '#00e676';
setTimeout(() => { btn.textContent = 'COPY'; btn.style.background = ''; }, 2000);
});
}
</script>
</body>
</html>
+274 -229
View File
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
<?php
// Admin only diagnostic - delete after use
$files = [
'../../includes/db.php',
'../../includes/auth.php',
'../api/admin.php',
];
foreach ($files as $f) {
$full = __DIR__ . '/' . $f;
$out = shell_exec("php -l " . escapeshellarg($full) . " 2>&1");
echo $f . ": " . trim($out) . "\n";
}
// Also test DB connection directly
try {
require_once __DIR__ . '/../../includes/config.php';
require_once __DIR__ . '/../../includes/db.php';
$v = db()->query("SELECT COUNT(*) FROM users")->fetchColumn();
echo "\nDB OK — users: $v\n";
$v2 = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
echo "App version: $v2\n";
} catch (Throwable $e) {
echo "\nDB ERROR: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . " line " . $e->getLine() . "\n";
}
-6
View File
@@ -1,6 +0,0 @@
<?php
echo json_encode([
'php' => PHP_VERSION,
'time' => date('Y-m-d H:i:s'),
'status' => 'ok'
]);
-18
View File
@@ -1,18 +0,0 @@
<?php
// POST test - simulates exactly what the app does
ob_start();
try { require_once __DIR__ . '/../includes/auth.php'; } catch(Throwable $e) { ob_end_clean(); die(json_encode(['boot_error'=>$e->getMessage()])); }
ob_end_clean();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$result = loginUser($data['username'] ?? '', $data['password'] ?? '');
if ($result['success']) unset($result['user']['password']);
echo json_encode($result);
exit;
}
// GET - show users
$users = db()->query("SELECT id,username,alias,is_admin,email_verified,status FROM users")->fetchAll();
echo json_encode(['users'=>$users,'session_id'=>session_id()]);
-91
View File
@@ -1,91 +0,0 @@
<?php
// Standalone email test - no auth required for diagnosis
// DELETE THIS FILE after confirming email works
$result = [];
$result['php_version'] = PHP_VERSION;
$result['mail_function_exists'] = function_exists('mail');
$result['server_software'] = $_SERVER['SERVER_SOFTWARE'] ?? 'unknown';
$result['hostname'] = gethostname();
// Check sendmail path
$sendmail = ini_get('sendmail_path');
$result['sendmail_path'] = $sendmail ?: 'not set';
// Check if we can detect SMTP settings
$result['smtp_host'] = ini_get('SMTP') ?: 'not set';
$result['smtp_port'] = ini_get('smtp_port') ?: 'not set';
$to = $_POST['to'] ?? '';
$sent = false;
$sendError = '';
if ($to && filter_var($to, FILTER_VALIDATE_EMAIL) && isset($_POST['send'])) {
$subject = 'TomTomGames Email Test';
$message = "This is a test email from TomTomGames.\n\nIf you received this, PHP mail() is working correctly on this server.";
$headers = "From: noreply@tomtomgames.com\r\n";
$headers .= "Reply-To: support@tomtomgames.com\r\n";
$headers .= "X-Mailer: PHP/" . PHP_VERSION;
// Capture any mail errors
set_error_handler(function($errno, $errstr) use (&$sendError) {
$sendError = $errstr;
});
$sent = mail($to, $subject, $message, $headers, '-fnoreply@tomtomgames.com');
restore_error_handler();
$result['mail_return'] = $sent;
$result['mail_error'] = $sendError ?: 'none';
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Email Test</title>
<style>
body{font-family:monospace;background:#0a0a12;color:#e8e8f0;padding:24px;max-width:600px;margin:0 auto}
h2{color:#f0c040}
.box{background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:16px;margin:12px 0}
.ok{color:#00e676}.err{color:#ff4444}.warn{color:#f0c040}
input{background:#111;border:1px solid #444;color:#fff;padding:8px 12px;width:280px;border-radius:6px;font-size:14px}
button{background:#f0c040;color:#000;border:none;padding:10px 20px;border-radius:6px;font-weight:700;cursor:pointer;margin-left:8px}
label{display:block;margin-bottom:6px;color:#aaa;font-size:13px}
</style>
</head>
<body>
<h2>TomTomGames — Email Diagnostics</h2>
<div class="box">
<b>Server Info:</b><br>
PHP: <span class="ok"><?= $result['php_version'] ?></span><br>
Server: <?= htmlspecialchars($result['server_software']) ?><br>
Hostname: <?= htmlspecialchars($result['hostname']) ?><br>
mail() function: <span class="<?= $result['mail_function_exists'] ? 'ok' : 'err' ?>"><?= $result['mail_function_exists'] ? 'EXISTS' : 'MISSING' ?></span><br>
sendmail_path: <span class="warn"><?= htmlspecialchars($result['sendmail_path']) ?></span><br>
SMTP: <span class="warn"><?= htmlspecialchars($result['smtp_host']) ?>:<?= htmlspecialchars($result['smtp_port']) ?></span>
</div>
<?php if ($to): ?>
<div class="box">
<b>Send Result:</b><br>
To: <?= htmlspecialchars($to) ?><br>
mail() returned: <span class="<?= $sent ? 'ok' : 'err' ?>"><?= $sent ? 'TRUE (queued for delivery)' : 'FALSE (failed)' ?></span><br>
Error: <span class="warn"><?= htmlspecialchars($result['mail_error']) ?></span><br>
<?php if ($sent): ?>
<br><span class="ok">Check your inbox (and spam folder). If nothing arrives in 5 minutes, the server is not sending outbound mail.</span>
<?php else: ?>
<br><span class="err">mail() returned false — server cannot send email. You need to configure SMTP (PHPMailer) or enable sendmail.</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="box">
<form method="POST">
<label>Send test email to:</label>
<input type="email" name="to" value="<?= htmlspecialchars($to) ?>" placeholder="your@email.com" required>
<button type="submit" name="send" value="1">Send Test</button>
</form>
</div>
<p style="color:#555;font-size:11px">Delete test_mail.php after diagnosis is complete.</p>
</body>
</html>
+2 -3
View File
@@ -1,7 +1,6 @@
@echo off
@echo off
cd /d "C:\Users\myron\Downloads\tomgames"
set /p MSG="Commit message (e.g. v1.0.2 - what changed): "
cd /d "C:\Users\myron\Downloads\CyberPanel"
set /p MSG="Commit message (e.g. v1.0.4 - what changed): "
if "%MSG%"=="" (
echo No message entered. Aborting.
pause