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

This commit is contained in:
2026-05-10 14:45:49 -05:00
commit c70027f8fc
61 changed files with 11762 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/mailer.php';
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: /'); exit;
}
}
function requireAdmin(): void {
requireLogin();
if (empty($_SESSION['is_admin'])) {
header('Location: /'); exit;
}
}
function currentUser(): ?array {
if (!isLoggedIn()) return null;
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch() ?: null;
}
function loginUser(string $username, string $password): array {
$stmt = db()->prepare("SELECT * FROM users WHERE username = ? AND status = 'active'");
$stmt->execute([strtolower(trim($username))]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
return ['success' => false, 'error' => 'Invalid username or password.'];
}
// Block unverified accounts — admins are always exempt
if (!$user['email_verified'] && !$user['is_admin']) {
return [
'success' => false,
'error' => 'Please verify your email address before logging in. Check your inbox for the verification link.',
'unverified' => true,
'email' => $user['email'],
];
}
// Auto-verify admin accounts so they're never locked out
if ($user['is_admin'] && !$user['email_verified']) {
db()->prepare("UPDATE users SET email_verified=1 WHERE id=?")->execute([$user['id']]);
$user['email_verified'] = 1;
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['alias'] = $user['alias'];
$_SESSION['is_admin'] = $user['is_admin'];
db()->prepare("UPDATE users SET last_login=NOW() WHERE id=?")->execute([$user['id']]);
return ['success' => true, 'user' => $user];
}
/**
* Stage a registration: store in pending_registrations, send verification email.
* Does NOT create the user row yet.
*/
function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array {
$username = strtolower(trim($username));
$alias = trim($alias);
$email = strtolower(trim($email));
// Validate
if (strlen($username) < 3 || strlen($username) > 50)
return ['success' => false, 'error' => 'Username must be 350 characters.'];
if (!preg_match('/^[a-z0-9_]+$/', $username))
return ['success' => false, 'error' => 'Username may only contain letters, numbers, and underscores.'];
if (strlen($password) < 6)
return ['success' => false, 'error' => 'Password must be at least 6 characters.'];
if (empty($alias))
return ['success' => false, 'error' => 'Alias is required.'];
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
return ['success' => false, 'error' => 'A valid email address is required to verify your account.'];
// Check username taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE username=?");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already taken.'];
// Check email taken (existing users)
$s = db()->prepare("SELECT id FROM users WHERE email=?");
$s->execute([$email]);
if ($s->fetch()) return ['success' => false, 'error' => 'An account with that email already exists.'];
// Check username in pending
$s = db()->prepare("SELECT id FROM pending_registrations WHERE username=? AND expires_at > NOW()");
$s->execute([$username]);
if ($s->fetch()) return ['success' => false, 'error' => 'Username already reserved. Try again in 24 hours or choose another.'];
// Check email in pending — resend if already pending
$s = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()");
$s->execute([$email]);
$existing = $s->fetch();
if ($existing) {
// Resend verification to same email
sendVerificationEmail($email, $alias, $existing['token']);
return ['success' => true, 'resent' => true, 'email' => $email];
}
// Delete any expired pending rows for this email/username
db()->prepare("DELETE FROM pending_registrations WHERE email=? OR (username=? AND expires_at <= NOW())")->execute([$email, $username]);
// Resolve referral code to user ID
$referrerId = null;
if ($referralCode) {
$refStmt = db()->prepare("SELECT id FROM users WHERE referral_code=? AND status='active'");
$refStmt->execute([strtoupper(trim($referralCode))]);
$refUser = $refStmt->fetch();
if ($refUser) $referrerId = (int)$refUser['id'];
}
$token = bin2hex(random_bytes(32));
$hash = password_hash($password, PASSWORD_BCRYPT);
$expiresAt = date('Y-m-d H:i:s', time() + VERIFY_TTL);
$stmt = db()->prepare("INSERT INTO pending_registrations (username, password, alias, email, token, referred_by, expires_at) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$username, $hash, $alias, $email, $token, $referrerId, $expiresAt]);
$sent = sendVerificationEmail($email, $alias, $token);
if (!$sent) {
// Email failed but keep registration — user can resend from login screen
error_log('[TomTomGames] Verification email failed for ' . $email);
return ['success' => true, 'email' => $email, 'mail_warning' => true];
}
return ['success' => true, 'email' => $email];
}
/**
* Consume a verification token: create the real user, delete pending row.
*/
function verifyEmailToken(string $token): array {
$token = trim($token);
if (empty($token)) return ['success' => false, 'error' => 'Invalid verification link.'];
$stmt = db()->prepare("SELECT * FROM pending_registrations WHERE token=? AND expires_at > NOW()");
$stmt->execute([$token]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'This verification link is invalid or has expired. Please register again.'];
}
// Check username/email not taken since pending was created
$s = db()->prepare("SELECT id FROM users WHERE username=? OR email=?");
$s->execute([$pending['username'], $pending['email']]);
if ($s->fetch()) {
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
return ['success' => false, 'error' => 'This username or email was already registered. Please log in.'];
}
db()->beginTransaction();
try {
// Create the user
$ins = db()->prepare("INSERT INTO users (username, password, alias, email, email_verified, status) VALUES (?,?,?,?,1,'active')");
$ins->execute([$pending['username'], $pending['password'], $pending['alias'], $pending['email']]);
$userId = db()->lastInsertId();
// Generate unique referral code
$code = strtoupper(substr(md5($userId . uniqid()), 0, 8));
db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code, $userId]);
// Track referral if referred_by is in pending
if (!empty($pending['referred_by'])) {
$referrerId = (int)$pending['referred_by'];
try {
db()->prepare("INSERT IGNORE INTO referrals (referrer_id, referred_id, status) VALUES (?,?,'pending')")
->execute([$referrerId, $userId]);
db()->prepare("UPDATE users SET referred_by=? WHERE id=?")->execute([$referrerId, $userId]);
} catch(Exception $e) {}
}
// Delete pending row
db()->prepare("DELETE FROM pending_registrations WHERE token=?")->execute([$token]);
db()->commit();
} catch (Exception $e) {
db()->rollBack();
return ['success' => false, 'error' => 'Account creation failed. Please try again.'];
}
// Auto-login
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $pending['username'];
$_SESSION['alias'] = $pending['alias'];
$_SESSION['is_admin'] = 0;
return ['success' => true, 'username' => $pending['username'], 'alias' => $pending['alias']];
}
/**
* Resend verification email for an unverified account.
*/
function resendVerification(string $email): array {
$email = strtolower(trim($email));
$stmt = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW() ORDER BY id DESC LIMIT 1");
$stmt->execute([$email]);
$pending = $stmt->fetch();
if (!$pending) {
return ['success' => false, 'error' => 'No pending registration found for that email, or it has expired. Please register again.'];
}
$alias = db()->prepare("SELECT alias FROM pending_registrations WHERE id=?");
$alias->execute([$pending['id']]);
$row = $alias->fetch();
sendVerificationEmail($email, $row['alias'] ?? 'Player', $pending['token']);
return ['success' => true];
}
function logoutUser(): void {
$_SESSION = [];
session_destroy();
}
// ─── Comprehensive Audit Logger ────────────────────────────
function logActivity(
string $action,
?int $userId = null,
?int $adminId = null,
string $entityType= '',
int $entityId = 0,
string $detail = '',
string $ip = '',
string $category = 'general',
string $oldValue = '',
string $newValue = '',
string $severity = 'info'
): void {
try {
// Auto-purge entries older than 90 days (probabilistic — 3% of calls)
if (rand(1, 100) <= 3) {
db()->exec("DELETE FROM activity_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
}
$ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '');
$userAgent = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 300);
$page = substr(($_SERVER['REQUEST_URI'] ?? ''), 0, 200);
$sessionId = session_id() ?: '';
db()->prepare("
INSERT INTO activity_log
(user_id, admin_id, action, category, entity_type, entity_id, detail,
old_value, new_value, ip, user_agent, page, session_id, severity)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
")->execute([
$userId ?: null,
$adminId ?: null,
substr($action, 0, 120),
$category,
$entityType,
$entityId ?: null,
substr($detail, 0, 2000),
substr($oldValue, 0, 2000),
substr($newValue, 0, 2000),
$ip,
$userAgent,
$page,
$sessionId,
$severity,
]);
} catch (Exception $e) { /* never fail silently */ }
}
// Convenience wrappers
function logPlayerAction(string $action, int $userId, string $detail='', string $category='player', string $severity='info'): void {
logActivity($action, $userId, null, 'user', $userId, $detail, '', $category, '', '', $severity);
}
function logAdminAction(string $action, int $adminId, string $entityType='', int $entityId=0, string $detail='', string $old='', string $new='', string $severity='info'): void {
logActivity($action, null, $adminId, $entityType, $entityId, $detail, '', 'admin', $old, $new, $severity);
}
function logSecurityEvent(string $action, ?int $userId=null, string $detail='', string $severity='warning'): void {
logActivity($action, $userId, null, 'security', 0, $detail, '', 'security', '', '', $severity);
}
// ─── App Version ───────────────────────────────────────────
function getAppVersion(): string {
try {
$v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn();
return $v ?: '1.0.0';
} catch(Exception $e) { return '1.0.0'; }
}