mirror of
https://github.com/myronblair/tomtomgames-app
synced 2026-06-30 17:49:57 -05:00
v1.0.4 - Clean start
This commit is contained in:
@@ -1,31 +1,24 @@
|
|||||||
# TomTomGames Platform
|
# TomTomGames Platform
|
||||||
|
|
||||||
Private gaming portal platform. Built on PHP/MySQL with LiteSpeed/CyberPanel hosting.
|
Private gaming portal. PHP/MySQL on LiteSpeed/CyberPanel.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- **Backend:** PHP 8.5, MySQL (CyberPanel/LiteSpeed)
|
- Backend: PHP 8.5, MySQL (MariaDB)
|
||||||
- **Payments:** Square SDK (card) + manual (Venmo/Zelle/CashApp/Chime)
|
- Payments: Square (card) + manual (Venmo/Zelle/CashApp/Chime)
|
||||||
- **Email:** SendGrid HTTP API
|
- Email: SendGrid HTTP API
|
||||||
- **Frontend:** Vanilla JS SPA
|
- Frontend: Vanilla JS SPA
|
||||||
|
|
||||||
## Structure
|
## Local path
|
||||||
```
|
`C:\Users\myron\Downloads\CyberPanel\`
|
||||||
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.
|
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Version | Date | Notes |
|
| Version | Date | Notes |
|
||||||
|---------|------|-------|
|
|---------|------|-------|
|
||||||
| 1.0.0 | 2026-05-08 | Initial release |
|
| 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
|
## ⚠️ 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
@@ -6,16 +6,31 @@ function isLoggedIn(): bool {
|
|||||||
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireLogin(): void {
|
|
||||||
if (!isLoggedIn()) {
|
function logoutUser(): void {
|
||||||
header('Location: /'); exit;
|
// 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 {
|
function requireAdmin(): void {
|
||||||
requireLogin();
|
requireLogin();
|
||||||
if (empty($_SESSION['is_admin'])) {
|
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();
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
if (!$user || !password_verify($password, $user['password'])) {
|
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.'];
|
return ['success' => false, 'error' => 'Invalid username or password.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block unverified accounts — admins are always exempt
|
|
||||||
if (!$user['email_verified'] && !$user['is_admin']) {
|
if (!$user['email_verified'] && !$user['is_admin']) {
|
||||||
return [
|
return ['success'=>false,'error'=>'Please verify your email address before logging in.','unverified'=>true,'email'=>$user['email']];
|
||||||
'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']) {
|
if ($user['is_admin'] && !$user['email_verified']) {
|
||||||
db()->prepare("UPDATE users SET email_verified=1 WHERE id=?")->execute([$user['id']]);
|
db()->prepare("UPDATE users SET email_verified=1 WHERE id=?")->execute([$user['id']]);
|
||||||
$user['email_verified'] = 1;
|
$user['email_verified'] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session_regenerate_id(true);
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
$_SESSION['alias'] = $user['alias'];
|
$_SESSION['alias'] = $user['alias'];
|
||||||
$_SESSION['is_admin'] = $user['is_admin'];
|
$_SESSION['is_admin'] = $user['is_admin'];
|
||||||
|
|
||||||
db()->prepare("UPDATE users SET last_login=NOW() WHERE id=?")->execute([$user['id']]);
|
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];
|
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 {
|
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
|
||||||
$username = strtolower(trim($username));
|
$username = strtolower(trim($username));
|
||||||
$alias = trim($alias);
|
$alias = trim($alias);
|
||||||
$email = strtolower(trim($email));
|
$email = strtolower(trim($email));
|
||||||
|
|
||||||
// Validate
|
|
||||||
if (strlen($username) < 3 || strlen($username) > 50)
|
if (strlen($username) < 3 || strlen($username) > 50)
|
||||||
return ['success' => false, 'error' => 'Username must be 3–50 characters.'];
|
return ['success'=>false,'error'=>'Username must be 3–50 characters.'];
|
||||||
if (!preg_match('/^[a-z0-9_]+$/', $username))
|
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)
|
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))
|
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))
|
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 = db()->prepare("SELECT id FROM users WHERE username=?");
|
||||||
$s->execute([$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 = db()->prepare("SELECT id FROM users WHERE email=?");
|
||||||
$s->execute([$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 = db()->prepare("SELECT id FROM pending_registrations WHERE username=? AND expires_at > NOW()");
|
||||||
$s->execute([$username]);
|
$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 = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()");
|
||||||
$s->execute([$email]);
|
$s->execute([$email]);
|
||||||
$existing = $s->fetch();
|
$existing = $s->fetch();
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
// Resend verification to same email
|
|
||||||
sendVerificationEmail($email, $alias, $existing['token']);
|
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]);
|
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;
|
$referrerId = null;
|
||||||
if ($referralCode) {
|
if ($referralCode) {
|
||||||
$refStmt = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
|
$rs = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
|
||||||
$refStmt->execute([strtoupper(trim($referralCode))]);
|
$rs->execute([strtoupper(trim($referralCode))]);
|
||||||
$refUser = $refStmt->fetch();
|
$ru = $rs->fetch();
|
||||||
if ($refUser) $referrerId = (int)$refUser['id'];
|
if ($ru) $referrerId = (int)$ru['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = bin2hex(random_bytes(32));
|
$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);
|
$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 = 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->execute([$username,$hash,$alias,$email,$token,$referrerId,$expiresAt]);
|
||||||
|
|
||||||
$sent = sendVerificationEmail($email, $alias, $token);
|
$sent = sendVerificationEmail($email, $alias, $token);
|
||||||
|
|
||||||
if (!$sent) {
|
if (!$sent) {
|
||||||
// Email failed but keep registration — user can resend from login screen
|
return ['success'=>true,'email'=>$email,'mail_warning'=>true];
|
||||||
error_log('[TomTomGames] Verification email failed for ' . $email);
|
|
||||||
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 {
|
function verifyEmailToken(string $token): array {
|
||||||
$token = trim($token);
|
$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 = db()->prepare("SELECT * FROM pending_registrations WHERE token=? AND expires_at > NOW()");
|
||||||
$stmt->execute([$token]);
|
$stmt->execute([$token]);
|
||||||
$pending = $stmt->fetch();
|
$pending = $stmt->fetch();
|
||||||
|
if (!$pending) return ['success'=>false,'error'=>'Verification link is invalid or has expired.'];
|
||||||
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();
|
db()->beginTransaction();
|
||||||
try {
|
try {
|
||||||
// Create the user
|
$ins = db()->prepare("INSERT INTO users (username,password,alias,email,email_verified,status) VALUES (?,?,?,?,1,'active')");
|
||||||
$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->execute([$pending['username'], $pending['password'], $pending['alias'], $pending['email']]);
|
|
||||||
$userId = db()->lastInsertId();
|
$userId = db()->lastInsertId();
|
||||||
|
|
||||||
// Generate unique referral code
|
$code = strtoupper(substr(md5($userId.uniqid()),0,8));
|
||||||
$code = strtoupper(substr(md5($userId . uniqid()), 0, 8));
|
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code,$userId]);
|
||||||
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'])) {
|
if (!empty($pending['referred_by'])) {
|
||||||
$referrerId = (int)$pending['referred_by'];
|
|
||||||
try {
|
try {
|
||||||
db()->prepare("INSERT IGNORE INTO referrals (referrer_id, referred_id, status) VALUES (?,?,'pending')")
|
db()->prepare("INSERT IGNORE INTO referrals (referrer_id,referred_id,status) VALUES (?,?,'pending')")->execute([$pending['referred_by'],$userId]);
|
||||||
->execute([$referrerId, $userId]);
|
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$pending['referred_by'],$userId]);
|
||||||
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$referrerId, $userId]);
|
|
||||||
} catch(Exception $e) {}
|
} catch(Exception $e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete pending row
|
|
||||||
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
|
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
|
||||||
|
|
||||||
db()->commit();
|
db()->commit();
|
||||||
} catch (Exception $e) {
|
} catch(Exception $e) {
|
||||||
db()->rollBack();
|
db()->rollBack();
|
||||||
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
|
return ['success'=>false,'error'=>'Account creation failed. Please try again.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-login
|
return ['success'=>true,'user_id'=>$userId,'username'=>$pending['username'],'alias'=>$pending['alias']];
|
||||||
$_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']];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── Activity Logger ────────────────────────────────────────
|
||||||
* Resend verification email for an unverified account.
|
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 {
|
||||||
*/
|
|
||||||
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 {
|
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)");
|
db()->exec("DELETE FROM activity_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
|
||||||
}
|
}
|
||||||
$ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '');
|
$ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||||
$userAgent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 300);
|
$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() ?: '';
|
$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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||||
db()->prepare("
|
->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]);
|
||||||
INSERT INTO activity_log
|
} catch(Exception $e) {}
|
||||||
(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 {
|
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 {
|
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 {
|
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 {
|
function getAppVersion(): string {
|
||||||
try {
|
try {
|
||||||
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
|
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
|
||||||
return $v ?: '1.0.0';
|
return $v ?: '1.0.2';
|
||||||
} catch(Exception $e) { return '1.0.0'; }
|
} 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
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
// ─── Database ─────────────────────────────────────────────
|
// ─── Database ─────────────────────────────────────────────
|
||||||
define('DB_HOST', 'localhost');
|
define('DB_HOST', 'localhost');
|
||||||
define('DB_NAME', 'tomt_tomgames');
|
define('DB_NAME', 'tomt_ttg_db');
|
||||||
define('DB_USER', 'tomt_tomgames');
|
define('DB_USER', 'tomt_ttg_user');
|
||||||
define('DB_PASS', 'It0Dmy2BlHP8GP1E');
|
define('DB_PASS', 'q#q+mrOcozsa7I6J');
|
||||||
|
|
||||||
// ─── Square ───────────────────────────────────────────────
|
// ─── Square ───────────────────────────────────────────────
|
||||||
define('SQUARE_ENV', 'production');
|
define('SQUARE_ENV', 'production');
|
||||||
@@ -41,10 +41,10 @@ define('PAY_ZELLE', 'tomgames@email.com');
|
|||||||
|
|
||||||
// ─── Token Packages ───────────────────────────────────────
|
// ─── Token Packages ───────────────────────────────────────
|
||||||
define('TOKEN_PACKAGES', json_encode([
|
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' => 10, 'price' => 10, 'label' => '10 Tokens', 'popular' => false],
|
||||||
['tokens' => 25, 'price' => 25, 'label' => '25 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' => 75, 'price' => 75, 'label' => '75 Tokens', 'popular' => false],
|
||||||
['tokens' => 100, 'price' => 100, 'label' => '100 Tokens', 'popular' => false],
|
['tokens' => 100, 'price' => 100, 'label' => '100 Tokens', 'popular' => false],
|
||||||
]));
|
]));
|
||||||
@@ -63,6 +63,4 @@ define('PLATFORMS', json_encode([
|
|||||||
error_reporting(0);
|
error_reporting(0);
|
||||||
ini_set('display_errors', 0);
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) { @session_start(); }
|
||||||
@session_start();
|
|
||||||
}
|
|
||||||
|
|||||||
+75
-17
@@ -1,37 +1,103 @@
|
|||||||
Options -Indexes
|
# ══════════════════════════════════════════════════════════
|
||||||
|
# TomTomGames Security Configuration
|
||||||
|
# ══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Options -Indexes -Includes
|
||||||
ServerSignature Off
|
ServerSignature Off
|
||||||
|
|
||||||
# ── Block sensitive files ────────────────────────────────
|
# ── Block all sensitive file types ───────────────────────
|
||||||
<FilesMatch "\.(sql|env|log|sh|md|git)$">
|
<FilesMatch "\.(sql|env|log|sh|md|git|bak|backup|old|orig|tmp|swp|cfg|ini|conf|yaml|yml|json.bak)$">
|
||||||
Order allow,deny
|
Order allow,deny
|
||||||
Deny from all
|
Deny from all
|
||||||
</FilesMatch>
|
</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>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteRule ^includes/ - [F,L]
|
RewriteRule ^includes/ - [F,L]
|
||||||
|
RewriteRule ^vendor/ - [F,L]
|
||||||
|
RewriteRule ^mail_queue/ - [F,L]
|
||||||
|
RewriteRule ^\.git/ - [F,L]
|
||||||
</IfModule>
|
</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>
|
<IfModule mod_headers.c>
|
||||||
|
# Prevent MIME type sniffing
|
||||||
Header always set X-Content-Type-Options "nosniff"
|
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"
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
|
||||||
|
# Referrer policy
|
||||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
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>
|
</IfModule>
|
||||||
|
|
||||||
# ── Canonical HTTPS redirect ──────────────────────────────
|
# ── Canonical HTTPS + non-www redirect ───────────────────
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteCond %{HTTPS} off
|
RewriteCond %{HTTPS} off
|
||||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
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]
|
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||||
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
|
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
# ── Block PHP execution in uploads folder (if it exists) ─
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteRule ^uploads/.*\.php$ - [F,L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
# ── Gzip compression ──────────────────────────────────────
|
# ── Gzip compression ──────────────────────────────────────
|
||||||
<IfModule mod_deflate.c>
|
<IfModule mod_deflate.c>
|
||||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json image/svg+xml
|
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 image/webp "access plus 1 month"
|
||||||
ExpiresByType application/json "access plus 1 day"
|
ExpiresByType application/json "access plus 1 day"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# ── LiteSpeed cache rules ─────────────────────────────────
|
|
||||||
<IfModule LiteSpeed>
|
|
||||||
CacheEnable public /assets/
|
|
||||||
CacheEnable public /manifest.json
|
|
||||||
CacheEnable public /sitemap.xml
|
|
||||||
CacheEnable public /robots.txt
|
|
||||||
</IfModule>
|
|
||||||
|
|||||||
+327
-247
File diff suppressed because it is too large
Load Diff
@@ -310,14 +310,16 @@ switch ($action) {
|
|||||||
$stmt->execute([$uid]);
|
$stmt->execute([$uid]);
|
||||||
$current = $stmt->fetchColumn();
|
$current = $stmt->fetchColumn();
|
||||||
$new_val = $current ? 0 : 1;
|
$new_val = $current ? 0 : 1;
|
||||||
// If granting admin, also set email_verified=1
|
|
||||||
if ($new_val) {
|
if ($new_val) {
|
||||||
db()->prepare("UPDATE users SET is_admin=1, email_verified=1 WHERE id=?")->execute([$uid]);
|
db()->prepare("UPDATE users SET is_admin=1, email_verified=1 WHERE id=?")->execute([$uid]);
|
||||||
} else {
|
} else {
|
||||||
db()->prepare("UPDATE users SET is_admin=0 WHERE id=?")->execute([$uid]);
|
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'));
|
// Force the affected user to re-login by invalidating their sessions
|
||||||
echo json_encode(['success'=>true, 'is_admin'=>$new_val]);
|
// 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;
|
break;
|
||||||
|
|
||||||
// ─── TOGGLE SUSPEND ───────────────────────────────────────
|
// ─── TOGGLE SUSPEND ───────────────────────────────────────
|
||||||
@@ -439,6 +441,24 @@ switch ($action) {
|
|||||||
echo json_encode(['success'=>true,'broadcasts'=>$rows]);
|
echo json_encode(['success'=>true,'broadcasts'=>$rows]);
|
||||||
break;
|
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':
|
case 'broadcast_send':
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; }
|
||||||
$d = json_decode(file_get_contents('php://input'), true);
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
@@ -468,6 +488,42 @@ switch ($action) {
|
|||||||
echo json_encode(['success'=>true]);
|
echo json_encode(['success'=>true]);
|
||||||
break;
|
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':
|
case 'broadcast_reads':
|
||||||
$bid = (int)($_GET['broadcast_id']??0);
|
$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 = 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");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
ob_start(); // Buffer any accidental output (PHP errors, notices, etc.)
|
ob_start();
|
||||||
try {
|
try {
|
||||||
require_once __DIR__ . '/../../includes/auth.php';
|
require_once __DIR__ . '/../../includes/auth.php';
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@@ -28,5 +28,8 @@ if (!$user) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync session is_admin with DB value — catches admin elevation/demotion
|
||||||
|
$_SESSION['is_admin'] = (int)$user['is_admin'];
|
||||||
|
|
||||||
unset($user['password']);
|
unset($user['password']);
|
||||||
echo json_encode(['success' => true, 'user' => $user]);
|
echo json_encode(['success' => true, 'user' => $user]);
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ if ($method === 'GET') {
|
|||||||
$user->execute([$userId]);
|
$user->execute([$userId]);
|
||||||
$u = $user->fetch();
|
$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
|
// Count verified referrals
|
||||||
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
|
$countStmt = db()->prepare("SELECT COUNT(*) FROM referrals WHERE referrer_id=? AND status='verified'");
|
||||||
$countStmt->execute([$userId]);
|
$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=?")
|
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]);
|
->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']]);
|
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();
|
db()->commit();
|
||||||
echo json_encode(['success'=>true,'tokens_awarded'=>$totalAward,'bonus'=>$bonusTokens,'tier'=>$tier['name']??'']);
|
echo json_encode(['success'=>true,'tokens_awarded'=>$totalAward,'bonus'=>$bonusTokens,'tier'=>$tier['name']??'']);
|
||||||
} catch(Exception $e) {
|
} 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]);
|
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') {
|
if ($status === 'approved') {
|
||||||
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$share['bonus_tokens'],$share['user_id']]);
|
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]);
|
echo json_encode(['success'=>true]);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 136 B |
@@ -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 |
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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";
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?php
|
|
||||||
echo json_encode([
|
|
||||||
'php' => PHP_VERSION,
|
|
||||||
'time' => date('Y-m-d H:i:s'),
|
|
||||||
'status' => 'ok'
|
|
||||||
]);
|
|
||||||
@@ -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()]);
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
@echo off
|
cd /d "C:\Users\myron\Downloads\CyberPanel"
|
||||||
cd /d "C:\Users\myron\Downloads\tomgames"
|
set /p MSG="Commit message (e.g. v1.0.4 - what changed): "
|
||||||
set /p MSG="Commit message (e.g. v1.0.2 - what changed): "
|
|
||||||
if "%MSG%"=="" (
|
if "%MSG%"=="" (
|
||||||
echo No message entered. Aborting.
|
echo No message entered. Aborting.
|
||||||
pause
|
pause
|
||||||
|
|||||||
Reference in New Issue
Block a user