Uploaded by George Githinji

Django rest framework Part 1 Custom User model 57bd7bbb40384182a53c117f695d7d37

advertisement
Django rest framework Part
1: Custom User model
Django comes with a built-in User model for authentication.
However, the official Django documentation recommends using a
custom user model. In this tutorial, we will create a project
with a custom user model.
Requirements
Python3 and above
Pipenv
Postgresql
Set up the project
On your local computer create a working folder and name it
blog
mkdir blog
Navigate into the working directory using
cd blog
Open the working directory using your favorite IDE, for those
using VsCode and you have setup the path command in your IDE,
use
code.
to open the working directory.
Django rest framework Part 1: Custom User model
1
On the terminal inside your working directory, run this command
to create and activate virtual env.
pipenv shell
You will notice filename Pipfile has been created in your working
directory. To install django and djangorestframework run the
following command.
pipenv install django djangorestframework
You will notice another file
working directory, it helps
Pipfile.lock
has been created in your
in tracking the version of dependencies installed.
Open the
settings.py
under
INSTALLED_APPS
and add
rest_framework
.
Your settings should look like this
[
...
'rest_framework',
...
]
Create a Django project using the following command. We will
name our project app
django-admin startproject app
This is our current directory structure
Django rest framework Part 1: Custom User model
2
Lets do some clean up in our directory structure. This will
eliminate the need of navigating to app folder to start the
server.
We are going to move all the files inside
this command mv app/app/* app
and move
app/app/
to
app/
use
to the top of our directory structure using mv
app/manage.py . Now delete the app folder inside the app using this
command rm -rf app/app This is what our project structure should
look like.
manage.py
├── app
│
├── asgi.py
│
│
│
│
├──
├──
└──
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
manage.py
Pipfile
Pipfile.lock
Run python manage.py runserver to start the server. Visit this link
http://127.0.0.1:8000/ you and you should see the Django welcome
screen.
Django rest framework Part 1: Custom User model
3
Create API app
Inside app folder, run django-admin startapp authentication you will notice
a authentication folder has been created inside app folder and
inside authentication there are a couple of files.
Register the authentication app with the app
project
We edit app/settings.py under INSTALLED_APPS and add
app.authentication
[
...
'app.authentication'
...
]
Django rest framework Part 1: Custom User model
4
Database connection
We are using PostgreSQL database. We will need to install
psycopg2 which is the most popular PostgreSQL database adapter
for the Python programming.
pipenv install psycopg2
Create a database and give it the name of your choice, I named
mine blog. Create a .env file to store the environment
variables.
export DB_NAME=<your_db_name>
export DB_USER=<your_db_user>
export DB_HOST=localhost
export DB_PASSWORD=<your_db_password>
Run
source .env
to set the environment variables
So we edit app/settings.py under DATABASES and do the following
changes as follows
import os
....
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': os.getenv('DB_NAME'),
'PASSWORD': os.getenv('DB_PASSWORD', ''),
'HOST': os.getenv('DB_HOST'),
'USER': os.getenv('DB_USER'),
'PORT': '5432',
}
}
Django rest framework Part 1: Custom User model
5
..
Migrate the database
Run
python manage.py migrate
to migrate default migrations. After
running the command above you will see the following output
without any failure.
If error, repeat the above step to ensure the db setup was done
correctly.
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Django rest framework Part 1: Custom User model
6
Creating Models
Navigate in app/authentication/model.py and add the following
code. This will override the custom authentication model.
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
class UserManager(BaseUserManager):
"""
Django requires that custom users define their own Manager c
inheriting from `BaseUserManager`, we get a lot of the same
Django to create a `User` for free.
All we have to do is override the `create_user` function whi
to create `User` objects.
"""
def create_user(self, username, email, password=None):
"""Create and return a `User` with an email, username an
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email address.')
user = self.model(username=username, email=self.normaliz
user.set_password(password)
user.save()
return user
def create_superuser(self, username, email, password):
"""
Django rest framework Part 1: Custom User model
7
Create and return a `User` with superuser powers.
Superuser powers means that this use is an admin that ca
they want.
"""
if password is None:
raise TypeError('Superusers must have a password.')
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(db_index=True, max_length=255, u
default="default-usernam
email = models.EmailField(db_index=True, unique=True)
is_active = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
objects = UserManager()
def __str__(self):
"""
Returns a string representation of this `User`.
This string is used when a `User` is printed in the cons
"""
return self.email
Then under app/settings.py change the AUTH_USER_MODEL to use our
custom model
Django rest framework Part 1: Custom User model
8
...
AUTH_USER_MODEL = 'authentication.User'
...
Make migrations
Whenever we define or change a model, we need to tell Django to
migrate those changes
$ python manage.py makemigrations
Migrations for 'authentication':
app/authentication/migrations/0001_initial.py
- Create model User
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, authentication, contenttype
Running migrations:
Applying authentication.0001_initial... OK
Keeping our code in order
We want to keep our code in order and we need to have our helper
functions in different files where we can reuse them.
Create a folder app/helpers and add these files
app/helpers/serialization_errors.py
error_dict = {
"required": "This field is required.",
"empty_field": "This field cannot be empty.",
Django rest framework Part 1: Custom User model
9
"invalid_name": "{0} cannot have spaces or special character
"invalid_id_number": "The ID number must contain numbers onl
'invalid_password': 'Password must have at least a number, a
"invalid_phone_no": "Phone number must be numbers of the for
"already_exist": "{0} already exist.",
"min_length": "{0} must be at least {1} characters long.",
"blank_field": "This field may not be blank.",
"does_not_exist": "{} does not exist",
}
jwt_errors = {
'token_expired': 'Token expired. Please login to get a new t
'invalid_token': 'Authorization failed due to an Invalid tok
'invalid_secret': 'Cannot verify the token provided as the e
'token_user_not_found': "No user found for token provided"
}
app/helpers/token.py
import datetime
import jwt
from django.conf import settings
from rest_framework import exceptions
from ..authentication.models import User
from .serialization_errors import jwt_errors
def generate_password_reset_token(email, id_):
"""
generates token that expires in an hour
Args:
email (str): user email
id_ (str): user id
Return:
Django rest framework Part 1: Custom User model
10
token (str): a token that expires in the specified time
"""
time = datetime.datetime.utcnow(
) + datetime.timedelta(hours=int(settings.RESET_TOKEN_EXP_TI
payload = {
'email': email,
'id': id_,
'exp': time
}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm=
return token.decode('utf-8')
def get_token_data(token):
"""
checks validity of a token
Args:
token (str): token to be validated
Return:
user (obj): valid user object
"""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms="HS256", )
user = User.objects.get(email=payload['email'])
except Exception as error:
exception_mapper = {
jwt.ExpiredSignatureError: jwt_errors['token_expired
jwt.DecodeError: jwt_errors['invalid_token'],
jwt.InvalidIssuerError: jwt_errors['invalid_secret']
User.DoesNotExist: jwt_errors['token_user_not_found
}
message = exception_mapper.get(
type(error), 'Authorization failed.')
Django rest framework Part 1: Custom User model
11
raise exceptions.AuthenticationFailed(message)
return user
Serialize the User model
Now we’re starting to get into some Restful concepts. We need to
tell REST Framework about our User model and how it should
serialize the data. Serialization is the process of converting a
Model to JSON. The serializer will convert the user model
instance to JSON representation. In turn when the user post JSON
data to our api, the serializer will convert it to User model
instance inorder for us to save.
So we will edit app/authentication/serializers.py
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from ..helpers.serialization_errors import error_dict
from .models import User
class RegistrationSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
required=True,
allow_null=False,
validators=[
UniqueValidator(
queryset=User.objects.all(),
message=error_dict['already_exist'].format("Emai
)
],
error_messages={
'required': error_dict['required'],
}
Django rest framework Part 1: Custom User model
12
)
password = serializers.RegexField(
regex=("^(?=.{8,}$)(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).*")
min_length=8,
max_length=30,
required=True,
allow_null=False,
write_only=True,
error_messages={
'required': error_dict['required'],
'min_length': error_dict['min_length'].format("Passw
'max_length': 'Password cannot be more than 30 chara
'invalid': error_dict['invalid_password'],
}
)
username = serializers.RegexField(
regex='^(?!.*\ )[A-Za-z\d\-\_]+$',
allow_null=False,
required=True,
validators=[
UniqueValidator(
queryset=User.objects.all(),
message=error_dict['already_exist'].format("User
)
],
error_messages={
'required': error_dict['required'],
'invalid': error_dict['invalid_name'].format('Userna
}
)
class Meta:
model = User
fields = ['email', 'username', 'id', 'password']
Django rest framework Part 1: Custom User model
13
@classmethod
def create(self, data):
# Use the `create_user` method we wrote earlier to creat
return User.objects.create_user(**data)
Display the data(Views)
We need to recive http request with JSON data via our api and
save it to our User model. Write the following code.
from
from
from
from
from
rest_framework import generics
rest_framework import status
rest_framework.permissions import AllowAny
django.shortcuts import render
rest_framework.response import Response
from .serializers import RegistrationSerializer, User
class RegistrationApiView(generics.CreateAPIView):
permission_classes = (AllowAny, )
serializer_class = RegistrationSerializer
def post(self, request):
user_data = request.data
serializer = self.serializer_class(data=user_data)
serializer.is_valid(raise_exception=True)
serializer.save
return_message = {
"message": "Registration successful",
"data": serializer.data
Django rest framework Part 1: Custom User model
14
}
return Response(return_message, status=status.HTTP_201_C
Default authentication class
Create app/authentication/backends.py and paste the following
code
from
from
from
from
django.conf import settings
rest_framework import authentication
rest_framework.exceptions import AuthenticationFailed
..helpers.token import get_token_data
class JWTAuthentication(authentication.BaseAuthentication):
"""This is called on every request to check if the user is a
"""
@classmethod
def authenticate(self, request):
"""
This checks that the passed JWT token is valid and retur
a user and his/her token on successful verification
"""
# Get the passed token
auth_header = authentication.get_authorization_header(
request).decode('utf-8')
if not auth_header or auth_header.split()[0].lower() !=
return None
token = auth_header.split(" ")[1]
# Attempt decoding the token
user = get_token_data(token)
Django rest framework Part 1: Custom User model
15
# Check this user is activated
if not user.is_active:
raise AuthenticationFailed('This user is deactivated
return (user, token)
Handling exceptions
Create app/core/exceptions.py
from rest_framework.views import exception_handler
def core_exception_handler(exc, context):
# If an exception is thrown that we don't explicitly handle
# to delegate to the default exception handler offered by DR
# handle this exception type, we will still want access to t
# generated by DRF, so we get that response up front.
response = exception_handler(exc, context)
handlers = {
'ValidationError': _handle_generic_error
}
# This is how we identify the type of the current exception
# this in a moment to see whether we should handle this exce
# Django REST Framework do it's thing.
exception_class = exc.__class__.__name__
if exception_class in handlers:
# If this exception is one that we can handle, handle it
# return the response generated earlier by the default e
# handler.
return handlers[exception_class](exc, context, response)
return response
Django rest framework Part 1: Custom User model
16
def _handle_generic_error(exc, context, response):
# This is about the most straightforward exception handler w
# We take the response generated by DRF and wrap it in the
response.data = {
'errors': response.data
}
return response
So we will edit app/settings.py and add the following
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'app.core.exceptions.core_exception_han
'NON_FIELD_ERRORS_KEY': 'error',
'DEFAULT_AUTHENTICATION_CLASSES': (
'app.authentication.backends.JWTAuthentication', # app/a
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
}
Handling Urls
We'll specify URLs as endpoints for consuming our API. Create a
app/authentication/url.py this is where we are going to define
our url patterns.
from django.urls import path
from .views import RegistrationApiView
urlpatterns = [
path('signup/', RegistrationApiView.as_view(), name='user-re
]
Django rest framework Part 1: Custom User model
17
Finally we add a url to the main app's urls.py file so that it
points to our API app. Open app/urls.py
"""app URL Configuration
The `urlpatterns` list routes URLs to views. For more informatio
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='hom
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name=
Including another URLconf
1. Import the include() function: from django.urls import in
2. Add a URL to urlpatterns: path('blog/', include('blog.ur
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('app.authentication.urls'))
]
Project structure
Our final directory structure should be like the following
├──
│
│
│
app
├── asgi.py
├── authentication
│
├── admin.py
│
│
├── apps.py
Django rest framework Part 1: Custom User model
18
│
│
│
│
│
│
├── backends.py
├── __init__.py
├── migrations
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
├──
├──
├──
├──
└──
│
│
│
│
│
│
│
├──
│
│
├──
│
│
│
core
├── exceptions.py
└── __init__.py
helpers
├── __init__.py
├── serialization_errors.py
└── token.py
│
│
│
│
├──
├──
├──
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
db.sqlite3
manage.py
Pipfile
├── 0001_initial.py
└── __init__.py
models.py
serializers.py
tests.py
urls.py
views.py
├── Pipfile.lock
└── README.md
Finally! Lets run
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
Django rest framework Part 1: Custom User model
19
System check identified no issues (0 silenced).
February 20, 2021 - 09:05:08
Django version 3.1.5, using settings 'app.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Navigate to http://127.0.0.1:8000/auth/signup/ .Yeeah!! it works
Django rest framework Part 1: Custom User model
20
Download