Skip to content

Commit 6c3c722

Browse files
committed
feat: Implement User Authentication and Serializers
1 parent 63a2d66 commit 6c3c722

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

app/user/filters.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import django_filters
2+
3+
from .enums import ROLE_CHOICE
4+
from .models import User
5+
6+
7+
class UserFilter(django_filters.FilterSet):
8+
class Meta:
9+
model = User
10+
fields = ['verified']

app/user/serializers.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
from datetime import datetime, timezone
2+
3+
from django.contrib.auth import authenticate, get_user_model
4+
from django.contrib.auth.hashers import make_password
5+
from django.db import transaction
6+
from django.utils.translation import gettext_lazy as _
7+
from rest_framework import exceptions, serializers
8+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
9+
10+
from .enums import TokenEnum
11+
from .models import PendingUser, Token, User
12+
from .tasks import send_phone_notification
13+
from .utils import clean_phone, generate_otp, is_admin_user
14+
15+
16+
class CustomObtainTokenPairSerializer(TokenObtainPairSerializer):
17+
18+
def validate(self, attrs):
19+
data = super().validate(attrs)
20+
refresh = self.get_token(self.user)
21+
access_token = refresh.access_token
22+
self.user.save_last_login()
23+
data['refresh'] = str(refresh)
24+
data['access'] = str(access_token)
25+
return data
26+
27+
@classmethod
28+
def get_token(cls, user: User):
29+
if not user.verified:
30+
raise exceptions.AuthenticationFailed(
31+
_('Account not verified.'), code='authentication')
32+
token = super().get_token(user)
33+
token.id = user.id
34+
token['firstname'] = user.firstname
35+
token['lastname'] = user.lastname
36+
token["email"] = user.email
37+
token["roles"] = user.roles
38+
return token
39+
40+
41+
class AuthTokenSerializer(serializers.Serializer):
42+
"""Serializer for user authentication object"""
43+
44+
email = serializers.CharField()
45+
password = serializers.CharField(
46+
style={"input_type": "password"}, trim_whitespace=False)
47+
48+
def validate(self, attrs):
49+
"""Validate and authenticate the user"""
50+
email = attrs.get("email")
51+
password = attrs.get("password")
52+
if email:
53+
user = authenticate(request=self.context.get(
54+
"request"), username=email.lower().strip(), password=password)
55+
56+
if not user:
57+
msg = _("Unable to authenticate with provided credentials")
58+
raise serializers.ValidationError(msg, code="authentication")
59+
attrs["user"] = user
60+
return attrs
61+
62+
63+
class PasswordChangeSerializer(serializers.Serializer):
64+
old_password = serializers.CharField(max_length=128, required=False)
65+
new_password = serializers.CharField(max_length=128, min_length=5)
66+
67+
def validate_old_password(self, value):
68+
request = self.context["request"]
69+
70+
if not request.user.check_password(value):
71+
raise serializers.ValidationError("Old password is incorrect.")
72+
return value
73+
74+
def save(self):
75+
user: User = self.context["request"].user
76+
new_password = self.validated_data["new_password"]
77+
user.set_password(new_password)
78+
user.save(update_fields=["password"])
79+
80+
81+
class CreatePasswordFromResetOTPSerializer(serializers.Serializer):
82+
otp = serializers.CharField(required=True)
83+
new_password = serializers.CharField(required=True)
84+
85+
86+
class AccountVerificationSerializer(serializers.Serializer):
87+
otp = serializers.CharField(required=True)
88+
phone = serializers.CharField(required=True, allow_blank=False)
89+
90+
def validate(self, attrs: dict):
91+
phone_number: str = attrs.get('phone').strip().lower()
92+
mobile: str = clean_phone(phone_number)
93+
pending_user: PendingUser = PendingUser.objects.filter(
94+
phone=mobile, verification_code=attrs.get('otp')).first()
95+
if pending_user and pending_user.is_valid():
96+
attrs['phone'] = mobile
97+
attrs['password'] = pending_user.password
98+
attrs['pending_user'] = pending_user
99+
else:
100+
raise serializers.ValidationError(
101+
{'otp': 'Verification failed. Invalid OTP or Number'})
102+
return super().validate(attrs)
103+
104+
@transaction.atomic
105+
def create(self, validated_data: dict):
106+
validated_data.pop('otp')
107+
pending_user = validated_data.pop('pending_user')
108+
User.objects.create_user_with_phone(**validated_data)
109+
pending_user.delete()
110+
return validated_data
111+
112+
113+
class EmailSerializer(serializers.Serializer):
114+
email = serializers.EmailField(required=True)
115+
116+
117+
class InitiatePasswordResetSerializer(serializers.Serializer):
118+
phone = serializers.CharField(required=True, allow_blank=False)
119+
120+
def validate(self, attrs: dict):
121+
phone = attrs.get('phone')
122+
strip_number = phone.lower().strip()
123+
mobile = clean_phone(strip_number)
124+
user = get_user_model().objects.filter(phone=mobile, is_active=True).first()
125+
if not user:
126+
raise serializers.ValidationError({'phone':'Phone number not registered.'})
127+
attrs['phone'] = mobile
128+
attrs['user'] = user
129+
return super().validate(attrs)
130+
131+
def create(self, validated_data):
132+
phone = validated_data.get('phone')
133+
user = validated_data.get('user')
134+
otp = generate_otp()
135+
token,_ = Token.objects.update_or_create(
136+
user=user,
137+
token_type=TokenEnum.PASSWORD_RESET,
138+
defaults={
139+
"user": user,
140+
"token_type": TokenEnum.PASSWORD_RESET,
141+
"token": otp,
142+
"created_at": datetime.now(timezone.utc)
143+
}
144+
)
145+
146+
message_info = {
147+
'message': f"Password Reset!\nUse {otp} to reset your password.\nIt expires in 10 minutes",
148+
'phone': phone
149+
}
150+
151+
send_phone_notification.delay(message_info)
152+
return token
153+
154+
155+
class ListUserSerializer(serializers.ModelSerializer):
156+
class Meta:
157+
model = get_user_model()
158+
fields = [
159+
"id",
160+
"firstname",
161+
"lastname",
162+
"email",
163+
"image",
164+
"verified",
165+
"created_at",
166+
"roles",
167+
]
168+
169+
extra_kwargs = {
170+
"verified": {"read_only": True},
171+
"roles": {"read_only": True},
172+
}
173+
174+
def to_representation(self, instance):
175+
return super().to_representation(instance)
176+
177+
178+
class UpdateUserSerializer(serializers.ModelSerializer):
179+
class Meta:
180+
model = get_user_model()
181+
fields = [
182+
"id",
183+
"firstname",
184+
"lastname",
185+
"image",
186+
"verified",
187+
"roles"
188+
]
189+
extra_kwargs = {
190+
"last_login": {"read_only": True},
191+
"verified": {"read_only": True},
192+
"roles": {"required": False},
193+
}
194+
195+
def validate(self, attrs: dict):
196+
"""Only allow admin to modify/assign role"""
197+
auth_user: User = self.context["request"].user
198+
new_role_assignment = attrs.get("roles", None)
199+
if new_role_assignment and is_admin_user(auth_user):
200+
pass
201+
else:
202+
attrs.pop('roles', None)
203+
return super().validate(attrs)
204+
205+
def update(self, instance, validated_data):
206+
"""Prevent user from updating password"""
207+
if validated_data.get("password", False):
208+
validated_data.pop('password')
209+
instance = super().update(instance, validated_data)
210+
return instance
211+
212+
213+
class BasicUserInfoSerializer(serializers.ModelSerializer):
214+
class Meta:
215+
model = get_user_model()
216+
fields = [
217+
"firstname",
218+
"lastname",
219+
]
220+
221+
222+
class OnboardUserSerializer(serializers.Serializer):
223+
"""Serializer for creating user object"""
224+
phone = serializers.CharField(required=True, allow_blank=False)
225+
password = serializers.CharField(min_length=8)
226+
227+
def validate(self, attrs: dict):
228+
phone = attrs.get('phone')
229+
strip_number = phone.lower().strip()
230+
cleaned_number = clean_phone(strip_number)
231+
if get_user_model().objects.filter(phone__iexact=cleaned_number).exists():
232+
raise serializers.ValidationError(
233+
{'phone': 'Phone number already exists'})
234+
attrs['phone'] = cleaned_number
235+
return super().validate(attrs)
236+
237+
def create(self, validated_data: dict):
238+
otp = generate_otp()
239+
phone_number = validated_data.get('phone')
240+
user, _ = PendingUser.objects.update_or_create(
241+
phone=phone_number,
242+
defaults={
243+
"phone": phone_number,
244+
"verification_code": otp,
245+
"password": make_password(validated_data.get('password')),
246+
"created_at": datetime.now(timezone.utc)
247+
}
248+
)
249+
message_info = {
250+
'message': f"Account Verification!\nYour OTP for BotoApp is {otp}.\nIt expires in 10 minutes",
251+
'phone': user.phone
252+
}
253+
send_phone_notification.delay(message_info)
254+
return user
File renamed without changes.

0 commit comments

Comments
 (0)