From 97bb196b2964bb6e182cf83e84f1fcaea4d51526 Mon Sep 17 00:00:00 2001 From: Bitrix user Date: Fri, 6 Mar 2026 19:26:11 +0300 Subject: [PATCH] Initial commit: Auth Server Base --- .gitignore | 25 ++ api/oauth/authorize.php | 3 + api/oauth/change_password.php | 3 + api/oauth/config.php | 2 + api/oauth/dictionaries.php | 3 + api/oauth/groups.php | 3 + api/oauth/refresh.php | 3 + api/oauth/register.php | 3 + api/oauth/token.php | 3 + api/oauth/update_profile.php | 3 + api/oauth/user.php | 3 + api/oauth/verify.php | 5 + auth/index.php | 152 ++++++++++++ .../conmed/sso.register/.class.php.0.bak | 103 ++++++++ .../components/conmed/sso.register/class.php | 105 ++++++++ .../templates/.default/.template.php.0.bak | 210 ++++++++++++++++ .../templates/.default/template.php | 220 +++++++++++++++++ .../conmed.authserver/.reinstall_agent.php | 92 +++++++ .../conmed.authserver/install/index.php | 44 ++++ .../lib/.groupstrait.php.bak | 18 ++ .../lib/.internaldatatrait.php.bak | 59 +++++ .../lib/.registrationtrait.php.bak | 29 +++ local/modules/conmed.authserver/lib/api.php | 38 +++ .../conmed.authserver/lib/authtokentrait.php | 58 +++++ .../lib/credentialstrait.php | 59 +++++ .../lib/dictionariestrait.php | 47 ++++ .../conmed.authserver/lib/groupstrait.php | 18 ++ .../lib/internaldatatrait.php | 67 +++++ .../conmed.authserver/lib/profiletrait.php | 155 ++++++++++++ .../lib/profiletrait.php.bak | 108 +++++++++ .../lib/registrationtrait.php | 94 +++++++ .../conmed.authserver/lib/securitytrait.php | 75 ++++++ .../modules/conmed.authserver/lib/webhook.php | 36 +++ .../conmed.authserver/lib/webhook.php.0.bak | 33 +++ local/modules/conmed.authserver/man.txt | 62 +++++ local/modules/conmed.authserver/options.php | 43 ++++ .../conmed.authserver/options.php.0.bak | 40 +++ personal/index.php | 229 ++++++++++++++++++ register/index.php | 19 ++ 39 files changed, 2272 insertions(+) create mode 100644 .gitignore create mode 100644 api/oauth/authorize.php create mode 100644 api/oauth/change_password.php create mode 100644 api/oauth/config.php create mode 100644 api/oauth/dictionaries.php create mode 100644 api/oauth/groups.php create mode 100644 api/oauth/refresh.php create mode 100644 api/oauth/register.php create mode 100644 api/oauth/token.php create mode 100644 api/oauth/update_profile.php create mode 100644 api/oauth/user.php create mode 100644 api/oauth/verify.php create mode 100644 auth/index.php create mode 100644 local/components/conmed/sso.register/.class.php.0.bak create mode 100644 local/components/conmed/sso.register/class.php create mode 100644 local/components/conmed/sso.register/templates/.default/.template.php.0.bak create mode 100644 local/components/conmed/sso.register/templates/.default/template.php create mode 100644 local/modules/conmed.authserver/.reinstall_agent.php create mode 100644 local/modules/conmed.authserver/install/index.php create mode 100644 local/modules/conmed.authserver/lib/.groupstrait.php.bak create mode 100644 local/modules/conmed.authserver/lib/.internaldatatrait.php.bak create mode 100644 local/modules/conmed.authserver/lib/.registrationtrait.php.bak create mode 100644 local/modules/conmed.authserver/lib/api.php create mode 100644 local/modules/conmed.authserver/lib/authtokentrait.php create mode 100644 local/modules/conmed.authserver/lib/credentialstrait.php create mode 100644 local/modules/conmed.authserver/lib/dictionariestrait.php create mode 100644 local/modules/conmed.authserver/lib/groupstrait.php create mode 100644 local/modules/conmed.authserver/lib/internaldatatrait.php create mode 100644 local/modules/conmed.authserver/lib/profiletrait.php create mode 100644 local/modules/conmed.authserver/lib/profiletrait.php.bak create mode 100644 local/modules/conmed.authserver/lib/registrationtrait.php create mode 100644 local/modules/conmed.authserver/lib/securitytrait.php create mode 100644 local/modules/conmed.authserver/lib/webhook.php create mode 100644 local/modules/conmed.authserver/lib/webhook.php.0.bak create mode 100644 local/modules/conmed.authserver/man.txt create mode 100644 local/modules/conmed.authserver/options.php create mode 100644 local/modules/conmed.authserver/options.php.0.bak create mode 100644 personal/index.php create mode 100644 register/index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83974a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +cat << 'EOF' > .gitignore +# 1. Игнорируем ВСЁ в корне сайта +/* + +# 2. Разрешаем (белый список) только нужные корневые папки +!/api/ +!/auth/ +!/personal/ +!/register/ +!/local/ +!/.gitignore + +# 3. Заходим внутрь local, но игнорируем там всё лишнее +/local/* +!/local/components/ +!/local/modules/ + +# 4. Внутри modules оставляем ТОЛЬКО conmed.authserver +/local/modules/* +!/local/modules/conmed.authserver/ + +# 5. Внутри components оставляем ТОЛЬКО conmed +/local/components/* +!/local/components/conmed/ +EOF \ No newline at end of file diff --git a/api/oauth/authorize.php b/api/oauth/authorize.php new file mode 100644 index 0000000..ed25b61 --- /dev/null +++ b/api/oauth/authorize.php @@ -0,0 +1,3 @@ +IsAuthorized() && !empty($_REQUEST["backurl"])) { + LocalRedirect($_REQUEST["backurl"]); +} +?> + + + + +
+ + SetTitle("Восстановление пароля"); + $APPLICATION->IncludeComponent("bitrix:system.auth.forgotpasswd", "bootstrap", Array()); + } + // --------------------------------------------------------- + // 2. РЕГИСТРАЦИЯ + // --------------------------------------------------------- + elseif ($_REQUEST["register"] == "yes" || $_REQUEST["register_submit_button"] == "Зарегистрироваться") { + $APPLICATION->SetTitle("Регистрация"); + $APPLICATION->IncludeComponent("bitrix:main.register", "bootstrap", Array( + "SHOW_FIELDS" => array("EMAIL", "NAME", "LAST_NAME", "PASSWORD", "CONFIRM_PASSWORD"), + "REQUIRED_FIELDS" => array("EMAIL", "NAME", "PASSWORD", "CONFIRM_PASSWORD"), + "AUTH" => "Y", + "USE_BACKURL" => "Y", + "SUCCESS_PAGE" => "/personal/", + "SET_TITLE" => "Y", + "USER_PROPERTY" => array(), + )); + } + // --------------------------------------------------------- + // 3. АВТОРИЗАЦИЯ (ВХОД) + // --------------------------------------------------------- + else { + $APPLICATION->SetTitle("Вход"); + $APPLICATION->IncludeComponent("bitrix:system.auth.authorize", "bootstrap", Array( + "AUTH_RESULT" => $APPLICATION->arAuthResult, + "REGISTER_URL" => "/register/", // Явно указываем ссылку + "FORGOT_PASSWORD_URL" => "?forgot_password=yes", // Явно указываем ссылку + "PROFILE_URL" => "/personal/", + )); + } + ?> + +
+ + \ No newline at end of file diff --git a/local/components/conmed/sso.register/.class.php.0.bak b/local/components/conmed/sso.register/.class.php.0.bak new file mode 100644 index 0000000..c9bdb29 --- /dev/null +++ b/local/components/conmed/sso.register/.class.php.0.bak @@ -0,0 +1,103 @@ +getRequest(); + + // 1. Обработка AJAX регистрации + if ($request->isPost() && $request->get('ajax_reg') == 'y') { + $this->handleRegister($request); + return; + } + + // 2. Подготовка данных для формы через InternalDataTrait + $geo = Api::getGeoForComponent(); + $this->arResult['COUNTRIES'] = $geo['countries']; + $this->arResult['CITIES'] = $geo['cities']; + $this->arResult['SPECIALTIES'] = Api::getSpecialtiesForComponent(); + + $this->includeComponentTemplate(); + } + + private function handleRegister($req) { + $GLOBALS['APPLICATION']->RestartBuffer(); + header('Content-Type: application/json'); + + if (!check_bitrix_sessid()) { + echo json_encode(['status' => 'error', 'message' => 'Сессия истекла']); die(); + } + + $email = trim($req->getPost("email")); + + if(!check_email($email)) die(json_encode(['status'=>'error','message'=>'Некорректный Email'])); + + $by = "ID"; $order = "ASC"; + if(\CUser::GetList($by, $order, ["=EMAIL" => $email])->Fetch()) { + die(json_encode(['status'=>'error','message'=>'Email уже занят'])); + } + + $pass = $req->getPost("password"); + // Теперь этот вызов сработает, так как метод PUBLIC + $v = Api::validatePassword($pass); + if($v !== true) die(json_encode(['status'=>'error','message'=>$v])); + + $arGroups = [2, 3, 4]; + $specCode = $req->getPost("specialty"); + if (!empty($specCode)) { + $rsGroup = \Bitrix\Main\GroupTable::getList(['filter' => ['=STRING_ID' => $specCode, '=ACTIVE' => 'Y'], 'select' => ['ID']])->fetch(); + if ($rsGroup) $arGroups[] = $rsGroup['ID']; + } + + $user = new \CUser; + $uid = $user->Add([ + "LOGIN" => $email, + "EMAIL" => $email, + "NAME" => $req->getPost("name"), + "LAST_NAME" => $req->getPost("last_name"), + "SECOND_NAME" => $req->getPost("second_name"), + "PERSONAL_PHONE" => $req->getPost("phone"), + "PERSONAL_CITY" => $req->getPost("city"), + "PERSONAL_COUNTRY" => $req->getPost("country"), + "PASSWORD" => $pass, + "CONFIRM_PASSWORD" => $pass, + "ACTIVE" => "Y", + "GROUP_ID" => $arGroups + ]); + + if($uid) { + $needConfirm = Option::get("main", "new_user_registration_email_confirmation", "N"); + if ($needConfirm !== "Y") { + global $USER; + $USER->Authorize($uid); + } + + $code = bin2hex(random_bytes(16)); + // Теперь этот вызов сработает, так как метод PUBLIC + $dcCodes = Api::getHlEntity('sso_codes'); + $dcCodes::add([ + 'UF_CODE' => $code, + 'UF_CLIENT_ID' => 'app_id_site', // Используем ID основного сайта по умолчанию + 'UF_USER_ID' => $uid, + 'UF_EXPIRES' => DateTime::createFromTimestamp(time() + 60) + ]); + + Api::audit("USER_REGISTERED", "app_id_site", $uid, "Email: ".$email); + echo json_encode(['status' => 'success', 'code' => $code]); + } else { + echo json_encode(['status' => 'error', 'message' => strip_tags($user->LAST_ERROR)]); + } + die(); + } +} \ No newline at end of file diff --git a/local/components/conmed/sso.register/class.php b/local/components/conmed/sso.register/class.php new file mode 100644 index 0000000..7b7a978 --- /dev/null +++ b/local/components/conmed/sso.register/class.php @@ -0,0 +1,105 @@ +getRequest(); + + // 1. Обработка AJAX регистрации + if ($request->isPost() && $request->get('ajax_reg') == 'y') { + $this->handleRegister($request); + return; + } + + // 2. Подготовка данных для формы через InternalDataTrait + $geo = Api::getGeoForComponent(); + $this->arResult['COUNTRIES'] = $geo['countries']; + $this->arResult['CITIES'] = $geo['cities']; + $this->arResult['SPECIALTIES'] = Api::getSpecialtiesForComponent(); + // Передаем публичный ID во фронтенд для формирования ссылки редиректа + $this->arResult['CLIENT_ID'] = Api::getDefaultClientId(); + + $this->includeComponentTemplate(); + } + + private function handleRegister($req) { + $GLOBALS['APPLICATION']->RestartBuffer(); + header('Content-Type: application/json'); + + if (!check_bitrix_sessid()) { + echo json_encode(['status' => 'error', 'message' => 'Сессия истекла']); die(); + } + + $email = trim($req->getPost("email")); + + if(!check_email($email)) die(json_encode(['status'=>'error','message'=>'Некорректный Email'])); + + $by = "ID"; $order = "ASC"; + if(\CUser::GetList($by, $order, ["=EMAIL" => $email])->Fetch()) { + die(json_encode(['status'=>'error','message'=>'Email уже занят'])); + } + + $pass = $req->getPost("password"); + $v = Api::validatePassword($pass); + if($v !== true) die(json_encode(['status'=>'error','message'=>$v])); + + $arGroups = [2, 3, 4]; + $specCode = $req->getPost("specialty"); + if (!empty($specCode)) { + $rsGroup = \Bitrix\Main\GroupTable::getList(['filter' =>['=STRING_ID' => $specCode, '=ACTIVE' => 'Y'], 'select' => ['ID']])->fetch(); + if ($rsGroup) $arGroups[] = $rsGroup['ID']; + } + + $user = new \CUser; + $uid = $user->Add([ + "LOGIN" => $email, + "EMAIL" => $email, + "NAME" => $req->getPost("name"), + "LAST_NAME" => $req->getPost("last_name"), + "SECOND_NAME" => $req->getPost("second_name"), + "PERSONAL_PHONE" => $req->getPost("phone"), + "PERSONAL_CITY" => $req->getPost("city"), + "PERSONAL_COUNTRY" => $req->getPost("country"), + "PASSWORD" => $pass, + "CONFIRM_PASSWORD" => $pass, + "ACTIVE" => "Y", + "GROUP_ID" => $arGroups + ]); + + if($uid) { + $needConfirm = Option::get("main", "new_user_registration_email_confirmation", "N"); + if ($needConfirm !== "Y") { + global $USER; + $USER->Authorize($uid); + } + + $code = bin2hex(random_bytes(16)); + $defaultClientId = Api::getDefaultClientId(); // Получаем безопасно из трейта + + $dcCodes = Api::getHlEntity('sso_codes'); + $dcCodes::add([ + 'UF_CODE' => $code, + 'UF_CLIENT_ID' => $defaultClientId, + 'UF_USER_ID' => $uid, + 'UF_EXPIRES' => DateTime::createFromTimestamp(time() + 60) + ]); + + Api::audit("USER_REGISTERED", $defaultClientId, $uid, "Email: ".$email); + echo json_encode(['status' => 'success', 'code' => $code]); + } else { + echo json_encode(['status' => 'error', 'message' => strip_tags($user->LAST_ERROR)]); + } + die(); + } +} \ No newline at end of file diff --git a/local/components/conmed/sso.register/templates/.default/.template.php.0.bak b/local/components/conmed/sso.register/templates/.default/.template.php.0.bak new file mode 100644 index 0000000..f1c7841 --- /dev/null +++ b/local/components/conmed/sso.register/templates/.default/.template.php.0.bak @@ -0,0 +1,210 @@ + + + + +
+
+

Регистрация специалиста

+

Создайте единый аккаунт для всех ресурсов Con-Med

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ 👁️ + 🎲 ГЕН +
+
+
+
+ + +
+
+ +
+ + +
+ + + + + +
+ Уже есть аккаунт? Войти +
+
+
+
+ + + + \ No newline at end of file diff --git a/local/components/conmed/sso.register/templates/.default/template.php b/local/components/conmed/sso.register/templates/.default/template.php new file mode 100644 index 0000000..4b12bd4 --- /dev/null +++ b/local/components/conmed/sso.register/templates/.default/template.php @@ -0,0 +1,220 @@ + + + + +
+
+

Регистрация специалиста

+

Создайте единый аккаунт для всех ресурсов Con-Med

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ 👁️ + 🎲 ГЕН +
+
+
+
+ + +
+
+ +
+ + +
+ + + + + +
+ Уже есть аккаунт? Войти +
+
+
+
+ + + + \ No newline at end of file diff --git a/local/modules/conmed.authserver/.reinstall_agent.php b/local/modules/conmed.authserver/.reinstall_agent.php new file mode 100644 index 0000000..f0caa06 --- /dev/null +++ b/local/modules/conmed.authserver/.reinstall_agent.php @@ -0,0 +1,92 @@ +IsAdmin()) die("Доступ запрещен"); + +$log = []; +$apiFile = $_SERVER["DOCUMENT_ROOT"]."/local/modules/conmed.authserver/lib/api.php"; + +// 1. ОБНОВЛЯЕМ ФАЙЛ API.PHP (чтобы метод точно был в классе) +$apiCode = <<<'PHP' + [' $date7], 'select' => ['ID']]); + while ($el = $rs->fetch()) { + $dcSec::delete($el['ID']); + } + + // 2. Очистка аудита (храним 60 дней) + $date60 = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 86400 * 60); + $rs = $dcAud::getList(['filter' => [' $date60], 'select' => ['ID']]); + while ($el = $rs->fetch()) { + $dcAud::delete($el['ID']); + } + + // 3. Очистка протухших кодов авторизации (храним 1 час для истории, потом удаляем) + $date1h = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 3600); + $rs = $dcCodes::getList(['filter' => [' $date1h], 'select' => ['ID']]); + while ($el = $rs->fetch()) { + $dcCodes::delete($el['ID']); + } + + } catch (\Exception $e) { + // Ошибки можно писать в системный лог php если нужно + } + + return "\Conmed\Authserver\Api::cleanLogsAgent();"; + } +} +PHP; + +if (file_put_contents($apiFile, $apiCode)) { + $log[] = "Файл lib/api.php обновлен (метод cleanLogsAgent добавлен)."; +} + +// 2. РЕГИСТРАЦИЯ АГЕНТА В БИТРИКСЕ +// Сначала удаляем старый, если он завис +\CAgent::RemoveAgent("\Conmed\Authserver\Api::cleanLogsAgent();", "conmed.authserver"); + +// Добавляем новый +// Параметры: Метод, Модуль, Периодичность (N=раз в интервал), Интервал (86400 = 24 часа) +\CAgent::AddAgent( + "\Conmed\Authserver\Api::cleanLogsAgent();", + "conmed.authserver", + "N", + 86400, + "", + "Y", + \Bitrix\Main\Type\DateTime::createFromTimestamp(time() + 60) // Первый запуск через минуту +); + +$log[] = "Агент очистки логов успешно зарегистрирован в системе."; + +echo "

Результат установки:

"; +echo "
  • " . implode("
  • ", $log) . "
"; +echo "

Проверить работу агента можно в админке: Настройки -> Настройки продукта -> Агенты

"; \ No newline at end of file diff --git a/local/modules/conmed.authserver/install/index.php b/local/modules/conmed.authserver/install/index.php new file mode 100644 index 0000000..bc884dd --- /dev/null +++ b/local/modules/conmed.authserver/install/index.php @@ -0,0 +1,44 @@ +MODULE_ID); + $this->InstallEvents(); // Вызываем регистрацию событий + } + + function DoUninstall() { + $this->UnInstallEvents(); // Вызываем удаление событий + ModuleManager::unRegisterModule($this->MODULE_ID); + } + + function InstallEvents() { + $eventManager = EventManager::getInstance(); + // Регистрация обработчика удаления пользователя + $eventManager->registerEventHandler( + "main", + "OnAfterUserDelete", + $this->MODULE_ID, + "Conmed\\Authserver\\Api", + "onAfterUserDeleteHandler" + ); + } + + function UnInstallEvents() { + $eventManager = EventManager::getInstance(); + // Удаление обработчика при деинсталляции модуля + $eventManager->unRegisterEventHandler( + "main", + "OnAfterUserDelete", + $this->MODULE_ID, + "Conmed\\Authserver\\Api", + "onAfterUserDeleteHandler" + ); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/.groupstrait.php.bak b/local/modules/conmed.authserver/lib/.groupstrait.php.bak new file mode 100644 index 0000000..b9f3ac2 --- /dev/null +++ b/local/modules/conmed.authserver/lib/.groupstrait.php.bak @@ -0,0 +1,18 @@ +getRequest(); + if(!self::checkClient($req->get("client_id"), $req->get("client_secret"))) die(json_encode(['error'=>'forbidden'])); + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=ACTIVE'=>'Y','=C_SORT'=>555],'select'=>['ID','NAME','STRING_ID'],'order'=>['NAME'=>'ASC']]); + $res = []; while($g = $rs->fetch()) if($g['STRING_ID']) $res[] = ['id'=>$g['ID'], 'name'=>$g['NAME'], 'code'=>$g['STRING_ID']]; + echo json_encode($res); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/.internaldatatrait.php.bak b/local/modules/conmed.authserver/lib/.internaldatatrait.php.bak new file mode 100644 index 0000000..c2f2e83 --- /dev/null +++ b/local/modules/conmed.authserver/lib/.internaldatatrait.php.bak @@ -0,0 +1,59 @@ + ['=ACTIVE' => 'Y', '=C_SORT' => 555], + 'select' => ['ID', 'NAME', 'STRING_ID'], + 'order' => ['NAME' => 'ASC'] + ]); + $res = []; + while($g = $rs->fetch()) { + if($g['STRING_ID']) { + $res[] = ['id' => $g['ID'], 'name' => $g['NAME'], 'code' => $g['STRING_ID']]; + } + } + return $res; + } + + /** + * Возвращает справочники стран и городов для PHP-компонента + */ + public static function getGeoForComponent() { + // 1. Страны (из ядра Битрикс) + $countries = []; + $arCountries = GetCountryArray(); + if (is_array($arCountries['reference_id'])) { + foreach ($arCountries['reference_id'] as $k => $id) { + $countries[] = [ + 'id' => $id, + 'name' => $arCountries['reference'][$k] + ]; + } + } + + // 2. Города (из модуля веб-аналитики) + $cities = []; + if (Loader::includeModule('statistic')) { + $rs = \CCity::GetList(['CITY_NAME' => 'ASC'], []); + while ($el = $rs->Fetch()) { + if (!empty($el['CITY_NAME'])) { + $cities[] = $el['CITY_NAME']; + } + } + $cities = array_values(array_unique($cities)); + } + + return [ + 'countries' => $countries, + 'cities' => $cities + ]; + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/.registrationtrait.php.bak b/local/modules/conmed.authserver/lib/.registrationtrait.php.bak new file mode 100644 index 0000000..150ce84 --- /dev/null +++ b/local/modules/conmed.authserver/lib/.registrationtrait.php.bak @@ -0,0 +1,29 @@ +getRequest(); + if(!self::checkClient($req->get("client_id"), $req->get("client_secret"))) die(json_encode(['error'=>'forbidden'])); + $email = trim($req->getPost("email")); + if(!check_email($email)) die(json_encode(['status'=>'error','message'=>'Некорректный Email'])); + if(\CUser::GetList($b,$o,["=EMAIL"=>$email])->Fetch()) die(json_encode(['status'=>'error','message'=>'Email существует'])); + + $pass = $req->getPost("password"); + $v = self::validatePassword($pass); if($v !== true) die(json_encode(['status'=>'error','message'=>$v])); + + $uid = (new \CUser)->Add(["LOGIN"=>$email,"EMAIL"=>$email,"NAME"=>$req->getPost("name"),"LAST_NAME"=>$req->getPost("last_name"),"PASSWORD"=>$pass,"CONFIRM_PASSWORD"=>$pass,"ACTIVE"=>"Y","GROUP_ID"=>[2,3,4]]); + if($uid) { + $code = bin2hex(random_bytes(16)); + self::getHlEntity('sso_codes')::add(['UF_CODE'=>$code, 'UF_CLIENT_ID'=>$req->get("client_id"), 'UF_USER_ID'=>$uid, 'UF_EXPIRES'=>DateTime::createFromTimestamp(time()+60)]); + self::audit("USER_REGISTERED", $req->get("client_id"), $uid); + echo json_encode(['status'=>'success','code'=>$code]); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/api.php b/local/modules/conmed.authserver/lib/api.php new file mode 100644 index 0000000..2f834b2 --- /dev/null +++ b/local/modules/conmed.authserver/lib/api.php @@ -0,0 +1,38 @@ + [' $date7], 'select' => ['ID']]); + while ($el = $rs->fetch()) $dcSec::delete($el['ID']); + + $date60 = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 86400 * 60); + $rs = $dcAud::getList(['filter' => [' $date60], 'select' => ['ID']]); + while ($el = $rs->fetch()) $dcAud::delete($el['ID']); + + $date1h = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 3600); + $rs = $dcCodes::getList(['filter' => [' $date1h], 'select' => ['ID']]); + while ($el = $rs->fetch()) $dcCodes::delete($el['ID']); + } catch (\Exception $e) {} + + return "\Conmed\Authserver\Api::cleanLogsAgent();"; + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/authtokentrait.php b/local/modules/conmed.authserver/lib/authtokentrait.php new file mode 100644 index 0000000..9d36ccd --- /dev/null +++ b/local/modules/conmed.authserver/lib/authtokentrait.php @@ -0,0 +1,58 @@ +getRequest(); + $cid = $req->get("client_id"); $uri = $req->get("redirect_uri"); + if(!self::checkClient($cid, false, $uri)) die("Access Denied"); + if(!$USER->IsAuthorized()) { LocalRedirect("/auth/?backurl=".urlencode(Context::getCurrent()->getServer()->getRequestUri())); die(); } + + $code = bin2hex(random_bytes(16)); + $dc = self::getHlEntity('sso_codes'); + $dc::add(['UF_CODE'=>$code, 'UF_CLIENT_ID'=>$cid, 'UF_USER_ID'=>$USER->GetID(), 'UF_EXPIRES'=>DateTime::createFromTimestamp(time()+60)]); + + $url = $uri . (strpos($uri, '?') === false ? '?' : '&') . 'code=' . $code . '&authservice=conmedauth'; + if($req->get("state")) $url .= '&state=' . urlencode($req->get("state")); + LocalRedirect($url); + } + + public static function tokenAction() { + header('Content-Type: application/json'); + $req = Context::getCurrent()->getRequest(); $cid = $req->get("client_id"); + if(!self::checkClient($cid, $req->get("client_secret"))) { self::registerAttempt(); die(json_encode(['error'=>'forbidden'])); } + $dc = self::getHlEntity('sso_codes'); + if($c = $dc::getList(['filter'=>['=UF_CODE'=>$req->get("code"),'=UF_CLIENT_ID'=>$cid,'>UF_EXPIRES'=>DateTime::createFromTimestamp(time())]])->fetch()) { + $dc::delete($c['ID']); + $acc = bin2hex(random_bytes(32)); $ref = bin2hex(random_bytes(32)); + $dt = self::getHlEntity('sso_tokens'); + $dt::add(['UF_TOKEN'=>$acc,'UF_REFRESH_TOKEN'=>$ref,'UF_USER_ID'=>$c['UF_USER_ID'],'UF_CLIENT_ID'=>$cid,'UF_EXPIRES'=>DateTime::createFromTimestamp(time()+3600),'UF_REFRESH_EXPIRES'=>DateTime::createFromTimestamp(time()+2592000)]); + echo json_encode(['access_token'=>$acc, 'refresh_token'=>$ref]); + } else { self::registerAttempt(); echo json_encode(['error'=>'invalid_code']); } + } + + public static function refreshAction() { + header('Content-Type: application/json'); + $req = Context::getCurrent()->getRequest(); $cid = $req->get("client_id"); + if(!self::checkClient($cid, $req->get("client_secret"))) die(json_encode(['error'=>'forbidden'])); + $dt = self::getHlEntity('sso_tokens'); + if($t = $dt::getList(['filter'=>['=UF_REFRESH_TOKEN'=>$req->get("refresh_token"),'=UF_CLIENT_ID'=>$cid,'>UF_REFRESH_EXPIRES'=>DateTime::createFromTimestamp(time())]])->fetch()) { + $acc = bin2hex(random_bytes(32)); $ref = bin2hex(random_bytes(32)); + $dt::update($t['ID'], ['UF_TOKEN'=>$acc, 'UF_REFRESH_TOKEN'=>$ref, 'UF_EXPIRES'=>DateTime::createFromTimestamp(time()+3600)]); + echo json_encode(['access_token'=>$acc, 'refresh_token'=>$ref]); + } else echo json_encode(['error'=>'invalid_refresh']); + } + + private static function getUidByToken($token) { + if(!$token) return false; + $dt = self::getHlEntity('sso_tokens'); + $t = $dt::getList(['filter'=>['=UF_TOKEN'=>$token, '>UF_EXPIRES'=>DateTime::createFromTimestamp(time())]])->fetch(); + return $t ? $t['UF_USER_ID'] : false; + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/credentialstrait.php b/local/modules/conmed.authserver/lib/credentialstrait.php new file mode 100644 index 0000000..2517950 --- /dev/null +++ b/local/modules/conmed.authserver/lib/credentialstrait.php @@ -0,0 +1,59 @@ +getRequest(); + + $clientId = $req->getPost("client_id"); + $clientSecret = $req->getPost("client_secret"); + $login = trim($req->getPost("login")); + $password = $req->getPost("password"); + + // 1. Проверка клиента (метод из SecurityTrait) + if(!self::checkClient($clientId, $clientSecret)) { + self::audit("VERIFY_REJECTED", $clientId, 0, "Invalid client secret"); + die(json_encode(['status' => 'error', 'message' => 'Forbidden'])); + } + + if (empty($login) || empty($password)) { + die(json_encode(['status' => 'error', 'message' => 'Empty credentials'])); + } + + // 2. Ищем пользователя через объект Query (чтобы разрешить PASSWORD) + $query = UserTable::query(); + $query->setSelect(['ID', 'PASSWORD', 'ACTIVE']); + $query->enablePrivateFields(); // РАЗРЕШАЕМ ДОСТУП К ПАРОЛЮ + $query->setFilter([ + 'LOGIC' => 'OR', + ['=LOGIN' => $login], + ['=EMAIL' => $login] + ]); + + $user = $query->exec()->fetch(); + + if ($user && $user['ACTIVE'] === 'Y') { + // 3. Проверяем пароль + if (Password::equals($user['PASSWORD'], $password)) { + self::audit("VERIFY_SUCCESS", $clientId, $user['ID'], "Login: $login"); + echo json_encode([ + 'status' => 'success', + 'user_id' => $user['ID'] + ]); + return; + } + } + + self::audit("VERIFY_FAILED", $clientId, 0, "Login: $login"); + echo json_encode(['status' => 'error', 'message' => 'Invalid login or password']); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/dictionariestrait.php b/local/modules/conmed.authserver/lib/dictionariestrait.php new file mode 100644 index 0000000..2d8a7aa --- /dev/null +++ b/local/modules/conmed.authserver/lib/dictionariestrait.php @@ -0,0 +1,47 @@ +getRequest(); + + // Проверка прав клиента + if(!self::checkClient($req->getPost("client_id"), $req->getPost("client_secret"))) { + die(json_encode(['error'=>'forbidden'])); + } + + // 1. Страны (Стандартный список Битрикс) + $countries = []; + $arCountries = GetCountryArray(); + if (is_array($arCountries['reference_id'])) { + foreach ($arCountries['reference_id'] as $k => $id) { + $countries[] = [ + 'id' => $id, + 'name' => $arCountries['reference'][$k] + ]; + } + } + + // 2. Города (Из модуля statistic) + $cities = []; + if (Loader::includeModule('statistic')) { + $rs = \CCity::GetList(['CITY_NAME' => 'ASC'], []); + while ($el = $rs->Fetch()) { + if (!empty($el['CITY_NAME'])) { + $cities[] = $el['CITY_NAME']; + } + } + $cities = array_unique($cities); + $cities = array_values($cities); + } + + echo json_encode([ + 'countries' => $countries, + 'cities' => $cities + ]); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/groupstrait.php b/local/modules/conmed.authserver/lib/groupstrait.php new file mode 100644 index 0000000..94da0ae --- /dev/null +++ b/local/modules/conmed.authserver/lib/groupstrait.php @@ -0,0 +1,18 @@ +getRequest(); + if(!self::checkClient($req->get("client_id"), $req->get("client_secret"))) die(json_encode(['error'=>'forbidden'])); + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=ACTIVE'=>'Y','=C_SORT'=>555],'select'=>['ID','NAME','STRING_ID'],'order'=>['NAME'=>'ASC']]); + $res = []; while($g = $rs->fetch()) if($g['STRING_ID']) $res[] = ['id'=>$g['ID'], 'name'=>$g['NAME'], 'code'=>$g['STRING_ID']]; + echo json_encode($res); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/internaldatatrait.php b/local/modules/conmed.authserver/lib/internaldatatrait.php new file mode 100644 index 0000000..76066e1 --- /dev/null +++ b/local/modules/conmed.authserver/lib/internaldatatrait.php @@ -0,0 +1,67 @@ +['=ACTIVE' => 'Y', '=C_SORT' => 555], + 'select' =>['ID', 'NAME', 'STRING_ID'], + 'order' => ['NAME' => 'ASC'] + ]); + $res =[]; + while($g = $rs->fetch()) { + if($g['STRING_ID']) { + $res[] = ['id' => $g['ID'], 'name' => $g['NAME'], 'code' => $g['STRING_ID']]; + } + } + return $res; + } + + /** + * Возвращает справочники стран и городов для PHP-компонента + */ + public static function getGeoForComponent() { + // 1. Страны (из ядра Битрикс) + $countries =[]; + $arCountries = GetCountryArray(); + if (is_array($arCountries['reference_id'])) { + foreach ($arCountries['reference_id'] as $k => $id) { + $countries[] =[ + 'id' => $id, + 'name' => $arCountries['reference'][$k] + ]; + } + } + + // 2. Города (из модуля веб-аналитики) + $cities =[]; + if (Loader::includeModule('statistic')) { + $rs = \CCity::GetList(['CITY_NAME' => 'ASC'],[]); + while ($el = $rs->Fetch()) { + if (!empty($el['CITY_NAME'])) { + $cities[] = $el['CITY_NAME']; + } + } + $cities = array_values(array_unique($cities)); + } + + return[ + 'countries' => $countries, + 'cities' => $cities + ]; + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/profiletrait.php b/local/modules/conmed.authserver/lib/profiletrait.php new file mode 100644 index 0000000..5e45c7c --- /dev/null +++ b/local/modules/conmed.authserver/lib/profiletrait.php @@ -0,0 +1,155 @@ +getRequest(); + + $auth = $req->getHeader('Authorization'); + $token = (preg_match('/Bearer\s+(.*)$/i', $auth, $m)) ? trim($m[1]) : $req->get("access_token"); + + $uid = self::getUidByToken($token); + + if($uid) { + $u = \CUser::GetByID($uid)->Fetch(); + + // --- ИСПРАВЛЕННАЯ ЛОГИКА ГРУПП --- + $specNames = []; // Названия (только 555) + $specCodes = []; // Коды (только 555) - для чекбоксов + $allCodes = []; // Все коды вообще - для прав доступа + + $rs = \Bitrix\Main\GroupTable::getList([ + 'filter' => ['ID' => \CUser::GetUserGroup($u['ID']), '=ACTIVE' => 'Y'], + 'select' => ['NAME', 'STRING_ID', 'C_SORT'] + ]); + + while($g = $rs->fetch()) { + // 1. В общий список прав добавляем все, у чего есть код + if($g['STRING_ID']) { + $allCodes[] = $g['STRING_ID']; + } + + // 2. В списки СПЕЦИАЛЬНОСТЕЙ - только с сортировкой 555 + if($g['C_SORT'] == 555) { + $specNames[] = $g['NAME']; + if($g['STRING_ID']) { + $specCodes[] = $g['STRING_ID']; + } + } + } + // --------------------------------- + + echo json_encode([ + 'id' => $u['ID'], + 'login' => $u['LOGIN'], + 'email' => $u['EMAIL'], + 'name' => $u['NAME'], + 'last_name' => $u['LAST_NAME'], + 'second_name' => $u['SECOND_NAME'], + 'city' => $u['PERSONAL_CITY'], + 'phone' => $u['PERSONAL_MOBILE'], // Ваша правка + 'country' => $u['PERSONAL_COUNTRY'], + + 'specialties' => $specNames, + 'specialties_code' => $specCodes, // Теперь тут только специальности! + 'groups_code' => $allCodes + ]); + } else { + header('HTTP/1.0 401 Unauthorized'); + } + } + + // 2. ОБНОВЛЕНИЕ ДАННЫХ + public static function updateAction() { + header('Content-Type: application/json'); + $req = Context::getCurrent()->getRequest(); + + if(!self::checkClient($req->getPost("client_id"), $req->getPost("client_secret"))) { + die(json_encode(['error'=>'forbidden'])); + } + + $uid = self::getUidByToken($req->getPost("access_token")); + if(!$uid) die(json_encode(['error'=>'invalid_token'])); + + $fields = [ + "NAME" => $req->getPost("name"), + "LAST_NAME" => $req->getPost("last_name"), + "SECOND_NAME" => $req->getPost("second_name"), + "PERSONAL_MOBILE" => $req->getPost("phone"), // Ваша правка + "PERSONAL_CITY" => $req->getPost("city"), + "PERSONAL_COUNTRY" => $req->getPost("country") + ]; + + $newSpecs = $req->getPost("specialties"); // Приходит массив кодов + + $resNames = []; + $resSpecCodes = []; + + if(is_array($newSpecs)) { + $curG = \CUser::GetUserGroup($uid); + $finalG = []; + $allSpecGIds = []; + + // Получаем ID всех групп-специальностей (555) + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=C_SORT'=>555],'select'=>['ID']]); + while($g = $rs->fetch()) $allSpecGIds[] = $g['ID']; + + // Оставляем у юзера только НЕ специальности + foreach($curG as $gid) { + if(!in_array($gid, $allSpecGIds)) $finalG[] = $gid; + } + + // Добавляем новые выбранные + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=STRING_ID'=>$newSpecs, '=C_SORT'=>555]]); + while($g = $rs->fetch()) { + $finalG[] = $g['ID']; + $resNames[] = $g['NAME']; + $resSpecCodes[] = $g['STRING_ID']; + } + $fields["GROUP_ID"] = $finalG; + } + + $user = new \CUser; + if($user->Update($uid, $fields)) { + self::audit("PROFILE_UPDATED", $req->getPost("client_id"), $uid); + + echo json_encode([ + 'status' => 'success', + 'new_specialties' => $resNames, + 'new_specialties_code' => $resSpecCodes + ]); + } else { + echo json_encode(['status' => 'error', 'message' => strip_tags($user->LAST_ERROR)]); + } + } + + public static function passwordAction() { + header('Content-Type: application/json'); + $req = Context::getCurrent()->getRequest(); + + if(!self::checkClient($req->getPost("client_id"), $req->getPost("client_secret"))) { + die(json_encode(['error'=>'forbidden'])); + } + + $uid = self::getUidByToken($req->getPost("access_token")); + if(!$uid) die(json_encode(['error'=>'invalid_token'])); + + $np = $req->getPost("new_password"); + $v = self::validatePassword($np); + if($v !== true) die(json_encode(['status'=>'error', 'message'=>$v])); + + if((new \CUser)->Update($uid, ["PASSWORD"=>$np, "CONFIRM_PASSWORD"=>$np])) { + self::audit("PASS_CHANGED", $req->getPost("client_id"), $uid); + echo json_encode(['status'=>'success']); + } else { + echo json_encode(['status'=>'error', 'message'=>strip_tags((new \CUser)->LAST_ERROR)]); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/profiletrait.php.bak b/local/modules/conmed.authserver/lib/profiletrait.php.bak new file mode 100644 index 0000000..6f70362 --- /dev/null +++ b/local/modules/conmed.authserver/lib/profiletrait.php.bak @@ -0,0 +1,108 @@ +getRequest(); + $auth = $req->getHeader('Authorization'); + $token = (preg_match('/Bearer\s+(.*)$/i', $auth, $m)) ? trim($m[1]) : $req->get("access_token"); + + $uid = self::getUidByToken($token); + if($uid) { + $u = \CUser::GetByID($uid)->Fetch(); + $gn = []; $gc = []; $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['ID'=>\CUser::GetUserGroup($u['ID']),'=ACTIVE'=>'Y'],'select'=>['NAME','STRING_ID','C_SORT']]); + while($g = $rs->fetch()) { + if($g['C_SORT']==555) $gn[]=$g['NAME']; + if($g['STRING_ID']) $gc[]=$g['STRING_ID']; + } + echo json_encode([ + 'id'=>$u['ID'], 'login'=>$u['LOGIN'], 'email'=>$u['EMAIL'], 'name'=>$u['NAME'], + 'last_name'=>$u['LAST_NAME'], 'second_name'=>$u['SECOND_NAME'], + 'specialties'=>$gn, + 'city'=>$u['PERSONAL_CITY'], + //'phone'=>$u['PERSONAL_PHONE'], + 'phone'=>$u['PERSONAL_MOBILE'], + 'country'=>$u['PERSONAL_COUNTRY'], // ДОБАВЛЕНО + 'specialties_code'=>$gc, 'groups_code'=>$gc + ]); + } else { header('HTTP/1.0 401 Unauthorized'); } + } + + public static function updateAction() { + header('Content-Type: application/json'); $req = Context::getCurrent()->getRequest(); + if(!self::checkClient($req->getPost("client_id"), $req->getPost("client_secret"))) die(json_encode(['error'=>'forbidden'])); + + $uid = self::getUidByToken($req->getPost("access_token")); + if(!$uid) die(json_encode(['error'=>'invalid_token'])); + + $fields = [ + "NAME" => $req->getPost("name"), + "LAST_NAME" => $req->getPost("last_name"), + "SECOND_NAME" => $req->getPost("second_name"), + //"PERSONAL_PHONE" => $req->getPost("phone"), + "PERSONAL_MOBILE" => $req->getPost("phone"), + + "PERSONAL_CITY" => $req->getPost("city"), + "PERSONAL_COUNTRY" => $req->getPost("country") // ДОБАВЛЕНО + ]; + + $newSpecs = $req->getPost("specialties"); + $newNames = []; + $newCodes = []; + + if(is_array($newSpecs)) { + $curG = \CUser::GetUserGroup($uid); + $finalG = []; + $allSpecG = []; + + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=C_SORT'=>555],'select'=>['ID']]); + while($g = $rs->fetch()) $allSpecG[] = $g['ID']; + + foreach($curG as $gid) if(!in_array($gid, $allSpecG)) $finalG[] = $gid; + + $rs = \Bitrix\Main\GroupTable::getList(['filter'=>['=STRING_ID'=>$newSpecs, '=C_SORT'=>555]]); + while($g = $rs->fetch()) { + $finalG[] = $g['ID']; + $newNames[] = $g['NAME']; + $newCodes[] = $g['STRING_ID']; + } + $fields["GROUP_ID"] = $finalG; + } + + if((new \CUser)->Update($uid, $fields)) { + self::audit("PROFILE_UPDATED", $req->getPost("client_id"), $uid); + echo json_encode([ + 'status' => 'success', + 'new_specialties' => $newNames, + 'new_specialties_code' => $newCodes + ]); + } else { + echo json_encode(['status'=>'error', 'message'=>'Update failed']); + } + } + + public static function passwordAction() { + header('Content-Type: application/json'); $req = Context::getCurrent()->getRequest(); + if(!self::checkClient($req->getPost("client_id"), $req->getPost("client_secret"))) die(json_encode(['error'=>'forbidden'])); + + $uid = self::getUidByToken($req->getPost("access_token")); + if(!$uid) die(json_encode(['error'=>'invalid_token'])); + + $np = $req->getPost("new_password"); + $v = self::validatePassword($np); + if($v !== true) die(json_encode(['status'=>'error', 'message'=>$v])); + + if((new \CUser)->Update($uid, ["PASSWORD"=>$np, "CONFIRM_PASSWORD"=>$np])) { + self::audit("PASS_CHANGED", $req->getPost("client_id"), $uid); + echo json_encode(['status'=>'success']); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/registrationtrait.php b/local/modules/conmed.authserver/lib/registrationtrait.php new file mode 100644 index 0000000..15a2e80 --- /dev/null +++ b/local/modules/conmed.authserver/lib/registrationtrait.php @@ -0,0 +1,94 @@ +getRequest(); + + // 1. Проверка прав клиента + if(!self::checkClient($req->get("client_id"), $req->get("client_secret"))) { + die(json_encode(['error'=>'forbidden'])); + } + + $email = trim($req->getPost("email")); + + // 2. Валидация входных данных + if(!check_email($email)) { + die(json_encode(['status'=>'error','message'=>'Некорректный Email'])); + } + + // Проверка на существование + $by = "ID"; $order = "ASC"; + if(\CUser::GetList($by, $order, ["=EMAIL" => $email])->Fetch()) { + die(json_encode(['status'=>'error','message'=>'Пользователь с таким Email уже существует'])); + } + + $pass = $req->getPost("password"); + $v = self::validatePassword($pass); + if($v !== true) { + die(json_encode(['status'=>'error','message'=>$v])); + } + + // 3. Подготовка групп (специальности) + $arGroups = [2, 3, 4]; // Базовые группы + $specCode = $req->getPost("specialty"); + + if (!empty($specCode)) { + $rsGroup = \Bitrix\Main\GroupTable::getList([ + 'filter' => ['=STRING_ID' => $specCode, '=ACTIVE' => 'Y'], + 'select' => ['ID'] + ])->fetch(); + + if ($rsGroup) { + $arGroups[] = $rsGroup['ID']; + } + } + + // 4. Создание пользователя + $user = new \CUser; + $arFields = [ + "LOGIN" => $email, + "EMAIL" => $email, + "NAME" => $req->getPost("name"), + "LAST_NAME" => $req->getPost("last_name"), + "SECOND_NAME" => $req->getPost("second_name"), + //"PERSONAL_PHONE" => $req->getPost("phone"), // Записываем телефон + "PERSONAL_MOBILE" => $req->getPost("phone"), // Записываем телефон + + "PERSONAL_CITY" => $req->getPost("city"), // Записываем город + "PERSONAL_COUNTRY" => $req->getPost("country"), // ФИКС: Добавлена страна + "PASSWORD" => $pass, + "CONFIRM_PASSWORD" => $pass, + "ACTIVE" => "Y", + "GROUP_ID" => $arGroups + ]; + + $uid = $user->Add($arFields); + + if($uid) { + // Генерируем код для мгновенного входа после регистрации + $code = bin2hex(random_bytes(16)); + $dcCodes = self::getHlEntity('sso_codes'); + $dcCodes::add([ + 'UF_CODE' => $code, + 'UF_CLIENT_ID' => $req->get("client_id"), + 'UF_USER_ID' => $uid, + 'UF_EXPIRES' => DateTime::createFromTimestamp(time() + 60) + ]); + + self::audit("USER_REGISTERED", $req->get("client_id"), $uid, "Email: ".$email); + + //echo json_encode(['status' => 'success', 'code' => $code]); + echo json_encode(['status' => 'success', 'code' => $code, 'user_id' => $uid]); + } else { + echo json_encode(['status' => 'error', 'message' => strip_tags($user->LAST_ERROR)]); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/securitytrait.php b/local/modules/conmed.authserver/lib/securitytrait.php new file mode 100644 index 0000000..a83061d --- /dev/null +++ b/local/modules/conmed.authserver/lib/securitytrait.php @@ -0,0 +1,75 @@ + 'SsoCodes', 'sso_tokens' => 'SsoTokens', + 'sso_audit' => 'SsoAudit', 'sso_security_log' => 'SsoSecurityLog' + ]; + + public static function checkClient($clientId, $clientSecret = false, $redirectUri = false) { + $rawList = Option::get("conmed.authserver", "client_list"); + foreach(explode("\n", str_replace("\r", "", $rawList)) as $line) { + $pair = explode(":", trim($line), 3); + if($pair[0] === $clientId) { + if($clientSecret !== false && !password_verify($clientSecret, $pair[1])) return false; + if($redirectUri !== false) { + $u = parse_url($redirectUri); + if(!$u || empty($u['host'])) return false; + $domain = str_replace(['https://','http://'], '', strtolower(trim($pair[2] ?? ""))); + if(strtolower($u['host']) !== strtolower(trim($domain, " /"))) return false; + } + return true; + } + } + return false; + } + + public static function checkRateLimit($action = 'AUTH') { + $ip = Context::getCurrent()->getRequest()->getRemoteAddress(); + $dc = self::getHlEntity('sso_security_log'); + $limit = DateTime::createFromTimestamp(time() - 300); + $count = $dc::getCount(['=UF_IP' => $ip, '>UF_DATE' => $limit]); + if ($count >= 20) { + header('HTTP/1.1 429 Too Many Requests'); + die(json_encode(['error' => 'Rate limit exceeded'])); + } + } + + public static function registerAttempt() { + try { + $dc = self::getHlEntity('sso_security_log'); + $dc::add(['UF_IP' => Context::getCurrent()->getRequest()->getRemoteAddress(), 'UF_DATE' => new DateTime()]); + } catch (\Exception $e) {} + } + + public static function audit($event, $clientId, $userId = 0, $details = "") { + try { + $dc = self::getHlEntity('sso_audit'); + $dc::add([ + 'UF_EVENT' => $event, 'UF_CLIENT_ID' => $clientId, 'UF_USER_ID' => (int)$userId, + 'UF_IP' => Context::getCurrent()->getRequest()->getRemoteAddress(), + 'UF_DATE' => new DateTime(), 'UF_DETAILS' => substr($details, 0, 255) + ]); + } catch (\Exception $e) {} + } + + public static function validatePassword($pass) { + if (strlen($pass) < 6) return "Минимум 6 символов"; + if (!preg_match("/[a-zA-Z]/", $pass) || !preg_match("/[0-9]/", $pass)) return "Пароль должен содержать буквы и цифры"; + return true; + } + + public static function getHlEntity($tableName) { + if (!isset(self::$allowedTables[$tableName])) die("Access denied"); + Loader::includeModule("highloadblock"); + $hl = HighloadBlockTable::getList(['filter'=>['=TABLE_NAME'=>$tableName]])->fetch(); + return HighloadBlockTable::compileEntity($hl)->getDataClass(); + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/webhook.php b/local/modules/conmed.authserver/lib/webhook.php new file mode 100644 index 0000000..49d94b0 --- /dev/null +++ b/local/modules/conmed.authserver/lib/webhook.php @@ -0,0 +1,36 @@ + false, 'socketTimeout' => 3]); + $isSslVerify = \Bitrix\Main\Config\Option::get("conmed.authserver", "ssl_verify", "Y") === "Y"; + $http = new HttpClient(['sslVerify' => $isSslVerify, 'socketTimeout' => 3]); + + $http->setHeader('X-SSO-Signature', $signature); + + // Отправляем асинхронно (короткий таймаут), чтобы не вешать админку + $http->post($url, ['user_id' => $userId, 'client_id' => $clientId]); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/lib/webhook.php.0.bak b/local/modules/conmed.authserver/lib/webhook.php.0.bak new file mode 100644 index 0000000..590d5c7 --- /dev/null +++ b/local/modules/conmed.authserver/lib/webhook.php.0.bak @@ -0,0 +1,33 @@ + false, 'socketTimeout' => 3]); + $http->setHeader('X-SSO-Signature', $signature); + + // Отправляем асинхронно (короткий таймаут), чтобы не вешать админку + $http->post($url, ['user_id' => $userId, 'client_id' => $clientId]); + } + } +} \ No newline at end of file diff --git a/local/modules/conmed.authserver/man.txt b/local/modules/conmed.authserver/man.txt new file mode 100644 index 0000000..cf8c563 --- /dev/null +++ b/local/modules/conmed.authserver/man.txt @@ -0,0 +1,62 @@ + +Эта документация описывает архитектуру созданной системы Single Sign-On (SSO) для сети сайтов con-med.ru. Система построена по принципу Identity Provider (IdP) и Service Provider (SP). +1. Общая архитектура +Мастер-сервер (auth.con-med.ru): «Паспортный стол». Здесь хранятся все учетные записи, пароли и основные группы (специальности). +Личный кабинет (id.con-med.ru): Основной интерфейс пользователя. Здесь происходит регистрация и редактирование профиля (данные транзитом улетают на мастер-сервер). +Сайты-потребители (vebinar, content): Проверяют авторизацию через мастер-сервер. +Сервер статистики (stat): Собирает логи действий пользователей со всех площадок. +2. Модуль сервера: conmed.authserver +Где лежит: auth.con-med.ru → /local/modules/conmed.authserver/ +Файловая структура: +lib/api.php: Фасад системы, собирающий функционал из трейтов. +lib/securitytrait.php: Валидация паролей, проверка ключей сайтов (хеши), Rate Limiting. +lib/authtokentrait.php: Логика OAuth 2.0 (выдача кодов, токенов, Refresh токенов). +lib/registrationtrait.php: Регистрация новых пользователей. +lib/profiletrait.php: Обновление ФИО, города, телефона и мульти-специальностей. +lib/groupstrait.php: Отдача списка специальностей (группы с C_SORT = 555). +/api/oauth/: Точки входа для внешних запросов (user.php, token.php и т.д.). +Ключевые особенности: +Безопасность: Секретные ключи сайтов хранятся в виде BCRYPT-хешей. +IDOR Protection: Пользователь может менять только свой профиль (проверка по Access Token). +Агент: Раз в сутки запускается Api::cleanLogsAgent() для очистки старых логов. +3. Модуль клиента: conmed.sso +Где лежит: Все сайты, кроме auth → /local/modules/conmed.sso/ +Файловая структура: +lib/auth.php: Основной класс. Реализует метод Authorize() (вход по коду) и AuthorizeByToken(). +lib/interceptor.php: «Тихий страж». На событии OnProlog проверяет наличие куки CONMED_REFRESH. Если сессия Битрикса истекла, он незаметно обновляет её через сервер. +lib/helper.php: Синхронизация групп (сопоставление по STRING_ID). +/ajax/sso_handler.php: Единственный прокси-файл для всех AJAX-запросов фронтенда. Использует локальное кеширование специальностей (24ч). +Компоненты: +conmed:sso.auth: Модальное окно входа и регистрации. +conmed:sso.profile: Личный кабинет с табами, мульти-выбором специальностей, поиском и списком дипломов. +4. Инструкции по эксплуатации +Как добавить новый сайт в систему +На сервере auth в настройках модуля conmed.authserver добавьте строку: +ID_САЙТА : ЛЮБОЙ_ПАРОЛЬ : https://domain.ru +Нажмите «Сохранить» (пароль превратится в хеш). +Скопируйте папку модуля conmed.sso на новый сайт. +Установите модуль в админке нового сайта. +В настройках модуля введите ID_САЙТА и тот самый ПАРОЛЬ. +Подключите компонент conmed:sso.auth в футере шаблона. +Как добавить новую специальность +На сервере auth создайте группу пользователей. +Установите ей Сортировку = 555. +Обязательно задайте Символьный идентификатор (латиницей). +Список на всех сайтах обновится автоматически (в течение 24 часов из-за кеша или мгновенно после его очистки). +Как работает связка пользователей +Система ищет совпадения в следующем порядке: +XML_ID (равен ID на сервере auth). +Если не нашли — LOGIN (для склейки старых записей). +Если не нашли — EMAIL. +Это гарантирует отсутствие дублей даже при переносе старой базы 200к+ пользователей. +5. Технический журнал (Где смотреть логи) +Мастер-сервер: HL-блок SsoAudit (успешные действия) и SsoSecurityLog (ошибки/атаки). +Сайт ID: Файл /sso_final.log (отладка входа) и /sso_client_debug.log (отладка прокси). +Дипломы: HL-блок UserDiplomas на сайте id.con-med.ru. +6. TODO (План развития) +Сайт статистики: Разместить на stat.con-med.ru HL-блок и файл-приемщик для функции sendStatToHub. +Email-уведомления: Настроить на сервере auth почтовые события, чтобы при смене пароля или регистрации через API пользователю уходило письмо. +Webhook удаления: Добавить логику: если пользователь удаляется на auth, отправлять запрос на id для блокировки локальной записи. +Синхронизация Аватаров: Добавить в API передачу PERSONAL_PHOTO через base64 или прямую ссылку. +Массовая миграция: Запустить /local/tools/diplom_import.php для переноса оставшихся 200 000 дипломов (рекомендуется делать пачками по ночам). +Документация актуальна на февраль 2026 года. \ No newline at end of file diff --git a/local/modules/conmed.authserver/options.php b/local/modules/conmed.authserver/options.php new file mode 100644 index 0000000..dae06ae --- /dev/null +++ b/local/modules/conmed.authserver/options.php @@ -0,0 +1,43 @@ +"edit1","TAB"=>"Настройки","TITLE"=>"Доверенные сайты"]]); +$tabControl->Begin(); +?> +
+ +BeginNextTab()?> + + Список клиентов (ID:SECRET:DOMAIN): + + +
+ Формат: ID : SECRET : https://id.con-med.ru (с протоколом, без слеша в конце) + + +Строгая проверка SSL S2S:> + +Buttons()?>End()?>
\ No newline at end of file diff --git a/local/modules/conmed.authserver/options.php.0.bak b/local/modules/conmed.authserver/options.php.0.bak new file mode 100644 index 0000000..e89e1ff --- /dev/null +++ b/local/modules/conmed.authserver/options.php.0.bak @@ -0,0 +1,40 @@ +"edit1","TAB"=>"Настройки","TITLE"=>"Доверенные сайты"]]); +$tabControl->Begin(); +?> +
+ +BeginNextTab()?> + + Список клиентов (ID:SECRET:DOMAIN): + + +
+ Формат: ID : SECRET : https://id.con-med.ru (с протоколом, без слеша в конце) + + +Buttons()?>End()?>
\ No newline at end of file diff --git a/personal/index.php b/personal/index.php new file mode 100644 index 0000000..c878250 --- /dev/null +++ b/personal/index.php @@ -0,0 +1,229 @@ +SetTitle("Личный кабинет"); + +global $USER; + +// Обработка выхода +if ($_GET["logout"] == "yes" && $USER->IsAuthorized()) { + $USER->Logout(); + LocalRedirect("/auth/?login=yes"); +} + +// Проверяем авторизацию +$isAuth = $USER->IsAuthorized(); + +// Если авторизован, получаем имя +$userName = ""; +if ($isAuth) { + $userName = $USER->GetFirstName(); + if (!$userName) $userName = $USER->GetLogin(); +} +?> + + + + + + \ No newline at end of file diff --git a/register/index.php b/register/index.php new file mode 100644 index 0000000..03c0e13 --- /dev/null +++ b/register/index.php @@ -0,0 +1,19 @@ +SetTitle("Регистрация в системе Con-Med"); +?> + +
+ IncludeComponent( + "conmed:sso.register", + ".default", + array( + "SUCCESS_PAGE" => "/personal/", // Куда вести, если нет backurl + ), + false + ); + ?> +
+ + \ No newline at end of file