Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/api/v1/v1_users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ class LoginSerializer(serializers.Serializer):
password = CustomCharField()


class ForgotPasswordSerializer(serializers.Serializer):
email = CustomEmailField()

def validate_email(self, email):
try:
user = SystemUser.objects.get(email=email)
except SystemUser.DoesNotExist:
raise ValidationError('Invalid email, user not found')
return user


class VerifyInviteSerializer(serializers.Serializer):
invite = CustomCharField()

Expand Down
13 changes: 13 additions & 0 deletions backend/api/v1/v1_users/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,16 @@ def test_login(self):
user,
content_type='application/json')
self.assertEqual(user.status_code, 400)

# test forgor password to valid email
user = {"email": "admin@rtmis.com"}
user = self.client.post('/api/v1/user/forgot-password',
user,
content_type='application/json')
self.assertEqual(user.status_code, 200)
# test forgor password to invalid email
user = {"email": "notuser@domain.com"}
user = self.client.post('/api/v1/user/forgot-password',
user,
content_type='application/json')
self.assertEqual(user.status_code, 400)
5 changes: 5 additions & 0 deletions backend/api/v1/v1_users/tests/tests_user_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ def test_get_email_template(self):
'/api/v1/email_template?type=user_approval',
content_type='application/json')
self.assertEqual(response.status_code, 200)
# test get user_forgot_password template
response = self.client.get(
'/api/v1/email_template?type=user_forgot_password',
content_type='application/json')
self.assertEqual(response.status_code, 200)
# test get data_approval template
response = self.client.get(
'/api/v1/email_template?type=data_approval',
Expand Down
5 changes: 3 additions & 2 deletions backend/api/v1/v1_users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from api.v1.v1_users.views import login, verify_invite, \
set_user_password, list_administration, add_user, list_users, \
get_profile, get_user_roles, list_levels, UserEditDeleteView
get_profile, get_user_roles, list_levels, UserEditDeleteView, \
forgot_password

urlpatterns = [
re_path(r'^(?P<version>(v1))/levels', list_levels),
Expand All @@ -14,10 +15,10 @@
re_path(r'^(?P<version>(v1))/users', list_users),
re_path(r'^(?P<version>(v1))/user/(?P<user_id>[0-9]+)',
UserEditDeleteView.as_view()),
re_path(r'^(?P<version>(v1))/user/forgot-password', forgot_password),
re_path(r'^(?P<version>(v1))/user/set-password', set_user_password),
re_path(r'^(?P<version>(v1))/user/roles', get_user_roles),
re_path(r'^(?P<version>(v1))/user', add_user),
re_path(r'^(?P<version>(v1))/invitation/(?P<invitation_id>.*)$',
verify_invite),

]
42 changes: 34 additions & 8 deletions backend/api/v1/v1_users/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Create your views here.
import os
import datetime
from math import ceil
from pathlib import Path
Expand Down Expand Up @@ -30,13 +31,16 @@
from api.v1.v1_users.serializers import LoginSerializer, UserSerializer, \
VerifyInviteSerializer, SetUserPasswordSerializer, \
ListAdministrationSerializer, AddEditUserSerializer, ListUserSerializer, \
ListUserRequestSerializer, ListLevelSerializer, UserDetailSerializer
ListUserRequestSerializer, ListLevelSerializer, UserDetailSerializer, \
ForgotPasswordSerializer
from rtmis.settings import REST_FRAMEWORK
from utils.custom_permissions import IsSuperAdmin, IsAdmin
from utils.custom_serializer_fields import validate_serializers_message
from utils.email_helper import send_email
from utils.email_helper import ListEmailTypeRequestSerializer, EmailTypes

webdomain = os.environ["WEBDOMAIN"]


@extend_schema(description='Use to check System health', tags=['Dev'])
@api_view(['GET'])
Expand All @@ -61,8 +65,7 @@ def get_config_file(request, version):
required=False,
enum=EmailTypes.FieldStr.keys(),
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY),
],
location=OpenApiParameter.QUERY)],
summary='To show email template by type')
@api_view(['GET'])
def email_template(request, version):
Expand Down Expand Up @@ -181,8 +184,7 @@ def set_user_password(request, version):
OpenApiParameter(name='filter',
required=False,
type=OpenApiTypes.NUMBER,
location=OpenApiParameter.QUERY),
],
location=OpenApiParameter.QUERY)],
summary='Get list of administration')
@api_view(['GET'])
def list_administration(request, version, administration_id):
Expand Down Expand Up @@ -263,8 +265,7 @@ def add_user(request, version):
required=False,
default=True,
type=OpenApiTypes.BOOL,
location=OpenApiParameter.QUERY),
])
location=OpenApiParameter.QUERY)])
@api_view(['GET'])
@permission_classes([IsAuthenticated, IsSuperAdmin | IsAdmin])
def list_users(request, version):
Expand Down Expand Up @@ -388,7 +389,7 @@ def delete(self, request, user_id, version):
},
tags=['User'],
description='Role Choice are SuperAdmin:1,Admin:2,Approver:3,'
'User:4',
'User:4,ReadOnly:5',
summary='To update user')
def put(self, request, user_id, version):
instance = get_object_or_404(SystemUser, pk=user_id)
Expand All @@ -402,3 +403,28 @@ def put(self, request, user_id, version):
serializer.save()
return Response({'message': 'User updated successfully'},
status=status.HTTP_200_OK)


@extend_schema(request=ForgotPasswordSerializer,
responses={
(200, 'application/json'):
inline_serializer(
"Response",
fields={"message": serializers.CharField()})
},
tags=['User'],
summary='To send reset password instructions')
@api_view(['POST'])
def forgot_password(request, version):
serializer = ForgotPasswordSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{'message': validate_serializers_message(serializer.errors)},
status=status.HTTP_400_BAD_REQUEST)
user: SystemUser = serializer.validated_data.get('email')
url = f"{webdomain}/login/{signing.dumps(user.pk)}"
data = {'button_url': url, 'send_to': [user.email]}
send_email(type=EmailTypes.user_forgot_password, context=data)
return Response(
{'message': 'Reset password instructions sent to your email'},
status=status.HTTP_200_OK)
39 changes: 38 additions & 1 deletion backend/rtmis/templates/email/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@
color: #fff;
}

.btn.btn-link {
border-radius: 5px;
background: transparent;
color: #000000;
border: 2px solid #000000;
min-width: 50%;
}

.btn.block {
width: 100%;
}

h1,
h2,
h3,
Expand Down Expand Up @@ -458,7 +470,7 @@
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background: #ffffff;
padding: 1em;
padding: 0 1em;
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
"
Expand Down Expand Up @@ -594,6 +606,30 @@
{% endif %}
</td>
</tr>
{% if button and button_url and button_text %}
<tr
style="
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
"
>
<td
style="
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background: #ffffff;
padding: 0 1em 2em 1em;
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
text-align: center;
"
>
<a href="{{button_url}}" target="_blank" rel="noreferrer">
<button class="btn btn-link">{{ button_text }}</button>
</a>
</td>
</tr>
{% endif %} {% if explore_button %}
<tr
style="
-ms-text-size-adjust: 100%;
Expand Down Expand Up @@ -623,6 +659,7 @@
>
</td>
</tr>
{% endif %}
<tr
style="
-ms-text-size-adjust: 100%;
Expand Down
37 changes: 32 additions & 5 deletions backend/utils/email_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class EmailTypes:
user_register = 'user_register'
user_approval = 'user_approval'
user_forgot_password = 'user_forgot_password'
data_approval = 'data_approval'
data_rejection = 'data_rejection'
batch_approval = 'batch_approval'
Expand All @@ -23,6 +24,7 @@ class EmailTypes:
FieldStr = {
user_register: 'user_register',
user_approval: 'user_approval',
user_forgot_password: 'user_forgot_password',
data_approval: 'data_approval',
data_rejection: 'data_rejection',
batch_approval: 'batch_approval',
Expand Down Expand Up @@ -55,7 +57,8 @@ def email_context(context: dict, type: str):
"success_text": "Successfully Registered",
"message_list": ["JMP/SDG Status",
"CLTS Progress",
"Water Infrastructure"]
"Water Infrastructure"],
"explore_button": True
})
if type == EmailTypes.user_approval:
context.update({
Expand All @@ -71,15 +74,32 @@ def email_context(context: dict, type: str):
}, {
"location": "Nakuru",
"credential": "View Only"
}]
}],
"explore_button": True
})
if type == EmailTypes.user_forgot_password:
button_url = "#"
if context.get("button_url"):
button_url = context.get("button_url")
context.update({
"subject": "Reset Password",
"body": '''You have submitted a password change request. If it wasn't you
please disregard this email and make sure you can still login
to your account. If it was you, then click the following
button:''',
"explore_button": False,
"button": True,
"button_url": button_url,
"button_text": "Reset Password"
})
if type == EmailTypes.data_approval:
context.update({
"subject": "Data Upload Approved",
"body": '''Your Data Upload has been approved by
Your admin - Ouma Odhiambo''',
"image": f"{webdomain}/email-icons/check-circle.png",
"success_text": "Filename Approved"
"success_text": "Filename Approved",
"explore_button": True
})
if type == EmailTypes.data_rejection:
context.update({
Expand All @@ -96,7 +116,8 @@ def email_context(context: dict, type: str):
"Quisque tincidunt diam in ligula ornare condimentum.",
"Vivamus sodales quam at felis scelerisque, ut tincidunt quam \
vestibulum.",
"Nullam sed magna a ligula ultrices rhoncus nec in sapien."]
"Nullam sed magna a ligula ultrices rhoncus nec in sapien."],
"explore_button": True
})
if type == EmailTypes.batch_approval:
batch = context.get("batch")
Expand All @@ -115,6 +136,7 @@ def email_context(context: dict, type: str):
"body": body,
"image": f"{webdomain}/email-icons/check-circle.png",
"success_text": success_text,
"explore_button": True
})
if type == EmailTypes.batch_rejection:
batch = context.get("batch")
Expand All @@ -133,6 +155,7 @@ def email_context(context: dict, type: str):
"body": body,
"image": f"{webdomain}/email-icons/close-circle.png",
"failed_text": failed_text,
"explore_button": True
})
if type == EmailTypes.pending_approval:
form = context.get("form")
Expand All @@ -151,11 +174,13 @@ def email_context(context: dict, type: str):
"body": body,
"image": f"{webdomain}/email-icons/info-circle.png",
"info_text": info_text,
"explore_button": True
})
if type == EmailTypes.new_request:
context.update({
"image": f"{webdomain}/email-icons/info-circle.png",
"info_text": "Data has been successfully validated and submitted",
"explore_button": True
})
if type == EmailTypes.upload_error:
context.update({
Expand All @@ -164,13 +189,15 @@ def email_context(context: dict, type: str):
please correct it and try again.''',
"image": f"{webdomain}/email-icons/close-circle.png",
"failed_text": "Upload Error",
"info_text": "Please find attached file for reference"
"info_text": "Please find attached file for reference",
"explore_button": True
})
if type == EmailTypes.unchanged_data:
context.update({
"subject": "No Data Updates found",
"image": f"{webdomain}/email-icons/info-circle.png",
"info_text": "No updated data found in the last uploaded file",
"explore_button": True
})
# prevent multiline if inside html template
show_content = context.get('message_list') \
Expand Down