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