diff --git a/admin/users/views.py b/admin/users/views.py index 38787a84a23..48c77a2bd26 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -12,7 +12,6 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.urls import reverse from django.core.exceptions import PermissionDenied -from django.core.mail import send_mail from django.shortcuts import redirect from django.core.paginator import Paginator from django.core.exceptions import ValidationError @@ -47,7 +46,8 @@ AddSystemTagForm ) from admin.base.views import GuidView -from website.settings import DOMAIN, OSF_SUPPORT_EMAIL +from api.users.services import send_password_reset_email +from website.settings import DOMAIN from django.urls import reverse_lazy @@ -523,17 +523,9 @@ class ResetPasswordView(UserMixin, View): def post(self, request, *args, **kwargs): email = self.request.POST['emails'] user = get_user(email) - url = furl(DOMAIN) - user.verification_key_v2 = generate_verification_key(verification_type='password_admin') - user.save() - url.add(path=f'resetpassword/{user._id}/{user.verification_key_v2["token"]}') - send_mail( - subject='Reset OSF Password', - message=f'Follow this link to reset your password: {url.url}\n Note: this link will expire in 12 hours', - from_email=OSF_SUPPORT_EMAIL, - recipient_list=[email] - ) + send_password_reset_email(user, email, institutional=False, verification_type='password_admin') + update_admin_log( user_id=self.request.user.id, object_id=user.pk, diff --git a/api/users/services.py b/api/users/services.py new file mode 100644 index 00000000000..3af86c0807c --- /dev/null +++ b/api/users/services.py @@ -0,0 +1,27 @@ +from furl import furl +from django.utils import timezone + +from framework.auth.core import generate_verification_key +from website import settings, mails + + +def send_password_reset_email(user, email, verification_type='password', institutional=False, **mail_kwargs): + """Generate a password reset token, save it to the user and send the password reset email. + """ + # new verification key (v2) + user.verification_key_v2 = generate_verification_key(verification_type=verification_type) + user.email_last_sent = timezone.now() + user.save() + + reset_link = furl(settings.DOMAIN).add(path=f'resetpassword/{user._id}/{user.verification_key_v2["token"]}').url + mail_template = mails.FORGOT_PASSWORD if not institutional else mails.FORGOT_PASSWORD_INSTITUTION + + mails.send_mail( + to_addr=email, + mail=mail_template, + reset_link=reset_link, + can_change_preferences=False, + **mail_kwargs, + ) + + return reset_link diff --git a/api/users/views.py b/api/users/views.py index a0ea1e171ee..5497feb6936 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -47,6 +47,7 @@ from api.registrations.serializers import RegistrationSerializer from api.resources import annotations as resource_annotations +from api.users.services import send_password_reset_email from api.users.permissions import ( CurrentUser, ReadOnlyOrCurrentUser, ReadOnlyOrCurrentUserRelationship, @@ -864,38 +865,30 @@ class ResetPassword(JSONAPIBaseView, generics.ListCreateAPIView): throttle_classes = (NonCookieAuthThrottle, BurstRateThrottle, RootAnonThrottle, SendEmailThrottle) def get(self, request, *args, **kwargs): + institutional = bool(request.query_params.get('institutional', None)) email = request.query_params.get('email', None) if not email: raise ValidationError('Request must include email in query params.') - institutional = bool(request.query_params.get('institutional', None)) - mail_template = mails.FORGOT_PASSWORD if not institutional else mails.FORGOT_PASSWORD_INSTITUTION - - status_message = language.RESET_PASSWORD_SUCCESS_STATUS_MESSAGE.format(email=email) - kind = 'success' # check if the user exists user_obj = get_user(email=email) - - if user_obj: + if user_obj and user_obj.is_active: # rate limit forgot_password_post if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): - status_message = 'You have recently requested to change your password. Please wait a few minutes ' \ - 'before trying again.' - kind = 'error' - return Response({'message': status_message, 'kind': kind}, status=status.HTTP_429_TOO_MANY_REQUESTS) - elif user_obj.is_active: - # new random verification key (v2) - user_obj.verification_key_v2 = generate_verification_key(verification_type='password') - user_obj.email_last_sent = timezone.now() - user_obj.save() - reset_link = f'{settings.RESET_PASSWORD_URL}{user_obj._id}/{user_obj.verification_key_v2['token']}/' - mails.send_mail( - to_addr=email, - mail=mail_template, - reset_link=reset_link, - can_change_preferences=False, - ) - return Response(status=status.HTTP_200_OK, data={'message': status_message, 'kind': kind, 'institutional': institutional}) + status_message = 'You have recently requested to change your password. ' \ + 'Please wait a few minutes before trying again.' + return Response({'message': status_message, 'kind': 'error'}, status=status.HTTP_429_TOO_MANY_REQUESTS) + + send_password_reset_email(user_obj, email, institutional=institutional) + + return Response( + status=status.HTTP_200_OK, + data={ + 'message': language.RESET_PASSWORD_SUCCESS_STATUS_MESSAGE.format(email=email), + 'kind': 'success', + 'institutional': institutional, + }, + ) def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) diff --git a/api_tests/users/views/test_user_settings.py b/api_tests/users/views/test_user_settings.py index a3750f959ba..b4fa2494174 100644 --- a/api_tests/users/views/test_user_settings.py +++ b/api_tests/users/views/test_user_settings.py @@ -205,7 +205,7 @@ def test_get(self, app, url, user_one): mock_send_mail.assert_called_with( to_addr=user_one.username, mail=mails.FORGOT_PASSWORD, - reset_link=f'{settings.RESET_PASSWORD_URL}{user_one._id}/{user_one.verification_key_v2['token']}/', + reset_link=f'{settings.DOMAIN}resetpassword/{user_one._id}/{user_one.verification_key_v2['token']}', can_change_preferences=False, )