django_real_time_validation
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
This website collects cookies to deliver better user experience
Build a multi-user authentication system. A user can either be a Student or a Lecturer ...
Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications
views.py
, urls.py
, forms.py
, and authentication.py
will be implemented.accounts/forms.py
: this holds everything form related.accounts/utils.py
: to avoid cluttering the views.py
file, helper functions will be domiciled here.accounts/authentication.py
: this houses the custom authentication backend we'll be using to enable signing in with both email address and username.┌──(sirneij@sirneij)-[~/Documents/Projects/Django/django_real_time_validation]
└─$[sirneij@sirneij django_real_time_validation]$ touch accounts/utils.py accounts/forms.py accounts/authentication.py
...Users can either login with any of Email/Password or Username/Password combination...
accounts/authentication.py
look like this:# accounts > authentication.py
from .models import User
class EmailAuthenticationBackend(object):
"""
Authenticate using an e-mail address.
"""
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password): # and user.is_active:
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
settings.py
file:# authentication > settings.py
...
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"accounts.authentication.EmailAuthenticationBackend", # our new authentication backend
]
...
User
model to django's admin page. Open up accounts/admin.py
and append the following:# accounts > admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
class CustomUserAdmin(UserAdmin):
model = User
readonly_fields = [
"date_joined",
]
actions = [
"activate_users",
]
list_display = (
"username",
"email",
"first_name",
"last_name",
"is_staff",
"is_student",
"is_lecturer",
)
def get_inline_instances(self, request, obj=None):
if not obj:
return list()
return super(CustomUserAdmin, self).get_inline_instances(request, obj)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
is_superuser = request.user.is_superuser
disabled_fields = set()
if not is_superuser:
disabled_fields |= {
"username",
"is_superuser",
}
# Prevent non-superusers from editing their own permissions
if not is_superuser and obj is not None and obj == request.user:
disabled_fields |= {
"is_staff",
"is_superuser",
"groups",
"user_permissions",
}
for f in disabled_fields:
if f in form.base_fields:
form.base_fields[f].disabled = True
return form
def activate_users(self, request, queryset):
cannot = queryset.filter(is_active=False).update(is_active=True)
self.message_user(request, "Activated {} users.".format(cannot))
activate_users.short_description = "Activate Users" # type: ignore
def get_actions(self, request):
actions = super().get_actions(request)
if not request.user.has_perm("auth.change_user"):
del actions["activate_users"]
return actions
admin.site.register(User, CustomUserAdmin)
activate user
which allows a large number of users to be activated at once. This was implemented in case the registration flow we are planning fails and we want the superuser to be empowered with the ability to mass-activate users. We also hide a couple of fields from any user who has access to the admin page but not a superuser
. This is for security concerns. To learn more about this, Haki Benita's article is an awesome guide.custom authentication
backend. First, we need a form to login users. Let's create it.# accounts > forms.py
from django import forms
class LoginForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"placeholder": "Username or Email"}))
password = forms.CharField(widget=forms.PasswordInput(attrs={"placeholder": "Password"}))
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
visible.field.widget.attrs["class"] = "validate"
username
and password
. However, the username
field also accommodates email
addresses. This is to conform with our specification. The __init__
dunder method applies class=validate
to all the visible fields in the form. It is a nice shortcut mostly when you are working with ModelForms
. This validate
class is available in materialize css
. The next agender is to use this form in the views.py
file.# accounts > views.py
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render
from django.urls.base import reverse
from .forms import LoginForm
...
def login_user(request):
form = LoginForm(request.POST or None)
msg = "Enter your credentials"
if request.method == "POST":
if form.is_valid():
username = form.cleaned_data.get("username").replace("/", "")
password = form.cleaned_data.get("password")
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
messages.success(request, f"Login successful!")
if "next" in request.POST:
return redirect(request.POST.get("next"))
else:
return redirect("accounts:index")
else:
messages.error(
request,
f"Login unsuccessful! Your account has not been activated. Activate your account via {reverse('accounts:resend_email')}",
)
msg = "Inactive account details"
else:
messages.error(request, f"No user with the provided details exists in our system.")
else:
messages.error(request, f"Error validating the form")
msg = "Error validating the form"
context = {
"form": form,
"page_title": "Login in",
"msg": msg,
}
return render(request, "accounts/login.html", context)
/
from the inputted username, in the case of students, and using our custom authentication backend:...
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
...
Until confirmation, no user is allowed to log in.
is_active=False
but since we are using custom authentication
backend, I feel we should enforce that. We could have done this earlier on in the authentication
backend code. Next, we check whether there is a page we need to redirect to by checking the content of next
. We will put this in our template soon. It is a nice way to redirect users back to wherever they wanted to visit before being asked to login.logout
view to our urls.py
file.# accounts > urls.py
from django.contrib.auth import views as auth_views
...
urlpatterns = [
...
path("login", views.login_user, name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
settings.py
file too.# accounts > settings.py
...
AUTH_USER_MODEL = "accounts.User"
LOGIN_URL = "accounts:login"
LOGOUT_URL = "accounts:logout"
LOGOUT_REDIRECT_URL = "accounts:index"
...
{% extends "base.html" %}
<!--static-->
{% load static %}
<!--title-->
{% block title %}{{page_title}}{% endblock %}
<!--content-->
{% block content%}
<h4 id="signup-text">Welcome back</h4>
<div class="form-container">
<!-- <h5 class="auth-header">Assignment Management System</h5>-->
<div class="signin-form">
<form method="POST" action="" id="loginForm">
{% csrf_token %}
<!---->
<h5 style="text-align: ceneter">{{msg}}</h5>
<div class="row">
{% for field in form %}
<div class="input-field col s12">
{% if forloop.counter == 1 %}
<i class="material-icons prefix">email</i>
{% elif forloop.counter == 2 %}
<i class="material-icons prefix">vpn_key</i>
{% endif %}
<label for="id_{{field.label|lower}}"> {{field.label}}* </label>
{{ field }}
<!---->
{% if field.errors %}
<span class="helper-text email-error">{{field.errors}}</span>
{% endif %}
</div>
{% endfor %}
</div>
<!---->
{% if request.GET.next %}
<input type="hidden" name="next" value="{{request.GET.next}}" />
{% endif %}
<button
class="btn waves-effect waves-light btn-large"
type="submit"
name="login"
id="loginBtn"
>
Log in
<i class="material-icons right">send</i>
</button>
</form>
<ul>
<li class="forgot-password-link">
<a href="#"> Forgot password?</a>
</li>
</ul>
</div>
<div class="signup-illustration">
<img
src="{% static 'img/sign-up-illustration.svg' %}"
alt="Sign in illustration"
/>
</div>
</div>
{% endblock %}
username/email
and password
, we use if
statement to check the forloop
counter and put icons
appropriately. Noticed this line?:{% if request.GET.next %}
<input type="hidden" name="next" value="{{request.GET.next}}" />
{% endif %}
next
field we discussed previously. It is a hidden input since we don't want users to see it's content, just for reference.Log in
button to be disabled until users type in both the username or email
and password
. That's enough for now.templates/accounts/login.html
file:<!---->
{% block js %}
<script>
const loginForm = document.getElementById("loginForm");
const formElements = document.querySelectorAll("#loginForm input");
loginForm.addEventListener("keyup", (event) => {
let empty = false;
formElements.forEach((element) => {
if (element.value === "") {
empty = true;
}
});
if (empty) {
$("#loginBtn").addClass("disabled");
} else {
$("#loginBtn").removeClass("disabled");
}
});
</script>
{% endblock js %}
keyup
events in any of the form's input elements. If any is empty, the button remains disabled, else? Enabled! Simple huh 😎!...
<button class="btn waves-effect waves-light btn-large disabled"
type="submit"
name="login"
id="loginBtn"
>
Log in
<i class="material-icons right">send</i>
</button>
...
js
block at the bottom of templates/base.html
filetemplates/includes/_header.html
so we can have easy navigation for both mobile and desktop portions....
<li><a href="{% url 'accounts:logout' %}">Logout</a></li>
...
<li><a href="{% url 'accounts:login' %}">Login</a></li>
...
Email or username
and password
.