Skip to content

Commit 2393eaf

Browse files
authored
Merge pull request #8 from devvspaces/2-forget-password-api
Complete and tested the forget password flow
2 parents c25c3d9 + 394a36a commit 2393eaf

File tree

4 files changed

+98
-26
lines changed

4 files changed

+98
-26
lines changed

src/account/api/base/serializers.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from django.contrib.auth.password_validation import validate_password
33
from rest_framework import serializers
44
from django.contrib.auth.models import AbstractBaseUser
5+
from django.contrib.auth import get_user_model
6+
from django.utils.encoding import force_str
7+
from django.utils.http import urlsafe_base64_decode
8+
from account.api.base.tokens import TokenGenerator
59

610
from account.models import Profile, User
711

@@ -66,6 +70,11 @@ def validate(self, attrs):
6670
return attrs
6771

6872

73+
class ForgetPasswordTokenSerializer(serializers.Serializer):
74+
token = serializers.CharField(required=True)
75+
uidb64 = serializers.CharField(required=True)
76+
77+
6978
class ValidateRegistrationOtpSerializer(ValidateOtpSerializer):
7079
def save(self, **kwargs):
7180
user: User = self.validated_data['email']
@@ -140,7 +149,29 @@ class ForgetPasswordSerializer(serializers.Serializer):
140149
write_only=True, required=True, validators=[validate_password])
141150

142151
def validate(self, attrs):
143-
# Validate the uidb64 and token
152+
"""
153+
Validate the token and uidb64
154+
"""
155+
uidb64 = attrs['uidb64']
156+
token = attrs['token']
157+
158+
User = get_user_model()
159+
user = None
160+
161+
try:
162+
uidb64 = force_str(urlsafe_base64_decode(uidb64))
163+
user = User.objects.get(id=uidb64)
164+
except (
165+
TypeError, ValueError, OverflowError,
166+
User.DoesNotExist
167+
):
168+
raise serializers.ValidationError('Invalid token')
169+
170+
generator = TokenGenerator()
171+
if not generator.check_token(user, token):
172+
raise serializers.ValidationError('Invalid token')
173+
174+
attrs['user'] = user
144175
return attrs
145176

146177
def save(self, **kwargs):

src/account/api/base/tokens.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import six
2+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
3+
4+
5+
class TokenGenerator(PasswordResetTokenGenerator):
6+
def _make_hash_value(self, user, timestamp):
7+
return (
8+
six.text_type(user.pk) +
9+
six.text_type(timestamp) +
10+
six.text_type(user.password)
11+
)

src/account/api/base/urls.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
views.UserRetrieveUpdateAPIView.as_view(),
1515
name='user_retrieve_update'),
1616
path(
17-
'request-password-reset/',
17+
'forget-password/request-reset/',
1818
views.RequestForgetPasswordView.as_view(),
1919
name='request_password_reset'),
2020
path(
21-
'password-reset/',
21+
'forget-password/validate-otp/',
22+
views.ValidateForgetPasswordOtpView.as_view(),
23+
name='request_password_reset'),
24+
path(
25+
'forget-password/reset/',
2226
views.ForgetPasswordView.as_view(),
2327
name='password_reset'),
2428
path('token/user/refresh/',

src/account/api/base/views.py

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from base64 import urlsafe_b64encode
12
from drf_yasg.utils import swagger_auto_schema
23
from rest_framework import generics, status
34
from rest_framework.response import Response
45
from rest_framework.views import APIView
56
from rest_framework_simplejwt.authentication import JWTAuthentication
67
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
78
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
9+
from account.api.base.tokens import TokenGenerator
10+
from django.utils.encoding import force_bytes
11+
from django.utils.http import urlsafe_base64_encode
812

913
from account.models import User
1014
from utils.base.general import get_tokens_for_user
@@ -60,28 +64,6 @@ def post(self, request, *args, **kwargs):
6064
)
6165

6266

63-
class ValidateForgetPasswordOtpView(generics.GenericAPIView):
64-
"""
65-
Validate the forget password otp sent to user's email.
66-
67-
Return a token to be used for resetting password.
68-
"""
69-
permission_classes = []
70-
serializer_class = serializers.ValidateRegistrationOtpSerializer
71-
72-
def post(self, request, *args, **kwargs):
73-
serializer = self.serializer_class(data=request.data)
74-
serializer.is_valid(raise_exception=True)
75-
76-
# Create a token for user to reset password
77-
# uidb64 and token are used to identify the user
78-
79-
return Response(
80-
data=serializer.data,
81-
status=status.HTTP_201_CREATED
82-
)
83-
84-
8567
class TokenVerifyAPIView(APIView):
8668
"""
8769
An authentication plugin that checks if a jwt
@@ -170,6 +152,12 @@ def get_queryset(self):
170152

171153

172154
class RequestForgetPasswordView(generics.GenericAPIView):
155+
"""
156+
Request a password reset email (otp).
157+
158+
Otp is sent to user's email.
159+
"""
160+
173161
serializer_class = serializers.RequestForgetPasswordSerializer
174162
permission_classes = []
175163

@@ -180,7 +168,45 @@ def post(self, request, *args, **kwargs):
180168
return Response(data=serializer.data)
181169

182170

171+
class ValidateForgetPasswordOtpView(generics.GenericAPIView):
172+
"""
173+
Validate the forget password otp sent to user's email.
174+
175+
Return a token to be used for resetting password.
176+
"""
177+
permission_classes = []
178+
serializer_class = serializers.ValidateOtpSerializer
179+
180+
@swagger_auto_schema(
181+
responses={200: serializers.ForgetPasswordTokenSerializer}
182+
)
183+
def post(self, request, *args, **kwargs):
184+
serializer = self.serializer_class(data=request.data)
185+
serializer.is_valid(raise_exception=True)
186+
187+
# Create a token for user to reset password
188+
generator = TokenGenerator()
189+
user = serializer.validated_data['email']
190+
uidb64 = urlsafe_base64_encode(force_bytes(user.pk))
191+
token = generator.make_token(user)
192+
# uidb64 and token are used to identify the user
193+
194+
return Response(
195+
data={
196+
'uidb64': uidb64,
197+
'token': token
198+
},
199+
status=status.HTTP_201_CREATED
200+
)
201+
202+
183203
class ForgetPasswordView(generics.GenericAPIView):
204+
"""
205+
Reset password using the token received by validating the otp.
206+
207+
User password will be reset to the new password.
208+
Returns a new access and refresh token including user details.
209+
"""
184210
serializer_class = serializers.ForgetPasswordSerializer
185211
permission_classes = []
186212

@@ -192,7 +218,7 @@ class ForgetPasswordView(generics.GenericAPIView):
192218
def post(self, request, *args, **kwargs):
193219
serializer = self.serializer_class(data=request.data)
194220
serializer.is_valid(raise_exception=True)
195-
user: User = serializer.validated_data.get('user')
221+
user = serializer.save()
196222
user_details = serializers.UserSerializer(user).data
197223
response_data = {
198224
'tokens': get_tokens_for_user(user),

0 commit comments

Comments
 (0)