Request password reset with recovery email address
This commit is contained in:
@@ -21,18 +21,24 @@ from edx_ace import ace
|
||||
from edx_ace.recipient import Recipient
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from openedx.core.djangoapps.user_api import accounts as accounts_settings
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from student.message_types import PasswordReset
|
||||
from student.models import CourseEnrollmentAllowed, email_exists_or_retired
|
||||
from student.models import AccountRecovery, CourseEnrollmentAllowed, email_exists_or_retired
|
||||
from util.password_policy_validators import validate_password
|
||||
|
||||
|
||||
def send_password_reset_email_for_user(user, request):
|
||||
def send_password_reset_email_for_user(user, request, preferred_email=None):
|
||||
"""
|
||||
Send out a password reset email for the given user.
|
||||
|
||||
Arguments:
|
||||
user (User): Django User object
|
||||
request (HttpRequest): Django request object
|
||||
preferred_email (str): Send email to this address if present, otherwise fallback to user's email address.
|
||||
"""
|
||||
site = get_current_site()
|
||||
message_context = get_base_template_context(site)
|
||||
@@ -51,7 +57,7 @@ def send_password_reset_email_for_user(user, request):
|
||||
})
|
||||
|
||||
msg = PasswordReset().personalize(
|
||||
recipient=Recipient(user.username, user.email),
|
||||
recipient=Recipient(user.username, preferred_email or user.email),
|
||||
language=get_user_preference(user, LANGUAGE_KEY),
|
||||
user_context=message_context,
|
||||
)
|
||||
@@ -95,6 +101,44 @@ class PasswordResetFormNoActive(PasswordResetForm):
|
||||
send_password_reset_email_for_user(user, request)
|
||||
|
||||
|
||||
class AccountRecoveryForm(PasswordResetFormNoActive):
|
||||
error_messages = {
|
||||
'unknown': _(
|
||||
HTML(
|
||||
'That secondary e-mail address doesn\'t have an associated user account. Are you sure you had added '
|
||||
'a verified secondary email address for account recovery in your account settings? Please '
|
||||
'<a href={support_url}">contact support</a> for further assistance.'
|
||||
)
|
||||
).format(
|
||||
support_url=configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK),
|
||||
),
|
||||
'unusable': _(
|
||||
Text(
|
||||
'The user account associated with this secondary e-mail address cannot reset the password.'
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
def clean_email(self):
|
||||
"""
|
||||
This is a literal copy from Django's django.contrib.auth.forms.PasswordResetForm
|
||||
Except removing the requirement of active users
|
||||
Validates that a user exists with the given secondary email.
|
||||
"""
|
||||
email = self.cleaned_data["email"]
|
||||
# The line below contains the only change, getting users via AccountRecovery
|
||||
self.users_cache = User.objects.filter(
|
||||
id__in=AccountRecovery.objects.filter(secondary_email__iexact=email).values_list('user')
|
||||
)
|
||||
|
||||
if not len(self.users_cache):
|
||||
raise forms.ValidationError(self.error_messages['unknown'])
|
||||
if any((user.password.startswith(UNUSABLE_PASSWORD_PREFIX))
|
||||
for user in self.users_cache):
|
||||
raise forms.ValidationError(self.error_messages['unusable'])
|
||||
return email
|
||||
|
||||
|
||||
class TrueCheckbox(widgets.CheckboxInput):
|
||||
"""
|
||||
A checkbox widget that only accepts "true" (case-insensitive) as true.
|
||||
|
||||
@@ -22,6 +22,7 @@ urlpatterns = [
|
||||
|
||||
# password reset in views (see below for password reset django views)
|
||||
url(r'^account/password$', views.password_change_request_handler, name='password_change_request'),
|
||||
url(r'^account/account_recovery', views.account_recovery_request_handler, name='account_recovery'),
|
||||
url(r'^password_reset/$', views.password_reset, name='password_reset'),
|
||||
url(
|
||||
r'^password_reset_confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
|
||||
|
||||
@@ -57,6 +57,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
|
||||
from openedx.core.djangoapps.user_api.config.waffle import PREVENT_AUTH_USER_WRITES, SYSTEM_MAINTENANCE_MSG, waffle
|
||||
from openedx.core.djangoapps.user_api.errors import UserNotFound, UserAPIInternalError
|
||||
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
|
||||
@@ -76,6 +77,7 @@ from student.helpers import (
|
||||
)
|
||||
from student.message_types import EmailChange, PasswordReset
|
||||
from student.models import (
|
||||
AccountRecovery,
|
||||
CourseEnrollment,
|
||||
PasswordHistory,
|
||||
PendingEmailChange,
|
||||
@@ -723,6 +725,59 @@ def password_change_request_handler(request):
|
||||
return HttpResponseBadRequest(_("No email address provided."))
|
||||
|
||||
|
||||
@require_http_methods(['POST'])
|
||||
def account_recovery_request_handler(request):
|
||||
"""
|
||||
Handle account recovery requests.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest)
|
||||
|
||||
Returns:
|
||||
HttpResponse: 200 if the email was sent successfully
|
||||
HttpResponse: 400 if there is no 'email' POST parameter
|
||||
HttpResponse: 403 if the client has been rate limited
|
||||
HttpResponse: 405 if using an unsupported HTTP method
|
||||
HttpResponse: 404 if account recovery feature is not enabled
|
||||
|
||||
Example:
|
||||
|
||||
POST /account/account_recovery
|
||||
|
||||
"""
|
||||
if not is_secondary_email_feature_enabled():
|
||||
raise Http404
|
||||
|
||||
limiter = BadRequestRateLimiter()
|
||||
if limiter.is_rate_limit_exceeded(request):
|
||||
AUDIT_LOG.warning("Account recovery rate limit exceeded")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
user = request.user
|
||||
# Prefer logged-in user's email
|
||||
email = request.POST.get('email')
|
||||
|
||||
if email:
|
||||
try:
|
||||
# Send an email with a link to direct user towards account recovery.
|
||||
from openedx.core.djangoapps.user_api.accounts.api import request_account_recovery
|
||||
request_account_recovery(email, request.is_secure())
|
||||
|
||||
# Check if a user exists with the given secondary email, if so then invalidate the existing oauth tokens.
|
||||
user = user if user.is_authenticated else User.objects.get(
|
||||
id=AccountRecovery.objects.get(secondary_email__iexact=email).user.id
|
||||
)
|
||||
destroy_oauth_tokens(user)
|
||||
except UserNotFound:
|
||||
AUDIT_LOG.warning(
|
||||
"Account recovery attempt via invalid secondary email '{email}'.".format(email=email)
|
||||
)
|
||||
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
return HttpResponseBadRequest(_("No email address provided."))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def password_reset(request):
|
||||
|
||||
Reference in New Issue
Block a user