В этом проекте мы узнаем, как реализовать функцию входа в систему с помощью React на внешнем интерфейсе NodeJS с бэкэндом GraphQL и хранить данные в базе данных SQL. При создании DAPP всегда необходимо реализовать функциональность, при которой пользователи могут входить в учетную запись метамаски в ваши приложения после их регистрации.
В этой статье мы будем генерировать токены JWT, чтобы предоставить пользователям доступ к защищенным маршрутам в NodeJS.
Давайте посмотрим на процесс аутентификации на изображении ниже.
Предпосылки
- NodeJS
- создать-реагировать-приложение
- Метамаска
Итак, давайте начнем. Перейдите в свой терминал, создайте новую папку с любым именем, которое вам нравится. Я назову это web3-login.
mkdir web3-login cd web3-login npx create-react-app . code .
Откроется папка в вашем VSCode. если вы используете атом, введите atom .
Теперь в этой папке давайте создадим еще один каталог, назовем его backend и установим пару зависимостей:
mkdir backend cd backend npm init -y npm i bcryptjs web3 ethereumjs-util uuid express sequelize jsonwebtoken npm i express-graphql graphql sqlite3 npm i -D nodemon mkdir graphql mkdir database models cd graphql && mkdir resolvers && mkdir schema
Теперь на вашем интерфейсе запустите в терминале следующее:
npm i @metamask/detect-provider web3
Ваши папки должны выглядеть так:
Теперь в вашем бэкэнд-каталоге создайте новый файл. назовите его index.js. введите следующие строки кода:
const express = require('express'); const {graphqlHTTP} = require('express-graphql'); // we will make this folder. Keep following const graphqlSchema = require('./graphql/schema/schema.graphql'); const graphqlResolver = require('./graphql/resolvers/resolvers.graphql.js'); const sequelize = require('./database/db'); const app = express(); app.use(express.json()); // you can also use the CORS library of nodejs app.use((req, res, next)=>{ res.setHeader("Access-Control-Allow-Origin","*"); res.setHeader("Access-Control-Allow-Methods","POST,GET,OPTIONS"); res.setHeader("Access-Control-Allow-Headers", 'Content-Type, Authorization'); if(req.method ==="OPTIONS"){ return res.sendStatus(200); } next(); }); // creates a graphql server app.use('/users',graphqlHTTP({ schema: graphqlSchema, rootValue: graphqlResolver, graphiql: true })) sequelize .sync() .then(res=>{ // only connects to app when app.listen(4000,()=>{ console.log("Backend Server is running!!"); }) }) .catch(err=>console.error);
Теперь перейдите в папку с вашей базой данных. создать новый файл. назовите это db.js. скопируйте и вставьте эту строку кода. Также создайте новый файл с расширением .sqlite. например db.sqlite
const { Sequelize } = require('sequelize'); // create a new sequelize instance. specifies database and location of database. //Sequelize supports many relational database. refer to documentation for more const sequelize = new Sequelize({ dialect: 'sqlite', //database used storage: './database/db.sqlite',//location of the database logging: false // disables logging of database queries to the console }); //ai sequelize.authenticate().then(()=>console.log("Connected to database")); module.exports = sequelize;
Давайте создадим модель пользователя. перейдите в папку моделей, создайте файл с именем user.model.js. Вставьте следующие строки кода:
const sequelize = require('../database/db'); const { DataTypes } = require('sequelize'); const User = sequelize.define('Users', { id: { type: DataTypes.UUIDV1, allowNull: false, primaryKey: true }, email: { type: DataTypes.STRING, allowNull: false, unique: true }, password: { type: DataTypes.STRING(64), allowNull: false }, address: { type: DataTypes.STRING, allowNull: false, unique: true }, nonce: { type: DataTypes.INTEGER, allowNull: false, unique: true }, },{ timestamps: true } ); module.exports = User;
Мы будем использовать параметр nonce для криптографической связи. Мы сохраняем адрес пользователя для проверки.
- Теперь перейдите в папку graphql/schema.
- Создайте новый файл. назовите это graphql.schema.js
- Вставьте следующие строки кода:
const {buildSchema} = require('graphql'); //we build the schema here. module.exports = buildSchema(` type User{ id: ID! email: String! password: String address: String! nonce: Int! } input UserInput{ email: String! password: String! address: String! } type AuthData{ userId: ID! token: String! tokenExpiration: Int! } type Token{ nonce: Int } type jwtToken{ token: String, message: String } type RootQuery { loginMetamask(address: String!): Token! login(email: String!, password: String!): AuthData! signatureVerify(address: String!,signature: String!): jwtToken! users: [User!] } type RootMutation { register(userInput: UserInput): User! } schema { query: RootQuery mutation: RootMutation } `);
- Перейдите в папку graphql/resolvers
- Создайте новый файл. назовите его graphql.resolvers.js
- Начнем с регистрации пользователя
const User = require('../../models/user.model.js'); module.exports = { //from our schema, we pass in the user email, password and address, // a nonce is generated register: (args) => { return User.create({ id: uuid.v1(), ...args.userInput, nonce: Math.floor(Math.random() * 1000000) }).then(res=>{ console.log(res); return res }).catch(err=>{ console.log(err); }) }, }
Теперь переходим на фронтенд:
return( <div className="login_metamask"> <h1>Metamask</h1> <div className="success"> <h3>Succcessfull Transaction</h3> <div className="animation-slider"></div> </div> <section className="metamask__action"> <button onClick={Enable_metamask} className="enable_metamask"> <img src={require("../images/metamask.png")} alt="metamaskimage" /> <h2>Enable Metamask</h2> </button> <button onClick={send_metamask} className="enable_metamask"> <img src={require("../images/metamask.png")} alt="metamaskimage" /> <h2>Send Ether</h2> </button> <button onClick={Login_metamask} className="enable_metamask"> <img src={require("../images/metamask.png")} alt="metamaskimage" /> <h2>Login with Metamask</h2> </button> <button onClick={Logout_metamask} className="enable_metamask"> <img src={require("../images/metamask.png")} alt="metamaskimage" /> <h2>LOGOUT</h2> </button> </section> </div> )
Теперь нам нужно включить метамаску. Когда вы нажмете кнопку «ВКЛЮЧИТЬ МЕТАМАСКУ», код запустится и откроется всплывающее окно. Введите свой пароль.
import Web3 from "web3"; import detectEthereumProvider from "@metamask/detect-provider"; async function Enable_metamask(){ const provider = await detectEthereumProvider(); if (provider) { window.web3 = new Web3(provider); web3 = window.web3; ethereum = window.ethereum; const chainId = await ethereum.request({ method: 'eth_chainId' }); const accounts = await ethereum.request({ method: 'eth_requestAccounts' }); account = accounts[0]; console.log(chainId, accounts); } }
Теперь давайте реализуем вход с помощью функциональности Metamask:
async function Login_metamask(){ //1. First we query for the user nonce in the backend passing in the user account address let requestBody={ query: ` query { loginMetamask(address: "${account}"){ nonce } } ` } const handleSignMessage = (nonce, publicAddress) => { return new Promise((resolve, reject) => web3.eth.personal.sign( web3.utils.fromUtf8(`Nonce: ${nonce}`), publicAddress, (err, signature) => { if (err) return reject(err); return resolve({ publicAddress, signature }); } ) ); } //2. if metamask is enabled we send in the request if(web3 && ethereum && account){ fetch('http://localhost:4000/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }) .then(res=>{ if(res.status !== 200 && res.status !==201){ //3. if we get an error, respond error throw new Error("Failed"); } return res.json(); }) .then(data => { console.log(data); //4. we log retrieve the nonce const nonce = data.data.loginMetamask.nonce; console.log(nonce); if(nonce != null){ //5. we then generate a signed message. and send it to the backend return handleSignMessage(nonce,account) .then((signedMessage)=>{ console.log(signedMessage.signature) requestBody = { query: ` query { signatureVerify(address: "${account}",signature: "${signedMessage.signature}"){ token message } } ` } fetch('http://localhost:4000/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }) .then(response =>{ if(response.status !== 200 && response.status !==201){ throw new Error('Failed'); } return response.json(); }) .then(data => { console.log(data); }) .catch(err=>console.error); }) }else{ //Redirect the user to registration site. console.log('Please Register at our site. ') } }) .catch((error) => { console.error('Error encountered:', error); }); }else{ await Enable_metamask(); } }
Теперь на нашем бэкенде мы получаем этот запрос и обрабатываем его:
const User = require('../../models/user.model.js'); function VerifySignature(signature,nonce) { const msg = `Nonce: ${nonce}`; //convert the message to hex const msgHex = ethUtil.bufferToHex(Buffer.from(msg)); //if signature is valid const msgBuffer = ethUtil.toBuffer(msgHex); const msgHash = ethUtil.hashPersonalMessage(msgBuffer); const signatureBuffer = ethUtil.toBuffer(signature); const signatureParams = ethUtil.fromRpcSig(signatureBuffer); const publicKey = ethUtil.ecrecover( msgHash, signatureParams.v, signatureParams.r, signatureParams.s ); const addressBuffer = ethUtil.publicToAddress(publicKey); const secondaddress = ethUtil.bufferToHex(addressBuffer); return secondaddress; } module.exports = { //from our schema, we pass in the user email, password and address, // a nonce is generated register: (args) => { return User.create({ id: uuid.v1(), ...args.userInput, nonce: Math.floor(Math.random() * 1000000) }).then(res=>{ console.log(res); return res }).catch(err=>{ console.log(err); }) }, loginMetamask: function({address}){ return User.findOne({address}) .then(user=>{ if(!user){ // if user doesn't exit return null return{ nonce: null } } return { //else return the nonce of user nonce: user.dataValues.nonce } }) .catch(err=>{ throw err; }) }, signatureVerify:async function({address, signature}){ const user =await User.findOne({address}); if(!user){ return{ token: null, message: 'User not Found. Sign In again' } } // then verify the signature sent by the user i let secondaddress = VerifySignature(signature,user.nonce) if(address.toLowerCase() === secondaddress.toLowerCase()){ //change the user nonce user.nonce = Math.floor(Math.random() * 1000000); await user.save() const token = await jwt.sign({address: user.address,email: user.email},'HelloMySecretKey',{expiresIn: '1h'}); return{ token: token, message: 'User not Found. Sign In again' } }else { return{ token: null, message: 'User not Found. Sign In again' } } }, }
После этого клиенту будет возвращен веб-токен json. Мы можем использовать это для аутентификации на защищенных маршрутах на нашем сервере. Теперь о пользовательском опыте:
Приложение для упаковки
Это всего лишь простой, но эффективный механизм аутентификации с Metamask.
Найдите исходный код по ссылке GitHub:
https://github.com/OliverMengich/web3LoginSendEtherMetamask.git
Спасибо.