A project that uses Django and React to create a full-stack starter web application.
The backend utilizes Django and Django REST framework to create a RESTful API with user authentication and registration. It uses a simple built-in authentication system using tokens and refresh tokens.
The frontend uses React and React Router to create a single-page application with a simple user interface. A custom api service is used to make requests to the backend.
This is a simple project that is a good launching point for creating a full-stack web application.
Here is a list of models used in the app.
Here is a list of views used in the app.
- Getting Started
- Schemas
- Usage
- Demo GIF
Anaconda was used to manage the environment for this project. You can install the required packages using the environment.yml file
. It includes the required packages for the backend and yarn for the frontend.
conda env create -f environment.yml
Activate the environment
conda activate django-rest
There are two separate .env files for this project: The client and the server. Example .envs are given in the root directory, but these files should be placed in the client and server directory, respectively, to work. You can copy the .env.example file and rename it to .env in both places. The .env file should not be committed to the repository. It is used to store sensitive information such as API keys and database passwords.
In the server .env file, you will need to set the following variables:
SUPER_USER
PASSWORD
SERVER_SECRET_KEY
DEBUG
TOKEN_ENCRYPTION_KEY
REFRESH_ENCRYPTION_KEY
ENCODE_ALGORITHM
TOKEN_EXPIRATION_HOURS
REFRESH_EXPIRATION_DAYS
The secret key is used in the settings.py file to provide encryption. The token encryption and the refresh encryption keys are used in the app's token generation. You should use seperate keys for these three variables. You can use randomkeygen.com to generate a secret key.
Use integers for the expiration times. The token expiration time is in hours and the refresh expiration time is in days.
The client .env file is used to store the API URL. You will need to set the following variable:
REACT_APP_API_URL
This is the URL that the client will use to make requests to the server. It should be set to the URL of the server. For development, it will be http://localhost:8000.
CD into the root directory and run the following commands to build the database.
cd django-rest-starter
python manage.py makemigrations
python manage.py migrate
You can create a superuser to access the admin panel.
python manage.py createsuperuser
Finally, run the server.
python manage.py runserver
yarn package manager is used to manage the client side of the start app. It should have been installed when the environment was created. If not, you can install it using the following command:
conda install conda-forge::yarn
CD into the client directory and run yarn
to install the required packages. Run the following command to start the client:
yarn start
This should start the client on http://localhost:3000 and open it in your default web browser.
Here is the documentation provided by create-react-app that details the other commands that is used throughout development and production.
The authentication system is built into the server with the following views ready to use:
/register/
- POST request to register a new user. Requires a username, email, and password./login/
- POST request to login a user. Requires a username and password./logout/
- GET request to logout a user. Requires a token./me/
- GET request to get the user's information. Requires a token./sessions/
- GET request to retrieve a token based on the Refresh cookie. Requires a refresh cookie.
The system uses access and refresh tokens to authenticate users. The access token is used to access protected views and the refresh token is used to stayed logged in and request a new access token when the old one expires. These tokens are encrypted and stored in the database so they can be checked against a user's request credentials. The tokens are generated using the core.utils.token_utils
module.
The views are protected by decorators that check the user's token. The core.decorators.token_decorators
module contains the decorators that are used to protect the views. The validate_token
decorator is used to check the user's access token and the validate_refresh
decorator is used to check the user's refresh token. To protect a view, the decorator is added above the view function like so:
from django.http import JsonResponse
...
# import the decorator
from core.decorators.token_decorators import validate_token
@api_view(['GET'])
@validate_token # add the decorator
def protected_view(request):
return JsonResponse({'message': 'This is a protected view'}, status=200)
@api_view(['GET']) # this view is not protected
def unprotected_view(request): #
return JsonResponse({'message': 'This is an unprotected view'}, status=200)
Use the validate_token
decorator for any views that require a token and is request private information. The validate_refresh
decorator only needs to be used for the /sessions/
view or anything that only requires a refresh token.
The validate_user
decorator is used to check if the user requesting the data is the same as the user in the token. This decorator requires a user_id
parameter in the URL. It is used like so:
from django.http import JsonResponse
...
# import the decorator
from core.decorators.user_decorators import validate_user
@api_view(['GET'])
@validate_token
@validate_user # add the decorator
def get_user(request, user_id):
user = User.objects.get(id=user_id)
return JsonResponse({'user': {'id': user.id, 'username': user.username, 'email': user.email}}, status=200)
The URL for this view would look like this: /get-user/<int:user_id>/
. The user_id
parameter is required in the URL.
The server can be extended by directly adding to the core directory. The core directory has been modularized into separate directories for models, views, and managers. Files can be added into these respective directories. Each directory has an __init__.py
file that imports the files from that directory. This will need to be modified to include the new files.
If a new model is added, the core directory will look like this:
core/
decorators/
managers/
migrations/
models/
__init__.py
token_models.py
user_models.py
new_models.py # ***New model file***
utils/
views/
and the __init__.py
file in the models directory will look like this:
from .user_models import *
from .token_models import *
from .new_models import * # ***New model file***
__all__ = [
'user_models',
'token_models',
'new_models' # ***New model file***
]
This way, the new models will be imported if the developer imports by using the following code: from core.models import *
. The same process can be done for views and managers.
The server can also be extended by creating a new app. This example can be viewed or in full or cloned at the ca/test-auth
branch in the GitHub repository. The following steps will be a guide to creating a new app called test_auth
with a new model call Click
.
1. Create a new app using the following command:
python manage.py startapp test_auth
2. Add the app to the installed apps in the server/settings.py
file:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
...
'core',
'test_auth', # ***New app***
]
3. Add a managers.py
file to the test_auth
directory. This file will contain the manager for the new model. The manager.py
, models.py
, and views.py
files can also be modularized into separate directories like the core
app. Here they are all in the same directory for simplicity.
The test_auth directory will look like this:
test_auth/
__init__.py
migrations/
admin.py
apps.py
managers.py # ***New manager file***
models.py
tests.py
views.py
4. Add the new model to the models.py
file:
from django.db import models
from test_auth.managers import ClickManager
class Click(models.Model):
class Meta:
db_table = 'test_auth_click'
id = models.AutoField(primary_key=True)
clicked_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey('core.CustomUser', on_delete=models.CASCADE)
objects = ClickManager()
def __str__(self):
return f'{self.user} clicked at {self.clicked_at}'
5. Add the new manager to the managers.py
file:
from django.db import models
from django.utils import timezone
class ClickManager(models.Manager):
def create_click(self, user):
new_click = self.create(user=user)
new_click.save()
return new_click
def get_click(self, id):
return self.get(id=id)
def get_clicks_by_user(self, user):
return self.filter(user=user)
def get_latest_click_by_user(self, user):
return self.filter(user=user).latest('clicked_at')
6. Add the new views to the views.py
file:
from django.http import JsonResponse
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from test_auth.models import Click
from rest_framework.decorators import api_view
from core.decorators.token_decorators import validate_token
from core.decorators.user_decorators import validate_user
User = get_user_model()
@api_view(['POST'])
@validate_token
def click(request):
user_id = request.data.get('user_id')
user = User.objects.get(id=user_id)
click = Click.objects.create_click(user)
return JsonResponse({'id': click.id, 'clicked_at': click.clicked_at}, status=201)
@api_view(['GET'])
@validate_token
@validate_user
def get_clicks(request, user_id):
user = User.objects.get(id=user_id)
clicks = Click.objects.get_clicks_by_user(user)
return JsonResponse({'clicks': [{'id': click.id, 'clicked_at': click.clicked_at} for click in clicks]}, status=200)
7. Add the new views to the server/urls.py
file:
from django.contrib import admin
from django.urls import path
from core.views import (
register as RegisterView,
login as LoginView,
logout as LogoutView,
me as MeView,
# test views
test_token as TestTokenView,
test_protected as TestProtectedView,
sessions as SessionsView,
)
from test_auth.views import ( # ***Import new views***
click as ClickView,
get_clicks as GetClicksView,
)
urlpatterns = [
path('admin/', admin.site.urls),
path('register/', RegisterView, name='register'),
path('login/', LoginView, name='login'),
path('logout/', LogoutView, name='logout'),
path('sessions/', SessionsView, name='sessions'),
path('me/', MeView, name='me'),
# test views
path('test-token/', TestTokenView, name='test-token'),
path('test-protected/', TestProtectedView, name='test-protected'),
path('click/', ClickView, name='click'), # ***New url***
path('get-clicks/<int:user_id>/', GetClicksView, name='get-clicks'), # ***New url***
]
8. Run the following commands to create the migrations and migrate the database:
python manage.py makemigrations
python manage.py migrate
9. To test the new views, the home page on the client side was modified to include a button that sends a POST request to the new click view. The following code was added to the client/src/components/home/_home.js
file.
10. Run the server and client to test the new views.
The rules for email validation are in the utils files validate_email.py
and validateEmail.js
for the server and client respectively. Validation happens on both ends of the project. These files can be found in the server/core/utils
and client/src/utils
directories. If there are any desired deletions or additions to these rules, please make the changes in both files and refer to the list of rules below.
There are constants built into both files that can be changed to adjust the rules. The constants are as follows:
-
SPECIAL_CHARS
- A list of the special characters that are used to check for doubles in the local portion, starts and ends in the local and domain portions, and the domain portion itself. -
ALLOWED_CHARS
- A list of the allowed special characters that can be used in the domain portion. -
MAX_LOCAL_LENGTH
- The max length of the local portion. The current max length is 64. -
MAX_DOMAIN_LENGTH
- The max length of the domain portion. The current max length is 255. -
MAX_SUBDOMAIN_LENGTH
- The max length of the subdomain. The current max length is 63. -
MAX_EMAIL_LENGTH
- The max length of the email. The current max length is 320.
General rules for email validation are as follows:
-
The email is the max length of the local portion (the part before the
@
symbol) and the domain portion (the part after the@
symbol) combined, plus 1 for the @ symbol. The current max length is 320. -
Cannot be empty.
-
Must contain an
@
symbol. -
Cannot contain any spaces. Email addresses can contain spaces if they are enclosed in double quotes, but this is not supported.
Local portion rules:
-
Cannot be empty or longer than the max length of 64.
-
Cannot contain double special characters (e.g.
..
). -
Cannot start or end in a special character.
Domain portion rules:
-
Cannot be empty or longer than the max length of 255.
-
Cannot contain a special character, except for the allowed special characters.
Subdomain rules:
-
There must be at least 2 subdomains, the domain and the top-level domain (TLD).
-
The subdomain cannot be empty or longer than the max length of 63.
-
Cannot start or end in a special character.
-
The TLD is at least 2 characters long.
These are the current rules for email validation. There are plenty of other rules that can be added, but these are the common ones. There are also REGEX patterns that can be used to validate emails, but these were written to allow for easier readability and maintainability.
The rules for password validation are in the utils files validate_password.py
and validatePassword.js
for the server and client respectively. Validation happens on both ends of the project. These files can be found in the server/core/utils
and client/src/utils
directories. If there are any desired deletions or additions to these rules, please make the changes in both files and refer to the list of rules below.
Regular expressions are used to validate the password. They are used here, because they are simpler to read and maintain in this instance. The expressions are held in constants on both the server and client side. The constants are as follows:
-
UPPER_REGEX
- A regular expression that checks for at least one uppercase letter. -
LOWER_REGEX
- A regular expression that checks for at least one lowercase letter. -
DIGIT_REGEX
- A regular expression that checks for at least one digit. -
SPECIAL_REGEX
- A regular expression that checks for at least one special character.
The rules for password validation are as follows:
-
The password must be at least 8 characters long.
-
The password must contain at least one uppercase letter.
-
The password must contain at least one lowercase letter.
-
The password must contain at least one digit.
-
The password must contain at least one special character.