[СДЕЛАЙ САМ] Авторизация и регистрация по СМС
Всем привет. Я ни на что не претендую, прекрасно понимаю, что вариантов решения данной задачи много, и мой, наверное, не лучший, но меня попросили написать решение и эту инструкцию, поэтому поехали.
Нам понадобится:
Начнём с чанка корзины. Добавим туда ссылку на страницу оформления заказа для авторизованных пользователей и кнопку открытия формы авторизации/регистрации для неавторизованных пользователей.
Сюда же добавим модальное окно с формой авторизации/регистрации
Чанк с формой авторизации/регистрации checkcodeForm.html выглядит так
Чтобы закончить с клиентской частью, создаем файл с названием, например, sms-auth.js, с таким контентом
И внесём небольшую правку в исходники ajaxForm, а именно закомментируем в файле assets/components/ajaxform/js/default.js 66 строку, там должно быть это
Для отправки СМС будем использовать api SMSЦентр. Скачиваем файл smsc_api.php и в начале файла меняем
Теперь можно создать сниппет, который будет запускать отправку смс, назовём его codeSender

Теперь надо рассказать modx как с этой таблицей работать, для этого воспользуемся дополнением CMPGenerator. Открываем его интерфейс и нажимаем Create Package. Появившуюся форму заполняем как наследующем скрине
Потом жмём сохранить. Переходим в Console и выполняем вот такой код
Можем переходить к сниппету smsAuth, основные моменты я прокомментировал.
Тут вы вероятно заметили, что есть сниппет genPassword, изнутри он выглядит так
Регистрацию я сделал так (Registration)
Авторизацию вот так (Authorization)
Теперь нужно сделать так, чтобы minishop2 использовал уже зарегистрированного пользователя в качестве получателя заказа. Для этого немного расширим класс core/components/minishop2/model/minishop2/msorderhandler.class.php, а точнее метод submit. Для этого создаем файл core/elements/code/customOrderHandler.class.php с вот таким содержимым
На этом всё!
Нам понадобится:
- Console
- CMP Generator
- AjaxForm
- miniShop2
Начнём с чанка корзины. Добавим туда ссылку на страницу оформления заказа для авторизованных пользователей и кнопку открытия формы авторизации/регистрации для неавторизованных пользователей.
{if $_modx->user.id > 0}
<a href="{13 | url}"> <!-- 13 это id ресурса с формой оформления заказа -->
<button type="submit" class="knopka2">
Оформить заказ
</button>
</a>
{else}
<button type="button" data-toggle="modal" data-target="#modalSMSAuth" class="knopka2">
Оформить заказ
</button>
{/if}Сюда же добавим модальное окно с формой авторизации/регистрации
<!-- MODAL -->
<div class="modal" id="modalSMSAuth" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close">X</button>
</div>
<div class="modal-body row">
{'!AjaxForm' | snippet:[
'snippet' =>'smsAuth',
'form' =>'@FILE chunks/forms/checkcodeForm.html',
'redirectTo' => 12
]}
</div>
</div>
</div>
</div>
<!-- /MODAL -->Чанк с формой авторизации/регистрации checkcodeForm.html выглядит так
<form class="col-12">
<div class="row">
<label class="col-12">
<input type="tel" class="d-block input_sms_nam" name="phone" placeholder="+7(999)888-77-66">
<span class="error error_phone"></span>
</label>
<label class="col-12">
<small class="modal__text">Нет пароля или Вы его забыли?
Оставьте поле пустым, и мы отправим новый пароль в СМС</small>
<input type="text" class="d-block input_sms_cod" name="code" placeholder="Пароль">
<span class="error error_code"></span>
</label>
<div class="col-lg-6 offset-lg-3">
<button class="btn btn-primary">Войти</button>
</div>
</div>
</form>Чтобы закончить с клиентской частью, создаем файл с названием, например, sms-auth.js, с таким контентом
// Маска ввода номера телефона
let phoneMask = document.querySelectorAll('input[type="tel"]');
for (let i = 0; i < phoneMask.length; i++) {
phoneMask[i].addEventListener('keydown', function (event) {
if (!(event.key == 'ArrowLeft' || event.key == 'ArrowRight' || event.key == 'Backspace' || event.key == 'Tab')) {
event.preventDefault()
}
let mask = '+7 (111) 111-11-11'; // Задаем маску
if (/[0-9\+\ \-\(\)]/.test(event.key)) {
let currentString = this.value;
let currentLength = currentString.length;
if (/[0-9]/.test(event.key)) {
for (let i = currentLength; i < mask.length; i++) {
let number = event.key;
if (mask[i] == '1') {
if (i == 4 && number != '9') {
number = '';
}
this.value = currentString + number;
break;
}
currentString += mask[i];
}
}
}
});
}
// Обработка ответа сервера
$(document).on('af_complete', function (e, response) {
if(response.data.redirectTo){
setTimeout(function(){
document.location.href = response.data.redirectTo; // просто делаем редирект на нужную страницу
},2000);
}
});И внесём небольшую правку в исходники ajaxForm, а именно закомментируем в файле assets/components/ajaxform/js/default.js 66 строку, там должно быть это
form[0].reset();Для отправки СМС будем использовать api SMSЦентр. Скачиваем файл smsc_api.php и в начале файла меняем
// SMSC.RU API (smsc.ru) версия 3.8 (03.07.2019)
define("SMSC_LOGIN", "login"); // логин клиента
define("SMSC_PASSWORD", "password"); // пароль
define("SMSC_POST", 0); // использовать метод POST
define("SMSC_HTTPS", 0); // использовать HTTPS протокол
define("SMSC_CHARSET", "windows-1251"); // кодировка сообщения: utf-8, koi8-r или windows-1251 (по умолчанию)
define("SMSC_DEBUG", 0); // флаг отладки
define("SMTP_FROM", "api@smsc.ru"); // e-mail адрес отправителяна// Подключаем Modx API
define('MODX_API_MODE', true);
require_once $_SERVER['DOCUMENT_ROOT'] . '/index.php';
// SMSC.RU API (smsc.ru) версия 3.8 (03.07.2019)
define("SMSC_LOGIN", $modx->getOption('smsc_login')); // логин клиента
define("SMSC_PASSWORD", $modx->getOption('smsc_password')); // пароль
define("SMSC_POST", $modx->getOption('smsc_post')); // использовать метод POST
define("SMSC_HTTPS", $modx->getOption('smsc_https')); // использовать HTTPS протокол
define("SMSC_CHARSET", $modx->getOption('smsc_charset')); // кодировка сообщения: utf-8, koi8-r или windows-1251 (по умолчанию)
define("SMSC_DEBUG", $modx->getOption('smsc_debug')); // флаг отладки
define("SMTP_FROM", $modx->getOption('smsc_smtp_from')); // e-mail адрес отправителяВсе параметры указанные в методах $modx->getOption надо создать в системных настройках, думаю это понятно.Теперь можно создать сниппет, который будет запускать отправку смс, назовём его codeSender
<?php
$file = MODX_CORE_PATH . 'components/codesender/smsc_api.php';
if(!file_exists($file)){
$modx->log(1, 'FILE NOT FOUND');
}
include_once $file;
if(!isset($translit)){
$translit = 1; // по умолчанию отправляем транслит, там можно впихнуть больше текста
}
$send = send_sms($number, $message, $translit);
return $send;Чтобы можно было проверять отправленные коды, нужно их куда-то сохранять. Для этого создадим в базе данных новую таблицу modx_codesender на 5 полей с настройками как на скриншоте ниже
Теперь надо рассказать modx как с этой таблицей работать, для этого воспользуемся дополнением CMPGenerator. Открываем его интерфейс и нажимаем Create Package. Появившуюся форму заполняем как наследующем скрине
Потом жмём сохранить. Переходим в Console и выполняем вот такой код
$modx->addExtensionPackage('codesender', '[[++core_path]]components/codesender/model/');Очищаем кэш и проверяем всё ли у нас получилось, выполнив следующий код$sms = $modx->newObject('Codesender', array('username' => '79998887766', 'code' => '0000', 'text' => 'Test'));
$sms->save();Если ошибок нет, значит всё хорошо, если есть внимательно проверяем пути к файлам.Можем переходить к сниппету smsAuth, основные моменты я прокомментировал.
<?php
if(!($username = preg_replace('/[^0-9]/', '', trim($_POST['phone'])))){ // проверяем введен ли номер телефона
return $AjaxForm->error('Исправьте ошибки в форме', array('phone' => 'Введите номер телефона'));
}
$user = $modx->getObject('modUser', array('username'=> $username)); // получаем пользователя
$inputCode = (int)$_POST['code']; // получаем введенный код
$codesender = $modx->getObject('Codesender', array('username' => $username)); // получаем данные об отправленном коде из БД
if(!$inputCode){ // если код не ввели
if($codesender){ // и если мы его уже отправили
$timeOut = 120; // задаем задержку повторной отправки
$sendTime = strtotime($codesender->get('date')); // получаем дату предыдущей отправки кода
$now = time(); // получаем текущее время
if(($now - $sendTime) < $timeOut){ // не даем слишком часто запрашивать пароль
return $AjaxForm->error('Повторно запросить код Вы сможете через ' .($timeOut - ($now - $sendTime)) . ' cек.' , array());
}
}
$inputCode = $modx->runSnippet('genPassword', array('length' => 6, 'type' => 'code')); // генерируем код
$text = 'Пароль для сайта ' . $modx->getOption('http_host').': ' . $inputCode; // формируем сообщение
$send = $modx->runSnippet('codeSender', array('number' => $username, 'message' => $text)); //отправляем СМС с кодом
// если отправка не удалась
if(count($send) == 2){
switch ($send[1]){
case -7:
$message = 'Неверный формат номера телефона. Код подтверждения не отправлен.';
break;
case -8:
$message = 'Сообщение на указанный номер не может быть доставлено. Введите другой номер и повторите попытку';
break;
default:
$message = 'Код подтверждения не отправлен. Введите другой номер и повторите попытку';
break;
}
return $AjaxForm->error($message, array());
}
// записываем код в БД
$sms = $modx->newObject('Codesender', array('username' => $username, 'code' => $inputCode, 'text' => $text));
$sms->save();
if($user){
// обновляем пароль пользователя
$user->set('password', $inputCode);
$user->save();
}
return $AjaxForm->success('Пароль успешно отправлен', array());
}
$params = array('username' => $username, 'password' => $inputCode, 'group' => 2, 'active' => true, 'blocked' => false); // массив с данными для авторизации/регистрации
if(!$user){ // проверяем существует ли пользователь
$dbcode = (int)$codesender->get('code'); // получаем отправленный код
if($inputCode != $dbcode){ // проверяем совпадают ли коды
return $AjaxForm->error('Исправьте ошибки в форме', array('code'=> 'Введен неверный пароль'));
}
$codesender->remove(); // удаляем код из БД если проверка прошла успешно
// если нет - регистрируем
$user = $modx->runSnippet('Registration', array('params' => $params));
$user->set('password', $inputCode);
$user->save();
}
$response = $modx->runSnippet('Authorization', array('params' => $params));
if(!$response['success']){
return $AjaxForm->error($response['message'], array());
}
$redirectId = $scriptProperties['redirectTo'] ?: 1;
$redirectUrl = $modx->makeUrl($redirectId, 'web', '', 'full');
return $AjaxForm->success($response['message'], array('redirectTo' => $redirectUrl));Тут вы вероятно заметили, что есть сниппет genPassword, изнутри он выглядит так
<?php
if(!$length){
$length = $modx->getOption('password_min_length');
}
if(!$type){
$type = 'pass';
}
//$modx->log(1, 'LENGTH ' . $length);
//$modx->log(1, 'TYPE ' . $type);
/* Более сложный генератор паролей с любыми символами */
$password = "";
/* Массив со всеми возможными символами в пароле */
switch ($type){
case 'pass':
$arr = array(
'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x',
'y', 'z', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', '1', '2',
'3', '4', '5', '6', '7', '8',
'9', '0', '#', '!', "?", "&"
);
break;
case 'hash':
$arr = array(
'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x',
'y', 'z', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', '1', '2',
'3', '4', '5', '6', '7', '8',
'9', '0'
);
break;
case 'code':
$arr = array('1', '2', '3', '4', '5', '6', '7', '8', '9', '0');
break;
}
for ($i = 0; $i < $length; $i++){
$password .= $arr[mt_rand(0, count($arr) - 1)]; // Берём случайный элемент из массива
}
return $password;Регистрацию я сделал так (Registration)
<?php
$user = $modx->newObject('modUser'); // создаем пользователя
$user->set('username', $params['username']); // задаем имя пользователя
$user->set('password', $params['password']); // задаем пароль
$profile = $modx->newObject('modUserProfile'); // создаем профиль
if(!$params['email']){
$params['email'] = $params['username'] . '@' . $_SERVER['HTTP_HOST'];
}
if(!$params['phone']){
$params['phone'] = preg_replace('/(\d)(\d{3})(\d{3})(\d{2})(\d{2})$/', '+7(\2)\3-\4-\5', $params['username']);
}
$params['extended'] = $extended;
// инициализируем поля
$profile->fromArray($params);
$user->addOne($profile); // добавляем профиль к пользователю
$profile->save(); // сохраняем профиль
$user->save(); // сохраняем пользователя
$user->joinGroup($params['group'],1); // добавляем пользователя в группу только после сохранения объекта пользователя
setcookie('user_id', $user->get('id'));
if(!$user){
return false;
}
return $user;Авторизацию вот так (Authorization)
<?php
$loginData = array(
'username' => $params['username'],
'password' => $params['password'],
'rememberme' => 1,
'login_context' => 'web'
);
$loginResponse = $modx->runProcessor('/security/login', $loginData);
if ($loginResponse->isError()) {
$modx->log(modX::LOG_LEVEL_ERROR, 'login error. Username: '.$params['username'].', Message: '.$loginResponse->getMessage());
return array('success' => false, 'message' => $loginResponse->getMessage());
}
return array('success' => true, 'message' => 'Вы успешно авторизованы');Теперь нужно сделать так, чтобы minishop2 использовал уже зарегистрированного пользователя в качестве получателя заказа. Для этого немного расширим класс core/components/minishop2/model/minishop2/msorderhandler.class.php, а точнее метод submit. Для этого создаем файл core/elements/code/customOrderHandler.class.php с вот таким содержимым
<?php
if(!class_exists('msOrderInterface')) {
require_once dirname(dirname(dirname(__FILE__))) . '/components/minishop2/model/minishop2/msorderhandler.class.php';
}
class customOrderHandler extends msOrderHandler implements msOrderInterface{
/**
* @param array $data
*
* @return array|string
*/
public function submit($data = array())
{
$response = $this->ms2->invokeEvent('msOnSubmitOrder', array(
'data' => $data,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
if (!empty($response['data']['data'])) {
$this->set($response['data']['data']);
}
$response = $this->getDeliveryRequiresFields();
if ($this->ms2->config['json_response']) {
$response = json_decode($response, true);
}
if (!$response['success']) {
return $this->error($response['message']);
}
$requires = $response['data']['requires'];
$errors = array();
foreach ($requires as $v) {
if (!empty($v) && empty($this->order[$v])) {
$errors[] = $v;
}
}
if (!empty($errors)) {
return $this->error('ms2_order_err_requires', $errors);
}
// custom code
$user_id = $_COOKIE['user_id'] ?: $this->ms2->getCustomerId();
if (empty($user_id) || !is_int($user_id)) {
return $this->error(is_string($user_id) ? $user_id : 'ms2_err_user_nf');
}
$cart_status = $this->ms2->cart->status();
if (empty($cart_status['total_count'])) {
return $this->error('ms2_order_err_empty');
}
$delivery_cost = $this->getCost(false, true);
$cart_cost = $this->getCost(true, true) - $delivery_cost;
$createdon = date('Y-m-d H:i:s');
/** @var msOrder $order */
$order = $this->modx->newObject('msOrder');
$order->fromArray(array(
'user_id' => $user_id,
'createdon' => $createdon,
'num' => $this->getNum(),
'delivery' => $this->order['delivery'],
'payment' => $this->order['payment'],
'cart_cost' => $cart_cost,
'weight' => $cart_status['total_weight'],
'delivery_cost' => $delivery_cost,
'cost' => $cart_cost + $delivery_cost,
'status' => 0,
'context' => $this->ms2->config['ctx'],
));
// Adding address
/** @var msOrderAddress $address */
$address = $this->modx->newObject('msOrderAddress');
$address->fromArray(array_merge($this->order, array(
'user_id' => $user_id,
'createdon' => $createdon,
)));
$order->addOne($address);
// Adding products
$cart = $this->ms2->cart->get();
$products = array();
foreach ($cart as $v) {
if ($tmp = $this->modx->getObject('msProduct', array('id' => $v['id']))) {
$name = $tmp->get('pagetitle');
} else {
$name = '';
}
/** @var msOrderProduct $product */
$product = $this->modx->newObject('msOrderProduct');
$product->fromArray(array_merge($v, array(
'product_id' => $v['id'],
'name' => $name,
'cost' => $v['price'] * $v['count'],
)));
$products[] = $product;
}
$order->addMany($products);
$response = $this->ms2->invokeEvent('msOnBeforeCreateOrder', array(
'msOrder' => $order,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
if ($order->save()) {
$response = $this->ms2->invokeEvent('msOnCreateOrder', array(
'msOrder' => $order,
'order' => $this,
));
if (!$response['success']) {
return $this->error($response['message']);
}
$this->ms2->cart->clean();
$this->clean();
if (empty($_SESSION['minishop2']['orders'])) {
$_SESSION['minishop2']['orders'] = array();
}
$_SESSION['minishop2']['orders'][] = $order->get('id');
// Trying to set status "new"
$response = $this->ms2->changeOrderStatus($order->get('id'), 1);
if ($response !== true) {
return $this->error($response, array('msorder' => $order->get('id')));
}
// Reload order object after changes in changeOrderStatus method
$order = $this->modx->getObject('msOrder', array('id' => $order->get('id')));
/** @var msPayment $payment */
if ($payment = $this->modx->getObject('msPayment',
array('id' => $order->get('payment'), 'active' => 1))
) {
$response = $payment->send($order);
if ($this->config['json_response']) {
@session_write_close();
exit(is_array($response) ? json_encode($response) : $response);
} else {
if (!empty($response['data']['redirect'])) {
$this->modx->sendRedirect($response['data']['redirect']);
} elseif (!empty($response['data']['msorder'])) {
$this->modx->sendRedirect(
$this->modx->context->makeUrl(
$this->modx->resource->id,
array('msorder' => $response['data']['msorder'])
)
);
} else {
$this->modx->sendRedirect($this->modx->context->makeUrl($this->modx->resource->id));
}
return $this->success();
}
} else {
if ($this->ms2->config['json_response']) {
return $this->success('', array('msorder' => $order->get('id')));
} else {
$this->modx->sendRedirect(
$this->modx->context->makeUrl(
$this->modx->resource->id,
array('msorder' => $response['data']['msorder'])
)
);
return $this->success();
}
}
}
return $this->error();
}
}В Console запускаем следующий кодif ($miniShop2 = $modx->getService('miniShop2')) {
$miniShop2->addService('order', 'CustomOrderHandler',
'{core_path}elements/code/customOrderHandler.class.php'
);
}В системных настройках меняем значение ms2_order_handler_class на customOrderHandler.На этом всё!
Комментарии: 25
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
От себя могу добавить, сниппет genPassword можно не писать. У класса modUser есть метод generatePassword. В который тоже можно передать длину пароля
Помесь стандартного и fenom синтаксиса это дурной тон.
Два некешированных вызова AjaxForm. А че ж не 5? Эту задачу можно решить одним вызовом сниппета.
Такого рода задачи решаются не сниппетами. Сниппеты это вообще про другое.
Более того, ты же ниже создаешь пакет, какая проблема примутить к нему класс-контроллер?
Опять же а) Не решаются такие задачи сниппетами б) mt_rand не является безопасным для задач уровня генерации паролей. Ты бы это знал, если бы писал это сам.
Снова, такие задачи не решаются сниппетами.
Из поста данные не валидируются. Пахнет безопасностью! Либо впадлу, либо не знаешь что это такое.
Тут кстати гораздо логичнее была бы проверка на false.
Опять никакой валидации. Еще и сырой запрос (вызов которого не оправдан ничем)
Ни единой проверки на то что модель не сохранилась. Неверное сохранение аггеративных моделей.
Итого: куски сомнительного кода не несущие абсолютно никакой ценности.
Я бы ни слова ни сказал, даже плюсик бы залепил если бы тут было хоть какое-то объяснение происходящего.
Почему так, а не иначе, почему это лучше, а это хуже.
Итого ты не учишь, а даешь людям копировать плохие практики.
Абсолютно заслуженный минус
У тебя валидация уровня required, я туда могу тебе прислать все что угодно.
Напишите и выложите свой подобный кейс, а то желающих поделиться разработкой не много
100$ и я готов написать решение многократно элегантнее этого с пояснением каждого шага для новичков. Готовы проспонсировать статью?
Дорого?
Можете использовать компонент Office который стоит половину стоимости и реализует задачу гораздо лучше
modstore.pro/packages/users/office
2. Я во вступлении написал, что это не лучший из возможных вариантов.
3. Есть ты и твои комментарии, кто-то проникнется и не станет это копировать, кто-то допилит, а кому-то и так сойдет.
2. Это не лучший, а прям наихудший способ. Кстати если так хотелось прикрутить авторизацию, то можно было доработать Login.
Вполне логичный и читабельный проект, только хотелось поподробнее про пути к файлам.(где их создавать?).