Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications - 2

In the previous part, we designed the database schema to address this part of the specification:

Build a multi-user authentication system. A user can either be a Student or a Lecturer ...

Source code

The source code to this point is hosted on github while the source code for the entire application is:

GitHub logo Sirneij / django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications

django_real_time_validation

Django and Ajax: Robust authentication and authorization system with real-time form validations for web applications



The project is also live on heroku and can be accessed via this django-authentication-app.herokuapp.com

In this part, we'll take a tour of how the logic will be implemented. Some part of views.py, urls.py, forms.py, and authentication.py will be implemented.

Let's put our coding hart 👲 on and get our hands 🧰 dirty!

Step 2: Creating other files

First off, we'll be using additional files as follows:

  • 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.

To create the files, navigate to your terminal and run the following command:

┌──(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

Step 3: Custom authentication backend

A section of the specification we are implementing says:

...Users can either login with any of Email/Password or Username/Password combination...

To do this, we need a custom authentication backend. Luckily, django gives us a pointer to how this can be done. Fire up you text editor and make 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

We aint inheriting any built-in backend here but this still works. However, we still fall back to Django's default authentication backend which authenticates with username.

Though we have written this self-explanatory code snippet, it does nothing yet. To make it do something, we need to register it. Append the snippet below to your project's settings.py file:

# authentication > settings.py
...
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.EmailAuthenticationBackend", # our new authentication backend
]
...

Let's add our new 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)

We have set up custom user admin business logic. In the code, we added a custom action 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.

Step 4: Login view logic

It's time to test our 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"

It is a very simple form with two fields: 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)

It is a basic authentication logic. Some pointers are removing all forward slashes , / from the inputted username, in the case of students, and using our custom authentication backend:

...
login(request, user, backend="accounts.authentication.EmailAuthenticationBackend")
...

to login users in. We also covered the part of the specification that says:

Until confirmation, no user is allowed to log in.

Though, by default, you can not login if 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.

Let's add this and django's built-in 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"),
]

By extension, let's register this in our 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"

...

We always want to go back to home page when we logout.

FInally, it's time to render it out.

{% 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 %}

It is a basic materialize css form with icons. Since we have only two fields, 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 %}

This is what saves the next field we discussed previously. It is a hidden input since we don't want users to see it's content, just for reference.

To incept the real-time form validation we've been clamouring for, let's add a bit of JavaScript to this form. At first, we want the Log in button to be disabled until users type in both the username or email and password. That's enough for now.

Append this code to 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 %}

It simply listens to keyup events in any of the form's input elements. If any is empty, the button remains disabled, else? Enabled! Simple huh 😎!

Modify the button to be disabled by default.

...

<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>

...

We have already created a js block at the bottom of templates/base.html file

Now, update your templates/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>

...

Can we test it out now? Because I can't wait 💃🕺.

Damn! It is appealing 🤗... Create a superuser account and test it out with either Email or username and password.

You want the code to this end? Get it on github

Let's end it here, it's becoming unbearably too long 😌. See ya 👋 🚶!!!

Outro

36