Sławomir Kwiatkowski

by: Sławomir Kwiatkowski

2024/11/03

Django Rest Framework - New User Registration

Content description:
In this post I'll describe how to create a new user.
I'll test creating new user API, sending email to verify the user and resending email if e.g. former token has exipred.

First, I'll make a method where I'll create an inactive user (who cannot log in yet).

     
from ..utils import send_activation_email

class NewUserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = ContractUserSerializer

    def create(self, request, *args, **kwargs):
        # create user & send email to verify account
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        user = ContractUser.objects.get(username=serializer.data["username"])
        send_activation_email(user, request.build_absolute_uri)
        return Response(serializer.data, status=status.HTTP_201_CREATED)


In the above method, I call my own send_activation_email() function that creates a token for the user, then prepares the email and sends it.

     
def send_activation_email(user, base_url):
    token = RefreshToken.for_user(user).access_token
    url = base_url() + "?token=" + str(token)
    message = f"Welcome {user.username}!\n 
    You can activate your account by clicking the link below:\n
    {url}"
    send_mail(
        subject="Account activation",
        message=message,
        from_email=os.getenv("EMAIL_HOST_USER"),
        recipient_list=[user.email],
        fail_silently=False,
    )

When the user confirms via the received email, the list method is called. The token is decoded and the is_active parameter in the user's account is changed, so that the user can now log in.

     
class NewUserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = ContractUserSerializer

    def list(self, request):
        # user verification
        token = request.GET.get("token")
        if token is not None:
            try:
                data = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
                user = ContractUser.objects.get(id=data["user_id"])
                user.is_active = True
                user.save()
                return Response(
                    {"detail": "Account activated"}, status=status.HTTP_200_OK
                )
            except jwt.ExpiredSignatureError:
                return Response(
                    {"error": "Token has expired"}, status=status.HTTP_400_BAD_REQUEST
                )
            except jwt.PyJWTError:
                return Response(
                    {"error": "Unknown error"}, status=status.HTTP_400_BAD_REQUEST
                )
        else:
            return Response({"error": "No token"}, status=status.HTTP_400_BAD_REQUEST)

If the token expires before the account is activated, the user can request a new email to be sent.

     
class NewUserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = ContractUserSerializer

    @action(detail=False, methods=["post"], url_path="resend-email")
    def resend_email(self, request):
        # api/user/resend-email/ to resend activation email
        username = request.data["username"]
        if username is not None:
            user = ContractUser.objects.filter(username=username).first()
            if user is not None:
                send_activation_email(user, request.build_absolute_uri)
                return Response({"detail": "Email sent"}, status=status.HTTP_200_OK)
            return Response(
                {"error": "No user with the username"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return Response({"error": "Bad request"}, status=status.HTTP_400_BAD_REQUEST)

I'll add a new entry to the router to do all of the above methods:

     
from rest_framework import routers
from .views import NewUserViewSet

router = routers.DefaultRouter()
router.register("user", NewUserViewSet, basename="user")
urlpatterns = router.urls

I then test the correctness of the above methods.

     
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.core import mail
from users.models import ContractUser


class NewUserAPITestCase(APITestCase):

    def test_user(self):
        # Test create inactive user and send verification email
        endpoint = reverse("user-list")
        data = {
            "username": "testUser",
            "email": "testUser@company.com",
            "password": "123456",
        }
        response = self.client.post(endpoint, data, format="json")

        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(len(mail.outbox), 1)  # test if email is sent

        # Test account verification
        token = mail.outbox[0].body.split("=")[1]
        response = self.client.get(endpoint, data={"token": token}, format="json")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # Test verification email resending
        endpoint = reverse("user-resend-email")
        data = {"username": "testUser"}
        response = self.client.post(endpoint, data, format="json")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(mail.outbox), 2)  # test if email is sent