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', то попробуйте это решение.
 

Вложения

Последнее редактирование:
Покажите мне кто ещё подробнее объясняет чем автор, я первым брошу в него камень!
Ооочень подробный мануал, все по полочками.
За старания большой плюс тебе.
 
Очень круто! Спасибо большое честно это самый топ форум где все понятно ) Ты топ Lev Angel :cool:
 
Спасибо!!! На видео хорошо обьясняешь(y) Когда будет код можно скачать?
 
перед тем как появляется меню авторизации, видно как спавнится игрок, как решить данный баг?
 
Сейчас мы инициируем показ окна входа с сервера. Если делать это сразу на клиенте, то окно будет выскакивать быстрее.
Как вариант еще поискать на вики, может быть функция которая отключает авто спавн.
 
Добавил исходники в первое сообщение
 
  • Like
Реакции: Jane
Для тех кто захочет дальше ковырять эту форму, напишу парочку идей того, что можно улучшить и доработать:
1. Добавить защиту от перебора паролей. Кикать после 3 неправильных вводов.
2. Написать функцию isPlayerLoggedIn() которая будет возвращать true если игрок авторизовался и false если еще нет.
3. Добавить столбец position в таблицу accounts. Записывать туда позицию игрока при выходе с сервера.
4. Добавить возможность восстановить пароль. Для этого понадобиться добавить поле для email аккаунта и какой-то способ чтобы отправлять электронные письма с сервера.
Может снимите продолжение? Будет интересено:):cool:(y)
 
Для тех кто захочет дальше ковырять эту форму, напишу парочку идей того, что можно улучшить и доработать:
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: позиция игрока обновлена.');
        });
    }
});

Буду рад любой критике :)
 
Последнее редактирование:
@geneff ого. Ты крут :cool: Молодец что сделал и еще +100 в карму что поделился результатами!!!

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

Лучше либо вынести эти координаты в какой-то конфиг или константу в начале скрипта, либо код спавна вынести в одно место (например, в playerReady сразу спавнить на стандартных координатах).
 
А что думаешь по поводу восстановления пароля? Получится сделать? Могу помочь если есть сложности:)
 
А что думаешь по поводу восстановления пароля? Получится сделать? Могу помочь если есть сложности:)
Честно говоря, понятия не имею как это сделать... Но думаю пару роликов на ютубе как о том как отправлять письма на почту это исправит. Правда я не понимаю как это было бы правильно сделать? Отсылать новый пароль на почту? Или отослать на почту код с потверждение, далее его ввести в игре и откроется меню смены пароля?
 
Честно говоря, понятия не имею как это сделать... Но думаю пару роликов на ютубе как о том как отправлять письма на почту это исправит. Правда я не понимаю как это было бы правильно сделать? Отсылать новый пароль на почту? Или отослать на почту код с потверждение, далее его ввести в игре и откроется меню смены пароля?
Я хоть таким не занимался, но думаю по логике нужен нам "gmail API" для отправки кода на почту, и уже сравнивать значения.
 
Для отправки почты с сервера нам понадобиться дополнительный пакет (по аналогии с пакетом mysql). Почта отправляется по SMTP протоколу, поэтому нам нужен какой-то SMTP-клиент. Например, https://nodemailer.com/about/
Далее нам нужен почтовый сервис к которому мы будем подключаться и через него будет отправляться почта. Например, можно создать почту на gmail и там глянуть настройки SMTP, чтобы указать их в скрипте.

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

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