Skip to content

Commit 1b81cbc

Browse files
authored
Merge pull request #23 from DevOps-Cloud-Team5/SCRUM-31_change_pw
password reset done
2 parents 191ad73 + fcf43b5 commit 1b81cbc

9 files changed

+158
-10
lines changed

.env_example

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
SECRET_KEY=<django_secret>
2+
3+
DB_NAME=<db_name>
4+
DB_HOST=<db_host>
5+
DB_USER=<db_user>
6+
DB_PORT=<db_port>
7+
DB_PASSWORD=<db_password>
8+
9+
AWS_ACCESS_KEY_ID=<aws_access_id>
10+
AWS_SECRET_ACCESS_KEY=<aws_access_key>
11+
AWS_STORAGE_BUCKET_NAME=<aws_bucket_name>
12+
13+
EMAIL_HOST=<email_host>
14+
EMAIL_PORT=<email_port>
15+
EMAIL_USER=<email_user>
16+
EMAIL_PASSWORD=<email_password>
17+
18+
FRONTEND_URL=<frontend_url> # No / after url

api/apps.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
class ApiConfig(AppConfig):
55
default_auto_field = 'django.db.models.BigAutoField'
66
name = 'api'
7+
8+
def ready(self):
9+
import api.signals

api/serializers.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,8 @@ def validate_attendence(self, username, attended):
138138

139139
def validate(self, attrs):
140140
for username, attended in attrs["usernames"].items(): self.validate_attendence(username, attended)
141-
return attrs
141+
return attrs
142+
143+
class MailTestSerializer(serializers.Serializer):
144+
email = serializers.CharField(required=True)
145+

api/settings.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
TEMPLATES = [
2828
{
2929
'BACKEND': 'django.template.backends.django.DjangoTemplates',
30-
'DIRS': [],
30+
'DIRS': [os.path.join(BASE_DIR, 'api', 'templates')],
3131
'APP_DIRS': True,
3232
'OPTIONS': {
3333
'context_processors': [
@@ -81,8 +81,9 @@
8181
'django.contrib.staticfiles',
8282
'corsheaders',
8383
'rest_framework',
84-
'drf_spectacular',
84+
'django_rest_passwordreset',
8585
'rest_framework_simplejwt.token_blacklist',
86+
'drf_spectacular',
8687
"api.apps.ApiConfig"
8788
]
8889

@@ -92,6 +93,15 @@
9293

9394
AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID')
9495

96+
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
97+
EMAIL_HOST = config('EMAIL_HOST')
98+
EMAIL_PORT = config('EMAIL_PORT')
99+
EMAIL_USE_TLS = True
100+
EMAIL_HOST_USER = config('EMAIL_USER')
101+
EMAIL_HOST_PASSWORD = config('EMAIL_PASSWORD')
102+
103+
FRONTEND_URL = config('FRONTEND_URL')
104+
95105
CORS_ALLOW_HEADERS = [
96106
'x-requested-with',
97107
'content-type',
@@ -141,4 +151,19 @@
141151
'rest_framework_simplejwt.authentication.JWTAuthentication',
142152
),
143153
'AUTH_HEADER_TYPES': ('JWT',),
144-
}
154+
}
155+
156+
DJANGO_REST_PASSWORDRESET_TOKEN_CONFIG = {
157+
"CLASS": "django_rest_passwordreset.tokens.RandomStringTokenGenerator",
158+
"OPTIONS": {
159+
"min_length": 20,
160+
"max_length": 30
161+
}
162+
}
163+
DJANGO_REST_MULTITOKENAUTH_RESET_TOKEN_EXPIRY_TIME = 30 # minutes
164+
165+
DJANGO_REST_PASSWORDRESET_EMAIL_TEMPLATES = {
166+
'subject': 'templates/email/password_reset_subject.txt',
167+
'plain_body': 'templates/email/password_reset_email.html',
168+
'html_body': 'templates/email/password_reset_email.html',
169+
}

api/signals.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from django.core.mail import EmailMultiAlternatives
2+
from django.dispatch import receiver
3+
from django.template.loader import render_to_string
4+
from django.urls import reverse
5+
from django.conf import settings
6+
7+
8+
from django_rest_passwordreset.signals import reset_password_token_created
9+
10+
11+
@receiver(reset_password_token_created)
12+
def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs):
13+
"""
14+
Handles password reset tokens
15+
When a token is created, an e-mail needs to be sent to the user
16+
:param sender: View Class that sent the signal
17+
:param instance: View Instance that sent the signal
18+
:param reset_password_token: Token Model Object
19+
:param args:
20+
:param kwargs:
21+
:return:
22+
"""
23+
# send an e-mail to the user
24+
context = {
25+
'current_user': reset_password_token.user,
26+
'username': reset_password_token.user.username,
27+
'email': reset_password_token.user.email,
28+
'reset_password_url': f"{settings.FRONTEND_URL}/reset_password?token={reset_password_token.key}"
29+
}
30+
31+
# render email text
32+
email_html_message = render_to_string('email/password_reset_email.html', context)
33+
email_plaintext_message = render_to_string('email/password_reset_email.txt', context)
34+
35+
msg = EmailMultiAlternatives(
36+
# title:
37+
"Password Reset for Attendunce",
38+
# message:
39+
email_plaintext_message,
40+
# from:
41+
"[email protected]", # Does not matter, it will be overridden
42+
# to:
43+
[reset_password_token.user.email]
44+
)
45+
msg.attach_alternative(email_html_message, "text/html")
46+
msg.send()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<p>Hello,</p>
2+
3+
<p>We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:</p>
4+
5+
<p><a href="{{ reset_password_url }}">{{ reset_password_url }}</a></p>
6+
7+
<p>The link will be valid for the next 30 minutes</p>
8+
9+
<p>If you did not request to reset your password, please ignore this email and your password will remain unchanged.</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Hello,
2+
3+
We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:</p>
4+
5+
{{ reset_password_url }}
6+
7+
The link will be valid for the next 30 minutes
8+
9+
If you did not request to reset your password, please ignore this email and your password will remain unchanged.

api/urls.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
1. Import the include() function: from django.urls import include, path
1414
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
1515
"""
16-
from django.urls import path
16+
from django.urls import include, path
1717
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView, TokenBlacklistView
1818
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
19+
from django_rest_passwordreset.views import ResetPasswordConfirm, ResetPasswordValidateToken
1920
from .views import (
2021
AddLectureView,
2122
GetCourseLecturesView,
@@ -24,8 +25,9 @@
2425
MassEnrollCourseView,
2526
SetStudentAttView,
2627
SetTeacherAttView,
28+
test,
29+
MailTestView,
2730
UnsetStudentAttView,
28-
test,
2931
genAdmin,
3032

3133
GetTokenView,
@@ -41,18 +43,24 @@
4143
DestroyCourseView,
4244
EnrollCourseView,
4345
GetCourseByName,
44-
GetCoursesAll,
45-
)
46+
GetCoursesAll
47+
)
4648

4749
urlpatterns = [
4850
path('test', test),
51+
path('send_welcome_email', MailTestView.as_view(), name='send_welcome_email'),
4952
path('genadmin', genAdmin),
5053

5154
# Manage tokens, by requesting one with credentials, refreshing or verifying one. Essentially the login API
5255
path('token/', GetTokenView.as_view(), name='token_obtain_pair'),
5356
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
5457
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
5558
path('token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
59+
60+
# Password resets
61+
path('password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')),
62+
path('password_reset/confirm', ResetPasswordConfirm.as_view(), name='password_reset_confirm'),
63+
path('password_reset/validate_token', ResetPasswordValidateToken.as_view(), name='password_reset_validate_token'),
5664

5765
# All user paths
5866
path('user/register/', CreateUserView.as_view(), name='user_register'),

api/views.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
from .permissions import IsTeacher, IsAdmin, IsStudent
1818
from .models import Course, AccountRoles, CourseLecture
19-
from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer
19+
from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer, MailTestSerializer
2020

21-
import pdb
21+
from django.core.mail import send_mail
2222

2323

2424
User = get_user_model()
@@ -37,6 +37,32 @@
3737
def test(request):
3838
return Response({"ping": "pong"}, 200)
3939

40+
@extend_schema(
41+
summary="Send Test Email",
42+
description="Tries to send a test email.",
43+
responses={
44+
status.HTTP_200_OK: OpenApiResponse(description="Success - An email was sent."),
45+
status.HTTP_400_BAD_REQUEST: OpenApiResponse(description="Failure - No email given as a parameter."),
46+
status.HTTP_418_IM_A_TEAPOT: OpenApiResponse(description="Failure - The email could not be sent.")
47+
}
48+
)
49+
class MailTestView(generics.CreateAPIView):
50+
serializer_class = MailTestSerializer
51+
52+
def post(self, request, *args, **kwargs):
53+
subject = 'Welcome to My Site'
54+
message = 'Thank you for creating an account!'
55+
from_email = '[email protected]'
56+
recipient_list = [request.POST.get("email", "")]
57+
if recipient_list[0] == "":
58+
Response({"status": "fail"}, 400)
59+
ret_code = send_mail(subject, message, from_email, recipient_list)
60+
if ret_code == 1:
61+
Response({"status": "success"}, 200)
62+
else:
63+
Response({"status": "fail", "return_code": ret_code}, 418)
64+
65+
4066
# DEBUG function
4167
# Generate an admin account if no admin accounts exists
4268
# Dangerous endpoint which should be turned off in production but can be used to setup the DB

0 commit comments

Comments
 (0)