From 7fbcca6ed1973e53b6da6a19223d977f4991c08b Mon Sep 17 00:00:00 2001 From: M Fahru Date: Mon, 27 Jan 2025 08:47:09 -0700 Subject: [PATCH] Merge pull request #22609 from overleaf/mf-migrate-aux-password-pages-to-bs5 [web] Migrate reset password page to bootstrap 5 and website redesign GitOrigin-RevId: 613865379d094b305c6dc78f74dc70341214f4ed --- .../PasswordReset/PasswordResetController.mjs | 35 ++++++--- .../web/app/views/_mixins/notification.pug | 6 +- .../web/app/views/user/passwordReset-bs5.pug | 76 +++++++++++++++++++ .../stylesheets/bootstrap-5/pages/all.scss | 1 + .../stylesheets/bootstrap-5/pages/auth.scss | 24 ++++++ services/web/locales/en.json | 2 + .../PasswordResetControllerTests.mjs | 7 ++ 7 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 services/web/app/views/user/passwordReset-bs5.pug create mode 100644 services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs index aa4ac057dc..3817bdf37f 100644 --- a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs +++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs @@ -8,6 +8,7 @@ import UserSessionsManager from '../User/UserSessionsManager.js' import OError from '@overleaf/o-error' import EmailsHelper from '../Helpers/EmailHelper.js' import { expressify } from '@overleaf/promise-utils' +import SplitTestHandler from '../SplitTests/SplitTestHandler.js' async function setNewUserPassword(req, res, next) { let user @@ -191,19 +192,29 @@ async function renderSetPasswordForm(req, res, next) { }) } -export default { - renderRequestResetForm(req, res) { - const errorQuery = req.query.error - let error = null - if (errorQuery === 'token_expired') { - error = 'password_reset_token_expired' - } - res.render('user/passwordReset', { - title: 'reset_password', - error, - }) - }, +async function renderRequestResetForm(req, res) { + const errorQuery = req.query.error + let error = null + if (errorQuery === 'token_expired') { + error = 'password_reset_token_expired' + } + const { variant } = await SplitTestHandler.promises.getAssignment( + req, + res, + 'auth-pages-bs5' + ) + const template = + variant === 'enabled' ? 'user/passwordReset-bs5' : 'user/passwordReset' + + res.render(template, { + title: 'reset_password', + error, + }) +} + +export default { + renderRequestResetForm: expressify(renderRequestResetForm), requestReset: expressify(requestReset), renderSetPasswordForm: expressify(renderSetPasswordForm), setNewUserPassword: expressify(setNewUserPassword), diff --git a/services/web/app/views/_mixins/notification.pug b/services/web/app/views/_mixins/notification.pug index 8af0dbf7a3..3e336affdf 100644 --- a/services/web/app/views/_mixins/notification.pug +++ b/services/web/app/views/_mixins/notification.pug @@ -5,6 +5,10 @@ mixin notificationIcon(type) span.material-symbols(aria-hidden="true") info else if type === 'success' span.material-symbols(aria-hidden="true") check_circle + else if type === 'error' + span.material-symbols(aria-hidden="true") error + else if type === 'warning' + span.material-symbols(aria-hidden="true") warning mixin notification(options) @@ -24,7 +28,7 @@ mixin notification(options) if title p b #{title} - | #{content} + | !{content} //- TODO: handle action //- if action //- .notification-cta diff --git a/services/web/app/views/user/passwordReset-bs5.pug b/services/web/app/views/user/passwordReset-bs5.pug new file mode 100644 index 0000000000..d92ec5ece6 --- /dev/null +++ b/services/web/app/views/user/passwordReset-bs5.pug @@ -0,0 +1,76 @@ +extends ../layout-website-redesign-bootstrap-5 +include ../_mixins/recaptcha +include ../_mixins/notification + +block vars + - var suppressNavbar = true + - var suppressFooter = true + +block content + - var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset) + + if showCaptcha + script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) + + main#main-content(data-ol-captcha-retry-trigger-area="") + a.auth-aux-logo(href="/") + img(src=buildImgPath("ol-brand/overleaf-o-dark.svg") alt=settings.appName) + .auth-aux-container + form( + data-ol-async-form + name="passwordResetForm" + action="/user/password/reset" + method="POST" + captcha=(showCaptcha ? '' : false) + captcha-action-name=(showCaptcha ? "passwordReset" : false) + ) + if error === 'password_reset_token_expired' + h1.h3.mb-3.mt-0 #{translate("sorry_your_token_expired")} + p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}. + else + h1.h3.mb-3.mt-0(data-ol-not-sent) #{translate("password_reset_sentence_case")} + h1.h3.mb-3.mt-0(hidden data-ol-sent) #{translate("check_your_email")} + p.mb-3.pb-3(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}. + + div(data-ol-not-sent) + +formMessages() + if error && error !== 'password_reset_token_expired' + +notification({ariaLive: 'assertive', type: 'error', className: 'mb-3', content: translate(error)}) + + div(data-ol-custom-form-message="no-password-allowed-due-to-sso" hidden) + +notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])}) + input(type="hidden" name="_csrf" value=csrfToken) + .form-group.mb-3 + label.form-label(for='email') #{translate("email")} + input.form-control#email( + aria-label="email" + type='email' + name='email' + required + autocomplete="username" + autofocus + ) + .actions + button.btn.btn-primary.w-100.mb-3( + type='submit' + data-ol-disabled-inflight + aria-label=translate('reset_password_sentence_case') + ) + span(data-ol-inflight="idle") + | #{translate("reset_password_sentence_case")} + span(hidden data-ol-inflight="pending") + | #{translate("requesting_password_reset")}… + a.btn.btn-ghost.w-100.mb-3(href="/login") #{translate("back_to_log_in")} + div(hidden data-ol-sent) + p.mb-4 #{translate('password_reset_email_sent')} + a.btn.btn-primary.w-100.mb-3(href="/login") #{translate('back_to_log_in')} + + if showCaptcha + +recaptchaConditions diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index ecde1a0140..7bf1668a7c 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -37,6 +37,7 @@ @import 'group-settings'; @import 'templates-v2'; @import 'homepage'; +@import 'auth'; @import 'login-register'; @import 'login'; @import 'register'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss new file mode 100644 index 0000000000..627a879958 --- /dev/null +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss @@ -0,0 +1,24 @@ +.auth-aux-logo { + position: absolute; + top: var(--spacing-06); + left: var(--spacing-06); + + img { + height: 40px; + } +} + +.auth-aux-container { + padding-top: calc($header-height + $spacing-11); + max-width: 320px; + min-height: 100vh; + margin-left: auto; + margin-right: auto; + + .recaptcha-branding { + @include body-xs; + + padding: 0; + text-align: left; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b0470edaa4..b05644fefc 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1485,6 +1485,7 @@ "password_managed_externally": "Password settings are managed externally", "password_reset": "Password Reset", "password_reset_email_sent": "You have been sent an email to complete your password reset.", + "password_reset_sentence_case": "Password reset", "password_reset_token_expired": "Your password reset token has expired. Please request a new password reset email and follow the link there.", "password_too_long_please_reset": "Maximum password length exceeded. Please reset your password.", "password_updated": "Password updated", @@ -1761,6 +1762,7 @@ "resending_confirmation_email": "Resending confirmation email", "reset_password": "Reset Password", "reset_password_link": "Click this link to reset your password", + "reset_password_sentence_case": "Reset password", "reset_your_password": "Reset your password", "resize": "Resize", "resolve": "Resolve", diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs index cd8f27e925..6df3c765b1 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs +++ b/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs @@ -60,6 +60,11 @@ describe('PasswordResetController', function () { removeReconfirmFlag: sinon.stub().resolves(), }, } + this.SplitTestHandler = { + promises: { + getAssignment: sinon.stub().resolves('default'), + }, + } this.PasswordResetController = await esmock.strict(MODULE_PATH, { '@overleaf/settings': this.settings, '../../../../app/src/Features/PasswordReset/PasswordResetHandler': @@ -81,6 +86,8 @@ describe('PasswordResetController', function () { '../../../../app/src/Features/User/UserSessionsManager': this.UserSessionsManager, '../../../../app/src/Features/User/UserUpdater': this.UserUpdater, + '../../../../app/src/Features/SplitTests/SplitTestHandler': + this.SplitTestHandler, }) })