add superadmin dashboard. subscriptions
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<?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',
|
||||
'Пароль успешно изменён. Теперь вы можете войти с новым паролем.'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user