Mastering E-Commerce Backend Development with Django: User Registration

Mastering E-Commerce Backend Development with Django: User Registration

·

19 min read

Hello developer! In the last two posts in this series, we looked into the series introduction and setting up our project. In this post, we now start with the e-commerce authentication system. But first, what's authentication?

Authentication systems serve as the gatekeepers of digital realms, verifying that users are indeed who they claim to be. Think of logging into platforms like Facebook or X (Formally Twitter). You input your email and password, seemingly simple steps, but what unfolds behind the scenes is a complex security dance. These platforms validate your credentials by cross-referencing them with their databases.

When authentication succeeds, you gain access to a world of content—be it your Facebook feed or an X timeline. But if the check fails, you're greeted with a familiar yet frustrating alert: "Incorrect email or password." It's a message most of us have encountered at some point, a stark reminder of the intricacies involved in keeping our digital lives secure.

In this post, we will build a system to register users, log users in, and log users out. Yes with our promise, we will be using Test Driven Development approach in our development.

Let's proceed by first planning out the data we would like to collect from our users when they sign up and log in.

Registration

We expect a user on the app, a user who comes to purchase products on the platform. The necessary fields I can think of are

email, password, username, first_name, last_name, phone_number, address, verified, created_from, updated_at, created_at, terms_nd_condition, image

With the vision in mind, it's time to mold this into reality. Recall the Django project we spun up in our last post? With your magical environment still active, let's write some tests to mimic a user registering on the platform, the tests will fail and we will write code to pass the tests. First, let's create the account app.

# Ensure you are in the project working directory that has manage.py file
python manage.py startapp account

With the account app created, our next expedition leads us to the tests.py file nested within. Here we shall put our test cases. Put the code below in the file

import pytest
from django.urls import reverse
from rest_framework.test import APIClient


@pytest.fixture
def api_client(db):
    return APIClient()


@pytest.fixture
def sample_user_data(db):
    return {'email': 'tut@mail.com', 'password': "tutad1234"}


def test_registration_pass(api_client, sample_user_data):
    sample_user_data.update({'confirm_password': sample_user_data['password']})
    registration_url = reverse('register')
    response = api_client.post(registration_url, data=sample_user_data)
    assert response.status_code == 201
    assert not Account.objects.get(email=sample_user_data['email']).verified

Okay, let's go through the code above

Starting with a couple of imports pytest - our chosen package for testing, reverse - for dynamically generating full path URL from URL namespace, APIClient - for making those API calls to test the endpoints.

Why Pytest?

pytest is a powerful and flexible testing framework for Python. It simplifies the process of writing small tests, yet scales to support complex functional testing. Here are some reasons why it's favored in this context:

  • Simplicity: Pytest's use of simple assert statements make tests readable and easy to write.

  • Fixtures: The fixture system in pytest is a highlight, allowing you to define test data and states that can be reused across multiple tests.

  • Parameterization: Pytest supports parameterizing tests, which means running the same test function with different data inputs.

  • Plugins: There's a rich ecosystem of plugins for integrating with other tools and expanding testing capabilities.

Two fixtures are defined

  1. api_client: This fixture is a function that returns an instance of APIClient. The db argument passed to the function signals to pytest to ensure a database is available for the tests. This api_client will be used to make HTTP requests in the tests.

  2. sample_user_data: This fixture provides a dictionary representing a sample user's data. This data is used to simulate a user's registration data that might be sent to your API.

The actual test function test_registration_pass does the following:

  1. The function is marked with api_client and sample_user_data fixtures as arguments, which means pytest will inject the values returned by these fixtures into the test.

  2. It uses the reverse function to get the URL for the registration endpoint, which is presumably named 'register' in your Django URL configuration.

  3. It then uses the api_client to send a POST request to the registration URL with the sample_user_data as the payload.

  4. Finally, the test asserts that the status code of the response is 201, which means "Created." This assertion checks if a new user is successfully registered, as HTTP 201 is the expected status code for a successful resource creation on the server.

Before we run our test, let's point pytest to our Django settings file by creating a pytest.ini file and adding the lines below

[pytest]
DJANGO_SETTINGS_MODULE = ecoms.settings
python_files = tests.py test_*.py *_tests.py

and it's time to install pytest-django and add to the list of requirements. pytest-django is a plugin for pytest that provides a set of useful tools for testing Django applications. It makes it easier to write tests by providing Django-specific features and fixtures. some of the key reasons why we use pytest-django

  • Django Integration: pytest-django configures pytest to use Django's test settings. This means that when you run your tests, pytest knows how to set up your Django environment without any extra work on your part.

  • Database Fixtures: The plugin provides built-in fixtures for database access which handle setup and teardown of the test database, allowing you to test code that requires database access without any extra configuration.

  • Admin Client: pytest-django includes a fixture for Django’s AdminClient, making it straightforward to write tests that require an authenticated admin user.

  • Mail Outbox: It provides access to Django’s mail-outbox for tests that involve sending emails. You can check that emails are sent correctly and inspect their content within your tests.

  • Compatibility: pytest-django makes it possible to leverage all the features of pytest, including parametrization, plugins, and its powerful assertion rewriting system, in the context of a Django project.

pip install pytest-django && pip freeze > requirements.txt

Now to run the test, with the virtual environment activated run the command below

$ pytest account

you should be presented with an error

FAILED account/tests.py::test_registration_pass -django.urls.exceptions.NoReverseMatch: Reverse for 'register' not found. 'r...

To make this test a success, we will need to create our account model, create the registration URL, and create the business logic to store the user's data in the database. Our next expedition leads us to the models.py script nestled within. Here, we shall sculpt the very essence of our Account model.

Update the file with the code below

from django.db import models
from django.contrib.auth.models import AbstractUser

class Account(AbstractUser):
    username = models.CharField(max_length=200, blank=True)
    email = models.EmailField(max_length=300, unique=True)
    verified = models.BooleanField(default=False)
    phone_number = models.CharField(max_length=20, blank=True)
    address = models.TextField(blank=True)
    image = models.ImageField(upload_to='account/profile', blank=True)
    created_from = models.CharField(max_length=15)
    term_and_condition = models.BooleanField(default=False)
    updated_at = models.DateField(auto_now=True)

    USERNAME_FIELD = 'email' # this will be used to log in users instead of username
    REQUIRED_FIELDS = ['username'] # this is for admin when creating superuser

But some fields are missing you asked? No, they are not, We have inherited some fields and methods from Django's built-in AbstractUser, these fields include first_name, last_name, password etc.

It's time to create the registration URL, first, create urls.py file in the account app directory. This file will store all the URLs for our account app and add the lines below

from django.urls import path
from . import views

urlpatterns = [
    path("register/", views.RegistrationView, name="register"),
]

and ensure you let Django know about this new urls.py file by going into the urls.py file in the project folder i.e the folder that has settings.py file and update with the lines

from django.urls import path, include

urlpatterns = [
    # ... other urls 
    path('account/', include('account.urls'))
]

Now to the registration business logic, proceed to views.py in the account folder and add the code below

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from .serializer import RegistrationSerializer

@api_view(["POST"])
@permission_classes([AllowAny])
def RegistrationView(request) -> Response:
    with transaction.atomic():
        try:
            serializer = RegistrationSerializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        except Exception as e:
            transaction.set_rollback(True)
            return Response(
                serializer.errors,
                status.HTTP_400_BAD_REQUEST,
            )

since we are importing from serializer.py, let's create it in the account folder and add the code below

# serializer.py file
from rest_framework import serializers
from .models import Account
from .utils import validate_passwords


class RegistrationSerializer(serializers.ModelSerializer):
    confirm_password = serializers.CharField(write_only=True)
    password = serializers.CharField(write_only=True)

    class Meta:
        model = Account
        fields = ["email","username","password","confirm_password",
            "term_and_condition","address"]

    def validate(self, data):
        password = data.get("password")
        confirm_password = data.get("confirm_password")
        validate_passwords(password, confirm_password)
        del data["confirm_password"]
        return data

and yes you guessed right, we will need to create a utils.py file in the account folder where we have a function to validate the user's password

# utils.py
from rest_framework import serializers

def validate_passwords(password, confirm_password) -> None:
    if len(password) < 8:
        raise serializers.ValidationError(
            {
                "password": "This password is too short. It must contain at least 8 characters."
            }
        )
    if password.isalpha() or password.isnumeric():
        raise serializers.ValidationError({"password": "password must be alphanumeric"})
    if confirm_password != password:
        raise serializers.ValidationError(
            {"confirm_password": "passwords do not match"}
        )

One last thing here, let's let Django know about our app and new user model to use instead of the built-in user model

Update the installed apps list in the settings.py file and add AUTH_USER_MODEL to the end of the file

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # local apps
    "account",
] 

# tells Django to use account model instead of User built-in model
AUTH_USER_MODEL = 'account.account'

Let's take our environment to the docker environment, but first, we will need to rebuild our containers

docker-compose up --build -d

and points our Django project to use the Postgres db container

import os
from dotenv import load_dotenv
load_dotenv()
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("DB_NAME"),  # database name
        "USER": os.getenv("DB_USER"),  # database user_name
        "PASSWORD": os.getenv("DB_PASSWORD"),  # the password you set for the PostgreSQL user
        "HOST": "db",  # set to the address of your PostgreSQL if not on the same machine
        "PORT": "5432",  #
    }
}

Don't forget to sync the model with the database

docker-compose exec -it web bash -c "python manage.py makemigrations && python manage.py migrate"

Yeah let's now our test to ensure it passes now

docker-compose exec -it web bash -c pytest account

Well done, we can now scale out our registration test to test for different cases. update account/tests.py

@pytest.mark.parametrize(
    "email, password, confirm_password, expected_status, expected_error_key, expected_message",
    [
        (
            "not-an-email","Password123","Password123",
            status.HTTP_400_BAD_REQUEST,"email","Enter a valid email address.",
        ), 
        (
            "existing@email.com","Password123","Password123",
            status.HTTP_400_BAD_REQUEST,"email","user with this email already exists.",
        ),
        (
            "valid@email.com","short","short",
            status.HTTP_400_BAD_REQUEST,"password","This password is too short. It must contain at least 8 characters.",
        ),
        (
            "valid@email.com","NoNumbers","NoNumbers",
            status.HTTP_400_BAD_REQUEST,"password","password must be alphanumeric",
        ),
        (
            "valid@email.com","NoNumbers5","NoNumbers3",
            status.HTTP_400_BAD_REQUEST,"confirm_password","passwords do not match",
        ),
    ],
)
def test_resister_user_fail(api_client,create_sample_user, sample_user_data,email,password,confirm_password,expected_status,expected_error_key,expected_message):
    register_url = reverse("register")  # get the registration URL
    if email == "existing@email.com":
        # create an existing user
        create_sample_user(
            email="existing@email.com",
            password="Password123",
        )
    sample_user_data.update(
        {"email": email, "password": password, "confirm_password": confirm_password}
    )
    response = api_client.post(register_url, data=sample_user_data)
    assert response.status_code == expected_status  # confirm status code
    assert (
        expected_message in response.json()[expected_error_key]
    )  # confirm error message

Run the tests again but with the -v flag i.e. in verbose mode

docker-compose exec -it web bash -c pytest account

Please take a coffee break, tests are green !!!

Engaging with your users from the moment they sign up is crucial for maintaining a solid relationship. One way to do this is by sending a warm welcome email, which also serves a practical purpose: email verification.

Below are the tests we wish to write code for with regards to sending email to new users

import jwt
from django.conf import settings
from django.utils import timezone

def test_confirm_registration_pass(api_client, create_sample_user, sample_user_data):
    confirm_registration_url = reverse("confirm_registration")
    sample_user_data.update(
        {"id": 9, "email": "tuttest1@ecoms.com", "password": "ecoms_1029"}
    )
    new_user = create_sample_user(**sample_user_data)
    token = new_user.get_confirmation_token
    response = api_client.get(f"{confirm_registration_url}?token={token}")

    assert response.status_code == 302
    assert Account.objects.get(email="tuttest1@ecoms.com").verified


def test_confirm_registration_fail(api_client, create_sample_user, sample_user_data):
    confirm_registration_url = reverse("confirm_registration")
    sample_user_data.update(
        {"id": 9, "email": "tuttest1@ecoms.com", "password": "ecoms_1029"}
    )
    new_user = create_sample_user(**sample_user_data)
    response = api_client.get(f"{confirm_registration_url}")

    assert response.status_code == 302
    assert not Account.objects.get(email="tuttest1@ecoms.com").verified

    # test for expired token
    token = new_user.get_confirmation_token
    payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
    payload["exp"] = int(timezone.now().strftime("%s"))
    token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
    response = api_client.get(f"{confirm_registration_url}?token={token}")
    assert response.status_code == 302
    assert not Account.objects.get(email="tuttest1@ecoms.com").verified

Push Notification

The moment a new user is added to our database, we want to send them a welcome email that also contains a verification link. This link is essential for confirming the legitimacy of the email address they've provided and for securing their account.

However, it's important to keep user experience in mind. We don't want our users to wait unnecessarily while the email is being dispatched. User satisfaction is key, and a responsive system plays a big role in that.

Leveraging Celery for Background Tasks

To avoid any delay in response, we employ Celery. Celery is an asynchronous task queue/job queue based on distributed message passing. It's focused on real-time operation but supports scheduling as well.

When a new user signs up, we'll quickly respond to the user's action, while Celery takes care of sending the email in the background. This means our application can handle the user's request and send the email without one process blocking the other. It's an efficient way to manage tasks that are secondary to the user's immediate experience on the platform.

Integrating Celery into our project involves setting up a few components, such as:

  • Celery Worker: This is the component that will process the background tasks asynchronously.

  • Message Broker: Celery requires a solution to send and receive messages, and for that, we typically use Redis or RabbitMQ.

  • Task Definition: We define a Celery task that outlines the steps to send the email, including preparing the email content and handling the sending process.

Once we've set up Celery and integrated it into our Django project, sending the email becomes a task that Celery will handle elegantly in the background. The user gets a swift response upon registration, and the welcome email arrives in their inbox without them ever knowing about the intricate dance of web services that made it happen.

Spin up Celery and Redis containers

In addition to our Django and PostgreSQL containers, we will now spin up two new containers, one for Celery and the other for Redis. Update the docker-compose.yml file with the Celery and Redis service definition

 redis:
    image: "redis:alpine"

  celery:
    build: .
    command: ["celery", "-A", "ecoms", "worker", "--loglevel=info"]
    volumes:
      - .:/app
    depends_on:
      - db
      - redis

We can now allow Celery to auto-pick up tasks, Begin by creating a new file named celery.py within your Django project's main directory - this is typically the one that contains settings.py. Add the code below to the file

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ecoms.settings')

app = Celery('ecoms')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

As outlined in the code above,

  • Imports: We're importing the necessary modules and Celery itself. The absolute_import from __future__ ensures compatibility between Python 2 and 3.

  • Environment Variable: We set the environment variable for the Django settings module, which tells Celery where to find the settings of the Django project.

  • Celery Instance: We create an instance of the Celery class, which will act as our Celery "application".

  • Configuration: Celery's configuration is loaded from the Django settings. We specify a namespace ('CELERY') so Celery will only read settings that start with this prefix.

  • Task Discovery: Finally, autodiscover_tasks() tells Celery to find tasks in all Django apps automatically. This way, if we define tasks in an app called account, Celery will find and be able to execute these tasks without us manually registering them.

Ensuring Celery Launches with Django

To make sure our Celery worker is ready to start processing tasks as soon as Django is up and running, we need to tweak the __init__.py file in the same directory where our settings.py resides.

Add the following to the __init__.py file:

from __future__ import absolute_import, unicode_literals

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ('celery_app',)
  • The from __future__ import ensures compatibility with Python 2 and makes Python 3's division and print function behave consistently.

  • We then import the Celery app instance from the celery.py file we created earlier. This import isn't merely to reference celery_app but to ensure that it's loaded into memory.

  • By setting __all__, we declare that celery_app is the only symbol that should be imported when from ecoms import * is encountered. It's a list of public objects of that module, as interpreted by import *.

What this means is that the Celery app is considered a part of the project's top-level imports, which Django will load during startup. As a result, all background tasks and worker processes become an integral part of your application lifecycle.

With Celery set up now, we can now create tasks for Celery to run i.e. email sending task. Create tasks.py file in the account app folder and add the code below

from django.core.mail import send_mail
from .models import FailedEmailTasks
from celery import shared_task, Task

# Define a custom Task class to handle failure scenarios
class CallbackTask(Task):
    def on_failure(self, exc, task_id, args, kwargs, einfo):
        """
        This method is called if the task throws an exception
        Here, we log the exception details to our FailedEmailTasks model, which can be used for debugging or retrying the task later.
        """
        FailedEmailTasks.objects.create(
            task_id=task_id, 
            exc=str(exc), 
            args=args, 
            kwargs=kwargs, 
            einfo=str(einfo)
        )

# Decorate the function with shared_task to turn it into a Celery task with additional settings.
@shared_task(bind=True, base=CallbackTask, max_retries=2)
def send_email_task(self, subject, message, from_email, recipient_list, html_message):
    """
    This is the Celery task for sending emails
    If the task fails, Celery will attempt to retry it based on the defined max_retries parameter.
    """
    try:
        send_mail(
            subject, 
            message, 
            from_email, 
            recipient_list, 
            html_message=html_message
        )
    except Exception as exc:
        # If an exception occurs, retry the task after a delay.
        self.retry(exc=exc, countdown=60)  # countdown is the delay before the retry, in seconds.

CallbackTask subclass that Celery will use to handle task failures elegantly. Should an email-sending operation fail, we record the failure details in a FailedEmailTasks model, enabling us to review or re-attempt the sending process at a later stage.

We then define send_email_task as a shared task, which allows us to call it from anywhere in our Django project. We specify the CallbackTask as its base to ensure that failure handling is as we've designed. With max_retries set to 2, Celery will try to resend the email up to two more times if the first attempt fails, with a 60-second pause between tries.

In the code above, we have introduced the FailedEmailTasks model, let's add the model to the models.py file

class FailedEmailTasks(models.Model):
    task_id = models.CharField(max_length=255)
    exc = models.TextField()
    args = models.JSONField()
    kwargs = models.JSONField()
    einfo = models.TextField()

next is to add to the registration logic to signal Celery to send an email after we successfully save the user's details to the DB. Update the RegistrationView as below

@api_view(["POST"])
@permission_classes([AllowAny])
def RegistrationView(request) -> Response:
    try:
        # add immediately before return line
        # send welcome mail to user
        subject = "Ecoms Welcome Mail"
        message = ""  # this is needed to be empty although html message is to be sent
        from_email = "noreply@example.com"
        recipient_list = [instance.email]
        generated_token = instance.get_confirmation_token
        # Render the HTML template
        html_message = render_to_string(
             "welcome_mail.html",
             {"request": request, "username": instance.username, "confirm_token": generated_token},
         )
       send_email_task.delay(subject, message, from_email, recipient_list, html_message=html_message)
    except:
        ....

Now as you might have guessed, we need to set up the HTML template and update our account model with get_confirmation_token method to generate a token which will be JWT wise.

In the project root directory, create a folder named "templates" and then a file in the templates folder named "welcome_mail.html". Add the html content below to it

<!-- templates/welcome_mail.html -->

<html>
  <head>
    <title>Account Confirmation</title>
  </head>
  <body>
    <h1>Hello, {{ username }}!</h1>
    <p>
      With great happiness I welcome you to our great platform, online shopping got
      much better with Ecoms platform.
    </p>
    <p>
      To conclude your registration. Please follow this link to confirm your mail:
    </p>
    <a
      href="{{ request.scheme }}://{{ request.get_host }}{% url 'confirm_registration' %}?token={{ confirm_token }}"
      >Confirm Account</a
    >
  </body>
</html>

we should not forget to update setting.py file to locate our templates files by updating the TEMPLATES list as shown below

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [ BASE_DIR / 'templates'], # updated line
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Next is to create the get_confirmation_token method to generate the verification token. update the account model in the models.py file in the account app folder as shown below

import jwt
from django.conf import settings
from django.utils import timezone
import secrets
import string
from datetime import timedelta

class Account(AbstractUser):
    # fields definitions...
    @property
    def get_confirmation_token(self):
        return self.generate_password_reset_account_validation_token()

    def generate_password_reset_account_validation_token(self):
        values = string.ascii_letters + string.digits
        token = "".join(secrets.choice(values) for _ in range(250))
        dt = timezone.now() + timedelta(seconds=settings.CONFIRMATION_LINK_TIMEOUT)
        encode_token = jwt.encode(
            {"token": token, "exp": int(dt.strftime("%s")), "id": self.id},
            settings.SECRET_KEY,
            algorithm="HS256",
        )

        return encode_token

don't forget to install jwt, with virtual environment activated

pip install PyJWT && pip freeze > requirements.txt

And if you have noticed, in the content sent to users as a confirmation email, there exists a link for confirmation. Let's take care of the link by updating the account app utls.py file and views.py file respectively as shown below

urlpatterns = [
    path("register/", views.RegistrationView, name="register"),
    #add new path
    path("confirm-registration/", views.ConfirmRegistration, name="confirm_registration"),
]
@api_view(["GET"])
@permission_classes([AllowAny])
def ConfirmRegistration(request):
    token = request.GET.get("token")
    # redirect user to page to tell them to request for new validation email
    if not token:
        return redirect(settings.CONFIRMATION_MAIL_REQUEST_PAGE)
    # get user id
    user_id, error = decode_jwt(token)
    if user_id and error is None:
        user = Account.objects.filter(id=user_id)
        if user.exists():
            user.update(verified=True)
            return redirect(settings.FRONTEND_HOMEPAGE)
    return redirect(settings.CONFIRMATION_MAIL_REQUEST_PAGE)

next is to update the utils.py file with the decode_jwt method and update settings.py file as shown below

from django.conf import settings
import jwt

def decode_jwt(token) -> tuple:
    try:
        decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
        user_id = decoded.get("id", None)

        if user_id is None:
            # return None, "Invalid token."
            raise jwt.InvalidTokenError("Invalid token")

        return user_id, None

    except jwt.ExpiredSignatureError:
        return None, "Invalid token."

    except jwt.InvalidTokenError as e:
        return None, "Invalid token."
# settings.py
CONFIRMATION_LINK_TIMEOUT = 3600


EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


CONFIRMATION_MAIL_REQUEST_PAGE = "https://google.com"
FRONTEND_HOMEPAGE = "https://google.com"

# Use Redis as a message broker.
CELERY_BROKER_URL = 'redis://redis:6379/0'

finally, let's rebuild the containers and spin up new ones, then run our tests

docker-compose up --build -d
docker-compose exec -it web bash -c "python manage.py makemigrations && python manage.py migrate"

docker-compose exec -it web bash -c "pytest account -v"

you should see mail sent to the console in the celery container

Now that we've set the stage with our backend configurations and user registration logic, it's time to interact with our API more tangibly. One of the most powerful tools at our disposal for testing and interacting with APIs is Insomnia. REST client. Insomnia is a desktop application that enables us to craft, send, and analyze HTTP requests and responses without the need to write additional code for testing

And that, concludes our deep dive into setting up user registration in our Django-powered e-commerce application. It's been quite the adventure—from integrating with Celery for asynchronous task processing to ensuring our backend robustly handles new user sign-ups. I trust the journey has been as enlightening for you as it was for me!

As we proceed, our next steps will bring us to the heart of user authentication. We'll explore the intricate processes of securely logging users in and out. Beyond that, we’ll develop a user-friendly password reset feature and delve into the mechanisms for changing passwords, ensuring our users can maintain the security of their accounts with ease.

Stay tuned for our upcoming post, where we'll unravel the complexities of authentication. We'll tackle JWTs, secure session management, and the integration of Django’s built-in authentication views with our RESTful approach.

Please clone the registration branch User Registration Repo to get the code for this post!

Until then, happy coding, and don't forget to push your commits!