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!\n Use { otp } to reset your password.\n It 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!\n Your OTP for BotoApp is { otp } .\n It expires in 10 minutes" ,
251+ 'phone' : user .phone
252+ }
253+ send_phone_notification .delay (message_info )
254+ return user
0 commit comments