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: /admin/login.php'); 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'])) { logActivity('LOGIN_FAILED', null, null, 'auth', 0, 'Failed login: '.$username, '', 'security', '', '', 'warning'); return ['success' => false, 'error' => 'Invalid username or password.']; } if (!$user['email_verified'] && !$user['is_admin']) { return ['success'=>false,'error'=>'Please verify your email address before logging in.','unverified'=>true,'email'=>$user['email']]; } 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]; } function initiateRegistration(string $username, string $password, string $alias, string $email, string $referralCode = ''): array { $username = strtolower(trim($username)); $alias = trim($alias); $email = strtolower(trim($email)); 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.']; $s = db()->prepare("SELECT id FROM users WHERE username=?"); $s->execute([$username]); if ($s->fetch()) return ['success'=>false,'error'=>'Username already taken.']; $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.']; $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.']; $s = db()->prepare("SELECT id, token FROM pending_registrations WHERE email=? AND expires_at > NOW()"); $s->execute([$email]); $existing = $s->fetch(); if ($existing) { sendVerificationEmail($email, $alias, $existing['token']); return ['success'=>true,'resent'=>true,'email'=>$email]; } db()->prepare("DELETE FROM pending_registrations WHERE email=? OR (username=? AND expires_at <= NOW())")->execute([$email, $username]); $referrerId = null; if ($referralCode) { $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, ['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]); $sent = sendVerificationEmail($email, $alias, $token); if (!$sent) { return ['success'=>true,'email'=>$email,'mail_warning'=>true]; } return ['success'=>true,'email'=>$email]; } 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'=>'Verification link is invalid or has expired.']; db()->beginTransaction(); try { $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(); $code = strtoupper(substr(md5($userId.uniqid()),0,8)); db()->prepare("UPDATE users SET referral_code=? WHERE id=?")->execute([$code,$userId]); if (!empty($pending['referred_by'])) { try { 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) {} } 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.']; } return ['success'=>true,'user_id'=>$userId,'username'=>$pending['username'],'alias'=>$pending['alias']]; } // ── 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 { 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,$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) {} } 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); } function getAppVersion(): string { try { $v = db()->query("SELECT version FROM app_version ORDER BY id DESC LIMIT 1")->fetchColumn(); 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); }