Sparrow SMS for NTC Ncell phone verification in Django REST API as a service

This is an under construction post. I'm still fixing title.

Sparrow SMS for NTC Ncell phone verification in Django REST API as a service
Photo by Quino Al / Unsplash

Introduction

In this post, we're going to know how to code modules in Python Django to use the Sparrow SMS API integration to verify a NTC or Ncell phone number using a generated OTP code. This is an works in production example of an API as a service backend web app built around Django and Django REST framework that allows clients like mobile apps or reactjs/ vuejs enabled frontends to integrate a phone number verification using Sparrow SMS service gateway. In addition this feature could be used as a sign in or sign up wall during the user authentication.

To follow this post ahead, you will need:

Step 1 — Appending to models.py

from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models
from django.contrib.auth.models import AbstractUser
imports
class AccountUser(AbstractUser):

# other very important user fields here

phone_regex = RegexValidator(regex=r'^98\d{8}$', message="Only 10 digits NTC or NCELL numbers allowed.")
phone_number = models.CharField(validators=[phone_regex], max_length=10, null=True, unique=True)
phone_verified = models.BooleanField(default=False)

The custom class AccountUser subclasses in-built AbstractUser from django.contrib.auth.models from the imports above. AbstractUser provides the full implementation of the default User as an abstract model.

class PhoneOtp(models.Model):
    phone_regex = RegexValidator(regex=r'^98\d{8}$', message="Only 10 digits NTC or NCELL numbers allowed.")
    phone_number = models.CharField(validators=[phone_regex], max_length=10, null=True, unique=True)
    validated = models.BooleanField(default=False, help_text="Ensure Mobile Verified Badge")
    otp_code = models.CharField(max_length=6, blank=True, null=True)
    otp_count = models.PositiveSmallIntegerField(default=0, help_text='Count of OTP sent.')

    def __str__(self):
        return self.otp_code + ' is the otp for ' + self.phone_number

The class PhoneOtp has a custom regex validator in line 2. If you look closely the regex allows phone numbers that only starts with 98 and any 8 digits afterwards. But there are phone numbers in Nepal that starts with 97 too. So change the regex as per your needs.

Step 2 — Appending to urls.py

from django.urls import path
from .views import ValidatePhoneSendOTP, ValidateOTP


app_name = 'authentication'
urlpatterns = [
    path('', ValidatePhoneSendOTP.as_view()),
    path('verify/', ValidateOTP.as_view()),
]

Step 3 — Appending to views.py

import json
import random
import requests

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated

from .models import AccountUser, PhoneOtp
imports
def generate_otp(phone_number):
    if phone_number:
    # generates only 6 digit random number
        key = random.randint(99999, 999999) 
        return key
    else:
        return False
Function to generate random 6 digit numbers only
def push_to_sparrow(otp, phone_number):
    with open('/etc/config.json') as config_file:
        config = json.load(config_file)
    if config:
        msg = " is your verification code for example.com. It expires in 5 minutes. Thank you for joining us. Download our app bit.ly/example.com/"
        r = requests.post(
            "http://api.sparrowsms.com/v2/sms/",
            data={'token': config['SPARROW_TOKEN'],
                  'from': config['SPARROW_IDENTITY'],
                  'to': phone_number,
                  'text': "<#> \n" + str(otp) + msg + " \[google api hash]"
                  })
        print("<#> \n" + str(otp) + msg + " \[google api hash]")

        status_code = r.status_code
        response = r.text
        response_json = r.json()
    else:
        return Response({
            'status': False,
            'details': 'Could not parse configurations.'
        })
    return status_code, response, response_json
Function to send generated OTP & Phone number to Sparrow SMS API Gateway 
class ValidatePhoneSendOTP(APIView):
    permission_classes = [IsAuthenticated, ]
    allowed_methods = ('POST', 'OPTIONS', 'HEAD')

    def post(self, request, *args, **kwargs):
        # assumes cleaned data is received here.. 10 digits ntc ncell
        phone_number = request.data.get('phone')
        if phone_number:
            user_with_phone = AccountUser.objects.filter(phone_number__iexact=phone_number)
            phone_in_temp_db = PhoneOtp.objects.filter(phone_number__iexact=phone_number)
            if user_with_phone.exists():
                verified_qs = user_with_phone.values('phone_verified')[0].values()
                verified_status = list(verified_qs)[0]
                username = user_with_phone.values('username')
                phone_in_db = user_with_phone.values('phone_number')
                print('====check===================', user_with_phone.exists(), verified_status, username, phone_in_db)
                if verified_status is True:
                    return Response({
                        'status': 403,
                        'details': 'Phone number already verified.'
                    }, status=status.HTTP_403_FORBIDDEN)
            elif phone_in_temp_db.exists():
                key = generate_otp(phone_number)
                if key:
                    old = PhoneOtp.objects.filter(phone_number__iexact=phone_number)
                    print('====================old user with verified False================', old)
                    if old.exists():
                        old = old.first()
                        count = old.otp_count
                        if count > 1:
                            return Response({
                                'status': 429,
                                'details': 'Too much OTP sent. Limit exceeded. Contact Support'
                            }, status=status.HTTP_429_TOO_MANY_REQUESTS)
                        old.otp_count = count + 1
                        old.otp_code = key
                        old.save()
                        status_code, response, response_json = push_to_sparrow(otp=key, phone_number=phone_number)
                        return Response({
                            'status': status_code,
                            'details': response_json
                        }, status=status_code)
            elif not user_with_phone.exists() and not phone_in_temp_db.exists():
                key = generate_otp(phone_number)
                print('======new number=================', phone_number)
                status_code, response, response_json = push_to_sparrow(otp=key, phone_number=phone_number)
                PhoneOtp.objects.create(phone_number=phone_number, otp_code=key, otp_count=1)
                return Response({
                    'status': status_code,
                    'details': response_json
                }, status=status_code)
            else:
                return Response({
                    'status': 403,
                    'details': 'Failed to send OTP. Try again'
                }, status=status.HTTP_403_FORBIDDEN)
        else:
            return Response({
                'status': 400,
                'details': 'Bad input for phone number in request.'
            }, status=status.HTTP_400_BAD_REQUEST)
Module to validate phone number integrity and call generate_otp, push_to_sparrow
class ValidateOTP(APIView):
    permission_classes = [IsAuthenticated, ]

    def post(self, request, *args, **kwargs):
        phone_number_sent_by_user = request.data.get('phone', False)
        otp_sent_by_user = request.data.get('otp', False)
        username_sent = request.data.get('username', False)

        if otp_sent_by_user and phone_number_sent_by_user and username_sent:
            username_queryset = AccountUser.objects.filter(username__iexact=username_sent)
            phone_queryset = PhoneOtp.objects.filter(phone_number__iexact=phone_number_sent_by_user)
            if username_queryset.exists() and phone_queryset.exists():
                verified_qs = username_queryset.values('phone_verified')[0].values()
                verified_status = list(verified_qs)[0]
                if verified_status is True:
                    return Response({
                        'status': 403,
                        'details': 'Either phone or user is already verified.'
                    }, status=status.HTTP_403_FORBIDDEN)
                user_qs = username_queryset.first()
                qs = phone_queryset.first()
                otp_in_db = qs.otp_code

                if str(otp_sent_by_user) == otp_in_db:
                    qs.validated = True
                    user_qs.phone_verified = True
                    user_qs.phone_number = phone_number_sent_by_user
                    qs.save()
                    user_qs.save()
                    return Response({
                        'status': 200,
                        'details': 'Success! You have verified your phone number.'
                    }, status=status.HTTP_200_OK)
                else:
                    return Response({
                        'status': 400,
                        'details': 'Incorrect OTP. Try again'
                    }, status=status.HTTP_400_BAD_REQUEST)
            else:
                return Response({
                    'status': 400,
                    'details': 'Phone number or username is mistaken. Please check.'
                }, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response({
                'status': 400,
                'details': 'Invalid phone, username or otp provided.'
            }, status=status.HTTP_400_BAD_REQUEST)
Module to validate the correctness of OTP, Phone number and Database entries
I edit and update this article timely. Thank you

Subscribe to verbose tethics

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe