diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.js b/services/web/app/src/Features/PasswordReset/PasswordResetController.js index 1bb819941a..c64e34aab1 100644 --- a/services/web/app/src/Features/PasswordReset/PasswordResetController.js +++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.js @@ -12,7 +12,11 @@ async function setNewUserPassword(req, res, next) { let user let { passwordResetToken, password } = req.body if (!passwordResetToken || !password) { - return res.sendStatus(400) + return res.status(400).json({ + message: { + key: 'invalid-password', + }, + }) } passwordResetToken = passwordResetToken.trim() delete req.session.resetToken @@ -31,8 +35,18 @@ async function setNewUserPassword(req, res, next) { auditLog ) const { found, reset, userId } = result - if (!found) return res.sendStatus(404) - if (!reset) return res.sendStatus(500) + if (!found) { + return res.status(404).json({ + message: { + key: 'token-expired', + }, + }) + } + if (!reset) { + return res.status(500).json({ + message: req.i18n.translate('error_performing_request'), + }) + } await UserSessionsManager.promises.revokeAllUserSessions( { _id: userId }, [] @@ -44,11 +58,21 @@ async function setNewUserPassword(req, res, next) { user = await UserGetter.promises.getUser(userId) } catch (error) { if (error.name === 'NotFoundError') { - return res.sendStatus(404) + return res.status(404).json({ + message: { + key: 'token-expired', + }, + }) } else if (error.name === 'InvalidPasswordError') { - return res.sendStatus(400) + return res.status(400).json({ + message: { + key: 'invalid-password', + }, + }) } else { - return res.sendStatus(500) + return res.status(500).json({ + message: req.i18n.translate('error_performing_request'), + }) } } diff --git a/services/web/app/views/user/setPassword.pug b/services/web/app/views/user/setPassword.pug index 2e5c10a455..76580f3c46 100644 --- a/services/web/app/views/user/setPassword.pug +++ b/services/web/app/views/user/setPassword.pug @@ -1,7 +1,4 @@ -extends ../layout - -block append meta - meta(name="ol-passwordStrengthOptions" data-type="json" content=settings.passwordStrengthOptions) +extends ../layout-marketing block content main.content.content-alt#main-content @@ -12,52 +9,68 @@ block content .page-header h1 #{translate("reset_your_password")} form( - async-form="password-reset", + data-ol-async-form, name="passwordResetForm", action="/user/password/set", method="POST", - ng-cloak ) - input(type="hidden", name="_csrf", value=csrfToken) - .alert.alert-success(ng-show="passwordResetForm.response.success") + div(data-ol-not-sent) + div(data-ol-form-messages) + + div.alert.alert-danger( + hidden + role="alert" + aria-live="assertive" + data-ol-custom-form-message='token-expired' + ) + | #{translate('password_reset_token_expired')} + br + a(href="/user/password/reset") + | #{translate('request_new_password_reset_email')} + + div.alert.alert-danger( + hidden + role="alert" + aria-live="assertive" + data-ol-custom-form-message='invalid-password' + ) + | #{translate('invalid_password')} + + div.alert.alert-success( + hidden + role="alert" + aria-live="assertive" + data-ol-sent + ) | #{translate("password_has_been_reset")}. br a(href='/login') #{translate("login_here")} - div(ng-show="passwordResetForm.response.error == true") - div(ng-switch="passwordResetForm.response.status") - .alert.alert-danger(ng-switch-when="404") - | #{translate('password_reset_token_expired')} - br - a(href="/user/password/reset") - | Request a new password reset email - .alert.alert-danger(ng-switch-when="400") - | #{translate('invalid_password')} - .alert.alert-danger(ng-switch-when="429") - | #{translate('rate_limit_hit_wait')} - .alert.alert-danger(ng-switch-default) - | #{translate('error_performing_request')} + input(type="hidden", name="_csrf", value=csrfToken) .form-group input.form-control#passwordField( type='password', name='password', placeholder='new password', - required, autocomplete="new-password", - ng-model="password", autofocus, - complex-password + required, + minlength=settings.passwordStrengthOptions.length.min, + maxlength=settings.passwordStrengthOptions.length.max ) - span.small.text-primary(ng-show="passwordResetForm.password.$error.complexPassword", ng-bind-html="complexPasswordErrorMessage") input( type="hidden", name="passwordResetToken", value=passwordResetToken - ng-non-bindable ) .actions button.btn.btn-primary( type='submit', - ng-disabled="passwordResetForm.$invalid" - ) #{translate("set_new_password")} + data-ol-disabled-inflight + aria-label=translate('set_new_password') + ) + span(data-ol-inflight="idle") + | #{translate('set_new_password')} + span(hidden data-ol-inflight="pending") + | #{translate('set_new_password')}… diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 41349b0bae..66394d13bb 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1113,6 +1113,7 @@ "support_lots_of_features": "We support almost all LaTeX features, including inserting images, bibliographies, equations, and much more! Read about all the exciting things you can do with __appName__ in our <0>__help_guides_link__", "latex_guides": "LaTeX guides", "reset_password": "Reset Password", + "request_new_password_reset_email": "Request a new password reset email", "set_password": "Set Password", "updating_site": "Updating Site", "bonus_please_recommend_us": "Bonus - Please recommend us", diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js index 037942afae..075ffcfd07 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js +++ b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.js @@ -22,7 +22,9 @@ describe('PasswordResetController', function () { password: this.password, }, i18n: { - translate() {}, + translate() { + return '.' + }, }, session: {}, query: {}, @@ -174,8 +176,12 @@ describe('PasswordResetController', function () { reset: false, userId: this.user_id, }) - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(404) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('token-expired') done() } this.PasswordResetController.setNewUserPassword(this.req, this.res) @@ -187,8 +193,12 @@ describe('PasswordResetController', function () { reset: false, userId: this.user_id, }) - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(500) + return this.res + } + this.res.json = data => { + expect(data.message).to.exist done() } this.PasswordResetController.setNewUserPassword(this.req, this.res) @@ -196,8 +206,12 @@ describe('PasswordResetController', function () { it('should return 400 (Bad Request) if there is no password', function (done) { this.req.body.password = '' - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(400) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('invalid-password') this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( false ) @@ -208,8 +222,12 @@ describe('PasswordResetController', function () { it('should return 400 (Bad Request) if there is no passwordResetToken', function (done) { this.req.body.passwordResetToken = '' - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(400) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('invalid-password') this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( false ) @@ -223,8 +241,12 @@ describe('PasswordResetController', function () { const err = new Error('bad') err.name = 'InvalidPasswordError' this.PasswordResetHandler.promises.setNewUserPassword.rejects(err) - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(400) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('invalid-password') this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( true ) @@ -265,8 +287,12 @@ describe('PasswordResetController', function () { const anError = new Error('oops') anError.name = 'NotFoundError' this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(404) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('token-expired') done() } this.PasswordResetController.setNewUserPassword(this.req, this.res) @@ -275,8 +301,12 @@ describe('PasswordResetController', function () { const anError = new Error('oops') anError.name = 'InvalidPasswordError' this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.sendStatus = code => { + this.res.status = code => { code.should.equal(400) + return this.res + } + this.res.json = data => { + data.message.key.should.equal('invalid-password') done() } this.PasswordResetController.setNewUserPassword(this.req, this.res) @@ -284,6 +314,14 @@ describe('PasswordResetController', function () { it('should return 500 for other errors', function (done) { const anError = new Error('oops') this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + this.res.status = code => { + code.should.equal(500) + return this.res + } + this.res.json = data => { + expect(data.message).to.exist + done() + } this.res.sendStatus = code => { code.should.equal(500) done()