Управление пользователями
Управление пользователями в XOOPS
Заголовок раздела «Управление пользователями в XOOPS»Система управления пользователями XOOPS предоставляет полный набор функций для обработки регистрации пользователей, аутентификации, управления профилем и предпочтений пользователей. Этот документ охватывает структуру объекта пользователя, потоки регистрации и практические примеры реализации.
Структура объекта пользователя
Заголовок раздела «Структура объекта пользователя»Основной объект пользователя в XOOPS — это класс XoopsUser, который инкапсулирует все данные пользователя и методы.
Схема базы данных
Заголовок раздела «Схема базы данных»CREATE TABLE xoops_users ( uid INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, uname VARCHAR(25) NOT NULL UNIQUE, email VARCHAR(60) NOT NULL, pass VARCHAR(255) NOT NULL, pass_expired DATETIME DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_login DATETIME DEFAULT NULL, login_attempts INT(11) DEFAULT 0, user_avatar VARCHAR(255) NOT NULL DEFAULT 'blank.gif', user_regdate INT(11) NOT NULL DEFAULT 0, user_icq VARCHAR(15) NOT NULL DEFAULT '', user_from VARCHAR(100) NOT NULL DEFAULT '', user_sig TEXT, user_sig_smilies TINYINT(1) NOT NULL DEFAULT 1, user_viewemail TINYINT(1) NOT NULL DEFAULT 0, user_attachsig TINYINT(1) NOT NULL DEFAULT 0, user_theme VARCHAR(32) NOT NULL DEFAULT '', user_language VARCHAR(32) NOT NULL DEFAULT '', user_openid VARCHAR(255) NOT NULL DEFAULT '', user_notify_method TINYINT(1) NOT NULL DEFAULT 0, user_notify_interval INT(11) NOT NULL DEFAULT 0);Свойства класса XoopsUser
Заголовок раздела «Свойства класса XoopsUser»class XoopsUser{ protected $uid; protected $uname; protected $email; protected $pass; protected $pass_expired; protected $created_at; protected $updated_at; protected $last_login; protected $login_attempts; protected $user_avatar; protected $user_regdate; protected $user_icq; protected $user_from; protected $user_sig; protected $user_sig_smilies; protected $user_viewemail; protected $user_attachsig; protected $user_theme; protected $user_language; protected $user_openid; protected $user_notify_method; protected $user_notify_interval;}Поток регистрации пользователя
Заголовок раздела «Поток регистрации пользователя»Диаграмма последовательности регистрации
Заголовок раздела «Диаграмма последовательности регистрации»sequenceDiagram participant User as Web User participant Browser participant Server as XOOPS Server participant DB as Database participant Email as Email Service
User->>Browser: Fill Registration Form Browser->>Server: POST /register
Server->>Server: Validate Input note over Server: Check username,<br/>email, password<br/>requirements
alt Validation Fails Server-->>Browser: Show Errors else Validation Passes Server->>Server: Hash Password Server->>DB: Insert User Record DB-->>Server: Success (uid)
Server->>Server: Generate Verification Token Server->>Email: Send Activation Email Email-->>User: Activation Link
Server-->>Browser: Registration Confirmation
User->>Email: Click Activation Link Email-->>Browser: Redirect to Activate URL Browser->>Server: GET /activate?token=xxx
Server->>DB: Mark User as Active DB-->>Server: Confirmed
Server-->>Browser: Account Activated Browser-->>User: Show Success Message endРеализация регистрации
Заголовок раздела «Реализация регистрации»<?php/** * User Registration Handler */class RegistrationHandler{ private $userHandler; private $configHandler;
public function __construct() { $this->userHandler = xoops_getHandler('user'); $this->configHandler = xoops_getHandler('config'); }
/** * Validate registration input * * @param array $data Registration form data * @return array Validation errors, empty if valid */ public function validateInput(array $data): array { $errors = [];
// Username validation if (empty($data['uname'])) { $errors[] = 'Username is required'; } elseif (strlen($data['uname']) < 3) { $errors[] = 'Username must be at least 3 characters'; } elseif (!preg_match('/^[a-zA-Z0-9_-]+$/', $data['uname'])) { $errors[] = 'Username contains invalid characters'; } elseif ($this->userHandler->getUserByName($data['uname'])) { $errors[] = 'Username already exists'; }
// Email validation if (empty($data['email'])) { $errors[] = 'Email is required'; } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors[] = 'Invalid email format'; } elseif ($this->userHandler->getUserByEmail($data['email'])) { $errors[] = 'Email already registered'; }
// Password validation if (empty($data['password'])) { $errors[] = 'Password is required'; } elseif (strlen($data['password']) < 8) { $errors[] = 'Password must be at least 8 characters'; } elseif ($data['password'] !== $data['password_confirm']) { $errors[] = 'Passwords do not match'; }
return $errors; }
/** * Register new user * * @param array $data Registration data * @return XoopsUser|false User object or false on failure */ public function registerUser(array $data) { // Validate input $errors = $this->validateInput($data); if (!empty($errors)) { return false; }
// Create user object $user = $this->userHandler->create(); $user->setVar('uname', $data['uname']); $user->setVar('email', $data['email']); $user->setVar('user_regdate', time());
// Hash password using bcrypt $hashedPassword = password_hash( $data['password'], PASSWORD_BCRYPT, ['cost' => 12] ); $user->setVar('pass', $hashedPassword);
// Set default preferences $user->setVar('user_theme', $this->configHandler->getConfig('default_theme')); $user->setVar('user_language', $this->configHandler->getConfig('default_language'));
// Save user if ($this->userHandler->insertUser($user)) { $uid = $user->getVar('uid');
// Generate verification token $token = bin2hex(random_bytes(32)); $this->saveVerificationToken($uid, $token);
// Send verification email $this->sendVerificationEmail($user, $token);
return $user; }
return false; }
/** * Save verification token * * @param int $uid User ID * @param string $token Verification token */ private function saveVerificationToken(int $uid, string $token): void { $tokenHandler = xoops_getHandler('usertoken'); $tokenHandler->saveToken($uid, $token, 'email_verification', 24); // 24 hours }
/** * Send verification email * * @param XoopsUser $user User object * @param string $token Verification token */ private function sendVerificationEmail(XoopsUser $user, string $token): void { global $xoopsConfig;
$siteUrl = $xoopsConfig['siteurl']; $activationUrl = $siteUrl . '/user.php?op=activate&token=' . $token;
$subject = 'Email Verification - ' . $xoopsConfig['sitename']; $body = "Hello " . $user->getVar('uname') . ",\n\n"; $body .= "Please click the link below to verify your email:\n"; $body .= $activationUrl . "\n\n"; $body .= "This link will expire in 24 hours.\n\n"; $body .= "Regards,\n" . $xoopsConfig['sitename'];
$mailHandler = xoops_getHandler('mail'); $mailHandler->send($user->getVar('email'), $subject, $body); }}Процесс аутентификации пользователя
Заголовок раздела «Процесс аутентификации пользователя»Диаграмма потока аутентификации
Заголовок раздела «Диаграмма потока аутентификации»graph TD A["Login Form"] --> B["Username/Email & Password"] B --> C{"Input Validation"} C -->|Invalid| D["Show Error"] C -->|Valid| E["Query User Database"] E --> F{"User Found?"} F -->|No| G["Invalid Credentials"] G --> H["Log Failed Attempt"] H --> I{"Max Attempts?"} I -->|Yes| J["Account Locked"] I -->|No| D F -->|Yes| K["Verify Password Hash"] K --> L{"Hash Match?"} L -->|No| H L -->|Yes| M["Check Account Status"] M --> N{"Account Active?"} N -->|No| O["Account Inactive/Suspended"] N -->|Yes| P["Create Session"] P --> Q["Store Session Token"] Q --> R{"Remember Me?"} R -->|Yes| S["Set Long-lived Cookie"] R -->|No| T["Set Session Cookie"] S --> U["Update Last Login"] T --> U U --> V["Redirect to Dashboard"]Реализация аутентификации
Заголовок раздела «Реализация аутентификации»<?php/** * Authentication Handler */class AuthenticationHandler{ private $userHandler; private $maxLoginAttempts = 5; private $lockoutDuration = 900; // 15 minutes
public function __construct() { $this->userHandler = xoops_getHandler('user'); }
/** * Authenticate user by username/email and password * * @param string $username Username or email * @param string $password Plain text password * @param bool $rememberMe Remember login * @return XoopsUser|false Authenticated user or false */ public function authenticate( string $username, string $password, bool $rememberMe = false ) { // Check account lockout if ($this->isAccountLocked($username)) { throw new Exception('Account temporarily locked due to failed login attempts'); }
// Find user by username or email $user = $this->userHandler->getUserByName($username); if (!$user) { $user = $this->userHandler->getUserByEmail($username); }
if (!$user) { $this->recordFailedAttempt($username); return false; }
// Verify password if (!password_verify($password, $user->getVar('pass'))) { $this->recordFailedAttempt($username); return false; }
// Check account status if ($user->getVar('level') == 0) { throw new Exception('Account is inactive'); }
// Clear failed attempts $this->clearFailedAttempts($user->getVar('uid'));
// Update last login $user->setVar('last_login', date('Y-m-d H:i:s')); $this->userHandler->insertUser($user);
// Create session $this->createSession($user, $rememberMe);
return $user; }
/** * Create authenticated session * * @param XoopsUser $user User object * @param bool $rememberMe Enable persistent login */ private function createSession(XoopsUser $user, bool $rememberMe = false): void { // Generate session token $token = bin2hex(random_bytes(32));
$_SESSION['xoopsUserId'] = $user->getVar('uid'); $_SESSION['xoopsUserName'] = $user->getVar('uname'); $_SESSION['xoopsSessionToken'] = $token; $_SESSION['xoopsSessionCreated'] = time();
// Store token in database for validation $this->storeSessionToken($user->getVar('uid'), $token);
if ($rememberMe) { // Create persistent login cookie (14 days) $cookieToken = bin2hex(random_bytes(32)); setcookie( 'xoops_persistent_login', $cookieToken, time() + (14 * 24 * 60 * 60), '/', '', true, // HTTPS only true // HttpOnly );
// Store cookie token hash $this->storePersistentToken( $user->getVar('uid'), hash('sha256', $cookieToken) ); } }
/** * Record failed login attempt * * @param string $username Username or email */ private function recordFailedAttempt(string $username): void { $key = 'login_attempt_' . md5($username); $attempts = apcu_fetch($key) ?: 0; apcu_store($key, $attempts + 1, $this->lockoutDuration); }
/** * Check if account is locked * * @param string $username Username or email * @return bool True if locked */ private function isAccountLocked(string $username): bool { $key = 'login_attempt_' . md5($username); $attempts = apcu_fetch($key) ?: 0; return $attempts >= $this->maxLoginAttempts; }
/** * Clear failed attempts * * @param int $uid User ID */ private function clearFailedAttempts(int $uid): void { $user = $this->userHandler->getUser($uid); $user->setVar('login_attempts', 0); $this->userHandler->insertUser($user); }
/** * Store session token * * @param int $uid User ID * @param string $token Session token */ private function storeSessionToken(int $uid, string $token): void { // Store in database or cache $tokenData = [ 'uid' => $uid, 'token' => hash('sha256', $token), 'created' => time(), 'expires' => time() + (8 * 60 * 60) // 8 hours ];
$db = XoopsDatabaseFactory::getDatabaseConnection(); $db->query("INSERT INTO xoops_sessions (uid, token, created, expires) VALUES (?, ?, ?, ?)", array($uid, $tokenData['token'], $tokenData['created'], $tokenData['expires'])); }}Управление профилем
Заголовок раздела «Управление профилем»Реализация обновления профиля
Заголовок раздела «Реализация обновления профиля»<?php/** * User Profile Management */class ProfileManager{ private $userHandler; private $avatarHandler;
public function __construct() { $this->userHandler = xoops_getHandler('user'); $this->avatarHandler = xoops_getHandler('avatar'); }
/** * Update user profile * * @param int $uid User ID * @param array $data Profile data * @return bool Success status */ public function updateProfile(int $uid, array $data): bool { $user = $this->userHandler->getUser($uid); if (!$user) { return false; }
// Update profile fields if (isset($data['email'])) { // Verify email is unique (excluding current user) $existingUser = $this->userHandler->getUserByEmail($data['email']); if ($existingUser && $existingUser->getVar('uid') !== $uid) { throw new Exception('Email already in use'); } $user->setVar('email', $data['email']); }
if (isset($data['user_icq'])) { $user->setVar('user_icq', sanitize_text_field($data['user_icq'])); }
if (isset($data['user_from'])) { $user->setVar('user_from', sanitize_text_field($data['user_from'])); }
if (isset($data['user_sig'])) { $sig = $data['user_sig']; if (strlen($sig) > 500) { throw new Exception('Signature too long'); } $user->setVar('user_sig', $sig); }
if (isset($data['user_sig_smilies'])) { $user->setVar('user_sig_smilies', (int)$data['user_sig_smilies']); }
if (isset($data['user_viewemail'])) { $user->setVar('user_viewemail', (int)$data['user_viewemail']); }
if (isset($data['user_attachsig'])) { $user->setVar('user_attachsig', (int)$data['user_attachsig']); }
if (isset($data['user_theme'])) { $user->setVar('user_theme', $data['user_theme']); }
if (isset($data['user_language'])) { $user->setVar('user_language', $data['user_language']); }
return $this->userHandler->insertUser($user); }
/** * Change user password * * @param int $uid User ID * @param string $currentPassword Current password * @param string $newPassword New password * @return bool Success status */ public function changePassword( int $uid, string $currentPassword, string $newPassword ): bool { $user = $this->userHandler->getUser($uid); if (!$user) { return false; }
// Verify current password if (!password_verify($currentPassword, $user->getVar('pass'))) { throw new Exception('Current password is incorrect'); }
// Validate new password if (strlen($newPassword) < 8) { throw new Exception('New password must be at least 8 characters'); }
// Hash new password $hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 12]); $user->setVar('pass', $hashedPassword);
return $this->userHandler->insertUser($user); }
/** * Get user profile data * * @param int $uid User ID * @return array Profile data */ public function getProfile(int $uid): array { $user = $this->userHandler->getUser($uid); if (!$user) { return []; }
return [ 'uid' => $user->getVar('uid'), 'uname' => $user->getVar('uname'), 'email' => $user->getVar('email'), 'user_regdate' => $user->getVar('user_regdate'), 'user_icq' => $user->getVar('user_icq'), 'user_from' => $user->getVar('user_from'), 'user_sig' => $user->getVar('user_sig'), 'user_viewemail' => $user->getVar('user_viewemail'), 'user_attachsig' => $user->getVar('user_attachsig'), 'user_theme' => $user->getVar('user_theme'), 'user_language' => $user->getVar('user_language'), 'last_login' => $user->getVar('last_login'), 'avatar' => $user->getVar('user_avatar') ]; }}Обработка аватара
Заголовок раздела «Обработка аватара»Управление аватаром
Заголовок раздела «Управление аватаром»<?php/** * User Avatar Handler */class AvatarHandler{ private $avatarPath = '/uploads/avatars/'; private $maxSize = 2097152; // 2MB private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
/** * Upload user avatar * * @param int $uid User ID * @param array $file $_FILES array * @return string|false Avatar filename or false */ public function uploadAvatar(int $uid, array $file) { // Validate file if ($file['error'] !== UPLOAD_ERR_OK) { throw new Exception('File upload error: ' . $file['error']); }
if ($file['size'] > $this->maxSize) { throw new Exception('File too large (max 2MB)'); }
if (!in_array($file['type'], $this->allowedTypes)) { throw new Exception('Invalid file type'); }
// Verify MIME type $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo);
if (!in_array($mimeType, $this->allowedTypes)) { throw new Exception('Invalid file content'); }
// Generate unique filename $extension = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = 'avatar_' . $uid . '_' . time() . '.' . $extension;
// Create upload directory $uploadDir = XOOPS_ROOT_PATH . $this->avatarPath; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); }
$filepath = $uploadDir . $filename;
// Move uploaded file if (!move_uploaded_file($file['tmp_name'], $filepath)) { throw new Exception('Failed to move uploaded file'); }
// Resize image to standard size (150x150) $this->resizeImage($filepath, 150, 150);
// Update user avatar $userHandler = xoops_getHandler('user'); $user = $userHandler->getUser($uid);
// Delete old avatar if exists $oldAvatar = $user->getVar('user_avatar'); if ($oldAvatar && $oldAvatar !== 'blank.gif') { $oldPath = $uploadDir . $oldAvatar; if (file_exists($oldPath)) { unlink($oldPath); } }
// Save new avatar $user->setVar('user_avatar', $filename); $userHandler->insertUser($user);
return $filename; }
/** * Resize image to specified dimensions * * @param string $filepath Path to image file * @param int $width Target width * @param int $height Target height */ private function resizeImage(string $filepath, int $width, int $height): void { if (!extension_loaded('gd')) { return; // GD not available, skip resizing }
$image = imagecreatefromstring(file_get_contents($filepath)); if (!$image) { return; }
$resized = imagecreatetruecolor($width, $height);
// Preserve transparency for PNG and GIF $format = mime_content_type($filepath); if ($format === 'image/png' || $format === 'image/gif') { imagealphablending($resized, false); imagesavealpha($resized, true); }
imagecopyresampled( $resized, $image, 0, 0, 0, 0, $width, $height, imagesx($image), imagesy($image) );
// Save resized image $ext = pathinfo($filepath, PATHINFO_EXTENSION); if (strtolower($ext) === 'png') { imagepng($resized, $filepath, 9); } else { imagejpeg($resized, $filepath, 90); }
imagedestroy($image); imagedestroy($resized); }
/** * Delete user avatar * * @param int $uid User ID * @return bool Success status */ public function deleteAvatar(int $uid): bool { $userHandler = xoops_getHandler('user'); $user = $userHandler->getUser($uid);
if (!$user) { return false; }
$avatar = $user->getVar('user_avatar'); if ($avatar && $avatar !== 'blank.gif') { $filepath = XOOPS_ROOT_PATH . $this->avatarPath . $avatar; if (file_exists($filepath)) { unlink($filepath); } }
$user->setVar('user_avatar', 'blank.gif'); return $userHandler->insertUser($user); }}Предпочтения пользователя
Заголовок раздела «Предпочтения пользователя»Система предпочтений
Заголовок раздела «Система предпочтений»<?php/** * User Preferences Handler */class UserPreferencesHandler{ private $userHandler; private $prefixCache = 'user_pref_';
public function __construct() { $this->userHandler = xoops_getHandler('user'); }
/** * Get user preference * * @param int $uid User ID * @param string $prefKey Preference key * @param mixed $default Default value * @return mixed Preference value */ public function getPreference(int $uid, string $prefKey, $default = null) { // Try cache first $cacheKey = $this->prefixCache . $uid . '_' . $prefKey; $cached = apcu_fetch($cacheKey); if ($cached !== false) { return $cached; }
// Get from database $db = XoopsDatabaseFactory::getDatabaseConnection(); $result = $db->query( "SELECT pref_value FROM xoops_user_preferences WHERE uid = ? AND pref_key = ?", array($uid, $prefKey) );
if ($result && $db->getRowCount($result) > 0) { $row = $db->fetchArray($result); $value = unserialize($row['pref_value']); apcu_store($cacheKey, $value, 3600); // Cache for 1 hour return $value; }
return $default; }
/** * Set user preference * * @param int $uid User ID * @param string $prefKey Preference key * @param mixed $prefValue Preference value * @return bool Success status */ public function setPreference(int $uid, string $prefKey, $prefValue): bool { $db = XoopsDatabaseFactory::getDatabaseConnection();
// Check if preference exists $result = $db->query( "SELECT id FROM xoops_user_preferences WHERE uid = ? AND pref_key = ?", array($uid, $prefKey) );
$serialized = serialize($prefValue);
if ($db->getRowCount($result) > 0) { // Update existing preference $success = $db->query( "UPDATE xoops_user_preferences SET pref_value = ? WHERE uid = ? AND pref_key = ?", array($serialized, $uid, $prefKey) ); } else { // Insert new preference $success = $db->query( "INSERT INTO xoops_user_preferences (uid, pref_key, pref_value) VALUES (?, ?, ?)", array($uid, $prefKey, $serialized) ); }
if ($success) { // Clear cache $cacheKey = $this->prefixCache . $uid . '_' . $prefKey; apcu_delete($cacheKey); }
return (bool)$success; }
/** * Get all user preferences * * @param int $uid User ID * @return array All preferences */ public function getAllPreferences(int $uid): array { $db = XoopsDatabaseFactory::getDatabaseConnection(); $result = $db->query( "SELECT pref_key, pref_value FROM xoops_user_preferences WHERE uid = ?", array($uid) );
$prefs = []; while ($row = $db->fetchArray($result)) { $prefs[$row['pref_key']] = unserialize($row['pref_value']); }
return $prefs; }
/** * Delete user preference * * @param int $uid User ID * @param string $prefKey Preference key * @return bool Success status */ public function deletePreference(int $uid, string $prefKey): bool { $db = XoopsDatabaseFactory::getDatabaseConnection(); $success = $db->query( "DELETE FROM xoops_user_preferences WHERE uid = ? AND pref_key = ?", array($uid, $prefKey) );
if ($success) { $cacheKey = $this->prefixCache . $uid . '_' . $prefKey; apcu_delete($cacheKey); }
return (bool)$success; }}Примеры операций с пользователями
Заголовок раздела «Примеры операций с пользователями»Основные операции с пользователями
Заголовок раздела «Основные операции с пользователями»<?php/** * Common user operations examples */
// Get current logged-in user$xoopsUser = $GLOBALS['xoopsUser'];if ($xoopsUser instanceof XoopsUser) { $userId = $xoopsUser->getVar('uid'); $username = $xoopsUser->getVar('uname');}
// Get user by ID$userHandler = xoops_getHandler('user');$user = $userHandler->getUser(1);echo $user->getVar('uname');
// Get user by username$user = $userHandler->getUserByName('admin');if ($user) { echo $user->getVar('email');}
// Get user by email$user = $userHandler->getUserByEmail('user@example.com');
// Get all users in a group$users = $userHandler->getUsersByGroup(1);foreach ($users as $user) { echo $user->getVar('uname') . "\n";}
// Create new user$user = $userHandler->create();$user->setVar('uname', 'newuser');$user->setVar('email', 'newuser@example.com');$user->setVar('pass', password_hash('password', PASSWORD_BCRYPT));$user->setVar('user_regdate', time());
if ($userHandler->insertUser($user)) { echo "User created: " . $user->getVar('uid');}
// Delete user$userHandler->deleteUser(123);
// Get user object from ID$user = $userHandler->getUser(5);$profile = [ 'username' => $user->getVar('uname'), 'email' => $user->getVar('email'), 'regdate' => date('Y-m-d', $user->getVar('user_regdate')), 'avatar' => $user->getVar('user_avatar'),];Лучшие практики безопасности
Заголовок раздела «Лучшие практики безопасности»Безопасность паролей
Заголовок раздела «Безопасность паролей»- Всегда используйте
password_hash()с алгоритмомPASSWORD_BCRYPT - Используйте параметр cost равный 12 для bcrypt
- Никогда не храните пароли в открытом виде
- Реализуйте политики истечения паролей
- Требуйте изменения пароля для скомпрометированных учетных записей
Безопасность сеансов
Заголовок раздела «Безопасность сеансов»<?php// Session configurationsession_set_cookie_params([ 'lifetime' => 0, // Session cookie (deleted on browser close) 'path' => '/', 'domain' => '', 'secure' => true, // HTTPS only 'httponly' => true, // Inaccessible to JavaScript 'samesite' => 'Strict' // CSRF protection]);
session_start();
// Regenerate session ID after loginsession_regenerate_id(true);
// Validate session tokenif (!isset($_SESSION['xoopsSessionToken'])) { session_destroy(); redirect('login');}Связанные ссылки
Заголовок раздела «Связанные ссылки»- Group System.md
- Permission System.md
- Authentication.md
- ../../Security/Security-Guidelines.md
#users #registration #authentication #profiles #password-security #sessions