Skip to content

Commit 7888dc9

Browse files
committed
add permission and token validation
1 parent 9749a43 commit 7888dc9

11 files changed

Lines changed: 165 additions & 108 deletions

File tree

server/api/apps/auth/passwords/apps.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

server/api/apps/auth/passwords/urls.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

server/api/apps/auth/passwords/views.py

Lines changed: 0 additions & 85 deletions
This file was deleted.
File renamed without changes.
File renamed without changes.
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from rest_framework import serializers
2-
from api.apps.users.models import User
2+
from django.contrib.auth.password_validation import validate_password
3+
34

45
class ChangePasswordSerializer(serializers.Serializer):
56
email = serializers.CharField(required=True)
67
new_password = serializers.CharField(required=True)
78

9+
def validate_new_password(self, value):
10+
validate_password(value)
11+
return value
12+
813

914
class EmailSerializer(serializers.Serializer):
1015
email = serializers.EmailField()
1116

1217
class Meta:
13-
fields = ("email")
18+
fields = "email"

server/api/apps/auth/reset/urls.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.urls import path
2+
from .views import ResetPasswordView, EmailResetLinkView
3+
4+
urlpatterns = [
5+
path("email/", EmailResetLinkView.as_view(), name="send-reset-email"),
6+
path(
7+
"",
8+
ResetPasswordView.as_view(),
9+
name="reset",
10+
),
11+
]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from smtplib import SMTPAuthenticationError
2+
3+
from django.conf import settings
4+
from django.contrib.auth import get_user_model
5+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
6+
from django.core.mail import send_mail
7+
from django.utils.encoding import force_bytes, force_str
8+
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
9+
from rest_framework import generics, status
10+
from rest_framework.response import Response
11+
12+
from .serializers import ChangePasswordSerializer, EmailSerializer
13+
14+
15+
# Create your views here.
16+
class ResetPasswordView(generics.RetrieveUpdateAPIView):
17+
"""
18+
An endpoint for changing password.
19+
"""
20+
21+
serializer_class = ChangePasswordSerializer
22+
23+
def validate_token(self, request):
24+
token = request.GET.get("token", "").rstrip("/")
25+
uid = None
26+
try:
27+
uidb64 = request.GET.get("uid", "").rstrip("/")
28+
if uidb64 != "":
29+
uid = force_str(urlsafe_base64_decode(uidb64))
30+
except ValueError:
31+
pass
32+
33+
user = get_user_model().objects.filter(email=uid).first()
34+
35+
is_valid_token = PasswordResetTokenGenerator().check_token(
36+
token=token, user=user
37+
)
38+
return is_valid_token
39+
40+
def get(self, request, *args, **kwargs):
41+
if not self.validate_token(request):
42+
return Response(
43+
{
44+
"message": (
45+
"the password reset token is invalid or has expired."
46+
)
47+
},
48+
status=status.HTTP_400_BAD_REQUEST,
49+
)
50+
51+
return Response(
52+
{"message": "the password reset token is valid"},
53+
status=status.HTTP_200_OK,
54+
)
55+
56+
def update(self, request, *args, **kwargs):
57+
if not self.validate_token(request):
58+
return Response(
59+
{
60+
"message": (
61+
"the password reset token is invalid or has expired."
62+
)
63+
},
64+
status=status.HTTP_400_BAD_REQUEST,
65+
)
66+
67+
serializer = self.get_serializer(data=request.data)
68+
69+
if not serializer.is_valid():
70+
return Response(
71+
serializer.errors, status=status.HTTP_400_BAD_REQUEST
72+
)
73+
74+
user = (
75+
get_user_model()
76+
.objects.filter(email=serializer.data.get("email"))
77+
.first()
78+
)
79+
80+
if not user:
81+
return Response(
82+
{"message": "Email does not exist"},
83+
status=status.HTTP_400_BAD_REQUEST,
84+
)
85+
86+
user.set_password(serializer.data.get("new_password"))
87+
user.save()
88+
89+
return Response(
90+
{"message": "Password updated successfully"},
91+
status=status.HTTP_200_OK,
92+
)
93+
94+
95+
class EmailResetLinkView(generics.GenericAPIView):
96+
"""
97+
An endpoint for emailing reset password links.
98+
"""
99+
100+
serializer_class = EmailSerializer
101+
102+
def post(self, request):
103+
serializer = self.get_serializer(data=request.data)
104+
if not serializer.is_valid():
105+
return Response(
106+
serializer.errors, status=status.HTTP_400_BAD_REQUEST
107+
)
108+
109+
email = serializer.data["email"]
110+
user = get_user_model().objects.filter(email=email).first()
111+
112+
if not user:
113+
return Response(
114+
{"message": "Email does not exist"},
115+
status=status.HTTP_400_BAD_REQUEST,
116+
)
117+
118+
token = PasswordResetTokenGenerator().make_token(user)
119+
uid = urlsafe_base64_encode(force_bytes(user.email))
120+
121+
reset_url = f"{settings.FRONTEND_URL}/reset/?token={token}&uid={uid}/"
122+
123+
try:
124+
send_mail(
125+
"Elucidate Password Reset",
126+
f"Your password reset link can be found at: {reset_url}\n"
127+
+ "It will expire in"
128+
f" {settings.PASSWORD_RESET_TIMEOUT / 60} minutes.",
129+
settings.EMAIL_HOST_USER,
130+
[email],
131+
fail_silently=False,
132+
)
133+
except SMTPAuthenticationError:
134+
return Response(
135+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
136+
)
137+
138+
return Response(
139+
{"message": "Email has been sent"}, status=status.HTTP_200_OK
140+
)

server/api/apps/auth/serializers.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1+
from django.contrib.auth import get_user_model
12
from django.contrib.auth.password_validation import validate_password
23
from rest_framework import serializers
34
from rest_framework.serializers import ValidationError
45

5-
from api.apps.users.models import User
6-
76

87
class RegistrationSerializer(serializers.ModelSerializer):
98
class Meta:
10-
model = User
9+
model = get_user_model()
1110
fields = (
1211
"id",
1312
"email",
@@ -33,6 +32,6 @@ def validate_password(self, value):
3332
return value
3433

3534
def create(self, validated_data):
36-
user = User.objects.create_user(**validated_data)
35+
user = get_user_model().objects.create_user(**validated_data)
3736

3837
return user

server/api/apps/auth/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
path("refresh/", refresh_jwt_token, name="refresh-jwt-token"),
1313
path("verify/", verify_jwt_token, name="verify-jwt-token"),
1414
path("register/", RegistrationView.as_view(), name="register"),
15-
path("reset/", include("api.apps.auth.passwords.urls")),
15+
path("reset/", include("api.apps.auth.reset.urls")),
1616
]

0 commit comments

Comments
 (0)