User Authentication: Register & Login using JWT in NextJs & MYSQL

 In the earlier tutorial, you learnt data migration to synchronize Sequelize model with MYSQL database. This tutorial teaches you authentication using JWT (JSON Web Token) with MYSQL database. 

The app allows a user to simply register an account using username, email, and password. The password is encrypted using bcryptjs. A successful registration will create a new record in users table of the MYSQL database. 

To log in, the app requires a user to input username and password. After successful login, the app generates a token on server using jsonwebtoken. Then the token is returned to the client and stored at the client side using js-cookie for further API access. 
Run the following commands to install bcrypt, jsonwebtoken, and js-cookie:

npm install bcrypt jsonwebtoken js-cookie

We build login and registration forms using reactstrap components. Thus, install the following dependencies:

npm install bootstrap reactstrap

In this project, we use axios to access server APIs and next-connect to do APIs method routing and middleware. Run the following commands to install axios and next-connect.

npm install axios next-connect

In the pages directory, create user directory. In the user directory, create three files: login.js, logout.js , and register.js.

login.js

import React, { useState } from "react"
import axios from "axios";
import Link from 'next/link';
import Router, { useRouter } from 'next/router';
import Layout from "../../components/layout";
import Cookies from 'js-cookie';

import {
  Button,
  FormGroup,Card,
  CardHeader,CardBody,CardFooter,  
  Input,
  Form,
  Label,
} from "reactstrap";


const Login = (props) => {
      const [username, setUsername]=useState('');
      const [password,setPassword]=useState('');
      const [logerr,setLogError] = useState('');  
      const router = useRouter();

      const [token, setToken]= useState('');
      
      async function handleSubmit(e){

        e.preventDefault();

        axios({
            method: 'post',
            url: `/api/auth`,
            data: {username: username,password:password},
            headers: { "Content-type":"application/json",}
        }).then(response=>{
                
                try {
                    //console.log("token",response.data);
                    let resobj=JSON.parse(JSON.stringify(response.data));
                    if(resobj.success){
                        
                        //token valid in 30 days
                        Cookies.set('token', resobj.token,{ expires: 30 });
                        // update token variable
                        setToken(resobj.token);
                        // redirect to home
                        Router.push("/");
                    }
                    else{
                        setLogError(resobj.error);
                    }
                    
                } catch (e) {
                    setLogError('Error in login');
                }
                
                
                
        });
        
      
    }
      const handleUsernameInputChange = (event) => {
        setUsername(event.target.value);
      }
      const handlePasswordInputChange = (event) => {
        setPassword(event.target.value);
      }
      return (

        <Layout
          title='login'
          token={token}
        >
            <Card
                className="my-2" style={{width: '25rem'}}
            >

            <CardHeader>Login</CardHeader>
            
            <Form className="form">
                <CardBody>
                    <FormGroup>

                    <Input
                        type="text"
                        name="username"
                        placeholder="Enter username"
                        value={username}
                        onChange={(e) =>handleUsernameInputChange(e)}
                        required
                        />
        
                    </FormGroup>
                    <FormGroup>

                        <Input
                        type="password"
                        name="password"
                        placeholder="Enter password"
                        value={password}
                        onChange={(e) =>handlePasswordInputChange(e)}
                        required
                        />
        
                    </FormGroup>
                </CardBody>   

                <CardFooter>
                    <FormGroup>
        
                    <Button color="primary" onClick={handleSubmit}>Login</Button>
                    <Label style={{marginLeft: '5px'}}>Don&apos;t have an account?</Label>  <Link  href={{
pathname: '/user/register',}}>create user</Link> <Label>{logerr}</Label> </FormGroup> </CardFooter> </Form> </Card> </Layout> ) } export default Login

logout.js
import React,{useEffect, useState} from "react"
import Router from 'next/router';
import Layout from "../../components/layout";
import Cookies from 'js-cookie';
const Logout = (props) => {

      const [token,setToken]=useState(''); 

      useEffect(() => {

        Cookies.remove('token');
        setToken(null);
        Router.push("/");
        
         }, []);

      return (

        <Layout token={token}>
          <div>Logout</div>
        </Layout>
      
      )
}
export default Logout

register.js
import React, { useState } from "react"
import axios from "axios";
import Router, { useRouter } from 'next/router';

import {
  Button,
  FormGroup,Card,
  CardHeader,CardBody,CardFooter,  
  Input,
  Form,
} from "reactstrap";
import Layout from "../../components/layout";

const Register = (props) => {

      const [errors,setErrors] = useState({});  
      const [inputs, setInputs] = useState({});
      const router = useRouter();
      // inputs validation
      function handleValidation() {
        
        let formIsValid = true;
        let es = {};
        //Name
        if (!inputs.username) {
          formIsValid = false;
          es['username']="can not empty!";
         
        }
    
        if (typeof inputs.username !== "undefined") {
            
          if (!inputs.username.match(/^[a-zA-Z0-9]+$/)) {
            formIsValid = false;
            es['username']="only letters and numbers allowed!";
          }
        }
        // password
        if (!inputs.password) {
            formIsValid = false;
            es['password']="can not empty!";
          }
      
          if (typeof inputs.password !== "undefined") {
            if (inputs.password.length<8) {
              formIsValid = false;
              es['password']="week password!";
            }
          }
    
        //Email
        if (!inputs.email) {
          formIsValid = false;
          es['email']="can not empty!";
        }
    
        if (typeof inputs.email!== "undefined") {
        
          if (!(/\S+@\S+\.\S+/.test(inputs.email))) {
            formIsValid = false;
            es['email']="invalid email!";
          }
        }
        setErrors(es);
        return formIsValid;
      }

      function handleSubmit(e){
        e.preventDefault();
        if(handleValidation()){
            axios({
                method: 'post',
                url: `/api/register`,
                data: {username: inputs.username,password:inputs.password,email:inputs.email},
                headers: { "Content-type":"application/json"}
            }).then(response=>{

                    try {

                        console.log('new user=',response);
                        const resobjt=JSON.parse(JSON.stringify(response));
                        if(resobjt.data.status === 'success'){
                            Router.push("/user/login");
                        }
                        else{
                            alert('Failed to create user!');
                        }
                        
                    } catch (e) {
                        alert('Failed to create user!'+e);
                    }
                    
                    
                    
            });
        }
      }
      
      const handleChange = (event) =>{
        const name = event.target.name;
        const value = event.target.value;
        setInputs(values => ({...values, [name]: value}))
      }
      return (
        <Layout
         title='register user'
        >
            <Card
                className="my-2" style={{width: '18rem'}}
            >

            <CardHeader>Register</CardHeader>
            
            <Form className="form">
                <CardBody>
                <FormGroup>
                    <Input
                        type="text"
                        name="username"
                        placeholder="Enter username"
                        value={inputs.username || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.username}</span>
                    </FormGroup>
                   
                    <FormGroup>

                    <Input
                        type="email"
                        name="email"
                        placeholder="Enter email"
                        value={inputs.email || ""} 
                        onChange={handleChange}
    
                        />
                    <span style={{color: '#ff2222'}}>{errors.email}</span>
                    </FormGroup>
                    <FormGroup>

                        <Input
                        type="password"
                        name="password"
                        placeholder="Enter password"
                        value={inputs.password || ""} 
                        onChange={handleChange}
                       
                        />
                    <span style={{color: '#ff2222'}}>{errors.password}</span>
                    </FormGroup>
                </CardBody>   

                <CardFooter>
                    <FormGroup>
        
                    <Button color="primary" onClick={handleSubmit}>Register</Button>
                    </FormGroup>
                </CardFooter>
            </Form>  
            </Card>      
            </Layout>
      
      )
}

export default Register

Run npm run dev to launch the nextjs app. On top menu, click login icon.
Login form

Click create user to open registration form:


Now we finish login and registration in client side. We need to create APIs in server side to handle registration and login against MYSQL database. 

in the pages/api folder, create register.js:

import nextConnect from 'next-connect';
import bcrypt from 'bcryptjs';
import db from '../../db/models/index';
const handler = nextConnect()
.post(async (req, res) => {

    if(req.body){

        const body = JSON.parse(req.body);

        const { username, email, password } = body;

        let password_encrypted= await bcrypt.hash(password, 10);
        
        const newUser = await db.User.create({
         username:username,
         email:email,
         password:password_encrypted,
        });
        return res.status(200).json({
         status: 'success',
         message: 'done',
         data: newUser,
        });
        
    }
 
 });
   

  export default handler;

Save the project. Then, while MYSQL is running, register a new user account using username, email, and password. The new user should be created in the database.

In the root folder of the project, create .env file to define SECURITY_KEY used in generating and verifying token.
In the pages/api folder, create auth.js file to handle login task.

import db from '../../db/models/index';
import nextConnect from 'next-connect';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

const KEY = process.env.JWT_KEY;

const handler = nextConnect()

  .post(async (req, res) => {
    /* Get Post Data */
    const body = JSON.parse(req.body);

    const { username, password } = body;

    /* Any how username or password is blank */
    if (!username || !password) {
      return res.json({
        status: 'error',
        error: 'Missing username or password!',
      });
    }
    /* Check user in database */
    const user = await db.User.findOne({
      where: { username: username },
      attributes: ['id', 'email', 'password'],
      limit: 1,
    });
    /* Check if exists */
    if (!user) {
      res.json({ status: 'error', error: 'User Not Found!' });
    }
    /* Define variables */
    const dataUser = user.toJSON();
    const userId = dataUser.id,
      userEmail = dataUser.email,
      userPassword = dataUser.password;
    /* Check and compare password */
    bcrypt.compare(password, userPassword).then(isMatch => {
      if (isMatch) {
        /* User matched */
        /* Create JWT Payload */
        const payload = {
          id: userId,
          email: userEmail,
        };
        /* Sign token */
        jwt.sign(
          payload,
          KEY,
          {
            expiresIn: 31556926, // 1 year in seconds
          },
          (err, token) => {
            res.json({
              success: true,
              token: token,
            });
          },
        );
      } else {
        res.json({ status: 'error', error: 'Password incorrect!' });
      }
    });
  });
export default handler;

In the root folder of the project, create middleware folder. In the middleware folder, create utils.js file. In the utils.js file we define two methods: verifyToken() and getAppCookie(). The verifyToken() will be used to very a token before allowing to access a restricted API. the verifyToken() method is used in the next tutorial. The getAppCookie() method is called to parse cookies to get the stored token.

import jwt from 'jsonwebtoken';

const SECRET_KEY = process.env.JWT_KEY;

//verify  token
export function verifyToken(jwtToken) {
  try {
    return jwt.verify(jwtToken, SECRET_KEY);
  } catch (e) {
    console.log('e:', e);
    return null;
  }
}

// get cookie
export function getAppCookies(req) {
  const parsedItems = {};
  if (req.headers.cookie) {
    const cookiesItems = req.headers.cookie.split('; ');
    cookiesItems.forEach(cookies => {
      const parsedItem = cookies.split('=');
      parsedItems[parsedItem[0]] = decodeURI(parsedItem[1]);
    });
  }
  return parsedItems;
}



Update the index.js file to use getServerSideProps() method  to get the token stored in the cookies. The getServerSideProps() executes on server and returns the token to the client Home component. From the Home component, the token is passed to Layout component so that the token is available in the Sidebar component. 

import Layout from '../components/layout';
import { absoluteUrl, getAppCookies } from "../middleware/utils";

function Home(props) {
  const {token} = props;
  console.log('t=',props);
  return (
   
      <Layout 
          title='Home Page'
          token={token}

      />

  )
}
/* getServerSideProps */
export async function getServerSideProps(context) {
  const { req } = context;
  const token = getAppCookies(req).token || '';

  return {
    props: {
      token
    },
  };
}
export default Home;

Save the project. Try to login using the account created above. If the login is successful, the app will redirect to the home page and the login menu item on the top bar change to logout.

Comments

Popular posts from this blog

Upload form data with file using Multer & MYSQL in NextJs

Protect APIs & Data filtering & paging in NextJs & MYSQL

Migration using Sequelize with MYSQL database in NextJs