В этом проекте мы узнаем, как реализовать функцию входа в систему с помощью React на внешнем интерфейсе NodeJS с бэкэндом GraphQL и хранить данные в базе данных SQL. При создании DAPP всегда необходимо реализовать функциональность, при которой пользователи могут входить в учетную запись метамаски в ваши приложения после их регистрации.

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

Давайте посмотрим на процесс аутентификации на изображении ниже.

Предпосылки

  1. NodeJS
  2. создать-реагировать-приложение
  3. Метамаска

Итак, давайте начнем. Перейдите в свой терминал, создайте новую папку с любым именем, которое вам нравится. Я назову это 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 для криптографической связи. Мы сохраняем адрес пользователя для проверки.

  1. Теперь перейдите в папку graphql/schema.
  2. Создайте новый файл. назовите это graphql.schema.js
  3. Вставьте следующие строки кода:
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
    }
`);
  1. Перейдите в папку graphql/resolvers
  2. Создайте новый файл. назовите его graphql.resolvers.js
  3. Начнем с регистрации пользователя
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

Спасибо.