/
bitrix-dev
/
Starter-kit
Обзор
Документация
Войти
/
bitrix-dev
/
Starter-kit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
main
local/php_interface/classes/mwi/User/Auth.php
665 строк
27 KB
Alexey Karizhenskiy
Первая сборка (из ранее используемого)
19 янв 2026, 21:00
19 янв 2026, 21:00
d843e36
Код
Авторство
О чём код?
<?php /* * Created by PhpStorm. * @author: AlexeyS <Karizhenskiy@bk.ru> * @date: 2025.05.22 */ namespace MWI\User; use Bitrix\Main\Error; use Bitrix\Main\Security\Random; use Bitrix\Main\Localization\Loc; use Bitrix\Main\Config\Option; use Bitrix\Main\Application; use Bitrix\Main\Entity\Base; use Bitrix\Main\Type\DateTime; use Bitrix\Main\UserPhoneAuthTable; use Bitrix\Main\Sms\Event as SmsEvent; use Bitrix\Main\Entity\ExpressionField; use MWI\User\UserConfirmCodeTable; require_once __DIR__ . "/UserConfirmCodeTable.php"; Loc::loadMessages(__FILE__); /** * Класс для авторизации и регистрации пользователей * Поддерживает авторизацию по email и телефону с подтверждением через код */ class Auth { // Константы для настройки const CODE_EXPIRATION_TIME = 300; // 5 минут const MAX_SEND_ATTEMPTS = 5; const SEND_INTERVAL = 60; // 1 минута const DEFAULT_USER_GROUP = 2; const MAX_ATTEMPTS = 3; // Максимальное количество попыток ввода кода protected string|int|null $userId = 0; protected bool $registrationAllowed = false; protected bool $registrationAllowedPhone = false; protected bool $isEmailRequired = false; protected bool $isPhoneRequired = false; protected bool $registrationEmailConfirmation = false; protected array $errors = []; public function __construct() { global $USER; $this->userId = $USER->IsAuthorized() ? $USER->GetID() : 0; // Разрешена ли регистрация $this->registrationAllowed = "Y" == Option::get("main", "new_user_registration", "N"); if ($this->registrationAllowed) { // Разрешена ли регистрация по номеру телефона $this->registrationAllowedPhone = "Y" == Option::get("main", "new_user_phone_auth", "N"); // Обязательность заполнения email и телефона $this->isEmailRequired = "Y" == Option::get("main", "new_user_email_required", "N"); $this->isPhoneRequired = "Y" == Option::get("main", "new_user_phone_required", "N"); // Требуется подтверждение регистрации по email $this->registrationEmailConfirmation = "Y" == Option::get("main", "new_user_registration_email_confirmation", "N"); } if (!Application::getConnection()->isTableExists(Base::getInstance('\MWI\User\UserConfirmCodeTable')->getDBTableName())) { Base::getInstance('\MWI\User\UserConfirmCodeTable')->createDBTable(); } } /** * Добавляет ошибку в массив ошибок * @param Error $error Объект ошибки */ protected function addError(Error $error): void { $this->errors[] = $error; } /** * Возвращает массив ошибок * @return array Массив объектов Error */ public function getErrors(): array { return $this->errors; } /** * Проверяет ограничения на отправку кода подтверждения * @param string $contact Контакт (email или телефон) * @return bool true если можно отправить код, false если превышены лимиты */ protected function checkSendLimits(string $contact): bool { try { // Используем текущее время сервера $currentTime = new DateTime(); $intervalStart = new DateTime(); $intervalStart->add('-PT' . self::SEND_INTERVAL . 'S'); // Вычитаем интервал в секундах // Проверяем количество попыток за последние 5 минут $attemptsCount = UserConfirmCodeTable::getList([ 'filter' => [ '=CONTACT' => $contact, '>DATE_CREATED' => $intervalStart ], 'select' => ['CNT'], 'runtime' => [ new ExpressionField('CNT', 'COUNT(*)') ] ])->fetch(); if ($attemptsCount && $attemptsCount['CNT'] >= self::MAX_SEND_ATTEMPTS) { $this->addError(new Error(Loc::GetMessage('AUTH_MAX_ATTEMPTS_EXCEEDED'), 'MAX_ATTEMPTS_EXCEEDED')); return false; } // Проверяем интервал между отправками $lastSend = UserConfirmCodeTable::getList([ 'filter' => [ '=CONTACT' => $contact, '>DATE_CREATED' => $intervalStart ], 'select' => ['DATE_CREATED'], 'order' => ['DATE_CREATED' => 'DESC'], 'limit' => 1 ])->fetch(); if ($lastSend) { $lastSendTime = $lastSend['DATE_CREATED']; $timeSinceLastSend = $currentTime->getTimestamp() - $lastSendTime->getTimestamp(); if ($timeSinceLastSend < self::SEND_INTERVAL) { $waitTime = self::SEND_INTERVAL - $timeSinceLastSend; $this->addError(new Error(Loc::GetMessage('AUTH_RESEND_WAIT_1_MINUTE', ['#SECONDS#' => $waitTime]), 'RESEND_WAIT')); return false; } } return true; } catch (\Exception $e) { // Логируем реальную ошибку для отладки \CEventLog::Add([ "SEVERITY" => "ERROR", "AUDIT_TYPE_ID" => "AUTH_CHECK_LIMITS_ERROR", "MODULE_ID" => "main", "DESCRIPTION" => $e->getMessage(), "ITEM_ID" => $contact ]); $this->addError(new Error(Loc::GetMessage('AUTH_CHECK_LIMITS_ERROR'), 'CHECK_LIMITS_ERROR')); return false; } } /** * Отправляет код подтверждения на указанный контакт * * @param string $contact Контакт для отправки (email или телефон) * @param string $contactType Тип контакта ('email' или 'phone') * @return array Результат операции с полями: * - success: bool - успешность операции * - isNew: int - 1 для новых пользователей, 0 для существующих * - message: string - сообщение о результате * - errors: array - массив ошибок */ public function sendCode(string $contact, string $contactType): array { if (!$this->checkSendLimits($contact)) { return ['errors' => $this->getErrors()]; } if (!$this->validateContact($contact, $contactType)) { $this->addError(new Error(Loc::GetMessage('AUTH_INVALID_CONTACT_FORMAT'), 'INVALID_CONTACT_FORMAT')); return ['errors' => $this->getErrors()]; } $result = []; if ('phone' == $contactType) { // Нормализация номера телефона $contact = UserPhoneAuthTable::normalizePhoneNumber($contact); // Поиск пользователя по телефону $resPhoneAuthTable = UserPhoneAuthTable::getList([ 'filter' => ['PHONE_NUMBER' => $contact] ]); if ($arPhoneAuth = $resPhoneAuthTable->fetch()) { $userId = $arPhoneAuth["USER_ID"]; } if ($userId || $this->registrationAllowedPhone) { $code = $this->generateCode(); // Сохраняем код в базе данных if (!$this->saveConfirmationCode($code, $contact, 'phone', $userId ?? 0)) { return [ 'errors' => $this->getErrors(), 'error_code' => 'CODE_SAVE_ERROR' ]; } // Отправка SMS через сервис $eventFields = [ 'USER_PHONE' => $contact, 'CODE' => $code, 'PHONE' => $contact ]; // Используем SMS-сервис для отправки кода $smsGateway = new SmsEvent("SMS_USER_CONFIRM_NUMBER", $eventFields); $smsGateway->setSite(SITE_ID); $smsResult = $smsGateway->send(true); if (!$smsResult->isSuccess()) { $this->addError(new Error(Loc::GetMessage('AUTH_SMS_SEND_ERROR'), 'SMS_SEND_ERROR')); return ['errors' => $this->getErrors()]; } // Также отправляем уведомление администратору и записываем в журнал $event = new \CEvent; $event->SendImmediate("PHONE_CONFIRM", SITE_ID, $eventFields); // Запись в журнал \CEventLog::Add(array( "SEVERITY" => "SECURITY", "AUDIT_TYPE_ID" => "PHONE_CONFIRM_REQUEST", "MODULE_ID" => "main", "DESCRIPTION" => Loc::GetMessage('AUTH_PHONE_CODE_SENT_LOG', ['#CODE#' => $code, '#PHONE#' => $contact, '#USER_ID#' => $userId]), )); $result['success'] = true; $result['isNew'] = $userId ? 0 : 1; $result['message'] = Loc::GetMessage('AUTH_CODE_SENT_TO_PHONE', ['#PHONE#' => $contact]); } else { $this->addError(new Error(Loc::GetMessage('AUTH_USER_WITH_PHONE_NOT_FOUND'), 'USER_NOT_FOUND')); return ['errors' => $this->getErrors()]; } } elseif ('email' == $contactType) { // Поиск пользователя по email $arUser = \CUser::GetList(($by = "ID"), ($order = "desc"), array("EMAIL" => $contact))->Fetch(); $userId = $arUser ? intval($arUser['ID']) : 0; if ($userId || $this->registrationAllowed) { $code = $this->generateCode(); // Сохраняем код в базе данных if (!$this->saveConfirmationCode($code, $contact, 'email', $userId)) { return ['errors' => $this->getErrors()]; } // Отправка email $eventFields = [ 'CONFIRM_CODE' => $code, 'MESSAGE_SUBJECT' => Loc::GetMessage('AUTH_EMAIL_CONFIRM_SUBJECT'), 'EMAIL_TO' => $contact, 'CODE' => $code, ]; $event = new \CEvent; $event->SendImmediate("MAIN_MAIL_CONFIRM_CODE", SITE_ID, $eventFields); // Запись в журнал \CEventLog::Add(array( "SEVERITY" => "SECURITY", "AUDIT_TYPE_ID" => "EMAIL_CONFIRM_REQUEST", "MODULE_ID" => "main", "DESCRIPTION" => Loc::GetMessage('AUTH_EMAIL_CODE_SENT_LOG', ['#CODE#' => $code, '#EMAIL#' => $contact, '#USER_ID#' => $userId]), )); $result['success'] = true; $result['isNew'] = $userId ? 0 : 1; $result['message'] = Loc::GetMessage('AUTH_CODE_SENT_TO_EMAIL', ['#EMAIL#' => $contact]); } else { $this->addError(new Error(Loc::GetMessage('AUTH_USER_WITH_EMAIL_NOT_FOUND'), 'USER_NOT_FOUND')); return ['errors' => $this->getErrors()]; } } return $result; } /** * Проверяет код подтверждения и выполняет авторизацию или регистрацию * todo: отделить авторизацию и регистрацию, оставив только подтверждение кода * * @param string $code Код подтверждения * @param string $contact Контакт (email или телефон) * @param string $contactType Тип контакта ('email' или 'phone') * @return array Результат операции с полями: * - success: bool - успешность операции * - errors: array - массив ошибок */ public function verifyCode(string $code, string $contact, string $contactType): array { $result = []; // Проверяем код в базе данных if (!$this->checkConfirmationCode($code, $contact)) { $this->addError(new Error(Loc::GetMessage('AUTH_INVALID_CONFIRMATION_CODE'), 'INVALID_CONFIRMATION_CODE')); return ['errors' => $this->getErrors()]; } // Получаем информацию о пользователе $userId = 0; if ('phone' == $contactType) { $resPhoneAuthTable = UserPhoneAuthTable::getList([ 'filter' => ['PHONE_NUMBER' => $contact] ]); if ($arPhoneAuth = $resPhoneAuthTable->fetch()) { $userId = $arPhoneAuth["USER_ID"]; } } else { $arUser = \CUser::GetList(($by = "ID"), ($order = "desc"), array("EMAIL" => $contact))->Fetch(); $userId = $arUser ? intval($arUser['ID']) : 0; } if ($userId) { // Авторизация, но кроме админа global $USER; // Проверяем что пользователь не админ $isAdmin = false; $userGroups = \CUser::GetUserGroup($userId); if (in_array(1, $userGroups)) { $isAdmin = true; } if (!$isAdmin && $USER->Authorize($userId, true)) { $result['success'] = true; $result['userId'] = $userId; $result['isNew'] = 0; // Логируем успешную авторизацию \CEventLog::Add(array( "SEVERITY" => "SECURITY", "AUDIT_TYPE_ID" => "USER_AUTH_SUCCESS", "MODULE_ID" => "main", "DESCRIPTION" => Loc::GetMessage('AUTH_SUCCESS_LOG', ['#USER_ID#' => $userId]), )); } else { $this->addError(new Error(Loc::GetMessage('AUTH_AUTHORIZATION_ERROR'), 'AUTHORIZATION_ERROR')); return ['errors' => $this->getErrors()]; } } else { if ($this->registrationAllowed) { $registerResult = $this->registerUser([ "PHONE" => $contactType == 'phone' ? $contact : '', "EMAIL" => $contactType == 'email' ? $contact : '', ]); global $USER; // Авторизуем пользователя if ($registerResult['success'] && $USER->Authorize($registerResult['userId'], true)) { $result['success'] = true; $result['userId'] = $registerResult['userId']; $result['isNew'] = 1; // Логируем успешную регистрацию \CEventLog::Add(array( "SEVERITY" => "SECURITY", "AUDIT_TYPE_ID" => "USER_REGISTER_SUCCESS", "MODULE_ID" => "main", "DESCRIPTION" => Loc::GetMessage('AUTH_REGISTER_SUCCESS_LOG', ['#USER_ID#' => $registerResult['userId']]), )); // Очищаем коды подтверждения для этого контакта $this->clearConfirmationCodes($fields["PHONE"] ?? '', $fields["EMAIL"] ?? ''); } else { return ['errors' => $this->getErrors()]; } } else { $this->addError(new Error(Loc::GetMessage('AUTH_REGISTRATION_DISABLED'), 'REGISTRATION_DISABLED')); } } return $result; } /** * Регистрирует нового пользователя * * @param array $fields Массив полей пользователя: * - PHONE: string - номер телефона * - EMAIL: string - email * - NAME: string - имя пользователя * @return array Результат операции с полями: * - success: bool - успешность операции * - userId: int - ID созданного пользователя * - errors: array - массив ошибок */ protected function registerUser(array $fields): array { global $APPLICATION, $DB; $result = []; // Валидация входных данных if (empty($fields["PHONE"]) && empty($fields["EMAIL"])) { $this->addError(new Error(Loc::GetMessage('AUTH_CONTACT_REQUIRED'), 'CONTACT_REQUIRED')); return $result; } if (!empty($fields["PHONE"]) && !$this->validateContact($fields["PHONE"], 'phone')) { $this->addError(new Error(Loc::GetMessage('AUTH_INVALID_PHONE_FORMAT'), 'INVALID_PHONE_FORMAT')); return $result; } if (!empty($fields["EMAIL"]) && !$this->validateContact($fields["EMAIL"], 'email')) { $this->addError(new Error(Loc::GetMessage('AUTH_INVALID_EMAIL_FORMAT'), 'INVALID_EMAIL_FORMAT')); return $result; } $user = new \CUser; $password = \Bitrix\Main\Security\Random::getString(8); // Получаем группы по умолчанию для новых пользователей $defaultGroups = array(self::DEFAULT_USER_GROUP); $defGroupStr = Option::get("main", "new_user_registration_def_group", ""); if ($defGroupStr != "") { $defaultGroups = array_merge($defaultGroups, explode(",", $defGroupStr)); } // Если email - обязательное поле if ($this->isEmailRequired) { // Если email не указан, генерируем временный email из телефона if (empty($fields["EMAIL"]) && !empty($fields["PHONE"])) { $cleanPhone = preg_replace('/[^0-9]/', '', $fields["PHONE"]); $fields["EMAIL"] = $cleanPhone . "@" . $_SERVER['HTTP_HOST']; } } // Генерируем базовый логин из email или телефона $baseLogin = \Bitrix\Main\Security\Random::getString(10, true); if (!empty($fields["LOGIN"])) { $baseLogin = $fields["LOGIN"]; } else if (!empty($fields["EMAIL"])) { $baseLogin = $fields["EMAIL"]; } else if (!empty($fields["PHONE"])) { $baseLogin = $fields["PHONE"]; } // Проверяем существование логина и добавляем число если нужно $login = $baseLogin; $counter = 1; while (\CUser::GetByLogin(LOGIN: $login)->Fetch()) { $login = $baseLogin . $counter; $counter++; } $userData = array( "LOGIN" => $login, "NAME" => $fields["NAME"] ?? "", "EMAIL" => $fields["EMAIL"] ?? "", "PHONE_NUMBER" => $fields["PHONE"] ?? "", "PERSONAL_PHONE" => $fields["PHONE"] ?? "", "PASSWORD" => $password, "CONFIRM_PASSWORD" => $password, "ACTIVE" => "Y", "LID" => SITE_ID, "GROUP_ID" => $defaultGroups, "CHECKWORD" => \Bitrix\Main\Security\Random::getString(32), "~CHECKWORD_TIME" => $DB->CurrentTimeFunction(), "LANGUAGE_ID" => LANGUAGE_ID, "USER_IP" => $_SERVER["REMOTE_ADDR"], "USER_HOST" => @gethostbyaddr($_SERVER["REMOTE_ADDR"]) ); // Проверяем обязательные поля if (empty($userData["PHONE"]) && $this->isPhoneRequired) { $this->addError(new Error(Loc::GetMessage('AUTH_PHONE_REQUIRED'), 'PHONE_REQUIRED')); return $result; } if (empty($userData["EMAIL"]) && $this->isEmailRequired) { $this->addError(new Error(Loc::GetMessage('AUTH_EMAIL_REQUIRED'), 'EMAIL_REQUIRED')); return $result; } // Событие перед регистрацией $events = \GetModuleEvents("main", "OnBeforeUserRegister", true); foreach ($events as $arEvent) { if (\ExecuteModuleEventEx($arEvent, array(&$userData)) === false) { if ($err = $APPLICATION->GetException()) { $this->addError(new Error($err->GetString(), 'ON_BEFORE_USER_REGISTER')); return $result; } } } $ID = $user->Add($userData); if (intval($ID) > 0) { $result["success"] = true; $result["userId"] = $ID; // Отправляем уведомления $eventFields = $userData; unset($eventFields["PASSWORD"]); unset($eventFields["CONFIRM_PASSWORD"]); $event = new \CEvent; $event->SendImmediate("NEW_USER", SITE_ID, $eventFields); //if($this->arResult["USE_EMAIL_CONFIRMATION"] === "Y") { // $event->SendImmediate("NEW_USER_CONFIRM", SITE_ID, $eventFields); //} // Логируем успешную регистрацию if (Option::get("main", "event_log_register", "N") === "Y") { \CEventLog::Log("SECURITY", "USER_REGISTER", "main", $ID); } // Очищаем коды подтверждения для этого контакта $this->clearConfirmationCodes($fields["PHONE"] ?? '', $fields["EMAIL"] ?? ''); } else { $this->addError(new Error($user->LAST_ERROR), 'USER_ADD'); // Логируем ошибку регистрации if (Option::get("main", "event_log_register_fail", "N") === "Y") { \CEventLog::Log("SECURITY", "USER_REGISTER_FAIL", "main", 0, $user->LAST_ERROR); } } return $result; } /** * Очищает коды подтверждения для указанного контакта * * @param string $phone Номер телефона * @param string $email Email */ protected function clearConfirmationCodes(string $phone = '', string $email = ''): void { try { $filter = []; if (!empty($phone)) { $filter['=CONTACT'] = $phone; } if (!empty($email)) { $filter['=CONTACT'] = $email; } if (!empty($filter)) { $codes = UserConfirmCodeTable::getList([ 'filter' => $filter, 'select' => ['ID'] ]); while ($code = $codes->fetch()) { UserConfirmCodeTable::delete($code['ID']); } } } catch (\Exception $e) { // Логируем ошибку, но не прерываем выполнение \CEventLog::Log("ERROR", "CLEAR_CONFIRMATION_CODES", "main", 0, $e->getMessage()); } } /** * Проверяет валидность контакта (email или телефон) * * @param string $contact Контакт для проверки * @param string $type Тип контакта ('email' или 'phone') * @return bool true если контакт валиден, false в противном случае */ protected function validateContact(string $contact, string $type): bool { if ($type === 'email') { return filter_var($contact, FILTER_VALIDATE_EMAIL) !== false; } elseif ($type === 'phone') { // Базовая проверка формата телефона return preg_match('/^\+?[0-9]{10,15}$/', $contact) === 1; } return false; } /** * Генерирует код подтверждения * * @return string 4-значный код подтверждения */ protected function generateCode(): string { return Random::getInt(1000, 9999); } /** * Сохраняет код подтверждения в базе данных * * @param string $code Код подтверждения * @param string $contact Контакт (email или телефон) * @param string $type Тип контакта ('email' или 'phone') * @param int $userId ID пользователя (0 для новых пользователей) * @return bool true если код успешно сохранен, false в случае ошибки */ protected function saveConfirmationCode(string $code, string $contact, string $type, int $userId = 0): bool { try { $result = UserConfirmCodeTable::add([ 'CODE' => $code, 'CONTACT' => $contact, 'TYPE' => $type, 'USER_ID' => $userId, 'DATE_CREATED' => new DateTime(), 'IP' => $_SERVER['REMOTE_ADDR'], 'ATTEMPTS' => 0 ]); if (!$result->isSuccess()) { $this->addError(new Error(Loc::GetMessage('AUTH_CODE_SAVE_ERROR'), 'CODE_SAVE')); return false; } return true; } catch (\Exception $e) { $this->addError(new Error(Loc::GetMessage('AUTH_CODE_SAVE_ERROR'), 'CODE_SAVE')); return false; } } /** * Проверяет код подтверждения в базе данных * * @param string $code Код подтверждения * @param string $contact Контакт (email или телефон) * @return bool true если код валиден, false в противном случае */ protected function checkConfirmationCode(string $code, string $contact): bool { // Используем текущее время сервера $intervalStart = new DateTime(); $intervalStart->add('-PT' . self::CODE_EXPIRATION_TIME . 'S'); // Вычитаем интервал в секундах try { $codeRecord = UserConfirmCodeTable::getList([ 'filter' => [ '=CODE' => $code, '=CONTACT' => $contact, '>DATE_CREATED' => $intervalStart, '<ATTEMPTS' => self::MAX_ATTEMPTS ], 'order' => ['DATE_CREATED' => 'DESC'], 'limit' => 1 ])->fetch(); if ($codeRecord) { // Увеличиваем счетчик попыток UserConfirmCodeTable::update($codeRecord['ID'], [ 'ATTEMPTS' => $codeRecord['ATTEMPTS'] + 1 ]); return true; } return false; } catch (\Exception $e) { $this->addError(new Error(Loc::GetMessage('AUTH_CODE_CHECK_ERROR'), 'CODE_CHECK')); return false; } } }