How to Add Authentication and Swagger Documentation to Your Django Expense Tracker App | Step-by-Step Guide | Part 3

denisa_dev
Django Unleashed
Published in
5 min readJul 16, 2024
SignUp

If you enjoyed this, buy me a coffee! ☕💖

Welcome back to our Expense Tracker series! In this guide, we’ll show you how to add authentication to your Django application, including signup, login, and logout functionalities. Additionally, we’ll integrate Swagger documentation for better API management. Authentication is essential for securing your app and protecting users’ data. We’ll cover creating custom user models, serializers, views, and endpoints.

Step 1: Setup Configuration

Install Required Packages

Ensure you have the following packages installed in your Django project:

pip install djangorestframework
pip install djangorestframework-simplejwt
pip install drf-yasg
pip install django-cors-headers
pip install django-filter

Settings Configuration

Update your settings.py to include the necessary configurations for installed apps, middleware, and REST framework settings.

from datetime import timedelta
from decouple import config # Ensure you have python-decouple installed for this to work

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']

LOG_LEVEL = 'INFO'

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework_simplejwt",
"corsheaders",
"django_filters",
"drf_yasg",
"model",
"authentication",
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOW_ALL_ORIGINS = True

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_PAGINATION_CLASS': 'core.paginators.ExpenseTrackerPaginator',
'PAGE_SIZE': 10,
}

SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': config('SECRET_KEY'),
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ['rest_framework_simplejwt.tokens.AccessToken'],
'TOKEN_TYPE_CLAIM': 'token_type',
}

ROOT_URLCONF = "expense_tracker_project.urls"

AUTH_USER_MODEL = 'model.User'

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / 'templates'],
"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",
],
},
},
]

WSGI_APPLICATION = "expense_tracker_project.wsgi.application"

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'expense_tracker',
'USER': 'db_user',
'PASSWORD': 'db_password',
'HOST': 'localhost',
'PORT': '5432',
}
}

AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.mailjet.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')

MAILJET_APIKEY_PUBLIC = config('MAILJET_APIKEY_PUBLIC')
MAILJET_APIKEY_PRIVATE = config('MAILJET_APIKEY_PRIVATE')

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True

STATIC_URL = "static/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

Step 2: Define Custom User Model

First, let’s define our user model. Django provides a built-in user model, but for more control, we’ll create a custom user model that uses email instead of a username for authentication.

Create User Model

model/models.py

from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone

class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The Email field must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user

def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)

class User(AbstractUser):
email = models.EmailField(unique=True)
username = models.CharField(max_length=30, blank=True, null=True)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
date_joined = models.DateTimeField(default=timezone.now)
is_active = models.BooleanField(default=True)
email_confirmed = models.BooleanField(default=False, verbose_name='Email Confirmed')

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']

objects = CustomUserManager()

def __str__(self):
return self.email

Step 3: Create Serializers

We need serializers to handle the data transformation and validation for our sign-up and sign-in views.

Create Serializers

authentication/serializers.py

from django.contrib.auth import get_user_model, authenticate
from rest_framework import serializers

class SignupSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, style={'input_type': 'password'})

class Meta:
model = get_user_model()
fields = ('email', 'first_name', 'last_name', 'password')

def create(self, validated_data):
user = get_user_model().objects.create_user(**validated_data)
user.is_active = False # Make user inactive until email is verified
user.save()
return user

class LoginSerializer(serializers.Serializer):
email = serializers.CharField()
password = serializers.CharField(style={'input_type': 'password'})

def validate(self, data):
email = data.get('email')
password = data.get('password')

if email and password:
user = authenticate(email=email, password=password)
if user:
if user.is_active:
data['user'] = user
else:
raise serializers.ValidationError("User account is disabled.")
else:
raise serializers.ValidationError("Unable to log in with provided credentials.")
else:
raise serializers.ValidationError("Must include 'email' and 'password'.")

return data

Step 4: Create Views

Next, we’ll create views to handle sign-up, sign-in, and logout actions.

Create Views

authentication/views.py

from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import logout
from .serializers import SignupSerializer, LoginSerializer
from .models import User

class SignupView(APIView):
def post(self, request, *args, **kwargs):
serializer = SignupSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
# Additional code to send verification email can be added here
return Response({'success': 'User signed up successfully. Verification email sent.'}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class LoginView(APIView):
def post(self, request, *args, **kwargs):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = serializer.validated_data['user']
refresh = RefreshToken.for_user(user)
access = refresh.access_token
response_data = {
'user_id': user.id,
'email': user.email,
'token': str(access),
'refresh': str(refresh),
}
return Response(response_data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class LogoutView(APIView):
permission_classes = (IsAuthenticated,)

def post(self, request, *args, **kwargs):
logout(request)
return Response({'success': 'User logged out successfully.'}, status=status.HTTP_200_OK)

Step 5: Create URLs

Finally, we’ll set up the URLs for our new views.

Create URLs

authentication/urls.py

from django.urls import path
from .views import SignupView, LoginView, LogoutView

urlpatterns = [
path('signup/', SignupView.as_view(), name='signup'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
]

Add these URLs to your project’s main urls.py file.

expense_tracker_project/urls.py

from django.contrib import admin
from django.urls import path, include
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework.permissions import AllowAny
import authentication.urls as authentication_urls

schema_view = get_schema_view(
openapi.Info(
title="Expense Tracker API",
default_version='v1',
description="Expense Tracker API",
terms_of_service="https://www.example.com/terms/",
contact=openapi.Contact(email="contact@example.com"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=(AllowAny,),
)

urlpatterns = [
path("admin/", admin.site.urls),
path('docs/', include('authentication.swagger')),

path('swagger(?P<format>\.json|\.yaml)',
schema_view.without_ui(cache_timeout=0),
name='schema-json'),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0),
name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0),
name='schema-redoc'),


]

urlpatterns += core_urls.urlpatterns
urlpatterns += authentication_urls.urlpatterns

Swagger Documentation

Access the Swagger documentation at http://localhost:8000/swagger/ to explore and test your API endpoints.

Swagger API endpoints

Conclusion

In this part of our Expense Tracker series, we added essential authentication features to our Django app, including signup, login, and logout functionality. We also integrated Swagger documentation for easier API exploration and testing. These enhancements ensure a more secure and user-friendly application. Stay tuned for the next part, where we’ll cover adding email verification for new users. For more tips and tutorials on Django and REST Framework, follow our series and share your thoughts in the comments!

If you found this article helpful, don’t forget to share it with your friends and colleagues! 📤 Feel free to leave a comment below with your thoughts or questions 💬, and give it a like if you enjoyed it! 👍😊

To get exclusive posts and insights on full-stack projects, become a member on our Patreon page and support our work! 🚀💻

If you enjoyed this, consider buying me a Coffee! ☕💖

--

--