JS Регистрация и авторизация на CEF + MySQL

  • Автор темы Автор темы Lev Angel
  • Дата начала Дата начала
  • Featured
Сегодня мы с Вами напишем с нуля полноценный скрипт регистрации и авторизации для сервера rage mp. В качестве интерфейса мы не будем использовать команды, а сразу сделаем "красиво" на CEF. В качестве базы данных будем использовать MySQL.


Видео версия как обычно на youtube канале:
Видео версия урока

Для начала я нашел в Интернете простенький HTML шаблон страницы авторизации: https://codepen.io/colorlib/pen/rxddKy
Помещаем его в папку cef нашего клиентского скрипта accounts. Туда же ложим стили (style.css) и браузерные скрипты (script.js), которые мы напишем дальше.
Я немного модифицировал шаблон:
1. Добавил фоновую картинку
2. Расставил id для полей ввода, чтобы было удобнее работать с ними.
3. Убрал неиспользуемые стили
4. Добавил блок для вывода ошибок
5. Перевел на русский язык.

В итоге html файл выглядит так:
HTML:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="package://accounts/cef/style.css">
</head>
<body>

<div class="login-page">
    <div class="form">
        <div id="error"></div>
        <form class="register-form" id="register">
            <input id="reg-login" type="text" placeholder="Логин"/>
            <input id="reg-password" type="password" placeholder="Пароль"/>
            <input id="reg-password-confirm" type="password" placeholder="Повторите Пароль"/>
            <button type="button" onclick="registerAttempt()">Регистрация</button>
            <p class="message">Уже зарегистрированы? <a href="#" onclick="showLogin()">Войти</a></p>
        </form>
        <form class="login-form" id="login">
            <input id="log-login" type="text" placeholder="Логин"/>
            <input id="log-password" type="password" placeholder="Пароль"/>
            <button type="button" onclick="loginAttempt()">Войти</button>
            <p class="message">Не зарегистрированы? <a href="#" onclick="showRegister()">создать аккаунт</a></p>
        </form>
    </div>
</div>


<script src="package://accounts/cef/script.js"></script>

</body>
</html>

Чтобы показать этот интерфейс игроку при входе на сервер, будем дергать с сервера на клиент событие showLoginDialog.
JavaScript:
mp.events.add("playerReady", player => {
    player.call('showLoginDialog');
});

На клиентской стороне добавим обработчик этого события
Код:
let loginBrowser;

mp.events.add('showLoginDialog', () => {
    loginBrowser = mp.browsers.new('package://accounts/cef/index.html'); // инициализируем браузер и отображаем страничку входа
    loginBrowser.execute("mp.invoke('focus', true)"); // показываем курсор
    mp.gui.chat.activate(false); // блокируем открытие чата при вводе текста в поля формы
});

Теперь при входе игрока на сервер ему будет показываться на форма входа.

form.jpg

Вернемся теперь к index.html и браузерной части. У нас есть две формы (register и login). Форма login отображается по-умолчанию, а register скрыта в стилях. Внизу каждой формы есть ссылка на другую и при помощи функций showLogin() и showRegister() мы будем переключаться между ними. Также для кнопок входа и регистрации добавлен вызов функций registerAttempt() и loginAttempt(), которые будут вызываться по событию onclick

В script.js добавим реализацию этих функций. С переключением между формами все просто:
JavaScript:
function showRegister(){
    document.getElementById('login').style.display = 'none';
    document.getElementById('register').style.display = 'block';
}

function showLogin(){
    document.getElementById('login').style.display = 'block';
    document.getElementById('register').style.display = 'none';
}

При отправке запроса на вход или регистрацию нам нужно считать содержимое полей формы, выполнить их базовые проверки и передать в клиент rage mp
JavaScript:
function registerAttempt(){
    // считываем содержимое полей
    const login = document.getElementById('reg-login').value;
    const password = document.getElementById('reg-password').value;
    const passwordConfirm = document.getElementById('reg-password-confirm').value;
    resetError();

    // Проверяем чтобы поля были заполнены, они были нужной длинны и пароли совпадали
    if(!login || login.length < 3){
        return showError('Введите логин');
    }

    if(!password || password.length < 6){
        return showError('Введите пароль');
    }

    if(password != passwordConfirm){
        return showError('Пароли не совпадают');
    }

    // Отправляем логин и пароль на клиент
    mp.trigger('registerAttempt', JSON.stringify({ login, password }) );
}

function loginAttempt(){
    const login = document.getElementById('log-login').value;
    const password = document.getElementById('log-password').value;
    resetError();

    if(!login || login.length < 3){
        return showError('Введите логин');
    }

    if(!password || password.length < 6){
        return showError('Введите пароль');
    }

    mp.trigger('loginAttempt', JSON.stringify({ login, password }) );
}

mp.trigger позволяем нам отправить из браузера на клиент только один дополнительный параметр. И это может быть только строка или число. Нам же нужно отправить два значения. Мы не можем отправить напрямую массив или объект, но мы можем преобразовать наш объект с логином и паролем в json строку JSON.stringify({ login, password }). И теперь эту строку мы легко передаем в одном аргументе.

Также в коде Вы наверное заметили функции связанные с выводом ошибок в форму на нашей страничке. Здесь все просто. У нас есть div блок с id error. Он находится выше наших форм и поэтому может показываться независимо от того на какой форме сейчас пользователь.

JavaScript:
function showError(message){
    const errorBlock = document.getElementById('error');
    errorBlock.innerText = message;
    errorBlock.style.display = 'block';
}

function resetError(){
    const errorBlock = document.getElementById('error');
    errorBlock.innerText = '';
    errorBlock.style.display = 'none';
}

В клиентской части добавим обработчики событий loginAttempt и registerAttempt, которые будут вызываться из браузерного скрипта.
JavaScript:
mp.events.add('loginAttempt', (data) => {
    mp.events.callRemote('onLoginAttempt', data);
});

mp.events.add('registerAttempt', (data) => {
    mp.events.callRemote('onRegisterAttempt', data);
});

Они максимально простые и просто передают данные с браузера дальше на сервер при помощи callRemote. Напоминаю что в качестве data у нас JSON строка с логином и паролем. В таком виде мы передаем ее дальше, поскольку callRemote также позволяет нам передавать только простые строки и числа.

На серверной стороне прежде чем обработать события onLoginAttempt и onRegisterAttempt нужно кое-что подготовить:
  1. Добавить пакет mysql и настроить подключение к серверу MySQL. Структура базы данных и само подключение будет таким же, как и в уроке по подключению MySQL. У нас будет 1 таблица accounts с тремя столбцами: id, login и password
  2. Добавить пакет bcrypt для генерации хэша паролей и его проверки.
JavaScript:
mp.events.add('onLoginAttempt', (player, data) => {
    data = JSON.parse(data); // преобразовуем данные из json в объект
    DB.query('SELECT * FROM accounts WHERE login = ? LIMIT 1', [data.login], function (error, results) { // ищем аккаунт по логину
        if(results.length == 0) return player.call('showAuthError', ['Неверный Логин и/или Пароль']); // если аккаунт с таким логином не найден, то возвращаем на клиент текст ошибки

        const passwordHash = results[0].password; // если же аккаунт есть, то берем его хеш пароля
        bcrypt.compare(data.password, passwordHash, function(err, isMatched) { // сравниваем хэши паролей из базы данных и того что указал пользователь
            if( isMatched ) return player.call('hideLoginDialog');  // если пароли не совпадают, значит пользователь авторизовался успешно
            player.call('showAuthError', ['Неверный Логин и/или Пароль']); // если же пароли не совпали, то опять таки возвращаем на клиент текст ошибки при помощи события showAuthError
        });
    });
});

На клиенте событие showAuthError просто показывает текст ошибки в форме.
JavaScript:
mp.events.add('showAuthError', (errorMessage) => {
    loginBrowser.execute(`showError("${errorMessage}")`);
});

А при успешном входе мы скрываем окно авторизации и считаем что игрок авторизовался
JavaScript:
mp.events.add('hideLoginDialog', () => {
    loginBrowser.execute("mp.invoke('focus', false)");
    loginBrowser.active = false;
    mp.gui.chat.activate(true);
});

Для регистрации на серверной стороне обработчик onRegisterAttempt будет немного сложнее. Прежде чем добавить аккаунт нам нужно проверить его на уникальность и его такой логин уже есть в базе данных, то выдавать ошибку.

JavaScript:
mp.events.add('onRegisterAttempt', (player, data) => {
    data = JSON.parse(data);

    DB.query('SELECT id FROM accounts WHERE login = ?', [data.login], function (error, results) {  // Проверяем уникальность логина
        if(results.length > 0) return player.call('showAuthError', ['Аккаунт с таким Логином уже существует']); // Если такой логин уже есть, то возвращаем ошибку

        bcrypt.hash(data.password, saltRounds, function(err, passwordHash) { // Создаем хэш пароля
            DB.query('INSERT INTO accounts SET login = ?, password = ?', [data.login, passwordHash], function (error, results) { // Добавляем аккаунт в базу данных
                player.call('hideLoginDialog'); // Скрываем окно авторизации
            });
        });
    });

});


Для тех кто захочет дальше ковырять эту форму, напишу парочку идей того, что можно улучшить и доработать:
1. Добавить защиту от перебора паролей. Кикать после 3 неправильных вводов.
2. Написать функцию isPlayerLoggedIn() которая будет возвращать true если игрок авторизовался и false если еще нет.
3. Добавить столбец position в таблицу accounts. Записывать туда позицию игрока при выходе с сервера.
4. Добавить возможность восстановить пароль. Для этого понадобиться добавить поле для email аккаунта и какой-то способ чтобы отправлять электронные письма с сервера.

Решение задач от пользователя @geneff
Задачи 1-3
Задача 4 (восстановление пароля)

Если при установке библиотеки bcrypt появляется ошибка 'undefined symbol: napi_module_register', то попробуйте это решение.
 

Вложения

Последнее редактирование:
А где-то есть информация как сделать, чтобы после регистрации игрок создавал своего персонажа и кастомизировал?) Просто много где полазил только через NativeUi нашел ;c
 
А где-то есть информация как сделать, чтобы после регистрации игрок создавал своего персонажа и кастомизировал?) Просто много где полазил только через NativeUi нашел ;c
Через нативЮИ не советовал бы, на вики рейджа есть функции по тиму смены родителе и тп, много чего, главное понять как оно работает.
Изминение личика https://wiki.rage.mp/index.php?title=Player::setF
 
Проблемка маленькая не определяется permissionDesc программой... Программа таже что и у автора... Скажи плагин.... И желательно все которые стоят... Если не трудно
 
Проблемка маленькая не определяется permissionDesc программой... Программа таже что и у автора... Скажи плагин.... И желательно все которые стоят... Если не трудно
Не понял о чем речь. Что за permissionDesc?
 
Dark Purple Theme (1.2)
Phing (203.7148.74)
Database Tools and SQL (203.7148.74)
Docker (203.7148.74)
FTP/SFTP Connectivity (ex. Remote Hosts Access) (203.7148.74)
Vagrant (203.7148.74)
HTML Tools (203.7148.74)
Refactor-X (203.7148.74)
XPathView + XSLT (203.7148.74)
IDE Settings Sync (203.7148.74)
Settings Repository (203.7148.74)
Angular and AngularJS (203.7148.74)
CoffeeScript (203.7148.74)
JavaScript and TypeScript (203.7148.74)
JavaScript Debugger (203.7148.74)
JavaScript Intention Power Pack (203.7148.74)
Node.js (203.7148.74)
TSLint (203.7148.74)
Vue.js (203.7148.74)
Apache config (.htaccess) (203.7148.74)
DQL (203.7148.74)
Gherkin (203.7148.74)
GNU GetText files support (*.po) (203.7148.74)
Ini (203.7148.74)
Markdown (203.7148.74)
PHPT Support (203.7148.74)
Shell Script (203.7148.74)
YAML (203.7148.74)
Drupal (203.7148.74)
Joomla! (203.7148.74)
WordPress (203.7148.74)
PHP Docker (203.7148.74)
PHP Remote Interpreter (203.7148.74)
PHP WSL Support (203.7148.74)
Hunspell (203.7148.74)
CSS (203.7148.74)
Less (203.7148.74)
Sass (203.7148.74)
Tailwind CSS (203.7148.74)
W3C Validators (203.7148.74)
Blade (203.7148.74)
Haml (203.7148.74)
Twig (203.7148.74)
Behat Support (203.7148.74)
Codeception Framework (203.7148.74)
PHPSpec BDD Framework (203.7148.74)
Windows 10 Light Theme (203.7148.74)
ChangeReminder (203.7148.74)
Git (203.7148.74)
GitHub (203.7148.74)
Mercurial (203.7148.74)
Perforce (203.7148.74)
Subversion (203.7148.74)
Command Line Tool (203.7148.74)
Copyright (203.7148.74)
EditorConfig (203.7148.74)
File Watchers (203.7148.74)
Grazie (203.7148.74)
HTTP Client (203.7148.74)
IntelliLang (203.7148.74)
Machine Learning Code Completion (203.7148.74)
Performance Testing (203.7148.74)
PHPStan Support (203.7148.74)
PhpStorm Workshop (203.7148.74)
Psalm Support (203.7148.74)
ReStructuredText (203.7148.74)
Shared Indexes (203.7148.74)
Task Management (203.7148.74)
Terminal (203.7148.74)
TextMate Bundles (203.7148.74)
Time Tracking (203.7148.74)
UML (203.7148.74)

Серенький текст в аргументах это ж просто подсказка редактора, ее не нужно повторять. По факту там просто строка с SQL запросом.
 
Dark Purple Theme (1.2)
Phing (203.7148.74)
Database Tools and SQL (203.7148.74)
Docker (203.7148.74)
FTP/SFTP Connectivity (ex. Remote Hosts Access) (203.7148.74)
Vagrant (203.7148.74)
HTML Tools (203.7148.74)
Refactor-X (203.7148.74)
XPathView + XSLT (203.7148.74)
IDE Settings Sync (203.7148.74)
Settings Repository (203.7148.74)
Angular and AngularJS (203.7148.74)
CoffeeScript (203.7148.74)
JavaScript and TypeScript (203.7148.74)
JavaScript Debugger (203.7148.74)
JavaScript Intention Power Pack (203.7148.74)
Node.js (203.7148.74)
TSLint (203.7148.74)
Vue.js (203.7148.74)
Apache config (.htaccess) (203.7148.74)
DQL (203.7148.74)
Gherkin (203.7148.74)
GNU GetText files support (*.po) (203.7148.74)
Ini (203.7148.74)
Markdown (203.7148.74)
PHPT Support (203.7148.74)
Shell Script (203.7148.74)
YAML (203.7148.74)
Drupal (203.7148.74)
Joomla! (203.7148.74)
WordPress (203.7148.74)
PHP Docker (203.7148.74)
PHP Remote Interpreter (203.7148.74)
PHP WSL Support (203.7148.74)
Hunspell (203.7148.74)
CSS (203.7148.74)
Less (203.7148.74)
Sass (203.7148.74)
Tailwind CSS (203.7148.74)
W3C Validators (203.7148.74)
Blade (203.7148.74)
Haml (203.7148.74)
Twig (203.7148.74)
Behat Support (203.7148.74)
Codeception Framework (203.7148.74)
PHPSpec BDD Framework (203.7148.74)
Windows 10 Light Theme (203.7148.74)
ChangeReminder (203.7148.74)
Git (203.7148.74)
GitHub (203.7148.74)
Mercurial (203.7148.74)
Perforce (203.7148.74)
Subversion (203.7148.74)
Command Line Tool (203.7148.74)
Copyright (203.7148.74)
EditorConfig (203.7148.74)
File Watchers (203.7148.74)
Grazie (203.7148.74)
HTTP Client (203.7148.74)
IntelliLang (203.7148.74)
Machine Learning Code Completion (203.7148.74)
Performance Testing (203.7148.74)
PHPStan Support (203.7148.74)
PhpStorm Workshop (203.7148.74)
Psalm Support (203.7148.74)
ReStructuredText (203.7148.74)
Shared Indexes (203.7148.74)
Task Management (203.7148.74)
Terminal (203.7148.74)
TextMate Bundles (203.7148.74)
Time Tracking (203.7148.74)
UML (203.7148.74)

Серенький текст в аргументах это ж просто подсказка редактора, ее не нужно повторять. По факту там просто строка с SQL запросом.
Спасибо))))))
 
У меня случился баг. Я зарегистрировал аккаунт, далее ввожу правильные данные, и у меня ничего не срабатывает. Выводит неверный логин/и или пароль (примерно так). Хотя вроде как все верно. В базу данных все записывает, проверял неоднократно.
 
В консоли сервера есть какие-то ошибки?
В базу данных пароль записывается в зашифрованном виде?
 
Покажи свой серверный скрипт packages/accounts/index.js
 
Покажи свой серверный скрипт packages/accounts/index.js
JavaScript:
const mysql = require('mysql');
const bcrypt = require('bcrypt');
const saltRounds = 10;
let DB = false;


mp.events.add('packagesLoaded', () => {
   DB = mysql.createConnection({host: 'localhost', user: 'root', password: '', database: 'pjWarfare'});
   DB.connect(function(err){
       if(err) return console.log('Ошиюка подключения: ' + err.stack);
       console.log('Успешное подключение к базе данных');
   });
});

mp.events.add('playerReady', player => {
    player.call('showLoginDialog');
});

mp.events.add('onLoginAttempt', (player, data) => {
   data = JSON.parse(data);

   DB.query('SELECT * FROM accounts WHERE login = ? LIMIT 1', [data.login], function (err, results){
      if( results.length == 0) return player.call('showAuthError', ['Неверный Логин и/или Пароль']);

      const dbPassword = results[0].password;
       bcrypt.compare(data.password, dbPassword).then(function(isMatched) {
           if(isMatched) player.call('hideLoginDialog');
           player.call('showAuthError', ['Неверный Логин и/или Пароль']);
       });

   });
});

mp.events.add('onRegisterAttempt', (player, data) => {
    data = JSON.parse(data);

    DB.query('SELECT id FROM accounts WHERE login = ?', [data.login], function(err, results){
        if( results.length > 0) return player.call('showAuthError', ['Аккаунт с таким Логином уже существует']);

        bcrypt.hash(data.password, saltRounds, function(err, passwordHash) {
            DB.query('INSERT INTO accounts SET login = ?, password = ?', [data.login, passwordHash], function(err, results){
                player.call('hideLoginDialog');
            });
        });

    })
});
 
Назад
Верх