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

  • Автор темы Автор темы shevdev
  • Дата начала Дата начала

shevdev

Junior Developer
Скриптер




Создание формы регистрации и авторизации пользователя с CEF, используя базу данных MongoDB.

Решил немножко продвинуть тему создания каких-то проектов для Rage:MP на MongoDB.
Эта статья является текстовой документацией к видео: (обрабатывается YouTube).
Напомню, что код является шаблоном для ваших фантазий. Вы можете дорабатывать как его хотите и делиться этим.
Я в свою очередь, при хорошем активе по теме MongoDB, буду поддерживать эту тему добавляя новые функции и знакомя вас ближе с этой базой данных.

index.html: https://sourceb.in/hakBVeoBGV
style.css: https://sourceb.in/FGCSnL6rvD






1) Вызов окна браузера при входе в игру:
(Создаем папку и в ней создаем файл index.js)

В первую очередь, когда игрок только подключается к нашему серверу, ему должно высветиться окошко с регистрацией, либо авторизацией. Вызов этого окна должен происходить с серверной части. Для этого используем встроенное событие "playerJoin", срабатывающее в момент появление на сервере нового игрока. Через него вызываем на клиентскую часть, используя player.call, пользовательское событие "showBrowser". Оно будет отображать нам то самое окно.

JavaScript:
mp.events.add('playerJoin', async(player) => {
    player.call('showBrowser')
})



2) Инициализация окна браузера на клиенте:
(Для клиентской стороны, создаем папку login, в нее заносим файл index.js)
(В главном файле клиента, не забудьте подключить эту папку!)

Вот мы передали ивент на клиент. Теперь давайте рассмотрим, что нам необходимо для того чтобы установить камеру в определенном месте. Для начала мы объявим некоторые переменные. Комментарий с их пояснением прикреплен.

JavaScript:
let vector = { x: 212.9087371826172, y: -1397.1019287109375, z: 2700.027587890625} //Вектор камеры
let loginCamera = mp.cameras.new('start', vector, new mp.Vector3(-20,0,0), 40); //Cама камера и начало ее работы.
let loginScreen //Переменная окна браузера
let clientPlayer = mp.players.local //Переменная игрока(может не пригодится, но мало ли :D )

Ну вот, основные переменные для работы с клиентом мы создали, можно приступать к нашим ивентам! Первое событие, которое нам пригодится - showBrowser, и сразу разберем событие, которое будет отвечать за то, чтобы этот браузер скрыть hideBrowser. Второе будет вызываться в случае, если игрок удачно прошел авторизацию/регистрацию на нашем проекте.

JavaScript:
mp.events.add({
    'showBrowser': () => {
        loginCamera.setActive(true);//Устанавливаем камеру
        mp.game.cam.renderScriptCams(true, false, 0, true, false);//Указываем, что камера находится в режиме рендера

        mp.game.ui.displayHud(false)// Убираем худ
        mp.game.ui.displayRadar(false)// Убираем радар

        loginScreen = mp.browsers.new('package://web/login/index.html') //Подключаем CEF часть к переменной loginScreen
        loginScreen.execute("mp.invoke('focus', true)")//Даем возможность использовать курсор
        mp.gui.chat.show(false);//Убираем возможность использовать чат
    },
    'hideBrowser': () => {
        mp.game.cam.renderScriptCams(false, false, 0, true, false);//Убираем рендер, чтобы вернуть камеру к игроку
        loginCamera.setActive(false)//Отключаем камеру

        mp.game.ui.displayHud(true)//Включаем худ
        mp.game.ui.displayRadar(true)//Включаем радар

        loginScreen.execute("mp.invoke('focus', false)")//Убираем курсор у игрока
        mp.gui.chat.show(true)//Даем возможность писать что-либо в чат
        loginScreen.active = false//Отключаем нашу CEF часть.
    }
})



3) Работа с CEF частью. Вёрстка
(Cоздаем отдельную папку в клиентской части "web", а в ней папку "login". Здесь заносим наши файлы верстки и скрипты для формы логина.)

Теперь нам нужно сверстать то, что будем показывать пользователю, html и css файл вы могли скопировать в самом начале. Сейчас мы подробно разберем что и за что отвечает в JS скрипте. Вернитесь в HTML файл, и вы во-первых можете увидеть, что я повесил на кнопку в форме авторизации/регистрации событие onclick, в него занёс будущую функцию либо же checkLogin, либо же checkRegister. То есть в этих функциях содержится валидация, и если она будет пройдена успешно, то тогда на клиент, а потом на сервер будет отправляться JSON объект с данными введенными пользователем и уже тогда, если данные пройдут валидацию уже на БД, скроется окно браузера. Вот такая на первый взгляд замысловатая схема работы этой системы. Ах, еще! После каждого input создан span, он будет отвечать за вывод ошибки. Так вот, давайте смотреть, что у нас под капотом нашего JS файлика.


JavaScript:
let textLoginUsername = document.getElementById('textLoginUsername');//По ID указываем путь к спану логинНикнейма
let textLoginPassword = document.getElementById('textLoginPassword');//По ID указываем путь к спану логинПароля
function checkLogin() {//Функция валидации данных
    let userLogin = document.getElementById("userLogin").value//По ID указываем путь к инпуту логинНикнейма и его значению
    let passLogin = document.getElementById("passLogin").value//По ID указываем путь к инпуту логинПароля и его значению
    setErrorFor(textLoginUsername, '', '#000')//В случае чего сбрасываем ошибку(разбор этой функции ниже)
    setErrorFor(textLoginPassword, '', '#000')//В случае чего сбрасываем ошибку(разбор этой функции ниже)
    if(userLogin.length <= 4) {//Валидация никнейма и вывод ошибки в спан
        return setErrorFor(textLoginUsername, 'Никнейм слишком мал', '#ff0000')
    }
    if(passLogin.length <= 4) {//Валидация пароля и вывод ошибки в спан
        return setErrorFor(textLoginPassword, 'Пароль слишком мал', '#ff0000')
    }
    mp.trigger('loginClient', JSON.stringify({userLogin, passLogin}))//Если всё гуд, то вызов на клиент ивента с внесёнными данными
}

let textRegisterUsername = document.getElementById("textRegisterUsername")//По ID указываем путь к спану регистрНикнейма
let textRegisterPassword = document.getElementById("textRegisterPassword")//По ID указываем путь к спану регистрПароля
function checkRegister() {//Функция валидации данных
    let userRegister = document.getElementById("userRegister").value.trim()//По ID указываем путь к инпуту регистрНикнейма и его значению.
    let passRegister = document.getElementById("passRegister").value.trim()//По ID указываем путь к инпуту регистрПароля и его значению.
    setErrorFor(textRegisterUsername, '', '#000')//В случае чего сбрасываем ошибку(разбор этой функции ниже)
    setErrorFor(textRegisterPassword, '', '#000')//В случае чего сбрасываем ошибку(разбор этой функции ниже)
    if(userRegister.length <= 4) {//Валидация никнейма и вывод ошибки в спан
        return setErrorFor(textRegisterUsername, 'Никнейм слишком мал', '#ff0000')
    }
    if(passRegister.length <= 4) {//Валидация пароля и вывод ошибки в спан
        return setErrorFor(textRegisterPassword, 'Пароль слишком мал', '#ff0000')
    }
    mp.trigger('registerClient', JSON.stringify({userRegister, passRegister}))//Если всё гуд, то вызов на клиент ивента с внесёнными данными
}
//Функция которая будет заносить ошибку в спан.
//Первым аргументом указывается в какой именно, вторым аргументом - сообщение. Третьим - цвет
function setErrorFor(textPart, message, color) {
    textPart.innerHTML = message
    textPart.style.color = color
}
//Эта функция так же заносит в определенный спан ошибку.
//Но она срабатывает, когда вызывается с клиента на сервер, для валидации данных с БД.
function showError(textPart, message, color) {
    textPart.innerHTML = message
    textPart.style.color = color
}
//Не буду на этом заострять внимание. Этот кусок кода является как бы слайдером для перемещения
//Между формой регистрации и формой авторизации.
let loginForm = document.getElementById("login")
let regForm = document.getElementById("register")
let btn = document.getElementById("btn")
function loginScroll() {
    loginForm.style.left = "-400px"
    regForm.style.left = "50px"
    btn.style.left = "110px"
}
function registeScroll() {
    loginForm.style.left = "50px"
    regForm.style.left = "450px"
    btn.style.left = "0"
}




4) Отправляем форму с CEF на клиент и на сервер.
Мы при помощи mp.trigger вызвали с CEF на клиент события авторизации пользователя, либо регистрации. Данные которые внес пользователь, мы поместили в JSON объект и будет тащить их до самого сервера :D


JavaScript:
mp.events.add({
    //Вызываем с клиента на сервер событие о том, что пользователь хочет залогиниться и JSON объект с введенными данными.
    'loginClient': (logData) => {
        mp.events.callRemote('loginServer', logData)
    },
    //Вызываем с клиента на сервер событие о том, что пользователь хочет зарегистрироваться и JSON объект с введенными данными.
    'registerClient': (regData) => {
        mp.events.callRemote('registerServer', regData)
    }
})

Так же не забываем про то, что мы должны показывать ошибки от БД, к примеру мол при регистрации "Такой пользователь существует". Для этого добавляем еще на клиентскую сторону событие showError, которое будет вызывать функцию в браузер с текстом ошибки и заносить её в спан.

JavaScript:
//Это событие будет отвечать за вывод в инпут сообщения об ошибки от БД
mp.events.add('showError', (textPart, errorMessage) => {
    loginScreen.execute(`showError(${textPart}, '${errorMessage}', '#ff0000')`);
});




5) Подключение базы данных к серверу.
(На серверной стороне создаём отдельную папку mongoDB, в ней создаем папку models.)
(В папке mongoDB создаём файл index.js и mongo.js.)
(В папке models создаём файл player-schema.js.)
(Необходимо установить в server-files библиотеки bcrypt(Для хэширования данных) и mongoose(Для работы с MongoDB)

А теперь самое сладкое. Зачем вы здесь все собрались. Во первых краткий инструктаж почему я решил использовать базу данных, о которых может вы даже не слышали, если работали с SAMP. Это лично мое мнение. У меня нет большого опыта работы с MySQL, поэтому мнение можно считать субъективным. Пользуйтесь той, с которой работать вам удобнее всего.

MongoDB — документо-ориентированная система управления базами данных, не требующая описания схемы таблиц. Считается одним из классических примеров NoSQL-систем, использует JSON-подобные документы и схему базы данных. То есть - нереляционная БД.

Mongoose - это библиотека JavaScript, позволяющая вам определять схемы со строго-типизированными данными. Сразу после определения схемы Mongoose дает вам возможность создать Model (модель), основанную на определенной схеме. Затем модель синхронизируется с документом MongoDB с помощью определения схемы модели.

1)
Подведём небольшой итог. У нас есть MongoDB. Чтобы связаться с ней, нам необходим npm пакет mongoose. Так же существует несколько типов баз данных MongoDB. Это не только JSON объекты, это еще могут быть и графики (MongoDB Charts), а также MongoDB Realm. То есть выбор огромный! Но мы разберем и будем пользоваться другими двумя (на выбор) типами. Это MongoDB Compass и MongoDB Atlas.
Первый тип - это приложение, которое позволяет УДОБНО пользоваться базой на локальном уровне.
Второй тип - это страница в браузере. Рекомендую использовать второй, ибо там хранится все в облаке, и не возникнет проблем, с тем, что может перегрузиться и так далее.

2) Если вы планируете огромный проект, и вам нужен шустрый отклик, то MongoDB будет шустрее.

3)MongoDB имеет динамические запросы документов (document-based query)

4) Отсутствие сложных JOIN'ов.

5) Для того чтобы подключиться к ней. Вам не нужно запускать denwer, openServer. Достаточно зайти в приложение, если используете локальную, либо в браузер, если используете веб-версию.

6) Для хранения используемых в данный момент данных используется внутренняя память, что позволяет получать более быстрый доступ.

7) Работа с большими данными? Вам очень подойдет MongoDB.

Так вот. Ссылка на видео как подключить эту БД к своему компьютеру есть в описании к видео. А если к веб-версии, то вам достаточно лишь зарегистрироваться и войти в аккаунт!

Возвращаемся к разработке! Сейчас работаем с файлом mongo.js, в нём мы подключаем нашу базу данных к проекту и в дальнейшем мы сможем с ней работать из любой точки проекта не подключая нигде повторно! Для этого используется keepAlive: true. Смотрим и изучаем код!


JavaScript:
const mongoose = require('mongoose')//Подключаем библиотеку mongoose для связи с MongoDB

module.exports = async() => {//Экспортируем асинхронную функцию.
    await mongoose.connect('mongodb://localhost:27017/ragemp', {//С помощью метода "connect", подключаемся к MongoDB
        keepAlive: true,//Своего рода постоянное HTTP соединение.
        useNewUrlParser: true,//Отключает предупреждение. MongoDB меняет анализатор URL строки
        useUnifiedTopology: true,//Так же относится к обязательной настройке. Иначе будут предупреждения(soon)
        useFindAndModify: false//Так же относится к обязательной настройке. Иначе будут предупреждения(soon)
    })
    return mongoose//Возвращаем по сути наше подключение в конце функции.
}

Переносимся в код index.js(папка mongoDB) и просто сначала экспортируем функцию а потом её подключаем для корректной работы базы данных.


JavaScript:
const mongo = require('./mongo')//Экспортируем функцию

//Используя встроенный ивент загрузки пакетов, подключаем нашу БД ко всему проекту!
mp.events.add('packagesLoaded', async() => {
    await mongo()
})

А теперь еще интереснее. Ведь мы не только научимся работать с базой данных, но еще и наши пароли будут автоматически хэшироваться и проверяться в самой БД, без вызова библиотеки каждый раз.

Переходим в файл player-schema. Прежде чем начнете читать комментарии к коду, объясню. В начале мы создали объект Schema, который в конце экспортируем в нашу БД, но чтобы пароль не отправлялся в том виде, какой он есть, то мы его хэшируем сразу же методом .pre('save'). А так же создаем свой метод для проверки уже хэшированного пароля и пароля введенного пользователем. На самом деле MongoDB на столько надежна, что хэш паролей может иметь место для крупных проектов, но в любом случае лучше обезопасить себя и своих игроков!


JavaScript:
const mongoose = require('mongoose')//Подключаем модуль mongoose(подключение к mongoDB)
const bcrypt = require('bcrypt')//Подключаем модуль bcrypt(хэширование)
//Создаём объект схемы и передаём в него необходимые значения, указывая типы этих данных.
const Schema = mongoose.Schema({
    username: String,
    password: String,
})
//Если будет создаваться новая БД, то пароль в ней будет сразу хэширован автоматически.
Schema.pre('save', function(next) {
    if(!this.isModified('password'))
        return next();
    bcrypt.hash(this.password, 10,(err,passwordHash) => {
        if(err)
            return next(err)
        this.password = passwordHash
        next()
    })
})
//Создание собственного метода для своего рода валидации
//Хэшированнного пароля и пароля введенного пользователем
Schema.methods.comparePassword = function(candidatePassword, cb) {
    bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
        if (err) return cb(err);
        cb(null, isMatch);
    });
}
//Экспортирование нашей схемы в БД
module.exports = mongoose.model('player-schema', Schema)




6) Проверка данных на регистрацию и авторизацию пользователя.
(Работаем с index.js файлом в серверной части.)

Выходим на финишную прямую ребят! Сейчас будем обрабатывать пользовательские ивенты, которые выполняют функцию валидации пароля, но уже на стороне базы данных. Своего рода погружение в мир бэкенда :D
Опять же. В двух словах. Мы создаём константу в которую по сути заносим все нашу БД с игроками и уже в дальнейшем оперируем ей для всякой валидации. Находя определенное значение в ней и сравнивая выдавая результаты. Я постарался сделать в таком формате, который схож на работу с MySQL, чтобы вам было понятно, но при большой отдаче покажу БОЛЕЕ КОРОТКИЙ способ записи.

JavaScript:
//Подключаемся к нашей БД со всеми игроками проекта
const playerSchema = require('../mongoDB/models/player-schema')
//Обрабатываем ивент, когда пользователь отправил форму с данными входа на сервер
mp.events.add('loginServer', async(player, logData) => {
    //Парсим объект данных пользователя
    logData = JSON.parse(logData)
    //Ищем в нашей БД игрока с таким же ником, что ввёл пользователь в форму
    await playerSchema.findOne({username: logData.userLogin}, function(err, cb) {
        //Если такого игрока нет, то в спан выводится ошибка об этом
            if(!cb) player.call('showError', ['textLoginUsername', 'Такого игрока не существует!'])
            //В противном случае мы сравниваем хэш пароля в БД и указаного пользователем.
            //Если что-то пошло не так, выводим в спан ошибку.
            //Заметьте, что мы не подключали здесь библиотеку bcrypt. Все автоматически!
            cb.comparePassword(logData.passLogin, function(err, isMatch) {
                if(isMatch === true){
                    player.call('hideBrowser')
                } else {
                    player.call('showError', ['textLoginPassword', 'Неверный пароль'])
                }     
            })
        })
});
//Обрабатываем ивент, когда пользователь отправил форму с данными регистрации на сервер
mp.events.add('registerServer', async(player, regData) => {
    //Парсим объект данных пользователя
    regData = JSON.parse(regData)
    //Ищем в нашей БД игрока с таким же ником, что ввёл пользователь в форму
        await playerSchema.findOne({username: regData.userRegister}, function(err,data) {
            //Если такой игрок есть, то в спан выводится ошибка об этом
            if(data) {
               player.call('showError', ['textRegisterUsername', 'Такой игрок существует!'])
            }  else {
            //В противном случае мы создаём в БД нового пользователя с данными которые он внес.
            //Так же вызываем ивент, который скроет CEF часть и ТПнет игрока.
            //Заметьте, что мы не подключали здесь библиотеку bcrypt.
            //Пароль будет хэширован автоматически.
                player.call('hideBrowser')
                new playerSchema({
                    username: regData.userRegister,
                    password: regData.passRegister
                }).save()
            }
        
        })
});




Снимок экрана (1163).png






Я искренне надеюсь, что статья вам помогла, и возможно научила чему-то новому.
Делитесь своими мыслями, смотрите мои видосы и Льва. До встречи!

Связь со мной: shevdev#8621

Скачать архив: https://disk.yandex.ru/d/WUFBRVMwhN8Ntg
 
Последнее редактирование:
Спасибо за туториал! Получилось здорово:) Особенно понравилось как ты спрятал bcrypt в модельку. Красивое решение.
В целом если сранивать с регой на голом SQL, то выглядит удобнее и красивее. Но с ORM можно сделать так же, даже хуки навесить на модель.

По коду немного прокомментирую. Опять же это не критика, просто "мысли в слух":rolleyes:

1. Зачем в функции вывода ошибок передавать цвет, если он везде одинаковый и там будут только ошибки?

2. Зачем сделал две одинаковые функции setErrorFor и showError. Можно же с сервера дергать ту же setErrorFor.

3. Когда юзаешь playerSchema.findOne по идее он может вернуть тебе результат когда используешь await
Т. е. сделать не

JavaScript:
await playerSchema.findOne({username: logData.userLogin}, function(err, cb) {
    if(!cb) player.call('showError', ['textLoginUsername', 'Такого игрока не существует!'])
    //Код дальше
});

а вот так

JavaScript:
const account = await playerSchema.findOne({username: logData.userLogin});
if(!account) player.call('showError', ['textLoginUsername', 'Такого игрока не существует!'])
//Код дальше

Возможно и с comparePassword можно также провернуть. Что это дает? С точки зрения производительности ничего не поменяется. Оно работает так же асинхронно (магия js). Но у тебя убираются несколько уровней вложенности и код легче читать.
В первом случае даже по ходу не нужно ставить await, т. к. findOne и так работает асинхронно, а после него все равно ничего нет в этом ивенте. Поэтому нет смысла оборачивать его в await.


PS: Пасхалку в форме входа заметил, колокольчик поставил(y)
 
Спасибо за туториал! Получилось здорово:) Особенно понравилось как ты спрятал bcrypt в модельку. Красивое решение.
В целом если сранивать с регой на голом SQL, то выглядит удобнее и красивее. Но с ORM можно сделать так же, даже хуки навесить на модель.

По коду немного прокомментирую. Опять же это не критика, просто "мысли в слух":rolleyes:

1. Зачем в функции вывода ошибок передавать цвет, если он везде одинаковый и там будут только ошибки?

2. Зачем сделал две одинаковые функции setErrorFor и showError. Можно же с сервера дергать ту же setErrorFor.

3. Когда юзаешь playerSchema.findOne по идее он может вернуть тебе результат когда используешь await
Т. е. сделать не

JavaScript:
await playerSchema.findOne({username: logData.userLogin}, function(err, cb) {
    if(!cb) player.call('showError', ['textLoginUsername', 'Такого игрока не существует!'])
    //Код дальше
});

а вот так

JavaScript:
const account = await playerSchema.findOne({username: logData.userLogin});
if(!account) player.call('showError', ['textLoginUsername', 'Такого игрока не существует!'])
//Код дальше

Возможно и с comparePassword можно также провернуть. Что это дает? С точки зрения производительности ничего не поменяется. Оно работает так же асинхронно (магия js). Но у тебя убираются несколько уровней вложенности и код легче читать.
В первом случае даже по ходу не нужно ставить await, т. к. findOne и так работает асинхронно, а после него все равно ничего нет в этом ивенте. Поэтому нет смысла оборачивать его в await.


PS: Пасхалку в форме входа заметил, колокольчик поставил(y)
Спасибо за ревью!

1) Касательно асинхронности метода "findOne". async функции всегда будут возвращать промисы, поэтому и правда, в случае единственного значения (findOne) он не нужен. Спасибо!
2) Касательно корректировки валидации БД. Можно и таким способом, но опять же я писал в статье, что выбрал такой способ, чтбы он был чем-то похож на тот что пишут в mysql ( судя по твоим видосам :D ).
3) Насчет цвета. Изначально код был другой и эти спан, они были динамичными, соответственно там был не только красный цвет. Из-за опять же как и с await, невимательности, я не вынес его в функцию, тоже с по сути повторной функцией showError, она по сути не нужна и можно ее сменить setErrorFor.

Еще раз спасибо. Хорошего дня.
 
Если имеет смысл, могу подкорректировать статейку, очистив и упростив код.

?
 
Можно и таким способом, но опять же я писал в статье, что выбрал такой способ, чтбы он был чем-то похож на тот что пишут в mysql ( судя по твоим видосам :D ).
Я сам учусь по ходу дела)) В моих предыдущих видео я даже не использовал async/await. Недавно сел, немного разобрался что это такое и понял что с этим будет лучше. Дальше буду использовать.
 
Я сам учусь по ходу дела)) В моих предыдущих видео я даже не использовал async/await. Недавно сел, немного разобрался что это такое и понял что с этим будет лучше. Дальше буду использовать.
Спасибо еще раз)
 
Назад
Верх