Building a System for User Registration and Login using TypeScript (Part 2 )

Building a System for User Registration and Login using TypeScript (Part 2 )

Learning by Building

Check out Part 1 for the Application Demonstration and building the APIs using Django.

Prerequisite technologies and concepts:

  • React

  • TypeScript

  • React Router

  • Axios

  • JSON Web Tokens (JWT)

  • Hooks: useState, useEffect, createContext, useContext

Source Code

Source code of Frontend

Developing the Frontend

To begin with, we will be building the front end using React with TypeScript. As a prerequisite, you should have a basic understanding of JavaScript and TypeScript. If you are new to TypeScript, it is recommended to go through some introductory materials to get familiar with the language. Once you grasp the fundamentals of TypeScript, you can proceed to set up a React project.

Check out the React documentation on how to get started with React

Check out the TypeScript documentation on how to get started with TypeScript

You can begin the project by executing this command

npm create vite@latest

To name your project, choose "React" followed by the " TypeScript" option

Navigate to the React app directory

cd VerifyMe_Frontend

Next, install the necessary npm packages

npm install

Note: SCSS was used for styling in this project, however, you are free to use any other styling method you prefer

npm install axios jwt-decode react-icons react-loading react-router-dom sass

Subsequently, initiate the server by executing:

npm run dev

Go to the src directory and create a pages directory. Inside the pages directory, generate HomePage.tsx, LoginPage.tsx, and SignupPage.tsx

# /src

cd src
mkdir pages
cd pages

# For Linux:
touch HomePage.tsx LoginPage.tsx SignupPage.tsx

# For Windows:
new-item HomePage.tsx LoginPage.tsx SignupPage.tsx

Note: The main emphasis of this article is not on the layout and appearance of the website. However, you may review the GitHub repository for this project, or come up with your own design

To retrieve the user input on the LoginPage and transmit the request to the backend server when the submit button is clicked, we'll utilize the useState hook and establish a handle login function.

// src/pages/LoginPage.tsx

import React, { useState } from "react";
import axios from "axios";

const LoginPage: React.FC = () => {
  const [username, setUsername] = useState<string>("");
  const [password, setPassword] = useState<string>("");

    const handleLogin = (e: React.FormEvent) => {
          e.preventDefault();
          axios.post("http://127.0.0.1:8000/api/token/", {
               username, password })
            .then((response) => {
              console.log(response.data)
            })
            .catch((error) => {
              console.log(error.message);
            }
        )
    }


   return (
       <div>
            <input type="text"  value={username} placeholder="Username" onChange={(e) => setUsername(e.target.value)} />
            <input type="password" value={password} placeholder="Password"  onChange={(e) => setPassword(e.target.value)} />
        <button type="submit" onClick={handleLogin}> Submit</button>
        </div>
    )
};

export default LoginPage;

The SignupPage will be constructed similarly to the LoginPage, and we will also create a handleSignup function

// src/pages/SignupPage.tsx

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";

const SignupPage: React.FC  = () => {
  const [username, setUsername] = useState<string>("");
  const [first_name, setFirst_name] = useState<string>("");
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");

 let navigate = useNavigate()

  const handleSignup = (e: React.FormEvent) => {
      e.preventDefault();

      axios.post("http://127.0.0.1:8000/register/", {
          username,
          first_name,
          email,
          password,
        })
        .then((response) => {
          console.log(response.data);
          navigate("/login")
        })
        .catch((error) => {
          console.log(error.message);
        })
}

    return (
        <div>
            <input type="email" value={email} placeholder="Email" onChange={(e) => setEmail(e.target.value)} />
            <input type="text" value={username} 
placeholder="Username" onChange={(e) => setUsername(e.target.value)} />
            <input type="text" value={first_name} placeholder="Name" onChange={(e) => setFirst_name(e.target.value)} />
            <input type="password" value={password} placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
            <button type="submit" onClick={handleSignup}> Submit </button>
        </div>

    )
}

export default SignupPage;

In the Main.tsx file use Browser router to enable react-router.

// main.tsx or index.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
    <BrowserRouter>
      <App />
    </BrowserRouter>
);

Within App.tsx, configure the routes for both the LoginPage and SignupPage

// App.tsx

import React from "react";
import { Routes, Route } from "react-router-dom";
import LoginPage from "./pages/LoginPage";
import SignupPage from "./pages/SignupPage";

function App() {
    return (
      <div className="App">
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/signup" element={<SignupPage />} />
        </Routes>
      </div>
    )
}

export default App;

To allow requests from the frontend, enable CORS for localhost:5173

In the backend settings.py add CORS ALLOWED WHITELIST

MIDDLEWARE = [
    ---,
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ---,
]


CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']


CORS_ALLOWED_WHITELIST = [
    "http://localhost:5173",                                                                                                                                                     
]

Note: Please keep in mind that if you are running on port 3000, substitute port number 5173 with port 3000

Begin the backend server by executing:

python manage.py runserver

Initiate the frontend server by executing:

npm run dev

We can assess whether we are capable of registering, logging in, and receiving tokens by testing the system.

Testing Signup.......

Testing Sign-in......


Let's employ the CreateContext hook and useContext hook, which assists in distributing data between components that cannot be easily shared using props

Create a folder named context in the src directory. Inside the context folder, generate a file named AuthContext.tsx

// src/context/AuthContext.tsx

import React, { createContext, useState, ReactNode, useEffect } from "react";
import jwt_decode from "jwt-decode";
import axios from "axios";

Subsequently, let's create a type for the token received after a successful login

type AccessTokensType = {
  access: string | undefined;
  refresh: string | undefined;
};

We'll construct an interface for the use states utilized in this file.

interface CurrentUserContextType {
  authTokens: AccessTokensType;
  setAuthTokens: React.Dispatch<React.SetStateAction<AccessTokensType>>;
  user: string | undefined;
  setUser: React.Dispatch<React.SetStateAction<string | undefined>>;
  loading: boolean;
  setLoading: React.Dispatch<React.SetStateAction<boolean>>;
  callLogout: () => void;
}

All the useState variables will be mandatory

interface Props {
  children: ReactNode;
}

export const AuthContext = createContext<CurrentUserContextType>(
  {} as CurrentUserContextType
);

const AuthProvider: React.FC<Props> = ({ children }) => {
  let [authTokens, setAuthTokens] = useState<AccessTokensType>(() =>
    localStorage.getItem("authTokens")
      ? JSON.parse(localStorage.getItem("authTokens") || "")
      : undefined
  );

  let [user, setUser] = useState<string | undefined>(() =>
    localStorage.getItem("authTokens")
      ? jwt_decode(localStorage.getItem("authTokens") || "")
      : undefined
  );

  let [loading, setLoading] = useState<boolean>(false);
}

export default AuthProvider;

The authTokens useStated is utilized to retrieve and set the value of authTokens from the browser's localStorage. We use the method localStorage.getItem("authTokens") to retrieve the value of the authTokens key from localStorage.

The user useState is utilized for secure routes, where the user is redirected to the LoginPage if the variable is undefined.

The loading useState is a boolean state, with false as its default value. This is done so that until the tokens are updated, the website does not transmit requests using expired tokens, resulting in an error.

Next, we'll generate the function for updating tokens.

  // Updating tokens
  function updateAccess() {
    if (authTokens) {
      axios.post("http://127.0.0.1:8000/api/token/refresh/", {
          refresh: authTokens.refresh,
        })
        .then(function (response) {
          setAuthTokens(response.data);
          localStorage.setItem("authTokens", JSON.stringify(response.data));
        setUser(jwt_decode(response.data.access));
          setLoading(true);
        })
        .catch(function (error) {
          console.log(error);
        });
    }
  }

Subsequently, we'll produce a function for logging out the user

// calling Log out function

function callLogout() {
    setAuthTokens({ access: undefined, refresh: undefined });
    setUser(undefined);
    localStorage.removeItem("authTokens");
  }

After revisiting and accessing the token expiration time, we'll update the refresh token.

  // updating the refresh token after revisiting and accessing the token expiration time

  useEffect(() => {
    if (!loading) {
      updateAccess();
    }

    if (!authTokens) {
      setLoading(true);
    }

    let twentyMinutes = 1000 * 60 * 20;

    let interval = setInterval(() => {
      if (authTokens) {
        updateAccess();
      }
    }, twentyMinutes);
    return () => clearInterval(interval);
  }, [authTokens, loading]);

Finally, we'll return the values we want to offer to the children.

  return (
    <AuthContext.Provider
      value={{
        setAuthTokens,
        authTokens,
        setLoading,
        loading,
        callLogout,
        user,
        setUser
      }}
    >
      {loading ? children : null}
    </AuthContext.Provider>
  );

This is what our AuthContext.tsx file will look like this now

We'll now encase the components in App.tsx with AuthContext.

// App.tsx

import AuthProvider from "./context/AuthContext";

function App() {
    return (
      <AuthProvider>
      <div className="App">
           ------
      </div>
      </AuthProvider>
    )
}

Inside LoginPage.tsx, we'll import the accessToken useState and set the accessToken to the tokens received after logging in.

In the handleLogin function, we'll save the response to authTokens and navigate to the home page where we'll retrieve the user Info using the token to get the user data.

// LoginPage.tsx
import React, { useContext } from "react";
import { useNavigate } from "react-router-dom";
import jwt_decode from "jwt-decode";
import { AuthContext } from "../context/AuthContext";sssssssssssssssssssssssssssssssssssssssssss

const { setAuthTokens, setLoading, setUser } = useContext(AuthContext);
let navigate = useNavigate();

    const handleLogin = (e: React.FormEvent) => {
            ----
            .then((response) => {
              console.log(response.data)
              setAuthTokens(response.data);
              localStorage.setItem("authTokens", JSON.stringify(response.data));setUser(jwt_decode(response.data.access));
              setLoading(true);
              navigate("/");
            })
            ----
        )
    }

Navigate to the HomePage where we going to make a GET request and get the user data by sending the access token.

// HomePage.tsx

import React, {useState, useContext, useEffect} from "react";
import { AuthContext } from "../context/AuthContext";
import axios from "axios";

interface userinfoInterface {
  id: number;
  first_name: string;
  username: string;
  email: string;
}

const HomePage: React.FC = () => {
  const { authTokens, setLoading } = useContext(AuthContext);

  useEffect(() => {
    axios.get<userinfoInterface>("http://127.0.0.1:8000/user/", {
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer " + String(authTokens.access),
        },
      })
      .then((response) => {
        setUserInfos(response.data);
        setLoading(true);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  const [userInfos, setUserInfos] = useState<userinfoInterface>();
  return (
    <div>
        <p>Name: <span>{userInfos?.first_name}</span></p>
        <p>Email: <span>{userInfos?.email}</span></p>
        <p>Username: <span>{userInfos?.username}</span></p>
    </div>
  );
};

export default HomePage;

Next, we will create protective routes to restrict access to certain pages for users who have not logged in

Create a folder named utils inside the src directory and add a new file named RequireAuth.tsx.

// src/utils/RequireAuth.tsx

import React, { useContext } from "react";
import { Navigate, Outlet } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";

const RequireAuth: React.FC = () => {
  let { user } = useContext(AuthContext);
  if (!user) {
    return <Navigate to="/login" />;
  }
  return <Outlet />;
};

export default RequireAuth;

Now, let's add the HomePage component to the protected routes in App.tsx

import RequireAuth from "./utils/RequireAuth";
import HomePage from "./pages/HomePage";

function App() {
  return (
        ---
        <Routes>
          <Route element={<RequireAuth />}>
            <Route path="/" element={<HomePage />} />
          </Route>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/signup" element={<SignupPage />} />
        </Routes>
        ---
  );
}

Next, let's add a logout button to HomePage.tsx so that users can log out.

import the callLogout function

const { callLogout } = useContext(AuthContext);

Create a button to trigger the callLogout function

    <div>
        ---
        <p>Username: <span>{userInfos?.username}</span></p>
         <button onClick={callLogout}>Log out</button>
    </div>

Summary

This article is a comprehensive tutorial on building a front end for user authentication and authorization using TypeScript and React. It guides the reader through the entire process of setting up the project, creating Login and SignUp pages, creating context, installing packages, utilizing API endpoints for user registration, authentication, and data retrieval, and implementing protective routes for authenticated users. The tutorial is a step-by-step guide with clear instructions, making it easy for readers to follow along and build their own authentication system.

Thank you for reading this article! We hope it was informative and helped you in understanding the process of building a front-end for user sign-up, login, and authentication using TypeScript and React. If you have any questions or feedback, feel free to reach out. Happy coding!