Skip to content

Commit c391044

Browse files
authored
Merge branch 'main' into SCRUM-90-Course-info
2 parents 7931703 + 1b81cbc commit c391044

9 files changed

+160
-9
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
@@ -143,4 +143,8 @@ def validate_attendence(self, username, attended):
143143

144144
def validate(self, attrs):
145145
for username, attended in attrs["usernames"].items(): self.validate_attendence(username, attended)
146-
return attrs
146+
return attrs
147+
148+
class MailTestSerializer(serializers.Serializer):
149+
email = serializers.CharField(required=True)
150+

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,
@@ -25,8 +26,9 @@
2526
MassEnrollCourseView,
2627
SetStudentAttView,
2728
SetTeacherAttView,
29+
test,
30+
MailTestView,
2831
UnsetStudentAttView,
29-
test,
3032
genAdmin,
3133

3234
GetTokenView,
@@ -42,18 +44,24 @@
4244
DestroyCourseView,
4345
EnrollCourseView,
4446
GetCourseByName,
45-
GetCoursesAll,
46-
)
47+
GetCoursesAll
48+
)
4749

4850
urlpatterns = [
4951
path('test', test),
52+
path('send_welcome_email', MailTestView.as_view(), name='send_welcome_email'),
5053
path('genadmin', genAdmin),
5154

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

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

api/views.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

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

21-
import pdb
22+
23+
24+
from django.core.mail import send_mail
2225

2326

2427
User = get_user_model()
@@ -37,6 +40,32 @@
3740
def test(request):
3841
return Response({"ping": "pong"}, 200)
3942

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

0 commit comments

Comments
 (0)