Skip to content

Commit 63a2d66

Browse files
committed
feat: add twillio functionality and task queue
1 parent 3b4ab42 commit 63a2d66

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

app/user/models.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,94 @@
22
from datetime import datetime, timezone
33

44

5+
from core.models import AuditableModel
6+
from django.conf import settings
7+
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
8+
from django.db import models
9+
from django.utils.translation import gettext_lazy as _
10+
from django.contrib.postgres.fields import ArrayField
11+
from .enums import TOKEN_TYPE_CHOICE, ROLE_CHOICE
12+
from .managers import CustomUserManager
13+
14+
15+
def default_role():
16+
return ["CUSTOMER"]
17+
18+
19+
class User(AbstractBaseUser, PermissionsMixin):
20+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
21+
email = models.EmailField(
22+
_("email address"), null=True, blank=True, unique=True)
23+
password = models.CharField(max_length=255, null=True)
24+
firstname = models.CharField(max_length=255, blank=True, null=True)
25+
lastname = models.CharField(max_length=255, blank=True, null=True)
26+
image = models.FileField(upload_to="users/", blank=True, null=True)
27+
phone = models.CharField(max_length=30, unique=True, blank=True, null=True)
28+
is_locked = models.BooleanField(default=False)
29+
is_staff = models.BooleanField(default=False)
30+
is_active = models.BooleanField(default=False)
31+
is_admin = models.BooleanField(default=False)
32+
last_login = models.DateTimeField(null=True, blank=True)
33+
created_at = models.DateTimeField(auto_now_add=True)
34+
updated_at = models.DateTimeField(auto_now=True)
35+
verified = models.BooleanField(default=False)
36+
USERNAME_FIELD = "phone"
37+
REQUIRED_FIELDS = []
38+
objects = CustomUserManager()
39+
roles = ArrayField(models.CharField(max_length=20, blank=True,
40+
choices=ROLE_CHOICE), default=default_role, size=6)
41+
42+
class Meta:
43+
ordering = ("-created_at",)
44+
45+
def __str__(self) -> str:
46+
return self.phone
47+
48+
def save_last_login(self) -> None:
49+
self.last_login = datetime.now()
50+
self.save()
51+
52+
53+
class PendingUser(AuditableModel):
54+
phone = models.CharField(max_length=20)
55+
verification_code = models.CharField(max_length=8, blank=True, null=True)
56+
password = models.CharField(max_length=255, null=True)
57+
58+
59+
def __str__(self):
60+
return f"{str(self.phone)} {self.verification_code}"
61+
62+
def is_valid(self) -> bool:
63+
"""10 mins OTP validation"""
64+
lifespan_in_seconds = float(settings.OTP_EXPIRE_TIME * 60)
65+
now = datetime.now(timezone.utc)
66+
time_diff = now - self.created_at
67+
time_diff = time_diff.total_seconds()
68+
if time_diff >= lifespan_in_seconds:
69+
return False
70+
return True
71+
72+
73+
class Token(models.Model):
74+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
75+
user = models.ForeignKey(settings.AUTH_USER_MODEL,
76+
on_delete=models.CASCADE)
77+
token = models.CharField(max_length=8)
78+
token_type = models.CharField(max_length=100, choices=TOKEN_TYPE_CHOICE)
79+
created_at = models.DateTimeField(auto_now_add=True)
80+
81+
def __str__(self):
82+
return f"{str(self.user)} {self.token}"
83+
84+
def is_valid(self) -> bool:
85+
lifespan_in_seconds = float(settings.TOKEN_LIFESPAN * 60 )
86+
now = datetime.now(timezone.utc)
87+
time_diff = now - self.created_at
88+
time_diff = time_diff.total_seconds()
89+
if time_diff >= lifespan_in_seconds:
90+
return False
91+
return True
92+
93+
def reset_user_password(self, password: str) -> None:
94+
self.user.set_password(password)
95+
self.user.save()

app/user/task.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from core.celery import APP
2+
from .utils import send_sms
3+
#celery for asynchronous processing, to send phone notifications asynchronously.
4+
5+
6+
@APP.task()
7+
def send_phone_notification(user_data):
8+
send_sms(user_data['message'], user_data['phone'])
9+

app/user/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import base64
2+
import os
3+
import re
4+
import pyotp
5+
from django.conf import settings
6+
from rest_framework import permissions, serializers
7+
from twilio.rest import Client
8+
9+
from .enums import SystemRoleEnum
10+
from .models import User
11+
12+
client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
13+
14+
def get_user_role_names(user:User)->list:
15+
"""
16+
Returns a list of role names for the given user.
17+
"""
18+
return user.roles.values_list('name', flat=True)
19+
20+
def is_admin_user(user:User)->bool:
21+
"""
22+
Check an authenticated user is an admin or not
23+
"""
24+
return user.is_admin or SystemRoleEnum.ADMIN in user.roles
25+
26+
27+
class IsAdmin(permissions.BasePermission):
28+
"""Allows access only to Admin users."""
29+
message = "Only Admins are authorized to perform this action."
30+
31+
def has_permission(self, request, view):
32+
if not request.user.is_authenticated:
33+
return False
34+
return is_admin_user(request.user)
35+
36+
37+
def send_sms(message:str, phone:str):
38+
client.messages.create(
39+
body=message,
40+
from_=settings.TWILIO_PHONE_NUMBER,
41+
to=phone
42+
)
43+
return
44+
45+
46+
def clean_phone(number:str):
47+
"""Validates number start with +254 or 0, then 10 digits"""
48+
number_pattern = re.compile(r'^(?:\+234|0)\d{10}$')
49+
result = number_pattern.match(number)
50+
if result:
51+
if number.startswith('0'):
52+
return '+254' + number[1:]
53+
return number
54+
else:
55+
raise serializers.ValidationError({'phone': 'Incorrect phone number.'})
56+
57+
58+
def generate_otp()->int:
59+
totp = pyotp.TOTP(base64.b32encode(os.urandom(16)).decode('utf-8'))
60+
otp = totp.now()
61+
return otp

0 commit comments

Comments
 (0)