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

Сегодня мы с Вами напишем с нуля полноценный скрипт регистрации и авторизации для сервера 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 (восстановление пароля)
 

Вложения

  • mysql-reg.zip
    404,8 КБ · Просмотры: 199
  • db-dump.zip
    828 байт · Просмотры: 44
Последнее редактирование модератором:

evgee

Junior Developer
Покажите мне кто ещё подробнее объясняет чем автор, я первым брошу в него камень!
Ооочень подробный мануал, все по полочками.
За старания большой плюс тебе.
 

test

Junior Developer
Очень круто! Спасибо большое честно это самый топ форум где все понятно ) Ты топ Lev Angel :cool:
 

Jane

Junior Developer
Спасибо!!! На видео хорошо обьясняешь(y) Когда будет код можно скачать?
 

evgee

Junior Developer
перед тем как появляется меню авторизации, видно как спавнится игрок, как решить данный баг?
 

Lev Angel

Developer
Команда форума
Скриптер
Сейчас мы инициируем показ окна входа с сервера. Если делать это сразу на клиенте, то окно будет выскакивать быстрее.
Как вариант еще поискать на вики, может быть функция которая отключает авто спавн.
 

Lev Angel

Developer
Команда форума
Скриптер
Добавил исходники в первое сообщение
 
  • Like
Реакции: Jane

Jane

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

geneff

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

1) Добавить защиту от перебора паролей. Кикать после 3 неправильных вводов.

В ивенте playerReady добавляем инициализацию переменой player.loginAttemp, которая будет отвечать за кол-во спроб:
JavaScript:
mp.events.add('playerReady', player => {
    player.loginAttemp = 0;
    player.call('showLoginDialog');
});

Далее идем в ивент onLoginAttempt:
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');  // если пароли совпадают, значит пользователь авторизовался успешно
            else if(++player.loggedAttemp >= 3) // После того как не совпали пароли, идет эта проверка где инкрементируется переменная и если она равна 3 или больше 3 , то кикаем игрока
                player.kick(); // Кикаем игрока
            else player.call('showAuthError', ['Неверный Пароль']); // если же пароли не совпали, прошлое условие не сработало, выводим игроку сообщение что пароль введено не правильно
        });
    });
});
Ну вот и первое задание выполнено.

2) Написать функцию isPlayerLoggedIn() которая будет возвращать true если игрок авторизовался и false если еще нет.

Так же идем в ивент playerReady для инициализации переменных.
JavaScript:
mp.events.add('playerReady', player => {
    player.loginAttemp = 0;

    player.logged = false; // создаем переменную где будем хранить значение (false - не авторизирован | true - авторизирован)
    player.isPlayerLogged = () => player.logged; // функция которая возвращает значение переменой player.logged

    player.call('showLoginDialog');
});
Задание выполнено и этой функцией мы воспользуемся в следуещем задании.

3) Добавить столбец position в таблицу accounts. Записывать туда позицию игрока при выходе с сервера.
Для начала нужно в нашей таблицы MySQL создать три поля (float): posX, posY, posZ

Далее сделаем сохранение нашей позиции при выходе из сервера:
JavaScript:
mp.events.add("playerQuit", player => {
    if (player.isPlayerLogged()) { // проверка авторизации игрока
        DB.query('UPDATE accounts SET posX = ?, posY = ?, posZ = ? WHERE login = ? LIMIT 1', [player.position.x, player.position.y, player.position.z, player.login], err => {
            if (err)
                console.log('Error: не удалось обновить позицию игрока.');
            else
                console.log('Success: позиция игрока обновлена.');
        });
    }
});
Изменим немного код в ивенте onLoginAttempt, чтобы при входе на сервер мы появлялись там где выйшли:
JavaScript:
bcrypt.compare(data.password, passwordHash, function(err, isMatched) {
    if( isMatched ) {
        hideLoginDialog(player, data.login);

        const posX = results[0].posX, posY = results[0].posY, posZ = results[0].posZ; // подружаем координаты с бд
        if (posX != null && posY != null && posZ != null) // проверяем на всякий случаей не равны ли они null
            player.position = new mp.Vector3(posX, posY, posZ);
        else // если хоть одна из координат равна null  >> отправляем игрока на указанное нами место (я указал рандонмые координаты)
            player.position = new mp.Vector3(73, -72, 58);
    }
    else if (++player.loginAttemp >= 3)
        player.kick();
    else
        player.call('showAuthError', ['Неверный Логин и/или Пароль']);
});

И в onRegisterAttempt добавил дефолтною позицию спавна игрока (рандомные координаты, для демонстрации):
JavaScript:
hideLoginDialog(player, data.login);
player.position = new mp.Vector3(73, -72, 58);

Для удобства сделал отдельную функцию hideDialogLogin(), чтобы код не повторялся:
JavaScript:
const hideLoginDialog = (player, login) => {
    player.call('hideLoginDialog');
    player.logged = true; // ставим значение true (игрок авторизовался)
    player.login = login; // инициализируем новую переменную, в ней будет хранится наш логин
}

В общем добавлю под спойлер полный код, чтобы вы смогли разобратся сами, если чего-то не поняли до этого.
JavaScript:
const mysql = require('mysql');
const bcrypt = require('bcrypt');
let DB = false;

const hideLoginDialog = (player, login) => {
    player.call('hideLoginDialog');
    player.logged = true;
    player.login = login;
}

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

mp.events.add('playerReady', player => {
    player.loginAttemp = 0;

    player.logged = false;
    player.isPlayerLogged = () => player.logged;

    player.call('showLoginDialog');
});

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

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

       bcrypt.compare(data.password, passwordHash, function(err, isMatched) {
           if( isMatched ) {
               hideLoginDialog(player, data.login);

               const posX = results[0].posX, posY = results[0].posY, posZ = results[0].posZ;
               if (posX != null && posY != null && posZ != null)
                   player.position = new mp.Vector3(posX, posY, posZ);
               else
                   player.position = new mp.Vector3(73, -72, 58);
           }
           else if (++player.loginAttemp >= 3)
               player.kick();
           else
               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 (error, results) { // Добавляем аккаунт в базу данных
                hideLoginDialog(player, data.login);
                player.position = new mp.Vector3(73, -72, 58);
            });
        });
    });
});

mp.events.add("playerQuit", player => {
    if (player.isPlayerLogged()) {
        DB.query('UPDATE accounts SET posX = ?, posY = ?, posZ = ? WHERE login = ? LIMIT 1', [player.position.x, player.position.y, player.position.z, player.login], err => {
            if (err)
                console.log('Error: не удалось обновить позицию игрока.');
            else
                console.log('Success: позиция игрока обновлена.');
        });
    }
});

Буду рад любой критике :)
 
Последнее редактирование:

Lev Angel

Developer
Команда форума
Скриптер
@geneff ого. Ты крут :cool: Молодец что сделал и еще +100 в карму что поделился результатами!!!

Придраться не к чему, особо не покритикую. Единственное что я бы поменял это касается стандартного спавна (когда у игрока нет сохраненных координат в базе). У тебя он задается два раза в двух местах при регистрации и логине. Оно работает без проблем, но если ты захочешь поменять координаты, то нужно будет не забыть сделать это в двух местах. Из-за этого дублирование кода плохая идея.

Лучше либо вынести эти координаты в какой-то конфиг или константу в начале скрипта, либо код спавна вынести в одно место (например, в playerReady сразу спавнить на стандартных координатах).
 

Lev Angel

Developer
Команда форума
Скриптер
А что думаешь по поводу восстановления пароля? Получится сделать? Могу помочь если есть сложности:)
 

geneff

Middle Developer
Скриптер
А что думаешь по поводу восстановления пароля? Получится сделать? Могу помочь если есть сложности:)
Честно говоря, понятия не имею как это сделать... Но думаю пару роликов на ютубе как о том как отправлять письма на почту это исправит. Правда я не понимаю как это было бы правильно сделать? Отсылать новый пароль на почту? Или отослать на почту код с потверждение, далее его ввести в игре и откроется меню смены пароля?
 

Voldemor

Senior Developer
Скриптер
Честно говоря, понятия не имею как это сделать... Но думаю пару роликов на ютубе как о том как отправлять письма на почту это исправит. Правда я не понимаю как это было бы правильно сделать? Отсылать новый пароль на почту? Или отослать на почту код с потверждение, далее его ввести в игре и откроется меню смены пароля?
Я хоть таким не занимался, но думаю по логике нужен нам "gmail API" для отправки кода на почту, и уже сравнивать значения.
 

Lev Angel

Developer
Команда форума
Скриптер
Для отправки почты с сервера нам понадобиться дополнительный пакет (по аналогии с пакетом mysql). Почта отправляется по SMTP протоколу, поэтому нам нужен какой-то SMTP-клиент. Например, https://nodemailer.com/about/
Далее нам нужен почтовый сервис к которому мы будем подключаться и через него будет отправляться почта. Например, можно создать почту на gmail и там глянуть настройки SMTP, чтобы указать их в скрипте.

@geneff что касается алгоритма восстановления пароля. В задании не сказано как именно восстанавливать пароль, так что можно делать как угодно - главное достичь цели. Просто высылать новый пароль на почту будет неправильно. Тогда кто угодно, зная email аккаунта может менять пароль бесконечное количество раз :) Он конечно не будет знать новый пароль, но владельцу аккаунта будет весело :)

Второй вариант мне кажется более правильный. Генерируем код восстановления и отправляем его на почту. Этот код можно даже не хранить с базе, а только в памяти, поскольку когда игрок выйдет с сервера этот код уже будет недействителен. Так будет даже безопаснее, потенциальный хакер даже если и выманит этот код у игрока, то для него он будет бесполезен.
Ну и нужна простая форма в которую вводим код восстановления и два раза новый пароль.
 
Яндекс.Метрика
Верх