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