Search code examples
reactjsnode.jsauthenticationmernreact-context

Problem in Redirecting Users to Login Page If Not Authenticated and Redirect to Main App After Successful Login


Here is my Mainapp.js:

import React, { useContext, useEffect, useState } from "react";
import { IoIosAddCircle } from "react-icons/io";
import { FaUserCircle } from "react-icons/fa";
import { IoLogOutOutline } from "react-icons/io5";
import { useForm } from "react-hook-form";
import { LuLoader2 } from "react-icons/lu";
import { Outlet, useNavigate } from "react-router-dom";
import todoContext from "../context/todos/todoContext";
import userContext from "../context/user/userContext";
import showToast from "../components/Alert";

const MainApp = () => {
  let  navigate = useNavigate();
  const todoContextData = useContext(todoContext);
  const userContextData = useContext(userContext);
  const { CreateTodo } = todoContextData;
  const { userDetail, isTokenVerified,isLoading } = userContextData;
  const [addTaskDisplay, setAddTaskDisplay] = useState(true);
  
  useEffect(() => {
    if (!isLoading) {
      if (isTokenVerified) {
        
        console.log('Mainapp is authenticated');
      } else {
        console.log('Mainapp is not authenticated');
        navigate('/');

      }
    } else {
      console.log('still loading...');
    }
  }, [isLoading, isTokenVerified,navigate]);
  
  
  // useEffect(()=>{
  //   // getUser()
  // },[])
  // useEffect(() => {
  //   if (!isLoading) {
  //     if (isTokenVerified !== null) { // Check if token verification process is complete
  //       if (isTokenVerified) {
  //         showToast('Login success', 'success');
  //       } else {
  //         showToast('Login failure', 'error');
  //         navigate('/auth/login');
  //       }
  //     }
  //   }
  // }, []);
  
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm();

  const handleNewTask = () => {
    setAddTaskDisplay(!addTaskDisplay);
  };

  const handleCancel = () => {
    setAddTaskDisplay(true);
  };

  const onSubmit = async (data) => {
    CreateTodo(data.title);
    reset();
    showToast("Todo Created Successfully", "success");
    setAddTaskDisplay(true);
  };

  const logoutUser = () => {
    localStorage.removeItem("token");
    showToast("Logged Out Successfully", "success");
    navigate("/auth/login");
  };

  return (
    <>
      {isLoading ? (
        <div className="flex w-full justify-center items-center">
          Loading
          <div className="text-2xl animate-spin">
            <LuLoader2 />
          </div>
        </div>
      ) : (
        <div className="main-div">
          <div className="top-bar flex justify-between items-center bg-slate-50 border-b-[1px] border-b-gray-300 py-3 px-4">
            <div className="left">
              <div
                className="new-task-button font-semibold text-blue-500 rounded-md cursor-pointer bg-blue-100 hover:bg-blue-200 px-3 py-3 flex items-center"
                onClick={handleNewTask}
              >
                <IoIosAddCircle className="text-2xl mr-4 " /> New Task
              </div>
            </div>
            <div className="right flex">
              <div className="acc-name-disp font-semibold text-blue-500 rounded-md bg-slate-200  px-3 py-3 flex items-center">
                <FaUserCircle className="text-2xl mr-4 " /> {userDetail && userDetail.name}
              </div>
              <div
                onClick={logoutUser}
                className="logout-btn flex items-center px-3 py-3 bg-slate-200 rounded-md ml-2 text-black font-semibold cursor-pointer hover:bg-slate-300"
              >
                <IoLogOutOutline className="text-2xl mr-4 "></IoLogOutOutline>
                Logout
              </div>
            </div>
          </div>
          <div className="w-full h-full flex">
            <Outlet />
            <div
              className={`add-task-wrap ${
                addTaskDisplay ? "hidden" : ""
              } w-full h-[calc(100vh - 73px)] flex absolute justify-center`}
            >
              <form onSubmit={handleSubmit(onSubmit)}>
                <div className="add-task-cont mt-[20vh]">
                  <div className="add-task-card py-4 px-6 bg-white border-[1px] border-gray-300 shadow-md rounded-md">
                    <div className="md:w-96 w-80 flex flex-col justify-center">
                      <div className="title text-xl font-semibold mb-2 ">
                        Add Task
                      </div>
                      <input
                        {...register("title", { required: "Todo Cannot Be Blank" })}
                        className="border-[1px] border-gray-400 rounded-sm outline-none px-2 py-1 focus:border-blue-500 my-1"
                        placeholder="Enter Todo Here"
                        type="text"
                      />
                      {errors.title && (
                        <span className="text-red-600 font-medium text-sm">
                          {errors.title.message}
                        </span>
                      )}
                      <div className="buttons flex justify-end">
                        <button
                          className="bg-gray-200 py-1 px-2 my-2 mr-2 rounded-md  text-sm hover:bg-gray-300"
                          onClick={handleCancel}
                          type="button"
                        >
                          Cancel
                        </button>
                        <button
                          type="submit"
                          className="bg-blue-600 py-1 px-2 my-2 rounded-md text-white text-sm hover:bg-blue-700"
                        >
                          Add Task
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              </form>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

export default MainApp;

In this code basically i want that if a user without login goes to my mainapp.js it redirects the user to login else allow the user when clicked on login from login.js But When i Login succesfully ,it redirects to homepage from mainapp.js,and when try to go manually to the mainapp.js it works. I dont know what is causing this issue please help me out.

Here Are my remaing codes: Login.js

import React, { useState } from 'react';
import { Link,redirect, redirectDocument,useNavigate} from 'react-router-dom';
import { useForm } from 'react-hook-form';
import showToast from '../Alert';
import { ToastContainer } from 'react-toastify';

const Login = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm();

  const [backendError, setBackendError] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  let navigate = useNavigate()

  const onSubmit = async (data) => {
    try {
      let connectToBackend = await fetch('http://localhost:3050/api/auth/LoginUser', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      });
      if (!connectToBackend.ok) {
        let errorMessage = await connectToBackend.json();
        setBackendError(Array.isArray(errorMessage) ? errorMessage : [errorMessage.message]);
      } else {

        
        setBackendError([]);
        setIsLoggedIn(true);
        const json = await connectToBackend.json()
        console.log(json.authToken)
        localStorage.setItem('token', json.authToken); 
        showToast('Logined Succesfully','success')
        // const timeoutId = setTimeout(() => {
          navigate('/mainapp');
        // }, 2000);
    
        // Clean up the timer when the component unmounts
        // return () => clearTimeout(timeoutId);
      }
      
    } catch (error) {
      setBackendError([error.message]);
    }
  };
  
  return (
    <>
    
    <section className="text-gray-600 body-font">
      <div className="px-5 py-16 flex justify-center items-center">
        <form className="bg-gray-100 rounded-lg p-12 flex flex-col w-full lg:w-[35%] md:w-[50%]" onSubmit={handleSubmit(onSubmit)}>
          <div>
            <h2 className="text-gray-900 text-2xl font-bold title-font mb-3">Login</h2>
            {backendError && <span className='text-sm text-red-600 mb-3'>{backendError}</span>}
            <div className="relative mb-4">
              <label htmlFor="email" className="leading-7 text-sm text-gray-600">Email</label>
              <input {...register("email", { required: "Email is required" })} type="email" className={`${errors.email && 'focus:border-red-600 focus:ring-red-200 border-red-600 '} w-full bg-white rounded border border-gray-300 focus:border-blue-600 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out`} />
              {errors.email && <span className='text-red-600 font-medium text-sm'>{errors.email.message}</span>}
            </div>
            <div className="relative mb-4">
              <label htmlFor="password" className="leading-7 text-sm text-gray-600">Password</label>
              <input {...register("password", { required: "Password is required" })} type="password" className={`${errors.password && 'focus:border-red-600 focus:ring-red-200 border-red-600 '} w-full bg-white rounded border border-gray-300 focus:border-blue-600 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out`} />
              {errors.password && <span className='text-red-600 font-medium text-sm'>{errors.password.message}</span>}
            </div>
            <button type='submit' disabled={isSubmitting} className={` ${isSubmitting ? 'bg-gray-400 cursor-not-allowed' : 'hover:bg-blue-700'} text-white bg-blue-600 border-0 py-2 px-8 focus:outline-none rounded justify-center w-full flex text-lg`}>{isSubmitting ? 'Loading...' : 'Login'}</button>
            <p className="text-xs font-semibold text-gray-500 mt-3">Don't have an account? <Link to="/auth/signup" className="text-blue-700">Sign up here</Link></p>
          </div>
        </form>
      </div>
    </section></>
    
  );
};

export default Login;

userState.js

import { useState, useEffect } from "react";
import userContext from "./userContext";

const UserState = (props) => {
  const [userDetail, setUserDetail] = useState(null);
  const [isAdmin, setIsAdmin] = useState(false);
  const [isTokenVerified, setIsTokenVerified] = useState(false);
  const [isLoading, setIsLoading] = useState(true); // Added isLoading state

  const getUser = async () => {
    
    setIsLoading(true); // Set loading to true before fetching data
     const  token =  localStorage.getItem("token");
    if (token) {
      console.log(token)
      try {
        const response = await fetch("http://localhost:3050/api/auth/GetUser", {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            "auth-token": token,
          },
        });
        const userDetails = await response.json();
        if (response.ok) {
          setUserDetail(userDetails);
          setIsAdmin(userDetails.role === "ADMIN");
          setIsTokenVerified(true);
        } else {
          console.error(`Error fetching user data: ${userDetails.error}`);
          setIsTokenVerified(false);
          // Implement retry mechanism or display error message to the user
        }
      } catch (error) {
        console.error("Error fetching user data:", error);
        setIsTokenVerified(false);
        // Implement retry mechanism or display error message to the user
      } finally {
        setIsLoading(false); // Set loading to false after data is fetched
      }
    } else {
      console.log("Token not found");
      setIsTokenVerified(false);
      setIsLoading(false);
      // Redirect user to login page or display error message
    }
  };
  useEffect(()=>{
    
    getUser()
  },[])
  

   // Dependency array includes getUser

  return (
    <userContext.Provider
      value={{userDetail, isAdmin, isTokenVerified, isLoading }}
    >
      {props.children}
    </userContext.Provider>
  );
};

export default UserState;

Here is the video link of the problem:Video

I am expecting that on login user redirects to mainapp.js but if anyone who is not logged in try to access mainapp.js redirect to login page


Solution

  • First I would want to mention that the link, which is provided for the video, requires access. Please, provide access to everybody.

    The problem of the code is using of the context and the function onSubmit in Login component. The onSubmit function sets the state of a variable inside Login - isLoggedIn. That variable is not used anywhere and it must be removed. To make the code to works, in the context setters functions of the states have to be added too:

     <userContext.Provider
      value={{userDetail, isAdmin, isTokenVerified, isLoading, setIsLoading, setIsAdmin, setIsTokenVerified }}
    >
      {props.children}
    </userContext.Provider>
    

    After that, they have to be imported in Login component:

     const { setIsAdmin, setIsTokenVerified, setIsLoading } = useContext(userContext)
    

    In onSubmit function, when the json is received (I assume in that json there is info about is the user admin and the token) you have to pass the token and is admin response:

    else {         
        setBackendError([]);
        setIsTokenVerified(true);
        setIsAdmin(json.isAdmin)
        const json = await connectToBackend.json()
        console.log(json.authToken)
        localStorage.setItem('token', json.authToken); 
        showToast('Logined Succesfully','success')
        // const timeoutId = setTimeout(() => {
          navigate('/mainapp');
        // }, 2000);
    
        // Clean up the timer when the component unmounts
        // return () => clearTimeout(timeoutId);
      }
    

    With this code the state of the context will be changed and App component will re render with the new state.

    I want to mention that set of local storage do not trigger re render! For that when in component Login the local storage is just changed, the logic in the original code is not working correct.