diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py
index af9b6acfac..2fdbccd2fb 100644
--- a/common/djangoapps/student/views/management.py
+++ b/common/djangoapps/student/views/management.py
@@ -1159,25 +1159,18 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None):
if request.method == 'POST':
password = request.POST['new_password1']
- valid_link = True # password reset link will be valid if there is no security violation
- error_message = None
+
try:
validate_password(password, user=user)
- except SecurityPolicyError as err:
- error_message = err.message
- valid_link = False
except ValidationError as err:
- error_message = err.message
-
- if error_message:
# We have a password reset attempt which violates some security
# policy, or any other validation. Use the existing Django template to communicate that
# back to the user.
context = {
- 'validlink': valid_link,
+ 'validlink': True,
'form': None,
'title': _('Password reset unsuccessful'),
- 'err_msg': error_message,
+ 'err_msg': err.message,
}
context.update(platform_name)
return TemplateResponse(
diff --git a/common/static/js/src/ReactRenderer.jsx b/common/static/js/src/ReactRenderer.jsx
index bf295c0368..1876dfb478 100644
--- a/common/static/js/src/ReactRenderer.jsx
+++ b/common/static/js/src/ReactRenderer.jsx
@@ -1,7 +1,10 @@
-import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
+// babel-polyfill must be imported last because of https://github.com/facebook/react/issues/8379
+// which otherwise causes "Objects are not valid as a react child" errors in IE11.
+import 'babel-polyfill';
+
class ReactRendererException extends Error {
constructor(message) {
super(`ReactRendererException: ${message}`);
diff --git a/lms/djangoapps/courseware/tests/test_password_history.py b/lms/djangoapps/courseware/tests/test_password_history.py
index 17233ad8b8..8a4e5a87f9 100644
--- a/lms/djangoapps/courseware/tests/test_password_history.py
+++ b/lms/djangoapps/courseware/tests/test_password_history.py
@@ -71,7 +71,7 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
history = PasswordHistory()
history.create(user)
- def assertPasswordResetError(self, response, error_message, valid_link=False):
+ def assertPasswordResetError(self, response, error_message, valid_link=True):
"""
This method is a custom assertion that verifies that a password reset
view returns an error response as expected.
@@ -363,4 +363,4 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
'new_password1': password1,
'new_password2': password2,
}, follow=True)
- self.assertPasswordResetError(resp, err_msg, valid_link=True)
+ self.assertPasswordResetError(resp, err_msg)
diff --git a/lms/static/js/student_account/components/.eslintrc.js b/lms/static/js/student_account/components/.eslintrc.js
new file mode 100644
index 0000000000..838b853a82
--- /dev/null
+++ b/lms/static/js/student_account/components/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ extends: 'eslint-config-edx',
+ root: true,
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: 'webpack.dev.config.js',
+ },
+ },
+ },
+};
diff --git a/lms/static/js/student_account/components/PasswordResetConfirmation.jsx b/lms/static/js/student_account/components/PasswordResetConfirmation.jsx
new file mode 100644
index 0000000000..a18d2327f9
--- /dev/null
+++ b/lms/static/js/student_account/components/PasswordResetConfirmation.jsx
@@ -0,0 +1,142 @@
+/* globals gettext */
+
+import 'whatwg-fetch';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { Button, StatusAlert } from '@edx/paragon/static';
+
+import PasswordResetInput from './PasswordResetInput';
+
+// NOTE: Use static paragon with this because some internal classes (StatusAlert at least)
+// conflict with some standard LMS ones ('alert' at least). This means that you need to do
+// something like the following on any templates that use this class:
+//
+//
+//
+
+class PasswordResetConfirmation extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ password: '',
+ passwordConfirmation: '',
+ showMatchError: false,
+ isValid: true,
+ validationMessage: '',
+ };
+ this.onBlurPassword1 = this.onBlurPassword1.bind(this);
+ this.onBlurPassword2 = this.onBlurPassword2.bind(this);
+ }
+
+ onBlurPassword1(password) {
+ this.updatePasswordState(password, this.state.passwordConfirmation);
+ this.validatePassword(password);
+ }
+
+ onBlurPassword2(passwordConfirmation) {
+ this.updatePasswordState(this.state.password, passwordConfirmation);
+ }
+
+ updatePasswordState(password, passwordConfirmation) {
+ this.setState({
+ password,
+ passwordConfirmation,
+ showMatchError: !!password && !!passwordConfirmation && (password !== passwordConfirmation),
+ });
+ }
+
+ validatePassword(password) {
+ fetch('/api/user/v1/validation/registration', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ password,
+ }),
+ })
+ .then(res => res.json())
+ .then((response) => {
+ let validationMessage = '';
+ // Be careful about grabbing this message, since we could have received an HTTP error or the
+ // endpoint didn't give us what we expect. We only care if we get a clear error message.
+ if (response.validation_decisions && response.validation_decisions.password) {
+ validationMessage = response.validation_decisions.password;
+ }
+ this.setState({
+ isValid: !validationMessage,
+ validationMessage,
+ });
+ });
+ }
+
+ render() {
+ return (
+