DEV Community

Cover image for FullStack React & Django Authentication : Django REST ,TypeScript, Axios, Redux & React Router
Mangabo Kolawole
Mangabo Kolawole Subscriber

Posted on • Edited on

FullStack React & Django Authentication : Django REST ,TypeScript, Axios, Redux & React Router

As a full-stack developer, understand how to build an authentication system with backend technology and manage the authentication flow with a frontend technology is crucial.

In this tutorial, we'll together build an authentication system using React and Django.
We'll be using Django and Django Rest to build the API and create authentication endpoints. And after, set up a simple login and profile page with React and Tailwind, using Redux and React router by the way.

Backend

First of all, let's set up the project. Feel free to use your favorite python environment management tool. I’ll be using virtualenv here.




virtualenv --python=/usr/bin/python3.8 venv
source venv/bin/activate


Enter fullscreen mode Exit fullscreen mode
  • And after that, we install the libraries we’ll be using for the development and create the project.



pip install django djangorestframework djangorestframework-simplejwt

django-admin startproject CoreRoot .


Enter fullscreen mode Exit fullscreen mode
  • We'll first create an app that will contain all the project-specific apps.


django-admin startapp core


Enter fullscreen mode Exit fullscreen mode
  • After the creation, delete all files and folders except __init__.py and apps.py.
  • Then open the settings file containing Django configurations and add core to the INSTALLED_APPS :


    # CoreRoot/settings.py
    ...
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'core'


Enter fullscreen mode Exit fullscreen mode

We can now create the user application and start adding features.



cd core && python ../manage.py startapp user


Enter fullscreen mode Exit fullscreen mode


    # CoreRoot/settings.py
    ...
    'rest_framework',

    'core',
    'core.user'


Enter fullscreen mode Exit fullscreen mode

For this configuration to work, you'll need to modify the name of the app in core/user/apps.py



# core/user/apps.py
from django.apps import AppConfig


class UserConfig(AppConfig):
    name = 'core.user'
    label = 'core_user'


Enter fullscreen mode Exit fullscreen mode

And also the __init__.py file in core/user directory.



# core/user/__init__.py
default_app_config = 'core.user.apps.UserConfig'


Enter fullscreen mode Exit fullscreen mode

Writing User logic

Django comes with a built-in authentication system model which fits most of the user cases and is quite safe. But most of the time, we need to do rewrite it to adjust the needs of our project. You may add others fields like bio, birthday, or other things like that.

Creating a Custom User Model Extending AbstractBaseUser

A Custom User Model is a new user that inherits from AbstractBaseUser. But we’ll also rewrite the UserManager to customize the creation of a user in the database.
But it’s important to note that these modifications require special care and updates of some references through the settings.py.



# core/user/models.py
from django.db import models

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin


class UserManager(BaseUserManager):

    def create_user(self, username, email, password=None, **kwargs):
        """Create and return a `User` with an email, phone number, username and password."""
        if username is None:
            raise TypeError('Users must have a username.')
        if email is None:
            raise TypeError('Users must have an email.')

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_superuser(self, username, email, password):
        """
        Create and return a `User` with superuser (admin) permissions.
        """
        if password is None:
            raise TypeError('Superusers must have a password.')
        if email is None:
            raise TypeError('Superusers must have an email.')
        if username is None:
            raise TypeError('Superusers must have an username.')

        user = self.create_user(username, email, password)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)

        return user


class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(db_index=True, max_length=255, unique=True)
    email = models.EmailField(db_index=True, unique=True,  null=True, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    objects = UserManager()

    def __str__(self):
        return f"{self.email}"



Enter fullscreen mode Exit fullscreen mode

Now what we'll do next is specify to Django to use this new User model as the AUTH_USER_MODEL.



# CoreRoot/settings.py
...
AUTH_USER_MODEL = 'core_user.User'
...


Enter fullscreen mode Exit fullscreen mode

Adding User serializer

The next step when working with Django & Django Rest after creating a model is to write a serializer.
Serializer allows us to convert complex Django complex data structures such as querysets or model instances in Python native objects that can be easily converted JSON/XML format, but Serializer also serializes JSON/XML to naive Python.



# core/user/serializers.py
from core.user.models import User
from rest_framework import serializers


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'is_active', 'created', 'updated']
        read_only_field = ['is_active', 'created', 'updated']


Enter fullscreen mode Exit fullscreen mode

Adding User viewset

And the viewset. A viewset is a class-based view, able to handle all of the basic HTTP requests: GET, POST, PUT, DELETE without hard coding any of the logic. And if you have specific needs, you can overwrite those methods.



# core/user/viewsets.py

from core.user.serializers import UserSerializer
from core.user.models import User
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework import filters


class UserViewSet(viewsets.ModelViewSet):
    http_method_names = ['get']
    serializer_class = UserSerializer
    permission_classes = (IsAuthenticated,)
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ['updated']
    ordering = ['-updated']

    def get_queryset(self):
        if self.request.user.is_superuser:
            return User.objects.all()

    def get_object(self):
        lookup_field_value = self.kwargs[self.lookup_field]

        obj = User.objects.get(lookup_field_value)
        self.check_object_permissions(self.request, obj)

        return obj


Enter fullscreen mode Exit fullscreen mode

Authentication

REST framework provides several authentication schemes out of the box, but we can also implement our custom schemes. We'll use authentication using JWT tokens.
For this purpose, we'll use the djangorestframework-simplejwt to implement an access/refresh logic.
Add rest_framework_simplejwt.authentication.JWTAuthentication to the list of authentication classes in settings.py:



# CoreRoot/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    )
}


Enter fullscreen mode Exit fullscreen mode

The Simple JWT library comes with two useful routes:

  • One to obtain access and refresh token (login) 'api/token/'
  • And another one to obtain a new access token using the refresh token 'api/token/refresh/'
  • It can actually do all the work but there are some issues here :
  • The login routes only return a pair of token
  • In the user registration flow, the user will be obliged to sign in again to retrieve the pair of tokens.

And since we are using viewsets, there is a problem with consistency.
But here’s the solution :

  • Rewrite the login endpoint and serializer to return the pair of tokens and the user object as well
  • Generate a pair of tokens when a new user is created and send includes the tokens in the response object
  • Make sure that the class-based views will be viewsets.
  • Actually, it was a little bit challenging, but shout-out to djangorestframework-simplejwt contributors, it’s very simple to read the code, understand how it works and extend it successfully.
  • First of all, let's create a package auth in core.
  • In the package, create a file serializer.py which will contain the login and register serializers.


# core/auth/serializers.py
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.settings import api_settings
from django.contrib.auth.models import update_last_login
from django.core.exceptions import ObjectDoesNotExist

from core.user.serializers import UserSerializer
from core.user.models import User


class LoginSerializer(TokenObtainPairSerializer):

    def validate(self, attrs):
        data = super().validate(attrs)

        refresh = self.get_token(self.user)

        data['user'] = UserSerializer(self.user).data
        data['refresh'] = str(refresh)
        data['access'] = str(refresh.access_token)

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data


class RegisterSerializer(UserSerializer):
    password = serializers.CharField(max_length=128, min_length=8, write_only=True, required=True)
    email = serializers.EmailField(required=True, write_only=True, max_length=128)

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password', 'is_active', 'created', 'updated']

    def create(self, validated_data):
        try:
            user = User.objects.get(email=validated_data['email'])
        except ObjectDoesNotExist:
            user = User.objects.create_user(**validated_data)
        return user


Enter fullscreen mode Exit fullscreen mode

Then, we can write the viewsets.



# core/auth/viewsets
from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
from core.auth.serializers import LoginSerializer, RegistrationSerializer


class LoginViewSet(ModelViewSet, TokenObtainPairView):
    serializer_class = LoginSerializer
    permission_classes = (AllowAny,)
    http_method_names = ['post']

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        try:
            serializer.is_valid(raise_exception=True)
        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)


class RegistrationViewSet(ModelViewSet, TokenObtainPairView):
    serializer_class = RegisterSerializer
    permission_classes = (AllowAny,)
    http_method_names = ['post']

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        refresh = RefreshToken.for_user(user)
        res = {
            "refresh": str(refresh),
            "access": str(refresh.access_token),
        }

        return Response({
            "user": serializer.data,
            "refresh": res["refresh"],
            "token": res["access"]
        }, status=status.HTTP_201_CREATED)


class RefreshViewSet(viewsets.ViewSet, TokenRefreshView):
    permission_classes = (AllowAny,)
    http_method_names = ['post']

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        try:
            serializer.is_valid(raise_exception=True)
        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)


Enter fullscreen mode Exit fullscreen mode

The next step is to register the routes.
Create a file routers.py in the core directory.




# core/routers.py
from rest_framework.routers import SimpleRouter
from core.user.viewsets import UserViewSet
from core.auth.viewsets import LoginViewSet, RegistrationViewSet, RefreshViewSet


routes = SimpleRouter()

# AUTHENTICATION
routes.register(r'auth/login', LoginViewSet, basename='auth-login')
routes.register(r'auth/register', RegistrationViewSet, basename='auth-register')
routes.register(r'auth/refresh', RefreshViewSet, basename='auth-refresh')

# USER
routes.register(r'user', UserViewSet, basename='user')


urlpatterns = [
    *routes.urls
]



Enter fullscreen mode Exit fullscreen mode

And the last step, we'll include the routers.urls in the standard list of URL patterns in CoreRoot.



# CoreRoot/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('api/', include(('core.routers', 'core'), namespace='core-api')),
]


Enter fullscreen mode Exit fullscreen mode

The User endpoints, login, and register viewsets are ready. Don't forget to run migrations and start the server and test the endpoints.



python manage.py makemigrations
python manage.py migrate

python manage.py runserver


Enter fullscreen mode Exit fullscreen mode

If everything is working fine, let's create a user with an HTTP Client by requesting localhost:8000/api/auth/register/. I'll be using Postman but feel free to use any client.



{
    "email": "testuser@yopmail.com",
    "password": "12345678",
    "username": "testuser"
}


Enter fullscreen mode Exit fullscreen mode

Front-end with React

There are generally two ways to connect Django to your frontend :

  • Using Django Rest as a standalone API + React as Standalone SPA. (It needs token-based authentication)
  • Or include React in Django templates. (It's possible to use Django built-in authentication features)

The most used pattern is the first one, and we'll focus on it because we have already our token authentication system available.
Make sure you have the latest version of create-react-app in your machine.



yarn create react-app react-auth-app --template typescript
cd react-auth-app
yarn start


Enter fullscreen mode Exit fullscreen mode

Then open http://localhost:3000/ to see your app.

But, we'll have a problem. If we try to make a request coming from another domain or origin (here from our frontend with the webpack server), the web browser will throw an error related to the Same Origin Policy. CORS stands for Cross-Origin Resource Sharing and allows your resources to be accessed on other domains.
Cross-Origin Resource Sharing or CORS allows client applications to interface with APIs hosted on different domains by enabling modern web browsers to bypass the Same-origin Policy which is enforced by default.
Let's enable CORS with Django REST by using django-cors-headers.



pip install django-cors-headers


Enter fullscreen mode Exit fullscreen mode

If the installation is done, go to your settings.py file and add the package in INSTALLED_APPS and the middleware.



INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

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


Enter fullscreen mode Exit fullscreen mode

And add these lines at the end of the settings.py file.



CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000"
]


Enter fullscreen mode Exit fullscreen mode

We are good now. Let's continue with the front end by adding libraries we'll be using.

Creating the project

First of all, let's add tailwind and make a basic configuration for the project.



yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9


Enter fullscreen mode Exit fullscreen mode

Since Create React App doesn’t let you override the PostCSS configuration natively, we also need to install CRACO to be able to configure Tailwind.



yarn add @craco/craco


Enter fullscreen mode Exit fullscreen mode

Once it's installed, modify these lines in the package.json file. Replace react-
scripts by craco.



     "scripts": {
        "start": "craco start",
        "build": "craco build",
        "test": "craco test",
        "eject": "react-scripts eject"
      }


Enter fullscreen mode Exit fullscreen mode

Next, we'll create a craco config file in the root of the project, and add tailwindcss and autoprefixer as plugins.



//craco.config.js
module.exports = {
  style: {
    postcss: {
      plugins: [require("tailwindcss"), require("autoprefixer")],
    },
  },
};


Enter fullscreen mode Exit fullscreen mode

Next, we need to create a configuration file for tailwind.
Use npx tailwindcss-cli@latest init to generate tailwind.config.js file containing the minimal configuration for tailwind.



module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};


Enter fullscreen mode Exit fullscreen mode

The last step will be to include tailwind in the index.css file.



/*src/index.css*/

@tailwind base;
@tailwind components;
@tailwind utilities;


Enter fullscreen mode Exit fullscreen mode

We are done with the tailwind configuration.

Login and Profile Pages

Let's quickly create the Login Page and the Profile Page.



// ./src/pages/Login.tsx

import React, { useState } from "react";
import * as Yup from "yup";
import { useFormik } from "formik";
import { useDispatch } from "react-redux";
import axios from "axios";
import { useHistory } from "react-router";

function Login() {
  const [message, setMessage] = useState("");
  const [loading, setLoading] = useState(false);
  const dispatch = useDispatch();
  const history = useHistory();

  const handleLogin = (email: string, password: string) => {
    //
  };

  const formik = useFormik({
    initialValues: {
      email: "",
      password: "",
    },
    onSubmit: (values) => {
      setLoading(true);
      handleLogin(values.email, values.password);
    },
    validationSchema: Yup.object({
      email: Yup.string().trim().required("Le nom d'utilisateur est requis"),
      password: Yup.string().trim().required("Le mot de passe est requis"),
    }),
  });

  return (
    <div className="h-screen flex bg-gray-bg1">
      <div className="w-full max-w-md m-auto bg-white rounded-lg border border-primaryBorder shadow-default py-10 px-16">
        <h1 className="text-2xl font-medium text-primary mt-4 mb-12 text-center">
          Log in to your account πŸ”
        </h1>
        <form onSubmit={formik.handleSubmit}>
          <div className="space-y-4">
            <input
              className="border-b border-gray-300 w-full px-2 h-8 rounded focus:border-blue-500"
              id="email"
              type="email"
              placeholder="Email"
              name="email"
              value={formik.values.email}
              onChange={formik.handleChange}
              onBlur={formik.handleBlur}
            />
            {formik.errors.email ? <div>{formik.errors.email} </div> : null}
            <input
              className="border-b border-gray-300 w-full px-2 h-8 rounded focus:border-blue-500"
              id="password"
              type="password"
              placeholder="Password"
              name="password"
              value={formik.values.password}
              onChange={formik.handleChange}
              onBlur={formik.handleBlur}
            />
            {formik.errors.password ? (
              <div>{formik.errors.password} </div>
            ) : null}
          </div>
          <div className="text-danger text-center my-2" hidden={false}>
            {message}
          </div>

          <div className="flex justify-center items-center mt-6">
            <button
              type="submit"
              disabled={loading}
              className="rounded border-gray-300 p-2 w-32 bg-blue-700 text-white"
            >
              Login
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

export default Login;


Enter fullscreen mode Exit fullscreen mode

Here's a preview :

Preview of Login Page

And the profile page :



// ./src/pages/Profile.tsx

import React from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";

const Profile = () => {
  const dispatch = useDispatch();
  const history = useHistory();

  const handleLogout = () => {
    //
  };
  return (
    <div className="w-full h-screen">
      <div className="w-full p-6">
        <button
          onClick={handleLogout}
          className="rounded p-2 w-32 bg-red-700 text-white"
        >
          Deconnexion
        </button>
      </div>
      <div className="w-full h-full text-center items-center">
        <p className="self-center my-auto">Welcome</p>
      </div>
    </div>
  );
};

export default Profile;


Enter fullscreen mode Exit fullscreen mode

And here's the preview :

Screenshot 2021-06-26 at 02-10-29 React App.png

Env variables configurations

And the final step, we'll be making requests on an API. It's a good practice to configure environment variables. Fortunately, React allows us to make basic environment configurations.
Create a .env file at the root of the project and put this here.



./.env
REACT_APP_API_URL=localhost:8000/api


Enter fullscreen mode Exit fullscreen mode

Add Redux Store

Redux is a library to manage the global state in our application.
Here, we want the user to log in and go to the Profile Page. It will only work if the login is correct.
But that's not all: if the user has no active session -meaning that the refresh is expired or there is no trace of this user account or tokens in the storage of the frontend - he is directly redirected to the login page.

To make things simple, here's what we're going to do:

  • create a persisted store with (redux-persist) for our project, and write actions using slices from redux-toolkit to save, account state, and tokens when the user signs in. We'll also write an action for logout.
  • create a Protected route component, that'll check if the state of the user account null or exists and then redirect the user according to the results.

First of all, let's add the dependencies we need to configure the store.



yarn add @reduxjs/toolkit redux react-redux redux-persist


Enter fullscreen mode Exit fullscreen mode

Then, create a folder named store in src.
Add in this directory another folder named slices and create in this directory a file named auth.ts.
With Redux, a slice is a collection of reducer logic and actions for a single feature of our app.
But before adding content to this file, we need to write the interface for the user account.



// ./src/types.ts

export interface AccountResponse {
  user: {
    id: string;
    email: string;
    username: string;
    is_active: boolean;
    created: Date;
    updated: Date;
  };
  access: string;
  refresh: string;
}


Enter fullscreen mode Exit fullscreen mode

And now, we can write the authentication slice authSlice.



// ./src/store/slices/auth.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AccountResponse } from "../../types";

type State = {
  token: string | null;
  refreshToken: string | null;
  account: AccountResponse | null;
};

const initialState: State = { token: null, refreshToken: null, account: null };

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    setAuthTokens(
      state: State,
      action: PayloadAction<{ token: string; refreshToken: string }>
    ) {
      state.refreshToken = action.payload.refreshToken;
      state.token = action.payload.token;
    },
    setAccount(state: State, action: PayloadAction<AccountResponse>) {
      state.account = action.payload;
    },
    logout(state: State) {
      state.account = null;
      state.refreshToken = null;
      state.token = null;
    },
  },
});

export default authSlice;


Enter fullscreen mode Exit fullscreen mode

Now, move inside the store directory and create a file named index.ts. And add the following content.



// ./src/store/index.ts

import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import {
  FLUSH,
  PAUSE,
  PERSIST,
  persistReducer,
  persistStore,
  PURGE,
  REGISTER,
  REHYDRATE,
} from "redux-persist";
import storage from "redux-persist/lib/storage";
import authSlice from "./slices/auth";

const rootReducer = combineReducers({
  auth: authSlice.reducer,
});

const persistedReducer = persistReducer(
  {
    key: "root",
    version: 1,
    storage: storage,
  },
  rootReducer
);

const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
    },
  }),
});

export const persistor = persistStore(store);
export type RootState = ReturnType<typeof rootReducer>;

export default store;


Enter fullscreen mode Exit fullscreen mode

Now the store has been created, we need to make the store accessible for all components by wrapping <App /> (top-level-component) in :



// ./src/App.tsx

import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { Login, Profile } from "./pages";
import store, { persistor } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import { Provider } from "react-redux";
import ProtectedRoute from "./routes/ProtectedRoute";

export default function App() {
  return (
    <Provider store={store}>
      <PersistGate persistor={persistor} loading={null}>
        <Router>
          <div>
            <Switch>
              <Route exact path="/login" component={Login} />
              <ProtectedRoute exact path="/" component={Profile} />
            </Switch>
          </div>
        </Router>
      </PersistGate>
    </Provider>
  );
}


Enter fullscreen mode Exit fullscreen mode

The store is accessible by all components in our application now. The next step is to build a <ProtectedRoute /> component to help us hide pages that require sessions from the other ones.

Adding routes

We'll build the <ProtectedRoute />component using React Router.
React Router is a standard library for routing in React. It enables the navigation among views of various components in a React Application, allows changing the browser URL, and keeps the UI in sync with the URL.
In our application, If the user tries to access a protected page, we'll be redirected to the Login Page.



cd src & mkdir routes
cd routes


Enter fullscreen mode Exit fullscreen mode

In the routes, directory creates a file named ProtectedRoute.tsx , and write this :



// ./src/routes/ProtectedRoute.tsx

import React from "react";
import { Redirect, Route, RouteProps } from "react-router";
import { useSelector } from "react-redux";
import { RootState } from "../store";

const ProtectedRoute = (props: RouteProps) => {
  const auth = useSelector((state: RootState) => state.auth);

  if (auth.account) {
    if (props.path === "/login") {
      return <Redirect to={"/"} />;
    }
    return <Route {...props} />;
  } else if (!auth.account) {
    return <Redirect to={"/login"} />;
  } else {
    return <div>Not found</div>;
  }
};

export default ProtectedRoute;


Enter fullscreen mode Exit fullscreen mode

The first step here is to get the global state of auth. Actually, every time a user successfully signs in, we'll use the slices to persist the account state and the tokens in the storage.
If there is an account object, that means that there is an active session.
Then, we use this state to check if we have to redirect the user to the protected page return <Route {...props} />; or he is directly redirected to the login page return <Redirect to={"/login"} />;.
The last and final step is to rewrite the Login and Profile Page. Let's start with the Login Page.



// ./src/pages/Login.tsx
import authSlice from "../store/slices/auth";

    ...
    const handleLogin = (email: string, password: string) => {
        axios
          .post(`${process.env.REACT_APP_API_URL}/auth/login/`, { email, password })
          .then((res) => {
            dispatch(
              authSlice.actions.setAuthTokens({
                token: res.data.access,
                refreshToken: res.data.refresh,
              })
            );
            dispatch(authSlice.actions.setAccount(res.data.user));
            setLoading(false);
            history.push("/");
          })
          .catch((err) => {
            setMessage(err.response.data.detail.toString());
          });
      };
    ...


Enter fullscreen mode Exit fullscreen mode

And the profile Page,



// ./src/pages/Profile.tsx

import authSlice from "../store/slices/auth";

    ...
    const handleLogout = () => {
        dispatch(authSlice.actions.logout());
        history.push("/login");
      };
    ...


Enter fullscreen mode Exit fullscreen mode

And we're done with the front end. Start your server again and try to log in with the user-created with POSTMAN.

But there is something missing.
Our API is using refresh/access logic for authentication.
It means that when the access token expires (5 minutes), we need to get a new access token to make requests to protected resources.
It can be done in two ways:

  • Login again using the username/email and password (too much effort)
  • Or use the refresh token to request a new access token. For this, we'll write our own fetcher using axios and axios-auth-refresh. Here's how it'll work:
  • Make the request to the API using the access token
  • If the token is revoked, get a new access token using the refresh token
  • Use this new token to retry the failed request
  • If the refresh token is not working, log out and redirect the user to the login page

In your terminal, install a new package:



yarn add axios-auth-refresh


Enter fullscreen mode Exit fullscreen mode

Once it's done, create a new directory named utils, and inside this directory, create a file named axios.ts. It will contain the code of our fetcher.



import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import store from '../store';
import authSlice from '../store/slices/auth';

const axiosService = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
        'Content-Type': 'application/json',
    },
});

axiosService.interceptors.request.use(async (config) => {
    const { token } = store.getState().auth;

    if (token !== null) {
        config.headers.Authorization = 'Bearer ' + token;
        // @ts-ignore
        console.debug('[Request]', config.baseURL + config.url, JSON.stringify(token));
    }
    return config;
});

axiosService.interceptors.response.use(
    (res) => {
        // @ts-ignore
        console.debug('[Response]', res.config.baseURL + res.config.url, res.status, res.data);
        return Promise.resolve(res);
    },
    (err) => {
        console.debug(
            '[Response]',
            err.config.baseURL + err.config.url,
            err.response.status,
            err.response.data
        );
        return Promise.reject(err);
    }
);

// @ts-ignore
const refreshAuthLogic = async (failedRequest) => {
    const { refreshToken } = store.getState().auth;
    if (refreshToken !== null) {
        return axios
            .post(
                '/auth/refresh/',
                {
                    refresh: refreshToken,
                },
                {
                    baseURL: process.env.REACT_APP_API_URL
                }
            )
            .then((resp) => {
                const { access, refresh } = resp.data;
                failedRequest.response.config.headers.Authorization = 'Bearer ' + access;
                store.dispatch(
                    authSlice.actions.setAuthTokens({ token: access, refreshToken: refresh })
                );
            })
            .catch((err) => {
                if (err.response && err.response.status === 401) {
                    store.dispatch(authSlice.actions.setLogout());
                }
            });
    }
};

createAuthRefreshInterceptor(axiosService, refreshAuthLogic);

export function fetcher<T = any>(url: string) {
    return axiosService.get<T>(url).then((res) => res.data);
}

export default axiosService;


Enter fullscreen mode Exit fullscreen mode

Then let's use this on the profile page.



import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {useHistory, useLocation} from "react-router";
import authSlice from "../store/slices/auth";
import useSWR from 'swr';
import {fetcher} from "../utils/axios";
import {UserResponse} from "../utils/types";
import {RootState} from "../store";

interface LocationState {
    userId: string;
}


const Profile = () => {
  const account = useSelector((state: RootState) => state.auth.account);
  const dispatch = useDispatch();
  const history = useHistory();

  const userId = account?.id;

  const user = useSWR<UserResponse>(`/user/${userId}/`, fetcher)

  const handleLogout = () => {
    dispatch(authSlice.actions.setLogout());
    history.push("/login");
  };
  return (
    <div className="w-full h-screen">
      <div className="w-full p-6">
        <button
          onClick={handleLogout}
          className="rounded p-2 w-32 bg-red-700 text-white"
        >
          Deconnexion
        </button>
      </div>
        {
            user.data ?
                <div className="w-full h-full text-center items-center">
                    <p className="self-center my-auto">Welcome, {user.data?.username}</p>
                </div>
                :
                <p className="text-center items-center">Loading ...</p>
        }
    </div>
  );
};

export default Profile;


Enter fullscreen mode Exit fullscreen mode

The new Profile page will look like this.

Profile Page

That's some basic stuff if you need to build an authentication system with React and Django.
However, the application has some issues, and trying to perfect it here was only going to increase the length of the article.
So here are the issues and the solutions :

  • JWT : JSON Web Tokens come with some issues you should be aware of if you to make a great usage. Feel free to check this article, to learn how to use JWT effectively.
  • PostgreSQL : For this tutorial, I used sqlite3 to make things faster. If you are going to a production or a staging server, always use a database motor with good performances.
  • A refresh client : Actually, the user is logged, but when the time will come to make a request, you'll have only 5 minutes of access to the content.

Conclusion

In this article, We learned to build a CRUD application web with Django and React. And as every article can be made better so your suggestion or questions are welcome in the comment section. πŸ˜‰

Check the code of the Django app here and the React App here.

Ready to take your Django and React skills to the next level? Check out my book, Full Stack Django and React: Get hands-on experience in full-stack web development with Python, React, and AWS, for an in-depth guide to building web applications with these powerful tools. Whether you're a beginner or an experienced developer, you'll find practical tips and insights to help you succeed. Click here to order your copy now!

Top comments (30)

Collapse
 
okechedu profile image
Edward Okech

@koladev I have also realised that when running the command
$ yarn start after adding the craco.config.js file the following error is thrown

PS C:\Users\Administrator\Documents\EdwardFiles\Python\django_projects\Zetech-Uni-Portal\frontend> yarn start
yarn run v1.22.19
warning ..\..\..\..\..\..\package.json: No license field
$ craco start
C:\Users\Administrator\Documents\EdwardFiles\Python\django_projects\Zetech-Uni-Portal\frontend\node_modules\@craco\craco\lib\features\webpack\style\postcss.js:54
                craPlugins = match.loader.options.plugins();
                                                  ^

TypeError: match.loader.options.plugins is not a function

Enter fullscreen mode Exit fullscreen mode

After a bit of digging i found this article
github.com/facebook/create-react-a...

basicly replacing the following line in the croca.config.js file solves this issue.

module.exports = {
    style: {
       // Replace postcss with postcssOptions
        postcssOptions: {
        plugins: [require("tailwindcss"), require("autoprefixer")],
      },
    },
  };
Enter fullscreen mode Exit fullscreen mode

Thank you

Collapse
 
okechedu profile image
Edward Okech

@Mangabo Thanks for this amazing and insightful article I have learnt alot. However, i can't figure out why i can't login superusers in Django admin..getting an alert of
"
Please enter the correct email and password for a staff account. Note that both fields may be case-sensitive.
"
On terminal the following message is returned from the server

django.core.management.base.SystemCheckError: SystemCheckError: System check identified some issues:

ERRORS:
auth.User.groups: (fields.E304) Reverse accessor for 'auth.User.groups' clashes with reverse accessor for 'core_user.User.groups'.
        HINT: Add or change a related_name argument to the definition for 'auth.User.groups' or 'core_user.User.groups'.
auth.User.user_permissions: (fields.E304) Reverse accessor for 'auth.User.user_permissions' clashes with reverse accessor for 'core_user.User.user_permissions'.
        HINT: Add or change a related_name argument to the definition for 'auth.User.user_permissions' or 'core_user.User.user_permissions'.
core_user.User.groups: (fields.E304) Reverse accessor for 'core_user.User.groups' clashes with reverse accessor for 'auth.User.groups'.
        HINT: Add or change a related_name argument to the definition for 'core_user.User.groups' or 'auth.User.groups'.
core_user.User.user_permissions: (fields.E304) Reverse accessor for 'core_user.User.user_permissions' clashes with reverse accessor for 'auth.User.user_permissions'.
        HINT: Add or change a related_name argument to the definition for 'core_user.User.user_permissions' or 'auth.User.user_permissions'.
Enter fullscreen mode Exit fullscreen mode

Kindly any one with an idea of what might be the issue?

Collapse
 
koladev profile image
Mangabo Kolawole

Hi Edward. Thank you very much for your feedback. Let me try the code and come back to you.

Collapse
 
koladev profile image
Mangabo Kolawole • Edited

Interestingly, I cloned the source code from Github and setup the project. I can actually connect in the admin dashboard, but with my email, and not the username.

I think that I created this confusion tho as I only wanted the user to connect with the email.

In the User model, you can set these attributes so any user can connect with username and email.

class User(AbstractBaseUser, PermissionsMixin):
    ...

    USERNAME_FIELD = 'username'
    EMAIL_FIELD = 'email'

    objects = UserManager()

    def __str__(self):
        return f"{self.email}"
Enter fullscreen mode Exit fullscreen mode

Please, let me know if it works.

Thread Thread
 
okechedu profile image
Edward Okech

Hi Mangambo, i made the changes. However was still facing the same issue. I decided to change the Model as of below and it worked.

import uuid
from enum import unique
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.core.mail import send_mail
from django.core.validators import RegexValidator
from dateutil.relativedelta import relativedelta
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager


def two_days_from_now():
    # A helper function to deactivate email activation link after 2 days
    return timezone.now() + relativedelta(days=2)


class UserManager(BaseUserManager):
    """
    We use Django's inbuilt BaseUserManager Class
    """
    def _create_user(self, username, email, password, is_staff, is_superuser, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        now = timezone.now()
        if not email:
            raise ValueError('Users must have an email address')
        if not username:
            raise ValueError('Users must have a username')
        email = self.normalize_email(email)
        user = self.model(username=username,
                          email=email,
                          is_staff=is_staff, 
                          is_active=True,
                          is_superuser=is_superuser, 
                          last_login=now,
                          date_joined=now, 
                          **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email, password=None, **extra_fields):
        return self._create_user(username, email, password, False, False, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        return self._create_user(username, email, password, True, True, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    # The base Zetech user's model fields
    alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$', message='Only alphanumeric characters are allowed.')

    ### Redefine the basic fields that would normally be defined in User ###
    username = models.CharField(unique=True, max_length=20, validators=[alphanumeric])
    email = models.EmailField(verbose_name='email address', unique=True, max_length=255)
    first_name = models.CharField(max_length=30, null=True, blank=True)
    last_name = models.CharField(max_length=50, null=True, blank=True)
    date_joined = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True, 
                                    null=False, 
                                    help_text=_('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'))
    is_staff = models.BooleanField(default=False, 
                                   null=False, 
                                   help_text=_('Designates whether the user can log into this admin site.'))

    ### NOTE: is_superuser is not defined here. This is because PermissionsMixin already defines this, and if you override it,
    ### then all the default Django user permissions won't work correctly, so you have to make sure you don't override the
    ### is_superuser field.

    ### Our own fields - we shall use this for the user account profile ###
    phone_number = PhoneNumberField()
    profile_image = models.ImageField(upload_to="uploads", blank=False, null=False, default="/static/images/defaultuserimage.png")
    user_bio = models.CharField(max_length=600, blank=True)
    is_uni_staff = models.BooleanField(default=False,
                                     null=False,
                                     help_text=_('Designates whether user is a staff of the university'))
    is_student = models.BooleanField(default=False,
                                    null=False,
                                     help_text=_('Designates whether user is a student in the university'))

    objects = UserManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    class Meta:
        verbose_name = 'User'
        verbose_name_plural = 'Users'

    def get_full_name(self):
        fullname = "{first_name} {last_name}".format(first_name=self.first_name, last_name=self.last_name)
        return fullname

    def get_short_name(self):
        return self.username

    def __str__(self):
        return "{0} ({1})".format(self.username, self.email)

    def email_user(self, subject, message, from_email=None):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email])



class Registration(models.Model):
    uuid = models.UUIDField(primary_key=False, default=uuid.uuid4, editable=False)
    user = models.OneToOneField(User, related_name='registration', on_delete=models.CASCADE)
    expires = models.DateTimeField(default=two_days_from_now)
    type = models.CharField(max_length=10, choices=(
      ('register', 'register'),
      ('lostpass', 'lostpass'),
    ), default = 'register')
Enter fullscreen mode Exit fullscreen mode

Thanks for the article.

Collapse
 
ktoole profile image
Kristian Toole

Thanks for this awesome tutorial!! Is there a way to make the auth/register only available to super users? That way if the app is public facing, not just anyone can create themselves as a user. Any tips or advice for this?

Collapse
 
ktoole profile image
Kristian Toole

I think I just now figured it out. I updated the permission_classes for the registrationviewset. :)

Collapse
 
anjayluh profile image
Angella Naigaga • Edited

Thank you for coming up with this tutorials.
I am getting myself familiar with django so I landed on this tutorial to help me.
While following it, I encountered a couple of errors that i will mention here

  1. class RefreshViewSet(viewsets.ViewSet, TokenRefreshView):

There are two undefined vars here

  1. Add rest_framework_simplejwt.authentication.JWTAuthentication to the list of authentication classes in settings.py:

For my version of the app (I guess the recent django versions), the REST_FRAMEWORK object doesn't come included in settings.py. I had to add it myself

  1. If everything is working fine, let's create a user with an HTTP Client by requesting localhost:8000/api/auth/register/.

Here I ran into this error.
"""
ImproperlyConfigured at /api/auth/register/
Field name public_id is not valid for model User.
Request Method: POST
Request URL: localhost:8000/api/auth/register/
Django Version: 3.2.5
Exception Type: ImproperlyConfigured
Exception Value:

Field name public_id is not valid for model User.
Exception Location: /Users/anaconda3/lib/python3.8/site-packages/rest_framework/serializers.py, line 1317, in build_unknown_field
Python Executable: /Users/angellanaigaga/opt/anaconda3/bin/python
Python Version: 3.8.8
"""
My guess is it's coming from the serializers. At the point of writing this I am not sure how best to fix this bug, whether to rename public_id to id or remove it completely.

Otherwise, I appreciate the work well done

Collapse
 
anjayluh profile image
Angella Naigaga

Update
Renaming public_id to id fixes the last bug mentioned here

Collapse
 
koladev profile image
Mangabo Kolawole

Oh thanks Angella. I'll modify it

Collapse
 
koladev profile image
Mangabo Kolawole

typo fixed. Thanks again.

Collapse
 
michaeldbrant profile image
Michael Brant

Thanks for the guide! Is anyone else getting a β€œCSRF cookie not set” error? I tried putting @csrf_exempt as a decorator above the create method in the RegistrationViewSet but that didn’t fix it.

Collapse
 
zvolsky profile image
ZvolskΓ½

With Django/Vue/Axios I have such something in Axios settings:
axios.defaults.withCredentials = true
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
axios.defaults.xsrfCookieName = "csrftoken"

Collapse
 
koladev profile image
Mangabo Kolawole

Hey Michael!

What's your CORS configuration? And are you using React?

Collapse
 
michaeldbrant profile image
Michael Brant

Hey! Yup, I’m using React, testing in the browser and in Postman. I’m running on port 8000 so I have :
CORS_ALLOWED_ORIGINS = [β€œ127.0.0.1:8000”,
β€œlocalhost:8000”]

CORS_ALLOW_HEADERS = [β€˜Accept’, β€˜Accept-Language’, β€˜Authorization’, β€˜Content-Type’]

CORS_ALLOW_METHODS = [β€˜GET’, β€˜POST’, β€˜PUT’]

Collapse
 
wisdomtohe profile image
Wisdom TOHE

Awesome! Thank you!

Collapse
 
koladev profile image
Mangabo Kolawole

You are welcome mate

Collapse
 
karan_s_mittal profile image
Karan Mittal

It was an amazing and very well written article. Looking forward for more content from you.

Collapse
 
koladev profile image
Mangabo Kolawole

Thanks Karan.

For sure, more content are coming.

Collapse
 
muzammilaalpha profile image
muzammilaalpha

Good one!

Collapse
 
koladev profile image
Mangabo Kolawole

Thanks

Collapse
 
sm0ke profile image
Sm0ke

Nice .. πŸš€πŸš€

Collapse
 
koladev profile image
Mangabo Kolawole

Thanks Sm0ke πŸ€“

Collapse
 
lcfd profile image
Luca Fedrizzi

Awesome!

Collapse
 
koladev profile image
Mangabo Kolawole

Thank you Luca!