This is a ready-to-use boilerplate gathered from a bunch of different resources and explained briefly. Comes with a custom user model, log in using email, and authentication using JWT.
Third-Party Apps Used :
1.Django REST framework (DRF)
- Simple JWT (for Auth)
To start, let’s add the required constants in settings.py. [NOTE: Keep Django secret in .env format]
See below:
"""
Django settings for PrivateNetwork project.
Generated by 'django-admin startproject' using Django 3.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
import datetime
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-=!@!9#lb*_@25vi54rck=8-qqavliv6$y59+gwf=+nxn(2k3r8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
}
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users.apps.UsersConfig',
'rest_framework',
]
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',
]
ROOT_URLCONF = 'PrivateNetwork.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(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 = 'PrivateNetwork.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
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',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_PROFILE_MODULE = 'users.MyUser'
AUTH_USER_MODEL = 'users.MyUser'
Looking at the settings.py, there are some things you can ignore as they are specific to my current project. Things to consider are:
-
INSTALLED_APP: To include the third-party app, and the user app we will create.
-
REST_FRAMEWORK: A constant with the permission and Auth classes for default cases.
-
SIMPLE_JWT: A constant with the expiration of the access and refresh token.
-
AUTH PASSWORD VALIDATORS: Specific to the requirement of your custom user model.
-
AUTH_USER_MODEL and AUTH_PROFILE_MODEL: Pointing to your custom user model in the user app.
Once this is set up, we will make our custom user model that inherits the base Django AuthUser. Let’s start by creating a new app using python manage.py startapp users
.
In the Users folder, we add our model to models.py
. I made a UserManager that deals with the creation of user and superuser, and the MyUser class that uses the results from this manager. In the MyUser class, as you can see, several custom fields and methods have been introduced which can be added or removed as per requirement.
import datetime
from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser
)
from django.utils import timezone
DISCOUNT_CODE_TYPES_CHOICES = [
('percent', 'Percentage-based'),
('value', 'Value-based'),
]
# Create your models here
class MyUserManager(BaseUserManager):
def create_user(self, email, date_of_birth, password=None):
"""
Creates and saves a User with the given email, date of
birth and password.
"""
if not email:
raise ValueError('Users must have an email address')
user = self.model(
email=self.normalize_email(email),
date_of_birth=date_of_birth,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, date_of_birth, password=None):
"""
Creates and saves a superuser with the given email, date of
birth and password.
"""
user = self.create_user(
email,
password=password,
date_of_birth=date_of_birth,
)
user.is_admin = True
user.save(using=self._db)
return user
class MyUser(AbstractBaseUser):
email = models.EmailField(
max_length=255,
unique=True,
)
date_of_birth = models.DateField()
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
credits = models.PositiveIntegerField(default=100)
linkedin_token = models.TextField(blank=True, default='')
expiry_date = models.DateTimeField(null=True, blank=True)
objects = MyUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['date_of_birth']
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"Is the user a member of staff?"
# Simplest possible answer: All admins are staff
return self.is_admin
@property
def is_out_of_credits(self):
"Is the user out of credits?"
return self.credits > 0
@property
def has_sufficient_credits(self, cost):
return self.credits - cost >= 0
@property
def linkedin_signed_in(self):
return bool(self.linkedin_token) and self.expiry_date > timezone.now()
Next, we have to register this user model to admin and also override the default. Then, we need to create and update the user forms to include the new user fields.
This is done as shown below:
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from django import forms
from .models import MyUser
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = MyUser
fields = ('email', 'date_of_birth', 'credits')
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField()
class Meta:
model = MyUser
fields = ('email', 'password', 'date_of_birth', 'is_active', 'is_admin', 'credits')
def clean_password(self):
return self.initial["password"]
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
.
list_display = ('email', 'date_of_birth', 'is_admin', 'credits')
list_filter = ('is_admin', )
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Personal info', {'fields': ('date_of_birth',)}),
('Permissions', {'fields': ('is_admin',)}),
('Site Info', {'fields': ('credits', )}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'date_of_birth', 'password1', 'password2', 'credits'),
}),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
admin.site.register(MyUser, UserAdmin)
Pretty straightforward where we are overriding the BaseUser and introducing new fields, also setting the ones to display.
Using DRF and JWT now, we will create the endpoints for register, login, logout, and change password.
from django.urls import path
from .views import RegistrationView, LoginView, LogoutView,ChangePasswordView
from rest_framework_simplejwt import views as jwt_views
app_name = 'users'
urlpatterns = [
path('accounts/register', RegistrationView.as_view(), name='register'),
path('accounts/login', LoginView.as_view(), name='register'),
path('accounts/logout', LogoutView.as_view(), name='register'),
path('accounts/change-password', ChangePasswordView.as_view(), name='register'),
path('accounts/token-refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
]
Over here we are using a ready-made view for the refreshing of the token in jwt_views
.
In views, I like using class-based views, but you can prefer function-based views. Another way of doing this is to use a ViewSet and put all functions in one class.
This is as demonstrated below:
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .utils import get_tokens_for_user
from .serializers import RegistrationSerializer, PasswordChangeSerializer
# Create your views here.
class RegistrationView(APIView):
def post(self, request):
serializer = RegistrationSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LoginView(APIView):
def post(self, request):
if 'email' not in request.data or 'password' not in request.data:
return Response({'msg': 'Credentials missing'}, status=status.HTTP_400_BAD_REQUEST)
email = request.POST['email']
password = request.POST['password']
user = authenticate(request, email=email, password=password)
if user is not None:
login(request, user)
auth_data = get_tokens_for_user(request.user)
return Response({'msg': 'Login Success', **auth_data}, status=status.HTTP_200_OK)
return Response({'msg': 'Invalid Credentials'}, status=status.HTTP_401_UNAUTHORIZED)
class LogoutView(APIView):
def post(self, request):
logout(request)
return Response({'msg': 'Successfully Logged out'}, status=status.HTTP_200_OK)
class ChangePasswordView(APIView):
permission_classes = [IsAuthenticated, ]
def post(self, request):
serializer = PasswordChangeSerializer(context={'request': request}, data=request.data)
serializer.is_valid(raise_exception=True) #Another way to write is as in Line 17
request.user.set_password(serializer.validated_data['new_password'])
request.user.save()
return Response(status=status.HTTP_204_NO_CONTENT)
from rest_framework_simplejwt.tokens import RefreshToken
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
This is pretty standard as you would see in most resources/examples. Using serializer for registering and changing passwords, simple logout function and login function. Instead, we are returning the access and refresh tokens in log in using the simple_jwt
constructor. This token has to be sent in other requests, where permission of IsAuthenticated
is required, such as ChangePassword.
The token needs to be sent in the header like this:
{
Authorization: 'Bearer <access token>'
}
To refresh the access token, the refresh token needs to be sent to accounts/token-refresh
using a POST request. More information can be found here.
The serializer is pretty basic but needs to be changed in case you are changing the MyUser custom model and introducing a new field, or changing the login mechanism to username instead of email.
See below:
from rest_framework import serializers
from .models import MyUser
class RegistrationSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(style={"input_type": "password"}, write_only=True)
class Meta:
model = MyUser
fields = ['email', 'date_of_birth', 'password', 'password2']
extra_kwargs = {
'password': {'write_only': True}
}
def save(self):
user = MyUser(email=self.validated_data['email'], date_of_birth=self.validated_data['date_of_birth'])
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2:
raise serializers.ValidationError({'password': 'Passwords must match.'})
user.set_password(password)
user.save()
return user
class PasswordChangeSerializer(serializers.Serializer):
current_password = serializers.CharField(style={"input_type": "password"}, required=True)
new_password = serializers.CharField(style={"input_type": "password"}, required=True)
def validate_current_password(self, value):
if not self.context['request'].user.check_password(value):
raise serializers.ValidationError({'current_password': 'Does not match'})
return value
Et voila! This boilerplate is ready to use now.
This was published in a hurry since I was always looking for a boilerplate and documentation. I put snippets from one of my new projects and generalized it as best as I could. Please let me know if I missed anything, or overlooked a flaw in this boilerplate.
Might update this with more features as I continue developing new projects and will make this open source so people can contribute.
Back to making art with code guys!