userModel = new UserModel(); $this->orgModel = new OrganizationModel(); } /** * Главная страница профиля */ public function index() { $userId = $this->getCurrentUserId(); $user = $this->userModel->find($userId); return $this->renderTwig('profile/index', [ 'title' => 'Профиль', 'user' => $user, 'active_tab' => 'general', ]); } /** * Вкладка "Организации" */ public function organizations() { $userId = $this->getCurrentUserId(); $user = $this->userModel->find($userId); $currentOrgId = $this->session->get('active_org_id'); // Получаем все организации пользователя $orgUserModel = $this->getOrgUserModel(); $memberships = $orgUserModel->where('user_id', $userId)->findAll(); $orgIds = array_column($memberships, 'organization_id'); $organizations = []; if (!empty($orgIds)) { $organizations = $this->orgModel->whereIn('id', $orgIds)->findAll(); } // Объединяем данные $orgList = []; foreach ($organizations as $org) { // Находим соответствующий membership $membership = null; foreach ($memberships as $m) { if ($m['organization_id'] == $org['id']) { $membership = $m; break; } } $orgList[] = [ 'id' => $org['id'], 'name' => $org['name'], 'type' => $org['type'], 'role' => $membership['role'] ?? 'guest', 'status' => $membership['status'] ?? 'active', 'joined_at' => $membership['joined_at'] ?? null, 'is_owner' => ($membership['role'] ?? '') === 'owner', 'is_current_org' => ((int) $org['id'] === (int) $currentOrgId), ]; } return $this->renderTwig('profile/organizations', [ 'title' => 'Мои организации', 'user' => $user, 'organizations' => $orgList, 'active_tab' => 'organizations', ]); } /** * Вкладка "Безопасность" */ public function security() { $userId = $this->getCurrentUserId(); $user = $this->userModel->find($userId); // Получаем список активных сессий $sessions = $this->getUserSessions($userId); return $this->renderTwig('profile/security', [ 'title' => 'Безопасность', 'user' => $user, 'active_tab' => 'security', 'sessions' => $sessions, 'currentSessionId' => session_id(), ]); } /** * Получение списка активных сессий пользователя */ protected function getUserSessions(int $userId): array { $db = \Config\Database::connect(); // Получаем сессии из таблицы ci_sessions (требует настройки DatabaseHandler) // Также получаем remember-токены $rememberTokens = $db->table('remember_tokens') ->where('user_id', $userId) ->where('expires_at >', date('Y-m-d H:i:s')) ->get() ->getResultArray(); $sessions = []; // Добавляем remember-токены как сессии foreach ($rememberTokens as $token) { $sessions[] = [ 'id' => 'remember_' . $token['id'], 'type' => 'remember', 'device' => $this->parseUserAgent($token['user_agent'] ?? ''), 'ip_address' => $token['ip_address'] ?? 'Unknown', 'created_at' => $token['created_at'], 'expires_at' => $token['expires_at'], 'is_current' => false, ]; } return $sessions; } /** * Парсинг User Agent для получения информации об устройстве */ protected function parseUserAgent(string $userAgent): string { if (empty($userAgent)) { return 'Неизвестное устройство'; } // Определяем браузер $browser = 'Unknown'; if (preg_match('/Firefox\/([0-9.]+)/', $userAgent, $matches)) { $browser = 'Firefox'; } elseif (preg_match('/Chrome\/([0-9.]+)/', $userAgent, $matches)) { $browser = 'Chrome'; } elseif (preg_match('/Safari\/([0-9.]+)/', $userAgent, $matches)) { $browser = 'Safari'; } elseif (preg_match('/MSIE\s+([0-9.]+)/', $userAgent, $matches) || preg_match('/Trident\/([0-9.]+)/', $userAgent, $matches)) { $browser = 'Internet Explorer'; } elseif (preg_match('/Edg\/([0-9.]+)/', $userAgent, $matches)) { $browser = 'Edge'; } // Определяем ОС $os = 'Unknown OS'; if (preg_match('/Windows/', $userAgent)) { $os = 'Windows'; } elseif (preg_match('/Mac OS X/', $userAgent)) { $os = 'macOS'; } elseif (preg_match('/Linux/', $userAgent)) { $os = 'Linux'; } elseif (preg_match('/Android/', $userAgent)) { $os = 'Android'; } elseif (preg_match('/iPhone|iPad|iPod/', $userAgent)) { $os = 'iOS'; } return "{$browser} на {$os}"; } /** * Завершение конкретной сессии */ public function revokeSession() { $userId = $this->getCurrentUserId(); $sessionId = $this->request->getPost('session_id'); if (empty($sessionId)) { return redirect()->to('/profile/security')->with('error', 'Сессия не найдена'); } $db = \Config\Database::connect(); // Проверяем, что сессия принадлежит пользователю if (strpos($sessionId, 'remember_') === 0) { // Это remember-токен $tokenId = (int) str_replace('remember_', '', $sessionId); $token = $db->table('remember_tokens') ->where('id', $tokenId) ->where('user_id', $userId) ->get() ->getRowArray(); if ($token) { $db->table('remember_tokens')->where('id', $tokenId)->delete(); log_message('info', "User {$userId} revoked remember token {$tokenId}"); } } return redirect()->to('/profile/security')->with('success', 'Сессия завершена'); } /** * Завершение всех сессий (кроме текущей) */ public function revokeAllSessions() { $userId = $this->getCurrentUserId(); // Удаляем все remember-токены $db = \Config\Database::connect(); $db->table('remember_tokens')->where('user_id', $userId)->delete(); // Регенерируем текущую сессию $this->session->regenerate(true); log_message('info', "User {$userId} revoked all sessions"); return redirect()->to('/profile/security')->with( 'success', 'Все сессии на других устройствах завершены. Вы остались авторизованы на текущем устройстве.' ); } /** * Обновление имени пользователя */ public function updateName() { $userId = $this->getCurrentUserId(); $name = trim($this->request->getPost('name')); if (empty($name)) { $this->session->setFlashdata('error', 'Имя обязательно для заполнения'); return redirect()->to('/profile'); } if (strlen($name) < 3) { $this->session->setFlashdata('error', 'Имя должно содержать минимум 3 символа'); return redirect()->to('/profile'); } $this->userModel->update($userId, ['name' => $name]); $this->session->set('name', $name); $this->session->setFlashdata('success', 'Имя успешно обновлено'); return redirect()->to('/profile'); } /** * Загрузка аватара */ public function uploadAvatar() { $userId = $this->getCurrentUserId(); $file = $this->request->getFile('avatar'); if (!$file || !$file->isValid()) { $this->session->setFlashdata('error', 'Ошибка загрузки файла'); return redirect()->to('/profile'); } // Валидация $allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; $maxSize = 2 * 1024 * 1024; // 2MB if (!in_array($file->getMimeType(), $allowedTypes)) { $this->session->setFlashdata('error', 'Разрешены только файлы JPG, PNG и GIF'); return redirect()->to('/profile'); } if ($file->getSize() > $maxSize) { $this->session->setFlashdata('error', 'Максимальный размер файла - 2 МБ'); return redirect()->to('/profile'); } // Создаём директорию для аватаров если нет $uploadPath = ROOTPATH . 'public/uploads/avatars'; if (!is_dir($uploadPath)) { mkdir($uploadPath, 0755, true); } // Генерируем уникальное имя файла $extension = $file->getClientExtension(); $newFileName = 'avatar_' . $userId . '_' . time() . '.' . $extension; // Перемещаем файл $file->move($uploadPath, $newFileName); // Удаляем старый аватар если был $user = $this->userModel->find($userId); if (!empty($user['avatar']) && file_exists($uploadPath . '/' . $user['avatar'])) { @unlink($uploadPath . '/' . $user['avatar']); } // Обновляем путь к аватару в базе $this->userModel->update($userId, ['avatar' => $newFileName]); $this->session->setFlashdata('success', 'Аватар успешно загружен'); return redirect()->to('/profile'); } /** * Смена пароля */ public function changePassword() { $userId = $this->getCurrentUserId(); $user = $this->userModel->find($userId); $currentPassword = $this->request->getPost('current_password'); $newPassword = $this->request->getPost('new_password'); $confirmPassword = $this->request->getPost('confirm_password'); // Валидация if (empty($currentPassword)) { $this->session->setFlashdata('error', 'Введите текущий пароль'); return redirect()->to('/profile/security'); } if (empty($newPassword)) { $this->session->setFlashdata('error', 'Введите новый пароль'); return redirect()->to('/profile/security'); } if (strlen($newPassword) < 6) { $this->session->setFlashdata('error', 'Новый пароль должен содержать минимум 6 символов'); return redirect()->to('/profile/security'); } if ($newPassword !== $confirmPassword) { $this->session->setFlashdata('error', 'Пароли не совпадают'); return redirect()->to('/profile/security'); } // Проверяем текущий пароль if (!password_verify($currentPassword, $user['password'])) { $this->session->setFlashdata('error', 'Неверный текущий пароль'); return redirect()->to('/profile/security'); } // Обновляем пароль $this->userModel->update($userId, ['password' => $newPassword]); // Завершаем все сессии пользователя (кроме текущей) $this->endAllUserSessions($userId); $this->session->setFlashdata('success', 'Пароль успешно изменён. Для безопасности вы будете разлогинены на всех устройствах.'); return redirect()->to('/logout'); } /** * Завершение всех сессий пользователя */ private function endAllUserSessions(int $userId): void { // Удаляем все remember-токены пользователя $db = \Config\Database::connect(); $db->table('remember_tokens')->where('user_id', $userId)->delete(); // Регенерируем ID текущей сессии $this->session->regenerate(true); } }