Initial commit: Auth Server Base
This commit is contained in:
92
local/modules/conmed.authserver/.reinstall_agent.php
Normal file
92
local/modules/conmed.authserver/.reinstall_agent.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* REINSTALL LOG CLEANUP AGENT
|
||||
* Target: auth.con-med.ru
|
||||
*/
|
||||
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
|
||||
|
||||
if (!$USER->IsAdmin()) die("Доступ запрещен");
|
||||
|
||||
$log = [];
|
||||
$apiFile = $_SERVER["DOCUMENT_ROOT"]."/local/modules/conmed.authserver/lib/api.php";
|
||||
|
||||
// 1. ОБНОВЛЯЕМ ФАЙЛ API.PHP (чтобы метод точно был в классе)
|
||||
$apiCode = <<<'PHP'
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
/**
|
||||
* Класс-фасад API сервера авторизации
|
||||
* Собирает функционал из трейтов и содержит системные методы (агенты)
|
||||
*/
|
||||
class Api {
|
||||
use SecurityTrait, AuthTokenTrait, RegistrationTrait, ProfileTrait, GroupsTrait;
|
||||
|
||||
/**
|
||||
* Агент автоматической очистки логов
|
||||
* Выполняется раз в сутки
|
||||
*/
|
||||
public static function cleanLogsAgent() {
|
||||
if (!\Bitrix\Main\Loader::includeModule("highloadblock")) return "";
|
||||
|
||||
try {
|
||||
// Получаем классы таблиц через трейт безопасности
|
||||
$dcSec = self::getHlEntity('sso_security_log');
|
||||
$dcAud = self::getHlEntity('sso_audit');
|
||||
$dcCodes = self::getHlEntity('sso_codes');
|
||||
|
||||
// 1. Очистка логов безопасности (храним 7 дней)
|
||||
$date7 = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 86400 * 7);
|
||||
$rs = $dcSec::getList(['filter' => ['<UF_DATE' => $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' => ['<UF_DATE' => $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' => ['<UF_EXPIRES' => $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 "<h2>Результат установки:</h2>";
|
||||
echo "<ul><li>" . implode("</li><li>", $log) . "</li></ul>";
|
||||
echo "<p>Проверить работу агента можно в админке: <b>Настройки -> Настройки продукта -> Агенты</b></p>";
|
||||
44
local/modules/conmed.authserver/install/index.php
Normal file
44
local/modules/conmed.authserver/install/index.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
use Bitrix\Main\ModuleManager;
|
||||
use Bitrix\Main\EventManager;
|
||||
|
||||
class conmed_authserver extends CModule {
|
||||
var $MODULE_ID = "conmed.authserver";
|
||||
var $MODULE_NAME = "Con-Med: Сервер авторизации (Module Edition)";
|
||||
var $MODULE_VERSION = "1.2.0"; // Подняли версию, так как добавили события
|
||||
var $MODULE_DESCRIPTION = "Централизованное управление SSO и API";
|
||||
|
||||
function DoInstall() {
|
||||
ModuleManager::registerModule($this->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"
|
||||
);
|
||||
}
|
||||
}
|
||||
18
local/modules/conmed.authserver/lib/.groupstrait.php.bak
Normal file
18
local/modules/conmed.authserver/lib/.groupstrait.php.bak
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait GroupsTrait {
|
||||
public static function groupsAction() {
|
||||
header('Content-Type: application/json'); $req = Context::getCurrent()->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait InternalDataTrait {
|
||||
|
||||
/**
|
||||
* Возвращает список специальностей для PHP-компонента
|
||||
*/
|
||||
public static function getSpecialtiesForComponent() {
|
||||
$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']];
|
||||
}
|
||||
}
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait RegistrationTrait {
|
||||
public static function registerAction() {
|
||||
header('Content-Type: application/json'); $req = Context::getCurrent()->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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
local/modules/conmed.authserver/lib/api.php
Normal file
38
local/modules/conmed.authserver/lib/api.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
/**
|
||||
* Класс-фасад API сервера авторизации
|
||||
* Собирает функционал из трейтов и содержит системные методы (агенты)
|
||||
*/
|
||||
class Api {
|
||||
// ВНИМАТЕЛЬНО: Подключаем все 7 трейтов
|
||||
use SecurityTrait, AuthTokenTrait, RegistrationTrait, ProfileTrait, GroupsTrait, DictionariesTrait, CredentialsTrait, InternalDataTrait;
|
||||
|
||||
public static function onAfterUserDeleteHandler($userId) {
|
||||
Webhook::sendDelete($userId);
|
||||
}
|
||||
|
||||
public static function cleanLogsAgent() {
|
||||
if (!\Bitrix\Main\Loader::includeModule("highloadblock")) return "";
|
||||
try {
|
||||
$dcSec = self::getHlEntity('sso_security_log');
|
||||
$dcAud = self::getHlEntity('sso_audit');
|
||||
$dcCodes = self::getHlEntity('sso_codes');
|
||||
|
||||
$date7 = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 86400 * 7);
|
||||
$rs = $dcSec::getList(['filter' => ['<UF_DATE' => $date7], 'select' => ['ID']]);
|
||||
while ($el = $rs->fetch()) $dcSec::delete($el['ID']);
|
||||
|
||||
$date60 = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 86400 * 60);
|
||||
$rs = $dcAud::getList(['filter' => ['<UF_DATE' => $date60], 'select' => ['ID']]);
|
||||
while ($el = $rs->fetch()) $dcAud::delete($el['ID']);
|
||||
|
||||
$date1h = \Bitrix\Main\Type\DateTime::createFromTimestamp(time() - 3600);
|
||||
$rs = $dcCodes::getList(['filter' => ['<UF_EXPIRES' => $date1h], 'select' => ['ID']]);
|
||||
while ($el = $rs->fetch()) $dcCodes::delete($el['ID']);
|
||||
} catch (\Exception $e) {}
|
||||
|
||||
return "\Conmed\Authserver\Api::cleanLogsAgent();";
|
||||
}
|
||||
}
|
||||
58
local/modules/conmed.authserver/lib/authtokentrait.php
Normal file
58
local/modules/conmed.authserver/lib/authtokentrait.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait AuthTokenTrait {
|
||||
public static function authorizeAction() {
|
||||
global $USER; $req = Context::getCurrent()->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;
|
||||
}
|
||||
}
|
||||
59
local/modules/conmed.authserver/lib/credentialstrait.php
Normal file
59
local/modules/conmed.authserver/lib/credentialstrait.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\UserTable;
|
||||
use Bitrix\Main\Security\Password;
|
||||
|
||||
trait CredentialsTrait {
|
||||
/**
|
||||
* Прямая проверка логина и пароля без создания сессий
|
||||
* action: /api/oauth/verify.php
|
||||
*/
|
||||
public static function verifyAction() {
|
||||
header('Content-Type: application/json');
|
||||
$req = Context::getCurrent()->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']);
|
||||
}
|
||||
}
|
||||
47
local/modules/conmed.authserver/lib/dictionariestrait.php
Normal file
47
local/modules/conmed.authserver/lib/dictionariestrait.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait DictionariesTrait {
|
||||
public static function dictionariesAction() {
|
||||
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']));
|
||||
}
|
||||
|
||||
// 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
18
local/modules/conmed.authserver/lib/groupstrait.php
Normal file
18
local/modules/conmed.authserver/lib/groupstrait.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait GroupsTrait {
|
||||
public static function groupsAction() {
|
||||
header('Content-Type: application/json'); $req = Context::getCurrent()->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);
|
||||
}
|
||||
}
|
||||
67
local/modules/conmed.authserver/lib/internaldatatrait.php
Normal file
67
local/modules/conmed.authserver/lib/internaldatatrait.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait InternalDataTrait {
|
||||
|
||||
/**
|
||||
* Возвращает Client ID по умолчанию для локальной регистрации и системных нужд
|
||||
* @return string
|
||||
*/
|
||||
public static function getDefaultClientId() {
|
||||
return 'app_id_site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает список специальностей для PHP-компонента
|
||||
*/
|
||||
public static function getSpecialtiesForComponent() {
|
||||
$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']];
|
||||
}
|
||||
}
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
155
local/modules/conmed.authserver/lib/profiletrait.php
Normal file
155
local/modules/conmed.authserver/lib/profiletrait.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait ProfileTrait {
|
||||
|
||||
// 1. ПОЛУЧЕНИЕ ДАННЫХ (Вызывается при входе)
|
||||
public static function userAction() {
|
||||
header('Content-Type: application/json');
|
||||
$req = Context::getCurrent()->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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
local/modules/conmed.authserver/lib/profiletrait.php.bak
Normal file
108
local/modules/conmed.authserver/lib/profiletrait.php.bak
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait ProfileTrait {
|
||||
|
||||
public static function userAction() {
|
||||
header('Content-Type: application/json');
|
||||
$req = Context::getCurrent()->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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
local/modules/conmed.authserver/lib/registrationtrait.php
Normal file
94
local/modules/conmed.authserver/lib/registrationtrait.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait RegistrationTrait {
|
||||
public static function registerAction() {
|
||||
header('Content-Type: application/json');
|
||||
$req = Context::getCurrent()->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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
local/modules/conmed.authserver/lib/securitytrait.php
Normal file
75
local/modules/conmed.authserver/lib/securitytrait.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Context;
|
||||
use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Highloadblock\HighloadBlockTable;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
trait SecurityTrait {
|
||||
private static $allowedTables = [
|
||||
'sso_codes' => '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();
|
||||
}
|
||||
}
|
||||
36
local/modules/conmed.authserver/lib/webhook.php
Normal file
36
local/modules/conmed.authserver/lib/webhook.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Web\HttpClient;
|
||||
|
||||
class Webhook {
|
||||
public static function sendDelete($userId) {
|
||||
$rawList = Option::get("conmed.authserver", "client_list");
|
||||
$lines = explode("\n", str_replace("\r", "", $rawList));
|
||||
|
||||
foreach($lines as $line) {
|
||||
$pair = explode(":", trim($line), 3);
|
||||
if(count($pair) < 3) continue;
|
||||
|
||||
$clientId = trim($pair[0]);
|
||||
$secret = trim($pair[1]); // Здесь хеш
|
||||
$domain = trim($pair[2], " /");
|
||||
|
||||
// Формируем URL контроллера на удаленном сайте
|
||||
$url = $domain . "/bitrix/services/main/ajax.php?action=conmed:sso.api.webhook.delete";
|
||||
|
||||
// Подписываем запрос (используем часть хеша секрета как ключ)
|
||||
$signature = hash_hmac('sha256', $userId, $secret);
|
||||
|
||||
//$http = new HttpClient(['sslVerify' => 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
local/modules/conmed.authserver/lib/webhook.php.0.bak
Normal file
33
local/modules/conmed.authserver/lib/webhook.php.0.bak
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Conmed\Authserver;
|
||||
|
||||
use Bitrix\Main\Config\Option;
|
||||
use Bitrix\Main\Web\HttpClient;
|
||||
|
||||
class Webhook {
|
||||
public static function sendDelete($userId) {
|
||||
$rawList = Option::get("conmed.authserver", "client_list");
|
||||
$lines = explode("\n", str_replace("\r", "", $rawList));
|
||||
|
||||
foreach($lines as $line) {
|
||||
$pair = explode(":", trim($line), 3);
|
||||
if(count($pair) < 3) continue;
|
||||
|
||||
$clientId = trim($pair[0]);
|
||||
$secret = trim($pair[1]); // Здесь хеш
|
||||
$domain = trim($pair[2], " /");
|
||||
|
||||
// Формируем URL контроллера на удаленном сайте
|
||||
$url = $domain . "/bitrix/services/main/ajax.php?action=conmed:sso.api.webhook.delete";
|
||||
|
||||
// Подписываем запрос (используем часть хеша секрета как ключ)
|
||||
$signature = hash_hmac('sha256', $userId, $secret);
|
||||
|
||||
$http = new HttpClient(['sslVerify' => false, 'socketTimeout' => 3]);
|
||||
$http->setHeader('X-SSO-Signature', $signature);
|
||||
|
||||
// Отправляем асинхронно (короткий таймаут), чтобы не вешать админку
|
||||
$http->post($url, ['user_id' => $userId, 'client_id' => $clientId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
local/modules/conmed.authserver/man.txt
Normal file
62
local/modules/conmed.authserver/man.txt
Normal file
@@ -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 года.
|
||||
43
local/modules/conmed.authserver/options.php
Normal file
43
local/modules/conmed.authserver/options.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
$mid = "conmed.authserver";
|
||||
if($_POST['Update'] && check_bitrix_sessid()){
|
||||
$rawList = $_POST['client_list'];
|
||||
$lines = explode("\n", str_replace("\r", "", $rawList));
|
||||
$processedLines = [];
|
||||
|
||||
foreach($lines as $line) {
|
||||
$line = trim($line);
|
||||
if(empty($line)) continue;
|
||||
|
||||
// Лимит 3 позволяет корректно обрабатывать https:// в третьем поле
|
||||
$pair = explode(":", $line, 3);
|
||||
$clientId = trim($pair[0]);
|
||||
$secret = trim($pair[1]);
|
||||
$domain = isset($pair[2]) ? trim($pair[2], " /") : "";
|
||||
|
||||
if (strpos($secret, '$2y$') !== 0) {
|
||||
$secret = password_hash($secret, PASSWORD_BCRYPT);
|
||||
}
|
||||
$processedLines[] = $clientId . ":" . $secret . ":" . $domain;
|
||||
}
|
||||
\Bitrix\Main\Config\Option::set($mid, "client_list", implode("\n", $processedLines));
|
||||
\Bitrix\Main\Config\Option::set($mid, "ssl_verify", isset($_POST['ssl_verify']) ? "Y" : "N");
|
||||
}
|
||||
$clientList = \Bitrix\Main\Config\Option::get($mid, "client_list");
|
||||
$tabControl = new CAdminTabControl("tabControl", [["DIV"=>"edit1","TAB"=>"Настройки","TITLE"=>"Доверенные сайты"]]);
|
||||
$tabControl->Begin();
|
||||
?>
|
||||
<form method="post">
|
||||
<?=bitrix_sessid_post()?>
|
||||
<?$tabControl->BeginNextTab()?>
|
||||
<tr>
|
||||
<td width="40%" valign="top">Список клиентов (ID:SECRET:DOMAIN):</td>
|
||||
<td width="60%">
|
||||
<textarea name="client_list" rows="10" cols="90" style="font-family:monospace;"><?=htmlspecialcharsbx($clientList)?></textarea>
|
||||
<br>
|
||||
<small>Формат: <b>ID : SECRET : https://id.con-med.ru</b> (с протоколом, без слеша в конце)</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>Строгая проверка SSL S2S:</td><td><input type="checkbox" name="ssl_verify" value="Y" <?=\Bitrix\Main\Config\Option::get($mid, "ssl_verify", "Y") == "Y" ? "checked" : ""?>></td></tr>
|
||||
|
||||
<?$tabControl->Buttons()?><input type="submit" name="Update" value="Сохранить" class="adm-btn-save"><?$tabControl->End()?></form>
|
||||
40
local/modules/conmed.authserver/options.php.0.bak
Normal file
40
local/modules/conmed.authserver/options.php.0.bak
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
$mid = "conmed.authserver";
|
||||
if($_POST['Update'] && check_bitrix_sessid()){
|
||||
$rawList = $_POST['client_list'];
|
||||
$lines = explode("\n", str_replace("\r", "", $rawList));
|
||||
$processedLines = [];
|
||||
|
||||
foreach($lines as $line) {
|
||||
$line = trim($line);
|
||||
if(empty($line)) continue;
|
||||
|
||||
// Лимит 3 позволяет корректно обрабатывать https:// в третьем поле
|
||||
$pair = explode(":", $line, 3);
|
||||
$clientId = trim($pair[0]);
|
||||
$secret = trim($pair[1]);
|
||||
$domain = isset($pair[2]) ? trim($pair[2], " /") : "";
|
||||
|
||||
if (strpos($secret, '$2y$') !== 0) {
|
||||
$secret = password_hash($secret, PASSWORD_BCRYPT);
|
||||
}
|
||||
$processedLines[] = $clientId . ":" . $secret . ":" . $domain;
|
||||
}
|
||||
\Bitrix\Main\Config\Option::set($mid, "client_list", implode("\n", $processedLines));
|
||||
}
|
||||
$clientList = \Bitrix\Main\Config\Option::get($mid, "client_list");
|
||||
$tabControl = new CAdminTabControl("tabControl", [["DIV"=>"edit1","TAB"=>"Настройки","TITLE"=>"Доверенные сайты"]]);
|
||||
$tabControl->Begin();
|
||||
?>
|
||||
<form method="post">
|
||||
<?=bitrix_sessid_post()?>
|
||||
<?$tabControl->BeginNextTab()?>
|
||||
<tr>
|
||||
<td width="40%" valign="top">Список клиентов (ID:SECRET:DOMAIN):</td>
|
||||
<td width="60%">
|
||||
<textarea name="client_list" rows="10" cols="90" style="font-family:monospace;"><?=htmlspecialcharsbx($clientList)?></textarea>
|
||||
<br>
|
||||
<small>Формат: <b>ID : SECRET : https://id.con-med.ru</b> (с протоколом, без слеша в конце)</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?$tabControl->Buttons()?><input type="submit" name="Update" value="Сохранить" class="adm-btn-save"><?$tabControl->End()?></form>
|
||||
Reference in New Issue
Block a user