[СДЕЛАЙ САМ] Login + HybridAuth + АjaxForm

Всем привет, пишу это прежде всего для себя, чтобы не забыть что и как я делал. Прежде всего нам понадобится установить AjaxForm, FormIt, Login и HybridAuth. Тем кто никогда не настраивал регистрацию и авторизацию через Login обязательно прочитать серию статей на эту тему здесь itchief.ru/modx/login-registration. Если дополнения стоят и общий механизм понятен можно продолжать.
Насколько я понял HybridAuth после получения разрешений от пользователя отправляет его на адрес указанный в callback_uri, но проблема в том, что это может быть только Главная страница, поэтому я внёс небольшие изменения в файл core/components/hybridauth/model/hybridauth/hybridauth.class.php, для того чтобы можно бы указывать любую страницу сайта для редиректа.
Примерно на 133 строке есть код
try {  
    $config['callback'] = $this->modx->getOption('site_url') . '?hauth.done=' . $provider;
    $this->adapters[$provider] = new $class($config);
}catch (Exception $e) {
    $this->exceptionHandler($e);
}

Вот тут $config['callback'] хранится тот самый url, который нам и надо изменить. Для этого надо создать системную настройку, я назвал её ha_redirect_id, которая будет хранить id ресурса для редиректа. Затем мы добавляем в исходный код проверку задана эта настройка или нет. Если нет, то редирект будет на главную, если задана, то редирект будет на ту страницу, id которой хранится тут ha_redirect_id.
try {
    $redirectID = $this->modx->getOption('ha_redirect_id');
    if($redirectID){
         $resource = $this->modx->getObject('modResource', (int)$redirectID);
         $redirectResource = $this->modx->makeUrl($redirectID, $resource->context_key, '', 'full');
         $config['callback'] = $redirectResource. '/?hauth.done=' . $provider;
    }else{
        $config['callback'] = $this->modx->getOption('site_url') . '?hauth.done=' . $provider;
    }                        
    $this->adapters[$provider] = new $class($config);
} catch (Exception $e) {
    $this->exceptionHandler($e);
}

Перед тем как начать начать настройку нужно создать группу пользователей в которую мы будем записывать новых пользователей. Как это сделать написано тут itchief.ru/modx/login-registration. Теперь, когда группа пользователей создана, перейдем к настройке регистрации. За основу была взята эта заметка modx.pro/help/19551. Вызов сниппета выглядит так
{'!AjaxForm' | snippet:[
    'snippet' =>'custRegister' //имя сниппета в котором будет происходить регистрация
    'form' =>'@FILE chunks/forms/regForm.html' // чанк с формой  
    'activationEmailTpl' =>'lgnActivateEmailTpl' // чанк письма со ссылкой для подтверждения почты
    'activationEmailSubject' =>'Подтверждение регистрации' // тема письма
    'activationResourceId' => '30' // id ресурса на котором будет происходить подтверждение регистрации 
    'usergroups' =>'1' // группа в которую добавляем пользователя
    'usernameField' =>'email' //имя поля в котором содержится username
    'passwordField' =>'password' // имя поля с паролем
    'validate'=>'nospam:blank,
            password:required:minLength=^8^,
            password_confirm:password_confirm=^password^,
            fullname:required:minLength=^2^,
            acceptRules2:required,
            usergroups:required,
            email:required:email', //список полей для валидации
    'placeholderPrefix' =>'reg.' //префикс плейсхолдеров
]}
Чанк с формой регистрации выглядит так
<form id="registrationForm" action="{$_modx->resource.id | url}" method="post">
    <input type="hidden" name="nospam">
    <input type="hidden" name="usergroups">
    <input type="hidden" name="fullname">
    <div class="box-field">
        <div class="box-field__input">
            <input type="text" class="form-control" name="name"
                   placeholder="Имя (обязательно)">
            <label class="error error_fullname" ></label>
        </div>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="text" class="form-control" name="lastname"
                   placeholder="Фамилия">
        </div>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="email" class="form-control" name="email"
                   placeholder="Email (обязательно)">
            <label class="error error_email"></label>
        </div>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="tel" class="form-control js-mask-tel" name="phone"
                   placeholder="Телефон">
        </div>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="password" class="form-control" name="password"
                   placeholder="Пароль (обязательно)">
            <label class="error error_password"></label>
        </div>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="password" class="form-control" name="password_confirm"
                   placeholder="Пароль (обязательно)">
            <label class="error error_password_confirm"></label>
        </div>
    </div>
    <div class="radio-registration-title">Вы регистрируетесь как:</div>
    <div class="radio-registration flex-wrap justify-content-between">
        <div class="radio-registration__item mr-0">
            <label class="container">
                <span class="box-field__label">Франчайзи</span>
                <input type="radio" name="usergroup" value="3">
                <span class="checkmark"></span>
            </label>
        </div>
        <div class="radio-registration__item mr-0">
            <label class="container">
                <span class="box-field__label">Франчайзер</span>
                <input type="radio" name="usergroup" value="2">
                <span class="checkmark"></span>
            </label>
        </div>
        <div class="radio-registration__item mr-0">
            <label class="container">
                <span class="box-field__label">Другое</span>
                <input type="radio" name="usergroup" value="1">
                <span class="checkmark"></span>
            </label>
        </div>
        <div class="error error_usergroups w-100"></div>
    </div>

    <div class="popup-button">
        <input type="submit" class="btn btn_popup" name="signup-btn" form="registrationForm" value="Регистрация">
    </div>

    <div class="box-field box-field_checkbox box-field_checkbox-popup flex-wrap">
        <div class="error error_acceptRules2 w-100"></div>
        <label class="checkbox-element checkbox-element_2">
            <input type="checkbox" name="acceptRules2">
            <span class="check-label">
                     <span class="check"></span>
                     <span class="check-text">
                          Регистрируясь и совершая вход, Вы принимаете условия 
                          <a href="#" target="_blank">Политика конфиденциальности</a> и 
                          <a href="#"  target="_blank">Оферты на оказание услуг и сервисов.</a>
                     </span>
           </span>
        </label>
    </div>
</form>
Здесь стоит обратить внимание на классы error_имя_поля они используются в js. Сниппет custRegister содержит такой код
<?php
/* 
Слияние массивов нужно чтобы пользователь мог сам 
выбрать в качестве кого он будет зарегистрирован,
 т.е. в какую группу пользователей попадет 
*/
$scriptProperties = array_merge($scriptProperties, $_POST); 
$result = $modx->runSnippet('Register', $scriptProperties); //вызываем стандартный сниппет компонента Login

foreach($modx->placeholders as $key => $ph){
    if(strpos($key, $scriptProperties[placeholderPrefix].'error.') === 0){
        $placeholders[$key] = $ph; //проверяем есть ли в результате работы ошибки
    } 
}

if(count($placeholders)){
    return $AjaxForm->error('В форме содержатся ошибки', array('error' => $placeholders));
} else {
    /*
    'success' => '#successReg' - может быть и просто true, 
    но мне удобнее было передавать id модального окна которое надо закрыть;
    'email' - это я вставляю в текст сообщения об успехе регистрации  
    */
    return $AjaxForm->success('Данные отправлены', array('success' => '#successReg', 'email' => $_POST['email']));
}
Ответ от сервера получаем в стандартном событии, которое идёт в комплекте с AjaxForm
$(document).on('af_complete', function (event, response) {         
    if (!response.data.success) {
        for (let key in response.data.error) {
            let keys = key.split('.');
            $(response.form).find('.error_' + keys[2]).html(response.data.error[key]); // вывод ошибок
        }
    } else {
        /* здесь js-код который выполнится, если всё прошло хорошо */
        console.log(response.data);
    }
    response.message = ''; // отключаем показ стандартных всплывающих уведомлений
});

Страница для подтверждения регистрации содержит стандартный сниппет ConfirmRegister
{'!ConfirmRegister' | snippet:[
  'authenticate'=>'1',
  'redirectTo'=>'1',
  'errorPage'=>'1'
]}
Описание параметром смотрите в документации docs.modx.com/current/en/extras/login/login.confirmregister. На этом с регистрацией всё, переходим к авторизации. Тут я применил костыль в виде FormIt, поскольку так и не понял какие параметры надо передать в сниппет Login, чтобы он нормально отработал, но давайте по порядку.
Сначала вызываем сниппет custAuth через AjaxForm
{'!AjaxForm' | snippet:[
    'snippet' =>'custAuth',
    'form' =>'@FILE chunks/forms/authForm.html', // чанк с формой
    'loginResourceId' =>'33', // id личного кабинета
    'validate'=>'password:required,
        username:required,
        acceptRules2:required',
    'placeholderPrefix' =>'auth.' // префикс нужен для js
]}
Валидация здесь по проще, проверяем чтобы что-то ввели в поля. Чанк с формой ниже
<form id="loginForm">
    <div class="error error_msg"></div> // блок для вывода ошибок авторизации
    <div class="box-field">
        <div class="box-field__input">
            <input type="text" class="form-control" name="username" placeholder="Логин или Email">
        </div>
        <label class="error error_username"></label>
    </div>
    <div class="box-field">
        <div class="box-field__input">
            <input type="password" class="form-control" name="password" placeholder="Пароль">
        </div>
        <label class="error error_password"></label>
    </div>
    <div class="popup-button">
        <input type="submit" class="btn btn_popup" value="Войти">
    </div>
    <div class="box-field box-field_checkbox box-field_checkbox-popup flex-wrap">
        <div class="error error_acceptRules2 w-100"></div>
        <label class="checkbox-element checkbox-element_2">
            <input type="checkbox" name="acceptRules2">
            <span class="check-label">
                <span class="check"></span>
                <span class="check-text">
                    Регистрируясь и совершая вход, Вы принимаете условия
                    <a href="#" target="_blank">Политика конфиденциальности</a> и
                    <a href="#" target="_blank">Оферты на оказание услуг и сервисов.</a>
                </span>
            </span>
        </label>
    </div>
</form>
И собственно сам сниппет custAuth
<?php
/*  
Вызов FormIt это костыль для валидации полей, можно и без него, 
но тогда не получится проверить с помощью AjaxForm стоит ли галочка 
о согласии на обработку персональных данных, например.
*/
$result = $modx->runSnippet('FormIt', $scriptProperties);
foreach($modx->placeholders as $key => $ph){
    if(strpos($key, $scriptProperties[placeholderPrefix].'error.') === 0){
        $placeholders[$key] = $ph;
    } 
}

if (!count($placeholders)) {
    $loginResponse = $modx->runProcessor('/security/login', $_POST); /* стандартный процессор modx, 
важно чтобы названия полей соответствовали таковым в профиле пользователя в админке. 
Список полей можно посмотреть тут http://www.webapplex.ru/rabota-s-polzovatelyami,-modx-api,-xpdo 
или в БД или в схеме*/

    if(!$loginResponse->response['success']){
        $placeholders['auth.error.msg'] = '<span class="error">'.$loginResponse->response['message'].'</span>';
        return  $AjaxForm->error('В форме содержатся ошибки', array('error' => $placeholders));
    }else{
         $modx->user = $modx->getObject('modUser', array('username' => $_POST['username']));
         $resource = $modx->getObject('modResource', $scriptProperties['loginResourceId']);
         return $AjaxForm->success('Данные отправлены', array('success' => '#successAuth', 'url' => $modx->makeUrl($scriptProperties['loginResourceId'], $resource->get('context_key'), '', 'full' )));
    }
    
    }else{
    return  $AjaxForm->error('Form has errors', array('error' => $placeholders));
}
Переадресацию в кабинет можно сделать на js или не делать вообще если не нужно, я не делал, js-код выше. Дальше нужно будет дать возможность сбросить пароль, если пользователь его забыл. Для этого опять вызываем AjaxForm
{'!AjaxForm' | snippet:[
    'snippet' =>'custReset',
    'form' =>'@FILE chunks/forms/resetForm.html',
    'resetResourceId' => 32, // ресурс для активации нового пароля.
    'emailTpl' => 'mylgnForgotPassEmail'
    'emailSubject' => 'Восстановление пароля'
    'validate'=>'email:email:required,acceptRules2:required',
    'placeholderPrefix' =>'reset.'
]}
Чанки письма и формы любые на ваш вкус. Сниппет custReset выглядит так
$scriptProperties = array_merge($scriptProperties, $_POST);
$result = $modx->runSnippet('FormIt', $scriptProperties);
foreach($modx->placeholders as $key => $ph){
    if(strpos($key, 'reset.error.') === 0){
        $placeholders[$key] = $ph;
    } 
}
if(!count($placeholders)){
    $user = $modx->getObject('modUser', array('username' => $_POST['email']));
    if($user){
        $modx->runSnippet('ForgotPassword', $scriptProperties);
        return $AjaxForm->success('Данные отправлены', array('success' => '#successReset', 'email' => $_POST['email']));
    }else{
        $placeholders['reset.error.msg'] = '<span class="error">Пользователь не найден. Проверьте корректность email.</span>';
        return $AjaxForm->error('В форме содержатся ошибки', array('error' => $placeholders)); 
    }
}else{
    return $AjaxForm->error('В форме содержатся ошибки', array('error' => $placeholders));    
}
Здесь всё аналогично предыдущим сниппетам. FormIt для проверки полей, а дальше просто вызываем ForgotPassword, который сбрасывает пароль и отправляет на почту письмо со ссылкой для активации нового пароля. Также я добавил проверку на существование пользователя.
Ну и напоследок в модалке с формой авторbзации добавляем вызов HybridAuth
{'!HybridAuth' | snippet: [
    'providerTpl' => '@FILE chunks/hybridauth/socialsAuthItem.html',
    'loginTpl' => '@INLINE {$providers}'
]}
А в личном кабинете вызываем haProfile для того, чтобы можно было привязать аккаунты.
{'!haProfile' | snippet:[
    'profileTpl' => '@INLINE {$providers}',
    'providerTpl' => '@FILE chunks/hybridauth/socialsAuthItem.html',
     'activeProviderTpl' => '@FILE chunks/hybridauth/socialsAuthItemActive.html',
]}
Google требует отправлять приложение на проверку, причём при любых настройках api. На mailru звездец полный вот ссылка на нужную страницу api.mail.ru/sites/my/782604 или по запросу в Яндексе «mail.ru oauth api» первая ссылка ведёт на документацию из неё можно перейти к регистрации. И ещё все сниппеты компонента Login в чанках синтаксис fenom не признают от слова совсем, имейте в виду, кто не знал. Вроде всё. Будут вопросы или замечания, милости прошу в комментарии, надеюсь кому-то этот опус пригодится.
Артур Шевченко
25 ноября 2020, 23:41
modx.pro
3
3 049
+1
Поблагодарить автора Отправить деньги

Комментарии: 3

Maxim
27 декабря 2020, 23:11
0
Спасибо за статью. Надеюсь поможет.
    elec3c
    08 февраля 2021, 23:01
    0
    Хорошая статья, особенно учитывая, что из магазина пропал компонент AjaxLogin. Экономит время.
    Маленькая ошибка — при вызове сниппета custRegister пропущены запятые.
    Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
    3