DEV Community

Deepak Singh Kushwah
Deepak Singh Kushwah

Posted on

React Protected Routes - Role based

Introduction

Hi developers, in this tutorial, we will go through how to created protected routes in ReactJs. For this, I have create Node+Express+MongoDB server for user login/registration. You can use any other backend stack like PHP/Python or any other. For frontend state management I have used Zustand, you can choose any other library you prefer.

Requirements

You should have basic understanding of ReactJS.

Project Setup

npm create vite@latest
after above command, you will see wizard, choose react and javascript

For CSS and styling, I have used tailwindcss. I assume you know how to include tailwindcss in react projects. If not, please follow instruction on tailwindcss site.

Dependencies

npm i react-router-dom axios prop-types zustand

Code and Explanation

App.jsx

import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Error from "./pages/Error";
import Layout1 from "./components/Layouts/Layout1";
import ProtectedRoute from "./components/Auth/ProtectedRoute";
import Dashboard from "./pages/member-pages/Dashboard";
import Login from "./pages/Login";
import Unauthorized from "./pages/Unauthorized";
function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Layout1 />}>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/login" element={<Login />} />
            <Route path="/unauthorized" element={<Unauthorized />} />
            <Route path="*" element={<Error />} />
            <Route element={<ProtectedRoute allowedRoles={['registered']}/>}>
              <Route path="/dashboard" element={<Dashboard/>}/>
            </Route>
          </Route>
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Auth/ProtectedRoute.jsx

import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useUserStore } from "../../store/useStore";
import PropTypes from "prop-types";
const ProtectedRoute = (props) => {
  const user = useUserStore((state) => state.user);
  const location = useLocation();
  return props.allowedRoles.includes(user?.role)
  ? <Outlet /> 
  : user ? <Navigate to="/unauthorized" state={{from: location}} replace /> 
  : <Navigate to="/login" state={{ from: location }} replace />

};

ProtectedRoute.propTypes = {
  allowedRoles: PropTypes.array,
};

export default ProtectedRoute;

Enter fullscreen mode Exit fullscreen mode

Layouts/Layout1.jsx

import { Outlet } from "react-router-dom";
import Footer from "../page-parts/Footer";
import Header from "../page-parts/Header";

const Layout1 = () => {
  return (
    <>
      <div className="m-auto max-w-5xl border border-black bg-blue-950 text-white my-10 rounded-xl">
        <Header />
        <div className="p-10">
          <Outlet />
        </div>
        <Footer />
      </div>
    </>
  );
};

export default Layout1;

Enter fullscreen mode Exit fullscreen mode

page-parts/Header.jsx

import { Link } from "react-router-dom";
import { useUserStore } from "../../store/useStore";

const Header = () => {
  const user = useUserStore((state) => state.user);
  const signOut = useUserStore((state) => state.signOut);
  return (
    <div className="bg-gray-300 text-black rounded-t-xl">
      <nav className="flex justify-between">
        <img src="https://picsum.photos/100/50.jpg" />
        <ul className="flex gap-5 p-3">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/contact">Contact</Link>
          </li>
          <li>
            <Link to="/services">Services</Link>
          </li>

          {user ? (
            <>
              <li>
                <Link to="/dashboard">Dashboard</Link>
              </li>
              <li>
                <button onClick={() => signOut()}>Logout</button>
              </li>
            </>
          ) : (
            <li>
              <Link to="/login">Login</Link>
            </li>
          )}
        </ul>
      </nav>
    </div>
  );
};

export default Header;

Enter fullscreen mode Exit fullscreen mode

page-parts/Footer.jsx

const Footer = () => {
  return (
    <div className="text-white w-full ">
      <div className="flex items-center justify-center">Copyright &copy; 2023</div>
    </div>
  );
};

export default Footer;

Enter fullscreen mode Exit fullscreen mode

pages/Home.jsx

const Home = () => {
  return (
    <div className="text-justify">
      <h1 className="text-4xl">Welcome to Home Page</h1>      
    </div>
  )
}

export default Home
Enter fullscreen mode Exit fullscreen mode

pages/About.jsx

const About = () => {
  return (
    <div>About</div>
  )
}

export default About
Enter fullscreen mode Exit fullscreen mode

pages/Error.jsx

const Error = () => {
  return (
    <div>404 - not found</div>
  )
}

export default Error
Enter fullscreen mode Exit fullscreen mode

pages/Login.jsx

import { useState } from "react";
import { useUserStore } from "../store/useStore";
import { useNavigate } from "react-router-dom";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const signin = useUserStore((state) => state.signIn);
  const navigate = useNavigate();
  const handleLogin = async() => {
    const status = await signin(email, password);
    if (status) {
      navigate("/dashboard");
    }
  };
  return (
    <>
      <h3 className="text-2xl text-center">Login</h3>
      <hr className="headingHr" />
      <div className="m-auto w-[400px] rounded p-3 flex flex-row justify-center bg-slate-400">
        <table className="">
          <tbody>
            <tr>
              <td>
                <label htmlFor="email">Email</label>
              </td>
              <td>
                <input
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  name=""
                  id="email"
                />
              </td>
            </tr>

            <tr>
              <td>
                <label htmlFor="password">Password</label>
              </td>
              <td>
                <input
                  type="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  name=""
                  id="password"
                />
              </td>
            </tr>

            <tr>
              <td></td>
              <td>
                <button onClick={() => handleLogin()} className="button">
                  Login
                </button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </>
  );
};

export default Login;

Enter fullscreen mode Exit fullscreen mode

pages/Unauthorized.jsx

import { useUserStore } from "../store/useStore"

const Unauthorized = () => {
  const user = useUserStore(state => state.user);
  console.log(user);
  return (
    <div>You are not allowed to do this action!!!</div>
  )
}

export default Unauthorized
Enter fullscreen mode Exit fullscreen mode

pages/member-pages/dashboard

const Dashboard = () => {
  return (
    <div>Dashboard</div>
  )
}

export default Dashboard
Enter fullscreen mode Exit fullscreen mode

store/axiosHTTP.js

import axios from "axios";

const axiosHttp = axios.create({
  baseURL: 'http://localhost:3000'
});

export default axiosHttp;
Enter fullscreen mode Exit fullscreen mode

store/BaseSlice.js

export const BaseSlice = (set) => ({
  name: 'Base Slice',
  loading: false,
  setLoading: (stat) => {
    set((state) => ({
      ...stat,
      loading: state
    }))
  }
});
Enter fullscreen mode Exit fullscreen mode

store/UserSlice.js

import axiosHttp from "./axiosHttp";

export const UserSlice = (set) => ({
  name: 'User',
  user: null,
  token: null,
  setUser: async(user) => {
    set({user: user});
  },

  getUser: async(id) => {
    const json = await axiosHttp.get(`/user/${id}`);
    return await json.data;
  },
  signIn: async (email, password) => {
    try {
      //await timeout(2000);
      const json = await axiosHttp.post(`${import.meta.env.VITE_SERVER_URL}/api/v1/auth/signin`, { email, password });
      const data = await json.data;
      //console.log(data.user);
      set(() => ({
        user: data.user,
        token: data.token,
      }));
      return true;
    } catch (error) {
      return null;
    }
  },
  signUp: async (form) => {
    try {
      const json = await axiosHttp.post("/register", form);
      const data = await json.data;
      set(() => ({
        user: data.user,
        token: data.accessToken,
      }));
      return {token: data.accessToken, error: null, message: 'User registered'}
    } catch (error) {      
      const errMsg = await error.response;
      console.log(errMsg.data);
      return {message: errMsg.data, error: true, token: null};
    }
  },
  signOut: () => {
    set({
      user: null,
      token: null,
    });
  },

});
Enter fullscreen mode Exit fullscreen mode

store/useStore.js

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware"
import { BaseSlice } from "./BaseSlice";
import { UserSlice } from "./UserSlice";
import { SongSlice } from "./SongSlice";
export const useBaseStore = create((set, get) => ({
  ...BaseSlice(set, get)
}));

export const useUserStore = create(
  persist((set, get) => ({
    ...UserSlice(set, get)
}),{name: 'userStore',storage: createJSONStorage(() => sessionStorage)}
));
Enter fullscreen mode Exit fullscreen mode

From backend API, we need following format response at time of login.

Image description

Once you completed these steps, check your website.

Image description

Image description

Happy Coding.

Top comments (0)