add CRM module start

This commit is contained in:
root
2026-01-15 07:09:36 +03:00
parent b14f293a45
commit b810a17649
40 changed files with 3807 additions and 14 deletions
+41 -10
View File
@@ -126,6 +126,7 @@ class Auth extends BaseController
// Генерируем токен для подтверждения email
$verificationToken = bin2hex(random_bytes(32));
$tokenExpiresAt = date('Y-m-d H:i:s', strtotime('+24 hours'));
// 1. Создаем пользователя с токеном верификации
$userData = [
@@ -133,6 +134,7 @@ class Auth extends BaseController
'email' => $this->request->getPost('email'),
'password' => $this->request->getPost('password'), // Хешируется в модели
'verification_token' => $verificationToken,
'token_expires_at' => $tokenExpiresAt,
'email_verified' => 0,
];
@@ -215,6 +217,13 @@ class Auth extends BaseController
]);
}
// Проверяем срок действия токена
if (!empty($user['token_expires_at']) && strtotime($user['token_expires_at']) < time()) {
return $this->renderTwig('auth/verify_error', [
'message' => 'Ссылка для подтверждения истекла. Пожалуйста, запросите письмо повторно.'
]);
}
if ($user['email_verified']) {
return $this->renderTwig('auth/verify_error', [
'message' => 'Email уже подтверждён. Вы можете войти в систему.'
@@ -281,8 +290,10 @@ class Auth extends BaseController
// Генерируем новый токен
$newToken = bin2hex(random_bytes(32));
$newExpiresAt = date('Y-m-d H:i:s', strtotime('+24 hours'));
$userModel->update($user['id'], [
'verification_token' => $newToken
'verification_token' => $newToken,
'token_expires_at' => $newExpiresAt
]);
// Отправляем письмо повторно
@@ -347,10 +358,10 @@ class Auth extends BaseController
// === ЗАПОМНИТЬ МЕНЯ ===
$remember = $this->request->getPost('remember');
$redirectUrl = count($userOrgs) === 1 ? '/' : '/organizations';
if ($remember) {
$this->createRememberToken($user['id']);
// Устанавливаем сессию на 30 дней
$this->session->setExpiry(30 * 24 * 60 * 60); // 30 дней в секундах
$redirectUrl = $this->createRememberTokenAndRedirect($user['id'], $redirectUrl);
}
// АВТОМАТИЧЕСКИЙ ВЫБОР ОРГАНИЗАЦИИ
@@ -362,7 +373,9 @@ class Auth extends BaseController
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОГО ВХОДА ===
$this->resetRateLimit('login');
return redirect()->to('/');
return $redirectUrl !== '/'
? redirect()->to($redirectUrl)
: redirect()->to('/');
}
// ОЧИЩАЕМ active_org_id если несколько организаций
@@ -378,7 +391,9 @@ class Auth extends BaseController
// === СБРОС RATE LIMITING ПОСЛЕ УСПЕШНОГО ВХОДА ===
$this->resetRateLimit('login');
return redirect()->to('/organizations');
return $redirectUrl !== '/' && $redirectUrl !== '/organizations'
? redirect()->to($redirectUrl)
: redirect()->to('/organizations');
} else {
// === ЗАСЧИТЫВАЕМ НЕУДАЧНУЮ ПОПЫТКУ ===
$limitExceeded = $this->recordFailedAttempt('login');
@@ -420,8 +435,9 @@ class Auth extends BaseController
/**
* Создание remember-токена для автологина
* Возвращает массив с selector и validator
*/
protected function createRememberToken(int $userId): void
protected function createRememberTokenData(int $userId): array
{
$selector = bin2hex(random_bytes(16));
$validator = bin2hex(random_bytes(32));
@@ -439,9 +455,24 @@ class Auth extends BaseController
'ip_address' => $this->request->getIPAddress(),
]);
// Устанавливаем cookie на 30 дней
$cookie = \Config\Services::response()->setCookie('remember_selector', $selector, 30 * 24 * 60 * 60);
$cookie = \Config\Services::response()->setCookie('remember_token', $validator, 30 * 24 * 60 * 60);
return [
'selector' => $selector,
'validator' => $validator,
];
}
/**
* Создание токена и возврат редиректа с установленными куками
*/
protected function createRememberTokenAndRedirect(int $userId, string $redirectUrl)
{
$tokenData = $this->createRememberTokenData($userId);
$redirect = redirect()->to($redirectUrl);
$redirect->setCookie('remember_selector', $tokenData['selector'], 30 * 24 * 60 * 60);
$redirect->setCookie('remember_token', $tokenData['validator'], 30 * 24 * 60 * 60);
return $redirectUrl;
}
/**
+2 -1
View File
@@ -49,6 +49,7 @@ abstract class BaseController extends Controller
// Загружаем хелпер доступа для Twig
helper('access');
helper('crm_deals');
}
/**
@@ -269,7 +270,7 @@ abstract class BaseController extends Controller
public function renderTwig($template, $data = [])
{
helper('csrf');
helper('crm_deals');
$twig = \Config\Services::twig();
// oldInput из сессии добавляется в данные шаблона
+36
View File
@@ -25,6 +25,24 @@ class InvitationController extends BaseController
$invitation = $this->invitationService->orgUserModel->findByInviteToken($token);
if (!$invitation) {
// Проверяем, есть ли приглашение с таким токеном (может быть истекшим)
$db = \Config\Database::connect();
$expiredInvitation = $db->table('organization_users')
->where('invite_token', $token)
->get()
->getRowArray();
if ($expiredInvitation && !empty($expiredInvitation['invite_expires_at'])) {
$expiredAt = strtotime($expiredInvitation['invite_expires_at']);
$isExpired = $expiredAt < time();
return $this->renderTwig('organizations/invitation_expired', [
'title' => $isExpired ? 'Приглашение истекло' : 'Приглашение недействительно',
'expired' => $isExpired,
'expired_at' => $expiredInvitation['invite_expires_at'] ?? null,
]);
}
return $this->renderTwig('organizations/invitation_expired', [
'title' => 'Приглашение недействительно',
]);
@@ -135,6 +153,24 @@ class InvitationController extends BaseController
$invitation = $this->invitationService->orgUserModel->findByInviteToken($token);
if (!$invitation) {
// Проверяем, есть ли приглашение с таким токеном (может быть истекшим)
$db = \Config\Database::connect();
$expiredInvitation = $db->table('organization_users')
->where('invite_token', $token)
->get()
->getRowArray();
if ($expiredInvitation && !empty($expiredInvitation['invite_expires_at'])) {
$expiredAt = strtotime($expiredInvitation['invite_expires_at']);
$isExpired = $expiredAt < time();
return $this->renderTwig('organizations/invitation_expired', [
'title' => $isExpired ? 'Приглашение истекло' : 'Приглашение недействительно',
'expired' => $isExpired,
'expired_at' => $expiredInvitation['invite_expires_at'] ?? null,
]);
}
return $this->renderTwig('organizations/invitation_expired', [
'title' => 'Приглашение недействительно',
]);