diff --git a/README.md b/README.md index 5b46365..6bb143b 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,17 @@ DJANGO_REST_LOOKUP_FIELD = 'custom_email_field' ``` into Django settings.py file. +## Configurable Response + +For optional configurable response messages, add the following properties to settings.py +```python +PASSWORD_CHANGED = 'Your password has been changed' +TOKEN_EXPIRED = 'Your token has expired' +PASSWORD_REQUEST_ACCEPT = 'Please check your email for the reset password link' +TOKEN_NOT_FOUND = 'Your token is invalid' +TOKEN_VALID = 'Your token is valid' +``` + ## Custom Remote IP Address and User Agent Header Lookup If your setup demands that the IP adress of the user is in another header (e.g., 'X-Forwarded-For'), you can configure that (using Django Request Headers): @@ -339,6 +350,8 @@ Apparently, the following piece of code in the Django Model prevents MongodB fro See issue #49 for details. + + ## Contributions This library tries to follow the unix philosophy of "do one thing and do it well" (which is providing a basic password reset endpoint for Django Rest Framework). Contributions are welcome in the form of pull requests and issues! If you create a pull request, please make sure that you are not introducing breaking changes. diff --git a/django_rest_passwordreset/views.py b/django_rest_passwordreset/views.py index df7c8df..d04ba58 100644 --- a/django_rest_passwordreset/views.py +++ b/django_rest_passwordreset/views.py @@ -40,7 +40,32 @@ class ResetPasswordValidateToken(GenericAPIView): def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - return Response({'status': 'OK'}) + token = serializer.validated_data['token'] + response_dict = dict({"status_code": None, "status": None, "message": None}) + # get token validation time + password_reset_token_validation_time = get_password_reset_token_expiry_time() + + # find token + reset_password_token = ResetPasswordToken.objects.filter(key=token).first() + + if reset_password_token is None: + message = get_response_message("TOKEN_NOT_FOUND") + response_dict.update({"status_code": 404, "status": "notfound", "message": message}) + return Response(response_dict, status=status.HTTP_404_NOT_FOUND) + + # check expiry date + expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time) + + if timezone.now() > expiry_date: + # delete expired token + reset_password_token.delete() + message = get_response_message("TOKEN_EXPIRED") + response_dict.update({"status_code": 401, 'status': 'expired', "message": message}) + return Response(response_dict, status=status.HTTP_404_NOT_FOUND) + + message = get_response_message("TOKEN_VALID") + response_dict.update({"status_code": 200, 'status': 'OK', "message": message}) + return Response(response_dict) class ResetPasswordConfirm(GenericAPIView): @@ -56,10 +81,30 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) password = serializer.validated_data['password'] token = serializer.validated_data['token'] + response_dict = dict({"status_code": None, "message": None, "status": None}) + + # get token validation time + password_reset_token_validation_time = get_password_reset_token_expiry_time() # find token reset_password_token = ResetPasswordToken.objects.filter(key=token).first() + if reset_password_token is None: + message = get_response_message("TOKEN_NOT_FOUND") + response_dict.update({"status_code": 404, 'status': 'notfound', "message": message}) + return Response(response_dict, status=status.HTTP_404_NOT_FOUND) + + # check expiry date + expiry_date = reset_password_token.created_at + timedelta(hours=password_reset_token_validation_time) + + if timezone.now() > expiry_date: + # delete expired token + reset_password_token.delete() + message = get_response_message("TOKEN_EXPIRED") + response_dict.update({"status_code": 404, 'status': 'expired', "message": message}) + + return Response(response_dict, status=status.HTTP_404_NOT_FOUND) + # change users password (if we got to this code it means that the user is_active) if reset_password_token.user.eligible_for_reset(): pre_password_reset.send(sender=self.__class__, user=reset_password_token.user) @@ -83,7 +128,11 @@ def post(self, request, *args, **kwargs): # Delete all password reset tokens for this user ResetPasswordToken.objects.filter(user=reset_password_token.user).delete() - return Response({'status': 'OK'}) + # done + message = get_response_message("PASSWORD_CHANGED") + response_dict.update({"status_code": 200, "status": "OK", "message": message}) + + return Response(response_dict) class ResetPasswordRequestToken(GenericAPIView): @@ -126,6 +175,7 @@ def post(self, request, *args, **kwargs): # but not if DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE == True if not active_user_found and not getattr(settings, 'DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE', False): raise exceptions.ValidationError({ + 'email': [_( "We couldn't find an account associated with that email. Please try a different e-mail address.")], }) @@ -152,7 +202,21 @@ def post(self, request, *args, **kwargs): # let whoever receives this signal handle sending the email for the password reset reset_password_token_created.send(sender=self.__class__, instance=self, reset_password_token=token) # done - return Response({'status': 'OK'}) + message = get_response_message("PASSWORD_REQUEST_ACCEPT") + return Response({"status_code": 200, "status": "OK", "message": message}) + + +def get_response_message(message_key): + """ + If message_key exists in settings.py, the corresponding message will be returned. Otherwise message_key + is returned + :param message_key: Key of the message in settings.py + :return: status_message + """ + + status_message = getattr(settings, message_key, message_key) + + return status_message reset_password_validate_token = ResetPasswordValidateToken.as_view()