Procházet zdrojové kódy

[web] Create CIAM versions of the password reset screens (#30087)

* Make CIAM copies of Pug files

passwordResetCiam.pug
setPasswordCiam.pug

* Update controller with split test assignment

* Use CIAM layout in passwordResetCiam.pug

* Style passwordResetCiam according to designs

* Use CIAM layout in setPasswordCiam.pug

* Style setPasswordCiam according to designs

* Use settings value in registration screen for must_be_at_least_n_characters

* Retrieve email input with a script

* Replace mb-4 by --ds-spacing-800

* Add eye icon to toggle password visibility

* Avoid double dots after some translated strings

* Use `ciamCustomFormDangerMessage`

* Use `ciamErrorNotification`

* Use `ciamButtonContentLoading`

* Replace remaining "mb" classes

* Move new password errors to the top of the form

* Fix CIAM mixins path after rebase

* Use `ciamCustomFormDangerMessage`

* Add `data-ol-spinner-inflight` to buttons

* Replace classname ciam-notification by notification-ds

Remove borders from CIAM notifications
Fix font size

* Revert "Use settings value in registration screen for must_be_at_least_n_characters"

This reverts commit a0af95c11e171097750ad7ee871f6baf89d5c0cb.

(It's Friday afternoon so I don't want to update unrelated stuff :D)

* Update: check_your_inbox

* Remove `.ciam-card` min-height.

Unnecessary thanks to `.confirm-email-success-form`'s min-height: 400px;

* Use phosphor icons

* Style `formMessagesNewStyle` with DS notifications within CIAM pages

Alternatively, we could extend/duplicate `showMessagesNewStyle` with a CIAM variant

* Revert "Style `formMessagesNewStyle` with DS notifications within CIAM pages"

This reverts commit ed382dc1e8cdf5b916c1527f4da0a825167e9675.

* Fix styling of dynamically-created DS notifications

* Set password length info to secondary color

* Move `ciamSamlErrorNotLoggedIn` to saas-authentication module

Prevents errors in CE:

Error: ENOENT: no such file or directory, open '/overleaf/services/web/modules/saas-authentication/app/views/_mixins.pug'
    at /overleaf/services/web/app/views/_mixins/ciam_mixins.pug line 3

---------

Co-authored-by: Tim Down <158919+timdown@users.noreply.github.com>
GitOrigin-RevId: afe58f18ecee92460ab628a285b6edb48a5c678d
Antoine Clausse před 10 měsíci
rodič
revize
6eefe6dda4

+ 40 - 13
services/web/app/src/Features/PasswordReset/PasswordResetController.mjs

@@ -9,6 +9,7 @@ import OError from '@overleaf/o-error'
 import EmailsHelper from '../Helpers/EmailHelper.mjs'
 import { expressify } from '@overleaf/promise-utils'
 import { z, validateReq } from '../../infrastructure/Validation.mjs'
+import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
 
 const setNewUserPasswordSchema = z.object({
   body: z.object({
@@ -186,16 +187,20 @@ async function renderSetPasswordForm(req, res, next) {
         return res.redirect('/user/password/reset?error=token_expired')
       }
       req.session.resetToken = query.passwordResetToken
-      let emailQuery = ''
 
+      const params = new URLSearchParams()
       if (typeof query.email === 'string') {
         const email = EmailsHelper.parseEmail(query.email)
         if (email) {
-          emailQuery = `?email=${encodeURIComponent(email)}`
+          params.append('email', email)
         }
       }
-
-      return res.redirect('/user/password/set' + emailQuery)
+      if (req.query.uniaccessphase1) {
+        // Preserve uniaccessphase1 flag in the redirect so it can be tested
+        params.append('uniaccessphase1', req.query.uniaccessphase1)
+      }
+      const queryString = params.toString() ? `?${params.toString()}` : ''
+      return res.redirect('/user/password/set' + queryString)
     } catch (err) {
       if (err.name === 'ForbiddenError') {
         return next(err)
@@ -214,11 +219,22 @@ async function renderSetPasswordForm(req, res, next) {
   const passwordResetToken = req.session.resetToken
   delete req.session.resetToken
 
-  res.render('user/setPassword', {
-    title: 'set_password',
-    email,
-    passwordResetToken,
-  })
+  const ciamAssignment = await SplitTestHandler.promises.getAssignment(
+    req,
+    res,
+    'uniaccessphase1'
+  )
+
+  res.render(
+    ciamAssignment.variant === 'enabled'
+      ? 'user/setPasswordCiam'
+      : 'user/setPassword',
+    {
+      title: 'set_password',
+      email,
+      passwordResetToken,
+    }
+  )
 }
 
 const renderRequestResetFormSchema = z.object({
@@ -235,10 +251,21 @@ async function renderRequestResetForm(req, res) {
     error = 'password_reset_token_expired'
   }
 
-  res.render('user/passwordReset', {
-    title: 'reset_password',
-    error,
-  })
+  const ciamAssignment = await SplitTestHandler.promises.getAssignment(
+    req,
+    res,
+    'uniaccessphase1'
+  )
+
+  res.render(
+    ciamAssignment.variant === 'enabled'
+      ? 'user/passwordResetCiam'
+      : 'user/passwordReset',
+    {
+      title: 'reset_password',
+      error,
+    }
+  )
 }
 
 export default {

+ 6 - 8
services/web/app/views/_mixins/ciam_mixins.pug

@@ -1,6 +1,5 @@
 include terms_of_service
 include recaptcha
-include ../../../modules/saas-authentication/app/views/_mixins
 
 mixin ciamLogo
 	header.ciam-logo
@@ -10,7 +9,7 @@ mixin ciamLogo
 mixin ciamCardSeparator
 	hr.ciam-card-separator
 
-mixin ciamCardFooter
+mixin ciamCardFooter(short)
 	section.ciam-card-footer
 		+ciamCardSeparator
 		.ciam-footer-ds-logo
@@ -24,7 +23,10 @@ mixin ciamCardFooter
 					alt='Digital Science — home'
 				)
 		p
-			| !{translate('advancing_research_with', null, [{ name: 'a', attrs: { href: 'https://www.overleaf.com/', target: '_blank', rel: 'noopener noreferrer' }}, { name: 'a', attrs: { href: 'https://www.papersapp.com/', target: '_blank', rel: 'noopener noreferrer' }}])}
+			if short
+				| !{translate('overleaf_is_a_ds_product')}
+			else
+				| !{translate('advancing_research_with', null, [{ name: 'a', attrs: { href: 'https://www.overleaf.com/', target: '_blank', rel: 'noopener noreferrer' }}, { name: 'a', attrs: { href: 'https://www.papersapp.com/', target: '_blank', rel: 'noopener noreferrer' }}])}
 
 mixin ciamTermsOfServiceAgreement
 	p
@@ -36,7 +38,7 @@ mixin ciamRecaptchaConditions
 
 mixin ciamCustomFormDangerMessage(key)
 	div(
-		class='notification ciam-notification notification-type-error'
+		class='notification notification-ds notification-type-error'
 		hidden
 		data-ol-custom-form-message=key
 		role='alert'
@@ -47,10 +49,6 @@ mixin ciamCustomFormDangerMessage(key)
 		.notification-content.text-left
 			block
 
-mixin ciamSamlErrorNotLoggedIn(error)
-	+samlErrorNotLoggedIn(error)
-		ph-warning-circle(aria-hidden='true')
-
 mixin ciamFooter
 	footer
 		.footer-links

+ 98 - 0
services/web/app/views/user/passwordResetCiam.pug

@@ -0,0 +1,98 @@
+extends ../layout-website-redesign
+include ../_mixins/recaptcha
+include ../_mixins/notification
+include ../_mixins/ciam_mixins
+
+block vars
+	- var suppressNavbar = true
+	- var suppressFooter = true
+	- var suppressSkipToContent = true
+	- isWebsiteRedesign = 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'
+		)
+
+	.ciam-enabled.ciam-layout.ciam-password-reset(
+		data-ol-captcha-retry-trigger-area=''
+	)
+		+ciamLogo
+		.ciam-container
+			main#main-content.ciam-card
+				form(
+					name='passwordResetForm'
+					captcha-action-name=showCaptcha ? 'passwordReset' : false
+					data-ciam-form
+					data-ol-async-form
+					action='/user/password/reset'
+					method='POST'
+					captcha=showCaptcha ? '' : false
+				)
+					if error === 'password_reset_token_expired'
+						h1 #{translate("sorry_your_token_expired")}
+						p.intro-p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}.
+					else
+						h1(data-ol-not-sent) #{translate("reset_your_password")}
+						h1(hidden data-ol-sent) #{translate("check_your_inbox")} 
+						p.intro-p(data-ol-not-sent) #{translate("enter_your_email_and_we_will_send_reset_instructions")}
+
+					div(data-ol-not-sent)
+						+formMessagesNewStyle
+						if error && error !== 'password_reset_token_expired'
+							+ciamErrorNotification
+								p #{translate(error)}
+
+						div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden)
+							+ciamErrorNotification
+								p !{translate('you_cant_reset_password_due_to_sso', {}, [{name: 'a', attrs: {href: '/sso-login'}}])}
+						input(name='_csrf' type='hidden' value=csrfToken)
+						.form-group.form-group-ds
+							label.form-label(for='email') #{translate("email")}
+							input#email.form-control.form-control-ds.form-control-lg(
+								name='email'
+								type='email'
+								required
+								autocomplete='username'
+								autofocus
+							)
+						.actions
+							button.btn.btn-ds.btn-lg.btn-primary.w-100(
+								type='submit'
+								data-ol-disabled-inflight
+								data-ol-spinner-inflight
+								aria-label=translate('send_reset_link')
+							)
+								+ciamButtonContentLoading(`${translate('requesting_password_reset')}…`)= translate('send_reset_link')
+							p.ciam-login-text
+								a(href='/login') #{translate("back_to_log_in")}
+					div(hidden data-ol-sent)
+						p.intro-p !{translate('if_theres_an_account_youll_get_reset_email', {email: '<span id="sent-email-display">your email</span>'})}
+						p.ciam-login-text.text-start
+							a(href='/login') #{translate('back_to_log_in')}
+
+				if showCaptcha
+					.ciam-disclaimers
+						+ciamRecaptchaConditions
+
+				+ciamCardFooter(true)
+
+	// retrieve and display the email used for password reset after form is sent
+	script(nonce=scriptNonce).
+		document.querySelector('form[name="passwordResetForm"]').addEventListener('sent', function () {
+			const email = this.querySelector('input[name="email"]').value
+			const display = document.getElementById('sent-email-display')
+			if (display) display.textContent = email
+		})

+ 3 - 3
services/web/app/views/user/setPassword.pug

@@ -28,11 +28,11 @@ block content
 					+formMessagesNewStyle
 
 					+customFormMessageNewStyle('password-contains-email', 'danger')
-						| #{translate('invalid_password_contains_email')}.
+						| #{translate('invalid_password_contains_email')}
 						| #{translate('use_a_different_password')}.
 
 					+customFormMessageNewStyle('password-too-similar', 'danger')
-						| #{translate('invalid_password_too_similar')}.
+						| #{translate('invalid_password_too_similar')}
 						| #{translate('use_a_different_password')}.
 
 					+customFormMessageNewStyle('token-expired', 'danger')
@@ -65,7 +65,7 @@ block content
 							| #{translate('password_cant_be_the_same_as_current_one')}.
 
 						+customValidationMessageNewStyle('password-must-be-strong')
-							| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}.
+							| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}
 							| #{translate('use_a_different_password')}.
 
 						input(name='passwordResetToken' type='hidden' value=passwordResetToken)

+ 110 - 0
services/web/app/views/user/setPasswordCiam.pug

@@ -0,0 +1,110 @@
+extends ../layout-website-redesign
+include ../_mixins/ciam_mixins
+
+block vars
+	- var suppressNavbar = true
+	- var suppressFooter = true
+	- var suppressSkipToContent = true
+	- isWebsiteRedesign = true
+
+block content
+	.ciam-enabled.ciam-layout.ciam-password-set
+		+ciamLogo
+		.ciam-container
+			main#main-content.ciam-card
+				form(
+					name='passwordResetForm'
+					data-ciam-form
+					data-ol-async-form
+					action='/user/password/set'
+					method='POST'
+					data-ol-hide-on-error='token-expired'
+				)
+					div(hidden data-ol-sent)
+						h1 #{translate("your_password_has_been_reset")}
+						p.intro-p #{translate("you_can_now_sign_in_with_new_password")}.
+						a.btn.btn-ds.btn-lg.btn-primary.w-100(href='/login') #{translate("go_to_sign_in")}
+
+					div(data-ol-not-sent)
+						h1 #{translate("choose_a_new_password")}
+						+formMessagesNewStyle
+
+						+ciamCustomFormDangerMessage('password-contains-email')
+							| #{translate('invalid_password_contains_email')}
+							| #{translate('use_a_different_password')}.
+
+						+ciamCustomFormDangerMessage('password-too-similar')
+							| #{translate('invalid_password_too_similar')}
+							| #{translate('use_a_different_password')}.
+
+						+ciamCustomFormDangerMessage('token-expired')
+							| #{translate('password_reset_token_expired')}
+							br
+							a(href='/user/password/reset')
+								| #{translate('request_new_password_reset_email')}
+
+						+ciamCustomFormDangerMessage('invalid-password')
+							| #{translate('invalid_password')}.
+
+						+ciamCustomFormDangerMessage('password-must-be-different')
+							| #{translate('password_cant_be_the_same_as_current_one')}.
+
+						+ciamCustomFormDangerMessage('password-must-be-strong')
+							| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}
+							| #{translate('use_a_different_password')}.
+
+						input(name='_csrf' type='hidden' value=csrfToken)
+						input(
+							name='email'
+							type='text'
+							hidden
+							autocomplete='username'
+							value=email
+						)
+
+						.form-group.form-group-ds
+							label.form-label(
+								for='passwordField'
+								data-ol-hide-on-error-message='token-expired'
+							) #{translate("password")}
+							.form-group-password
+								.form-group-password-input
+									.form-complex-input-container
+										input#passwordField.form-control.form-control-ds.form-control-lg(
+											name='password'
+											type='password'
+											autocomplete='new-password'
+											autofocus
+											required
+											minlength=settings.passwordStrengthOptions.length.min
+											data-ol-password-visibility-target
+										)
+										button.visibility-toggle(
+											type='button'
+											data-ol-password-visibility-toggle='visibilityOn'
+											aria-controls='password'
+											aria-label=translate('turn_on_password_visibility')
+										)
+											ph-eye.form-input-icon-ds
+										button.visibility-toggle(
+											type='button'
+											data-ol-password-visibility-toggle='visibilityOff'
+											aria-controls='password'
+											aria-label=translate('turn_off_password_visibility')
+											hidden
+										)
+											ph-eye-slash.form-input-icon-ds
+
+							input(name='passwordResetToken' type='hidden' value=passwordResetToken)
+						div(data-ol-hide-on-error-message='token-expired')
+							p.password-policy #{translate('must_be_at_least_n_characters', {n: settings.passwordStrengthOptions.length.min})}
+						.actions
+							button.btn.btn-ds.btn-lg.btn-primary.w-100(
+								type='submit'
+								data-ol-disabled-inflight
+								data-ol-spinner-inflight
+								aria-label=translate('save_new_password')
+							)
+								+ciamButtonContentLoading(`${translate('processing')}…`)= translate('save_new_password')
+
+				+ciamCardFooter(true)

+ 3 - 1
services/web/frontend/js/features/form-helpers/hydrate-form.ts

@@ -304,6 +304,8 @@ function showMessagesNewStyle(
       el.hidden = true
     })
 
+  const isDsBranded = formEl.dataset.ciamForm !== undefined
+
   // Render messages
   messageBag.forEach(message => {
     const customErrorElements = message.key
@@ -322,6 +324,7 @@ function showMessagesNewStyle(
       messageElContainer.className = classNames('notification', {
         'notification-type-error': message.type === 'error',
         'notification-type-success': message.type !== 'error',
+        'notification-ds': isDsBranded,
       })
       const messageEl = document.createElement('div')
 
@@ -344,7 +347,6 @@ function showMessagesNewStyle(
       }
 
       // create the left icon
-      const isDsBranded = formEl.dataset.ciamForm !== undefined
       const messageIcon = document.createElement('div')
       messageIcon.className = 'notification-icon'
       if (

+ 3 - 3
services/web/frontend/js/features/settings/components/password-section.tsx

@@ -177,16 +177,16 @@ function PasswordForm() {
                       />,
                     ]}
                   />
-                  . {t('use_a_different_password')}.
+                  {t('use_a_different_password')}.
                 </>
               ) : getErrorMessageKey(error) === 'password-contains-email' ? (
                 <>
-                  {t('invalid_password_contains_email')}.{' '}
+                  {t('invalid_password_contains_email')}{' '}
                   {t('use_a_different_password')}.
                 </>
               ) : getErrorMessageKey(error) === 'password-too-similar' ? (
                 <>
-                  {t('invalid_password_too_similar')}.{' '}
+                  {t('invalid_password_too_similar')}{' '}
                   {t('use_a_different_password')}.
                 </>
               ) : (

+ 1 - 0
services/web/frontend/stylesheets/ciam/all.scss

@@ -2,6 +2,7 @@
 @import 'ciam-layout';
 @import 'ciam-links';
 @import 'ciam-login';
+@import 'ciam-password-reset';
 @import 'ciam-register';
 @import 'ciam-six-digits';
 @import 'ciam-try-premium';

+ 0 - 1
services/web/frontend/stylesheets/ciam/ciam-layout.scss

@@ -108,7 +108,6 @@
   border-radius: var(--ds-border-radius-400);
   max-width: 464px;
   margin: 0 auto;
-  min-height: 500px;
 
   @include media-breakpoint-up(sm) {
     padding: var(--ds-spacing-1300);

+ 28 - 0
services/web/frontend/stylesheets/ciam/ciam-password-reset.scss

@@ -0,0 +1,28 @@
+.ciam-password-set,
+.ciam-password-reset {
+  h1 {
+    margin-bottom: var(--ds-spacing-600);
+  }
+
+  p.intro-p {
+    margin-bottom: var(--ds-spacing-800);
+  }
+
+  .notification {
+    margin-bottom: var(--ds-spacing-800);
+  }
+
+  .password-policy {
+    color: var(--ds-color-text-secondary);
+  }
+
+  .form-group-ds {
+    margin-bottom: var(--ds-spacing-400);
+  }
+
+  .actions {
+    display: flex;
+    flex-direction: column;
+    gap: var(--ds-spacing-400);
+  }
+}

+ 3 - 1
services/web/frontend/stylesheets/pages/login-register.scss

@@ -36,7 +36,9 @@
   flex-direction: column;
 
   .form-group-password-input {
-    input.form-control {
+    input.form-control,
+    // needlessly specific selectors to override competing styles
+    input.form-control.form-control-ds.form-control-lg {
       padding-right: var(--password-visibility-toggle-width);
     }
   }

+ 10 - 0
services/web/locales/en.json

@@ -330,10 +330,12 @@
   "chat_error": "Could not load chat messages, please try again.",
   "chat_with_sales_team_50_or_more": "Chat with our sales team about larger discounts for groups of 50 or more.",
   "check_your_email": "Check your email",
+  "check_your_inbox": "Check your inbox",
   "checking": "Checking",
   "checking_dropbox_status": "Checking Dropbox status",
   "checking_project_github_status": "Checking project status in GitHub",
   "choose_a_custom_color": "Choose a custom color",
+  "choose_a_new_password": "Choose a new password",
   "choose_from_group_members": "Choose from group members",
   "choose_how_you_search_your_references": "Choose how you search your references",
   "choose_which_experiments": "Choose which experiments you’d like to try.",
@@ -720,6 +722,7 @@
   "enter_the_number_of_licenses_youd_like_to_add_to_see_the_cost_breakdown": "Enter the number of licenses you’d like to add to see the cost breakdown.",
   "enter_your_email": "Enter your email",
   "enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password": "Enter your email address below, and we will send you a link to reset your password",
+  "enter_your_email_and_we_will_send_reset_instructions": "Enter your email and we’ll send reset instructions.",
   "enter_your_password": "Enter your password",
   "equation_generator": "Equation Generator",
   "equation_preview": "Equation preview",
@@ -953,6 +956,7 @@
   "go_to_pdf_location_in_code": "Go to PDF location in code (Tip: double click on the PDF for best results)",
   "go_to_previous_page": "Go to previous page",
   "go_to_settings": "Go to settings",
+  "go_to_sign_in": "Go to sign in",
   "go_to_subscriptions": "Go to Subscriptions",
   "go_to_writefull": "Go to Writefull",
   "good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
@@ -1058,6 +1062,7 @@
   "id": "ID",
   "if_have_existing_can_link": "If you have an existing <b>__appName__</b> account on another email, you can link it to your <b>__institutionName__</b> account by clicking <b>__clickText__</b>.",
   "if_owner_can_link": "If you own the <b>__appName__</b> account with <b>__email__</b>, you will be allowed to link it to your <b>__institutionName__</b> institutional account.",
+  "if_theres_an_account_youll_get_reset_email": "If there’s an account for __email__, you’ll get an email with reset instructions.",
   "if_you_find_any_issues_with_texlive": "If you find any issues with TeX Live, packages, or compilation, please <0>provide feedback</0>. If you hit problems, you can <1>switch your compiler</1>.",
   "if_you_need_to_customize_your_table_further_you_can": "If you need to customize your table further, you can. Using LaTeX code, you can change anything from table styles and border styles to colors and column widths. <0>Read our guide</0> to using tables in LaTeX to help you get started.",
   "if_you_need_to_delete_your_writefull_account": "If you need to delete your Writefull account, go to your <a>Writefull account settings.</a>",
@@ -1597,6 +1602,7 @@
   "overleaf_group_plans": "Overleaf group plans",
   "overleaf_history_system": "Overleaf History System",
   "overleaf_individual_plans": "Overleaf individual plans",
+  "overleaf_is_a_ds_product": "Overleaf is a Digital Science product.",
   "overleaf_is_easy_to_use": "Overleaf is easy to use.",
   "overleaf_labs": "Overleaf Labs",
   "overleaf_logo": "Overleaf logo",
@@ -2003,6 +2009,7 @@
   "saml_response": "SAML Response",
   "save": "Save",
   "save_20_percent": "save 20%",
+  "save_new_password": "Save new password",
   "save_or_cancel-cancel": "Cancel",
   "save_or_cancel-or": "or",
   "save_or_cancel-save": "Save",
@@ -2083,6 +2090,7 @@
   "send_first_message": "Send your first message to your collaborators",
   "send_message": "Send message",
   "send_request": "Send request",
+  "send_reset_link": "Send reset link",
   "send_test_email": "Send a test email",
   "sending": "Sending",
   "sent": "Sent",
@@ -2801,6 +2809,7 @@
   "you_can_now_enable_sso": "You can now enable SSO on your group settings page.",
   "you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features</0>.",
   "you_can_now_search_and_add_references_from_your_rm_library_without_needing_to_import_files": "You can now search and add references from your __referenceManager__ library without needing to import files—just type <code>\\cite{}</code> in your .tex file. <a>Learn more</a>",
+  "you_can_now_sign_in_with_new_password": "You can now sign in with your new password.",
   "you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out</0> of the program at any time on this page",
   "you_can_request_a_maximum_of_limit_fixes_per_day": "You can request a maximum of __limit__ fixes per day. Please try again tomorrow.",
   "you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",
@@ -2853,6 +2862,7 @@
   "your_message_to_collaborators": "Send a message to your collaborators",
   "your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors": "Your name and email address will be visible to the project owner and other editors.",
   "your_new_plan": "Your new plan",
+  "your_password_has_been_reset": "Your password has been reset",
   "your_password_has_been_successfully_changed": "Your password has been successfully changed",
   "your_password_was_detected": "Your password is on a <0>public list of known compromised passwords</0>. Keep your account safe by changing your password now.",
   "your_plan": "Your plan",

+ 12 - 0
services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs

@@ -57,6 +57,11 @@ describe('PasswordResetController', function () {
         removeReconfirmFlag: sinon.stub().resolves(),
       },
     }
+    ctx.SplitTestHandler = {
+      promises: {
+        getAssignment: sinon.stub().resolves({ variant: 'default' }),
+      },
+    }
 
     vi.doMock('@overleaf/settings', () => ({
       default: ctx.settings,
@@ -105,6 +110,13 @@ describe('PasswordResetController', function () {
       default: ctx.UserUpdater,
     }))
 
+    vi.doMock(
+      '../../../../app/src/Features/SplitTests/SplitTestHandler',
+      () => ({
+        default: ctx.SplitTestHandler,
+      })
+    )
+
     ctx.PasswordResetController = (await import(MODULE_PATH)).default
   })