dashboard for org
This commit is contained in:
+236
-10
@@ -6,22 +6,147 @@ use App\Models\UserModel;
|
||||
use App\Models\OrganizationModel;
|
||||
use App\Models\OrganizationUserModel;
|
||||
use App\Libraries\EmailLibrary;
|
||||
use App\Services\RateLimitService;
|
||||
|
||||
class Auth extends BaseController
|
||||
{
|
||||
protected $emailLibrary;
|
||||
protected ?RateLimitService $rateLimitService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->emailLibrary = new EmailLibrary();
|
||||
|
||||
// Инициализируем rate limiting сервис (может быть null если Redis недоступен)
|
||||
try {
|
||||
$this->rateLimitService = RateLimitService::getInstance();
|
||||
} catch (\Exception $e) {
|
||||
// Если Redis недоступен - логируем и продолжаем без rate limiting
|
||||
log_message('warning', 'RateLimitService недоступен: ' . $e->getMessage());
|
||||
$this->rateLimitService = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка rate limiting перед действием
|
||||
*
|
||||
* @param string $action Тип действия (login, register, reset)
|
||||
* @return array|null Возвращает данные об ошибке если заблокирован, иначе null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись неудачной попытки
|
||||
*
|
||||
* @param string $action Тип действия
|
||||
* @return array|null Данные о превышении лимита или null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс счётчика после успешного действия
|
||||
*
|
||||
* @param string $action Тип действия
|
||||
* @return void
|
||||
*/
|
||||
protected function resetRateLimit(string $action): void
|
||||
{
|
||||
if ($this->rateLimitService !== null) {
|
||||
$this->rateLimitService->resetAttempts($action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование времени блокировки для отображения
|
||||
*
|
||||
* @param int $seconds Секунды
|
||||
* @return string
|
||||
*/
|
||||
protected function formatBlockTime(int $seconds): string
|
||||
{
|
||||
if ($seconds >= 60) {
|
||||
$minutes = ceil($seconds / 60);
|
||||
return $minutes . ' ' . $this->pluralize($minutes, ['минуту', 'минуты', 'минут']);
|
||||
}
|
||||
return $seconds . ' ' . $this->pluralize($seconds, ['секунду', 'секунды', 'секунд']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Склонение окончаний для чисел
|
||||
*
|
||||
* @param int $number
|
||||
* @param array $forms [одна, две, пять]
|
||||
* @return string
|
||||
*/
|
||||
protected function pluralize(int $number, array $forms): string
|
||||
{
|
||||
$abs = abs($number);
|
||||
$mod = $abs % 10;
|
||||
|
||||
if ($abs % 100 >= 11 && $abs % 100 <= 19) {
|
||||
return $forms[2];
|
||||
}
|
||||
if ($mod === 1) {
|
||||
return $forms[0];
|
||||
}
|
||||
if ($mod >= 2 && $mod <= 4) {
|
||||
return $forms[1];
|
||||
}
|
||||
return $forms[2];
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
|
||||
// === ПРОВЕРКА RATE LIMITING ===
|
||||
$rateLimitError = $this->checkRateLimit('register');
|
||||
if ($rateLimitError !== null) {
|
||||
return redirect()->back()
|
||||
->with('error', $rateLimitError['message'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
log_message('debug', 'POST запрос получен: ' . print_r($this->request->getPost(), true));
|
||||
// Валидация (упрощенная для примера)
|
||||
|
||||
// Валидация
|
||||
$rules = [
|
||||
'name' => 'required|min_length[3]',
|
||||
'email' => 'required|valid_email|is_unique[users.email]',
|
||||
@@ -49,11 +174,11 @@ class Auth extends BaseController
|
||||
'verification_token' => $verificationToken,
|
||||
'email_verified' => 0,
|
||||
];
|
||||
|
||||
|
||||
log_message('debug', 'Registration userData: ' . print_r($userData, true));
|
||||
|
||||
$userId = $userModel->insert($userData);
|
||||
|
||||
|
||||
log_message('debug', 'Insert result, userId: ' . $userId);
|
||||
|
||||
|
||||
@@ -81,6 +206,9 @@ class Auth extends BaseController
|
||||
$verificationToken
|
||||
);
|
||||
|
||||
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОЙ РЕГИСТРАЦИИ ===
|
||||
$this->resetRateLimit('register');
|
||||
|
||||
// 5. Показываем сообщение о необходимости подтверждения
|
||||
session()->setFlashdata('success', 'Регистрация успешна! Пожалуйста, проверьте вашу почту и подтвердите email.');
|
||||
return redirect()->to('/register/success');
|
||||
@@ -103,7 +231,7 @@ class Auth extends BaseController
|
||||
public function verify($token)
|
||||
{
|
||||
log_message('debug', 'Verify called with token: ' . $token);
|
||||
|
||||
|
||||
if (empty($token)) {
|
||||
return $this->renderTwig('auth/verify_error', [
|
||||
'message' => 'Отсутствует токен подтверждения.'
|
||||
@@ -114,7 +242,7 @@ class Auth extends BaseController
|
||||
|
||||
// Ищем пользователя по токену
|
||||
$user = $userModel->where('verification_token', $token)->first();
|
||||
|
||||
|
||||
log_message('debug', 'User found: ' . ($user ? 'yes' : 'no'));
|
||||
if ($user) {
|
||||
log_message('debug', 'User email_verified: ' . $user['email_verified']);
|
||||
@@ -138,9 +266,9 @@ class Auth extends BaseController
|
||||
'verified_at' => date('Y-m-d H:i:s'),
|
||||
'verification_token' => null, // Удаляем токен после использования
|
||||
];
|
||||
|
||||
|
||||
$result = $userModel->update($user['id'], $updateData);
|
||||
|
||||
|
||||
log_message('debug', 'Update result: ' . ($result ? 'success' : 'failed'));
|
||||
log_message('debug', 'Update data: ' . print_r($updateData, true));
|
||||
|
||||
@@ -162,8 +290,17 @@ class Auth extends BaseController
|
||||
public function resendVerification()
|
||||
{
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
|
||||
// === ПРОВЕРКА RATE LIMITING ===
|
||||
$rateLimitError = $this->checkRateLimit('reset');
|
||||
if ($rateLimitError !== null) {
|
||||
return redirect()->back()
|
||||
->with('error', $rateLimitError['message'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$email = $this->request->getPost('email');
|
||||
|
||||
|
||||
if (empty($email)) {
|
||||
return redirect()->back()->with('error', 'Введите email');
|
||||
}
|
||||
@@ -172,6 +309,8 @@ class Auth extends BaseController
|
||||
$user = $userModel->where('email', $email)->first();
|
||||
|
||||
if (!$user) {
|
||||
// Неудачная попытка - засчитываем для защиты от перебора
|
||||
$this->recordFailedAttempt('reset');
|
||||
return redirect()->back()->with('error', 'Пользователь с таким email не найден');
|
||||
}
|
||||
|
||||
@@ -192,6 +331,9 @@ class Auth extends BaseController
|
||||
$newToken
|
||||
);
|
||||
|
||||
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОЙ ОТПРАВКИ ===
|
||||
$this->resetRateLimit('reset');
|
||||
|
||||
return redirect()->back()->with('success', 'Письмо для подтверждения отправлено повторно. Проверьте почту.');
|
||||
}
|
||||
|
||||
@@ -201,6 +343,15 @@ class Auth extends BaseController
|
||||
public function login()
|
||||
{
|
||||
if ($this->request->getMethod() === 'POST') {
|
||||
|
||||
// === ПРОВЕРКА RATE LIMITING ===
|
||||
$rateLimitError = $this->checkRateLimit('login');
|
||||
if ($rateLimitError !== null) {
|
||||
return redirect()->back()
|
||||
->with('error', $rateLimitError['message'])
|
||||
->withInput();
|
||||
}
|
||||
|
||||
$userModel = new \App\Models\UserModel();
|
||||
$orgUserModel = new \App\Models\OrganizationUserModel();
|
||||
|
||||
@@ -238,6 +389,10 @@ class Auth extends BaseController
|
||||
// Если одна организация — заходим автоматически для удобства
|
||||
$sessionData['active_org_id'] = $userOrgs[0]['organization_id'];
|
||||
session()->set($sessionData);
|
||||
|
||||
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОГО ВХОДА ===
|
||||
$this->resetRateLimit('login');
|
||||
|
||||
return redirect()->to('/');
|
||||
}
|
||||
|
||||
@@ -251,10 +406,28 @@ class Auth extends BaseController
|
||||
// чтобы страница /organizations не редиректнула его обратно (см. Organizations::index)
|
||||
session()->setFlashdata('info', 'Выберите пространство для работы');
|
||||
|
||||
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОГО ВХОДА ===
|
||||
$this->resetRateLimit('login');
|
||||
|
||||
return redirect()->to('/organizations');
|
||||
//}
|
||||
} else {
|
||||
return redirect()->back()->with('error', 'Неверный логин или пароль');
|
||||
// === ЗАСЧИТЫВАЕМ НЕУДАЧНУЮ ПОПЫТКУ ===
|
||||
$limitExceeded = $this->recordFailedAttempt('login');
|
||||
|
||||
// Если лимит превышен - показываем сообщение
|
||||
if ($limitExceeded !== null && $limitExceeded['blocked']) {
|
||||
$message = "Слишком много неудачных попыток входа. ";
|
||||
$message .= "Доступ заблокирован на " . $this->formatBlockTime($limitExceeded['ttl']) . ".";
|
||||
return redirect()->back()->with('error', $message)->withInput();
|
||||
}
|
||||
|
||||
// Иначе показываем стандартное сообщение об ошибке
|
||||
$remaining = $this->rateLimitService ? $this->rateLimitService->checkAttempt('login')['remaining'] : 0;
|
||||
$message = 'Неверный логин или пароль';
|
||||
if ($remaining > 0 && $remaining <= 2) {
|
||||
$message .= " Осталось попыток: {$remaining}";
|
||||
}
|
||||
return redirect()->back()->with('error', $message)->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,4 +440,57 @@ class Auth extends BaseController
|
||||
session()->remove('active_org_id');
|
||||
return redirect()->to('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* DEBUG: Просмотр состояния rate limiting (только для разработки)
|
||||
* DELETE: Убрать перед релизом!
|
||||
*
|
||||
* GET /auth/rate-limit-status
|
||||
*/
|
||||
public function rateLimitStatus()
|
||||
{
|
||||
// В продакшене должен быть доступ только админам
|
||||
if (env('CI_ENVIRONMENT') === 'production') {
|
||||
return $this->response->setStatusCode(403)->setJSON(['error' => 'Forbidden']);
|
||||
}
|
||||
|
||||
if ($this->rateLimitService === null) {
|
||||
return $this->response->setJSON([
|
||||
'status' => 'unavailable',
|
||||
'message' => 'RateLimitService недоступен (Redis не подключен)',
|
||||
]);
|
||||
}
|
||||
|
||||
$loginStatus = $this->rateLimitService->getStatus('login');
|
||||
$registerStatus = $this->rateLimitService->getStatus('register');
|
||||
$resetStatus = $this->rateLimitService->getStatus('reset');
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ip' => service('request')->getIPAddress(),
|
||||
'redis_connected' => $this->rateLimitService->isConnected(),
|
||||
'rate_limiting' => [
|
||||
'login' => [
|
||||
'attempts' => $loginStatus['attempts'],
|
||||
'limit' => $loginStatus['limit'],
|
||||
'window_seconds' => $loginStatus['window'],
|
||||
'is_blocked' => $loginStatus['is_blocked'],
|
||||
'block_ttl_seconds' => $loginStatus['block_ttl'],
|
||||
],
|
||||
'register' => [
|
||||
'attempts' => $registerStatus['attempts'],
|
||||
'limit' => $registerStatus['limit'],
|
||||
'window_seconds' => $registerStatus['window'],
|
||||
'is_blocked' => $registerStatus['is_blocked'],
|
||||
'block_ttl_seconds' => $registerStatus['block_ttl'],
|
||||
],
|
||||
'reset' => [
|
||||
'attempts' => $resetStatus['attempts'],
|
||||
'limit' => $resetStatus['limit'],
|
||||
'window_seconds' => $resetStatus['window'],
|
||||
'is_blocked' => $resetStatus['is_blocked'],
|
||||
'block_ttl_seconds' => $resetStatus['block_ttl'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user