bp/app/Controllers/ForgotPassword.php

244 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Controllers;
use App\Models\UserModel;
use App\Libraries\EmailLibrary;
use App\Services\RateLimitService;
/**
* ForgotPasswordController - Восстановление пароля
*
* Обрабатывает запросы на сброс пароля:
* 1. Форма ввода email для отправки ссылки
* 2. Отправка email с ссылкой на сброс
* 3. Форма ввода нового пароля
* 4. Обновление пароля
*/
class ForgotPassword extends BaseController
{
protected UserModel $userModel;
protected EmailLibrary $emailLibrary;
protected ?RateLimitService $rateLimitService;
public function __construct()
{
$this->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',
'Пароль успешно изменён. Теперь вы можете войти с новым паролем.'
);
}
}