0, 'path' => '/', 'domain' => '', 'secure' => $secure, 'httponly' => true, 'samesite' => $samesite ]); if (!session_start()) { error_log("Failed to start session"); } } } start_secure_session(); /** * Authentication Helper */ class Auth { public static function isLoggedIn(): bool { return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']); } public static function requireLogin(): void { if (!self::isLoggedIn()) { header('Location: login.php'); exit; } } public static function login(int $userId, int $tenantId, string $role): void { if (session_status() === PHP_SESSION_NONE) { start_secure_session(); } $_SESSION['user_id'] = $userId; $_SESSION['tenant_id'] = $tenantId; $_SESSION['role'] = $role; // Important: Save session before geolocation which might be slow session_write_close(); // Tracking $ip = self::getIpAddress(); $country = self::getCountryFromIp($ip); $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null; try { // Re-open to update tracking info in DB if we want, // but we can just use a fresh DB connection $stmt = db()->prepare("INSERT INTO user_sessions (user_id, ip_address, country, user_agent) VALUES (?, ?, ?, ?)"); $stmt->execute([$userId, $ip, $country, $userAgent]); $stmt = db()->prepare("UPDATE users SET last_login_at = NOW(), last_login_ip = ? WHERE id = ?"); $stmt->execute([$ip, $userId]); } catch (\Throwable $e) { error_log("Auth::login tracking error: " . $e->getMessage()); } } public static function logout(): void { if (session_status() === PHP_SESSION_NONE) { start_secure_session(); } $_SESSION = []; $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); session_destroy(); header('Location: login.php', true, 302); exit; } public static function getIpAddress(): string { if (!empty($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } else { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } } public static function getCountryFromIp(string $ip): ?string { if ($ip === '127.0.0.1' || $ip === '::1') return 'Localhost'; try { $ctx = stream_context_create(['http' => ['timeout' => 2]]); $resp = @file_get_contents("http://ip-api.com/json/{$ip}?fields=country", false, $ctx); if ($resp) { $data = json_decode($resp, true); return $data['country'] ?? 'Unknown'; } } catch (\Throwable $e) { } return 'Unknown'; } public static function recordResetAttempt(string $email, string $ip): void { try { $stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)"); $stmt->execute([0, 'Password Reset Attempt', "Email: $email, IP: $ip"]); } catch (\Throwable $e) {} } }