userModel = new UserModel(); $this->emailLibrary = new EmailLibrary(); try { $this->rateLimitService = RateLimitService::getInstance(); } catch (\Exception $e) { log_message('warning', 'RateLimitService недоступен: ' . $e->getMessage()); $this->rateLimitService = null; } } /** * Проверка rate limiting */ protected function checkRateLimit(string $action): ?array { if ($this->rateLimitService === null) { return null; } if ($this->rateLimitService->isBlocked($action)) { $ttl = $this->rateLimitService->getBlockTimeLeft($action); return [ 'blocked' => true, 'message' => "Слишком много попыток. Повторите через {$ttl} секунд.", 'ttl' => $ttl, ]; } return null; } /** * Сброс счётчика после успешного действия */ protected function resetRateLimit(string $action): void { if ($this->rateLimitService !== null) { $this->rateLimitService->resetAttempts($action); } } /** * Запись неудачной попытки */ protected function recordFailedAttempt(string $action): ?array { if ($this->rateLimitService === null) { return null; } $result = $this->rateLimitService->recordFailedAttempt($action); if ($result['blocked']) { return [ 'blocked' => true, 'message' => "Превышено максимальное количество попыток. Доступ заблокирован на {$result['block_ttl']} секунд.", 'ttl' => $result['block_ttl'], ]; } return null; } /** * Отображение формы запроса сброса пароля */ public function index() { // Если пользователь уже авторизован - редирект на главную if (session()->get('isLoggedIn')) { return redirect()->to('/'); } return $this->renderTwig('auth/forgot_password'); } /** * Отправка email для сброса пароля */ public function sendResetLink() { if ($this->request->getMethod() !== 'POST') { return redirect()->to('/forgot-password'); } // Проверка rate limiting $rateLimitError = $this->checkRateLimit('reset'); if ($rateLimitError !== null) { return redirect()->back() ->with('error', $rateLimitError['message']) ->withInput(); } $email = trim($this->request->getPost('email')); if (empty($email)) { return redirect()->back()->with('error', 'Введите email адрес')->withInput(); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return redirect()->back()->with('error', 'Введите корректный email адрес')->withInput(); } $user = $this->userModel->findByEmail($email); // Независимо от результата показываем одно и то же сообщение // Это предотвращает перебор email адресов if (!$user) { // Засчитываем неудачную попытку для защиты от перебора $this->recordFailedAttempt('reset'); } if ($user) { // Генерируем токен сброса $token = $this->userModel->generateResetToken($user['id']); // Отправляем email $this->emailLibrary->sendPasswordResetEmail( $user['email'], $user['name'], $token ); log_message('info', "Password reset link sent to {$email}"); } // Всегда показываем одно и то же сообщение $this->resetRateLimit('reset'); return redirect()->back()->with( 'success', 'Если email зарегистрирован в системе, на него будет отправлена ссылка для сброса пароля.' ); } /** * Отображение формы сброса пароля (по токену из URL) */ public function reset($token = null) { // Если пользователь уже авторизован - редирект if (session()->get('isLoggedIn')) { return redirect()->to('/'); } if (empty($token)) { return redirect()->to('/forgot-password')->with('error', 'Недействительная ссылка для сброса пароля.'); } // Проверяем токен $user = $this->userModel->verifyResetToken($token); if (!$user) { return redirect()->to('/forgot-password')->with('error', 'Ссылка для сброса пароля истекла или недействительна.'); } return $this->renderTwig('auth/reset_password', [ 'token' => $token, 'email' => $user['email'], ]); } /** * Обновление пароля */ public function updatePassword() { if ($this->request->getMethod() !== 'POST') { return redirect()->to('/forgot-password'); } $token = $this->request->getPost('token'); $password = $this->request->getPost('password'); $passwordConfirm = $this->request->getPost('password_confirm'); // Валидация if (empty($token)) { return redirect()->back()->with('error', 'Ошибка валидации токена.'); } if (empty($password)) { return redirect()->back()->with('error', 'Введите новый пароль')->withInput(); } if (strlen($password) < 6) { return redirect()->back()->with('error', 'Пароль должен содержать минимум 6 символов')->withInput(); } if ($password !== $passwordConfirm) { return redirect()->back()->with('error', 'Пароли не совпадают')->withInput(); } // Проверяем токен $user = $this->userModel->verifyResetToken($token); if (!$user) { return redirect()->to('/forgot-password')->with('error', 'Ссылка для сброса пароля истекла или недействительна.'); } // Обновляем пароль $this->userModel->update($user['id'], ['password' => $password]); // Очищаем токен $this->userModel->clearResetToken($user['id']); // Удаляем все remember-токены пользователя (нужна будет новая авторизация) $db = \Config\Database::connect(); $db->table('remember_tokens')->where('user_id', $user['id'])->delete(); log_message('info', "Password reset completed for user {$user['email']}"); return redirect()->to('/login')->with( 'success', 'Пароль успешно изменён. Теперь вы можете войти с новым паролем.' ); } }