mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Show dashboard notification for unconfirmed emails and untrusted secondary emails (#23919)
* Show an aggressive dashboard notification for unconfirmed emails Show a persistent dashboard notification for untrusted secondary emails * For emails before the cutoffDate, start displaying the notification on the deletionDate and show the notification for 90 days * Update the email deletion logic for displaying the email notification and update test * Update test GitOrigin-RevId: 1b0e44f79592292d428c634dc1ec4df9e6ceaeb4
This commit is contained in:
@@ -305,6 +305,7 @@
|
|||||||
"confirm_reject_selected_changes": "",
|
"confirm_reject_selected_changes": "",
|
||||||
"confirm_reject_selected_changes_plural": "",
|
"confirm_reject_selected_changes_plural": "",
|
||||||
"confirm_remove_sso_config_enter_email": "",
|
"confirm_remove_sso_config_enter_email": "",
|
||||||
|
"confirm_secondary_email": "",
|
||||||
"confirm_your_email": "",
|
"confirm_your_email": "",
|
||||||
"confirming": "",
|
"confirming": "",
|
||||||
"conflicting_paths_found": "",
|
"conflicting_paths_found": "",
|
||||||
@@ -488,6 +489,7 @@
|
|||||||
"email_link_expired": "",
|
"email_link_expired": "",
|
||||||
"email_must_be_linked_to_institution": "",
|
"email_must_be_linked_to_institution": "",
|
||||||
"email_or_password_wrong_try_again": "",
|
"email_or_password_wrong_try_again": "",
|
||||||
|
"email_remove_by_date": "",
|
||||||
"emails_and_affiliations_explanation": "",
|
"emails_and_affiliations_explanation": "",
|
||||||
"emails_and_affiliations_title": "",
|
"emails_and_affiliations_title": "",
|
||||||
"empty": "",
|
"empty": "",
|
||||||
@@ -502,6 +504,7 @@
|
|||||||
"enables_real_time_syntax_checking_in_the_editor": "",
|
"enables_real_time_syntax_checking_in_the_editor": "",
|
||||||
"enabling": "",
|
"enabling": "",
|
||||||
"end_of_document": "",
|
"end_of_document": "",
|
||||||
|
"ensure_recover_account": "",
|
||||||
"enter_6_digit_code": "",
|
"enter_6_digit_code": "",
|
||||||
"enter_any_size_including_units_or_valid_latex_command": "",
|
"enter_any_size_including_units_or_valid_latex_command": "",
|
||||||
"enter_image_url": "",
|
"enter_image_url": "",
|
||||||
@@ -1185,7 +1188,8 @@
|
|||||||
"please_check_your_inbox_to_confirm": "",
|
"please_check_your_inbox_to_confirm": "",
|
||||||
"please_compile_pdf_before_download": "",
|
"please_compile_pdf_before_download": "",
|
||||||
"please_compile_pdf_before_word_count": "",
|
"please_compile_pdf_before_word_count": "",
|
||||||
"please_confirm_email": "",
|
"please_confirm_primary_email": "",
|
||||||
|
"please_confirm_secondary_email": "",
|
||||||
"please_confirm_your_email_before_making_it_default": "",
|
"please_confirm_your_email_before_making_it_default": "",
|
||||||
"please_contact_support_to_makes_change_to_your_plan": "",
|
"please_contact_support_to_makes_change_to_your_plan": "",
|
||||||
"please_enter_confirmation_code": "",
|
"please_enter_confirmation_code": "",
|
||||||
@@ -1300,6 +1304,7 @@
|
|||||||
"recompile": "",
|
"recompile": "",
|
||||||
"recompile_from_scratch": "",
|
"recompile_from_scratch": "",
|
||||||
"recompile_pdf": "",
|
"recompile_pdf": "",
|
||||||
|
"reconfirm_secondary_email": "",
|
||||||
"reconnect": "",
|
"reconnect": "",
|
||||||
"reconnecting": "",
|
"reconnecting": "",
|
||||||
"reconnecting_in_x_secs": "",
|
"reconnecting_in_x_secs": "",
|
||||||
@@ -1333,6 +1338,7 @@
|
|||||||
"remove": "",
|
"remove": "",
|
||||||
"remove_access": "",
|
"remove_access": "",
|
||||||
"remove_add_on": "",
|
"remove_add_on": "",
|
||||||
|
"remove_email_address": "",
|
||||||
"remove_from_group": "",
|
"remove_from_group": "",
|
||||||
"remove_link": "",
|
"remove_link": "",
|
||||||
"remove_manager": "",
|
"remove_manager": "",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import Notification from '../notification'
|
import Notification from '../notification'
|
||||||
import Icon from '../../../../../shared/components/icon'
|
|
||||||
import getMeta from '../../../../../utils/meta'
|
import getMeta from '../../../../../utils/meta'
|
||||||
import useAsync from '../../../../../shared/hooks/use-async'
|
import useAsync from '../../../../../shared/hooks/use-async'
|
||||||
import { useProjectListContext } from '../../../context/project-list-context'
|
import { useProjectListContext } from '../../../context/project-list-context'
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
import { UserEmailData } from '../../../../../../../types/user-email'
|
import { UserEmailData } from '../../../../../../../types/user-email'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||||
|
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||||
|
|
||||||
const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
|
const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
|
||||||
const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings')
|
const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings')
|
||||||
@@ -69,17 +69,71 @@ function isOnFreeOrIndividualPlan() {
|
|||||||
const showConfirmEmail = (userEmail: UserEmailData) => {
|
const showConfirmEmail = (userEmail: UserEmailData) => {
|
||||||
const { emailConfirmationDisabled } = getMeta('ol-ExposedSettings')
|
const { emailConfirmationDisabled } = getMeta('ol-ExposedSettings')
|
||||||
|
|
||||||
return (
|
return !emailConfirmationDisabled && !ssoAvailable(userEmail)
|
||||||
!emailConfirmationDisabled &&
|
|
||||||
!userEmail.confirmedAt &&
|
|
||||||
!ssoAvailable(userEmail)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
const EMAIL_DELETION_SCHEDULE = {
|
||||||
|
'2025-06-03': '2017-12-31',
|
||||||
|
'2025-07-03': '2019-12-31',
|
||||||
|
'2025-08-03': '2021-12-31',
|
||||||
|
'2025-09-03': '2025-03-03',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emails that remain unconfirmed after 90 days will be removed from the account
|
||||||
|
function getEmailDeletionDate(emailData: UserEmailData, signUpDate: string) {
|
||||||
|
if (emailData.default) return false
|
||||||
|
if (emailData.confirmedAt) return false
|
||||||
|
|
||||||
|
if (!signUpDate) return false
|
||||||
|
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
for (const [deletionDate, cutoffDate] of Object.entries(
|
||||||
|
EMAIL_DELETION_SCHEDULE
|
||||||
|
)) {
|
||||||
|
const emailSignupDate = new Date(signUpDate)
|
||||||
|
const emailCutoffDate = new Date(cutoffDate)
|
||||||
|
const emailDeletionDate = new Date(deletionDate)
|
||||||
|
|
||||||
|
if (emailSignupDate < emailCutoffDate) {
|
||||||
|
const notificationStartDate = new Date(
|
||||||
|
emailDeletionDate.getTime() - 90 * 24 * 60 * 60 * 1000
|
||||||
|
)
|
||||||
|
if (currentDate >= notificationStartDate) {
|
||||||
|
if (currentDate > emailDeletionDate) {
|
||||||
|
return new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
return emailDeletionDate.toLocaleDateString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmEmailNotification({
|
||||||
|
userEmail,
|
||||||
|
signUpDate,
|
||||||
|
}: {
|
||||||
|
userEmail: UserEmailData
|
||||||
|
signUpDate: string
|
||||||
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
|
const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
|
||||||
|
|
||||||
|
// We consider secondary emails added on or after 22.03.2024 to be trusted for account recovery
|
||||||
|
// https://github.com/overleaf/internal/pull/17572
|
||||||
|
const emailTrustCutoffDate = new Date('2024-03-22')
|
||||||
|
const emailDeletionDate = getEmailDeletionDate(userEmail, signUpDate)
|
||||||
|
const isPrimary = userEmail.default
|
||||||
|
|
||||||
|
const isEmailTrusted =
|
||||||
|
userEmail.lastConfirmedAt &&
|
||||||
|
new Date(userEmail.lastConfirmedAt) >= emailTrustCutoffDate
|
||||||
|
|
||||||
|
const shouldShowCommonsNotification =
|
||||||
|
emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()
|
||||||
|
|
||||||
const handleResendConfirmationEmail = ({ email }: UserEmailData) => {
|
const handleResendConfirmationEmail = ({ email }: UserEmailData) => {
|
||||||
runAsync(
|
runAsync(
|
||||||
postJSON('/user/emails/resend_confirmation', {
|
postJSON('/user/emails/resend_confirmation', {
|
||||||
@@ -92,20 +146,120 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!userEmail.lastConfirmedAt && !shouldShowCommonsNotification) {
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
type="warning"
|
||||||
|
content={
|
||||||
|
<div data-testid="pro-notification-body">
|
||||||
|
{isLoading ? (
|
||||||
|
<div data-testid="loading-resending-confirmation-email">
|
||||||
|
<LoadingSpinner
|
||||||
|
loadingText={t('resending_confirmation_email')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{isPrimary
|
||||||
|
? t('please_confirm_primary_email', {
|
||||||
|
emailAddress: userEmail.email,
|
||||||
|
})
|
||||||
|
: t('please_confirm_secondary_email', {
|
||||||
|
emailAddress: userEmail.email,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
{emailDeletionDate && (
|
||||||
|
<p>
|
||||||
|
{t('email_remove_by_date', { date: emailDeletionDate })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<>
|
||||||
|
<OLButton
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleResendConfirmationEmail(userEmail)}
|
||||||
|
>
|
||||||
|
{t('resend_confirmation_email')}
|
||||||
|
</OLButton>
|
||||||
|
<OLButton variant="link" href="/user/settings">
|
||||||
|
{isPrimary
|
||||||
|
? t('change_primary_email')
|
||||||
|
: t('remove_email_address')}
|
||||||
|
</OLButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmailTrusted && !isPrimary && !shouldShowCommonsNotification) {
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
type="warning"
|
||||||
|
content={
|
||||||
|
<div data-testid="not-trusted-notification-body">
|
||||||
|
{isLoading ? (
|
||||||
|
<div data-testid="error-id">
|
||||||
|
<LoadingSpinner
|
||||||
|
loadingText={t('resending_confirmation_email')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<b>{t('confirm_secondary_email')}</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t('reconfirm_secondary_email', {
|
||||||
|
emailAddress: userEmail.email,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p>{t('ensure_recover_account')}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<>
|
||||||
|
<OLButton
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleResendConfirmationEmail(userEmail)}
|
||||||
|
>
|
||||||
|
{t('resend_confirmation_email')}
|
||||||
|
</OLButton>
|
||||||
|
<OLButton
|
||||||
|
variant="link"
|
||||||
|
href="/user/settings"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
{t('remove_email_address')}
|
||||||
|
</OLButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Only show the notification if a) a commons license is available and b) the
|
// Only show the notification if a) a commons license is available and b) the
|
||||||
// user is on a free or individual plan. Users on a group or Commons plan
|
// user is on a free or individual plan. Users on a group or Commons plan
|
||||||
// already have premium features.
|
// already have premium features.
|
||||||
if (emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()) {
|
if (shouldShowCommonsNotification) {
|
||||||
return (
|
return (
|
||||||
<Notification
|
<Notification
|
||||||
type="info"
|
type="info"
|
||||||
content={
|
content={
|
||||||
<div data-testid="notification-body">
|
<div data-testid="notification-body">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<LoadingSpinner loadingText={t('resending_confirmation_email')} />
|
||||||
<Icon type="spinner" spin /> {t('resending_confirmation_email')}
|
|
||||||
…
|
|
||||||
</>
|
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -141,43 +295,15 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return null
|
||||||
<Notification
|
|
||||||
type="warning"
|
|
||||||
content={
|
|
||||||
<div data-testid="pro-notification-body">
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Icon type="spinner" spin /> {t('resending_confirmation_email')}
|
|
||||||
…
|
|
||||||
</>
|
|
||||||
) : isError ? (
|
|
||||||
<div aria-live="polite">{getUserFacingMessage(error)}</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{t('please_confirm_email', {
|
|
||||||
emailAddress: userEmail.email,
|
|
||||||
})}{' '}
|
|
||||||
<OLButton
|
|
||||||
variant="link"
|
|
||||||
onClick={() => handleResendConfirmationEmail(userEmail)}
|
|
||||||
className="btn-inline-link"
|
|
||||||
>
|
|
||||||
{t('resend_confirmation_email')}
|
|
||||||
</OLButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfirmEmail() {
|
function ConfirmEmail() {
|
||||||
const { totalProjectsCount } = useProjectListContext()
|
const { totalProjectsCount } = useProjectListContext()
|
||||||
const userEmails = getMeta('ol-userEmails') || []
|
const userEmails = getMeta('ol-userEmails') || []
|
||||||
|
const signUpDate = getMeta('ol-user')?.signUpDate
|
||||||
|
|
||||||
if (!totalProjectsCount || !userEmails.length) {
|
if (!totalProjectsCount || !userEmails.length || !signUpDate) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +314,7 @@ function ConfirmEmail() {
|
|||||||
<ConfirmEmailNotification
|
<ConfirmEmailNotification
|
||||||
key={`confirm-email-${userEmail.email}`}
|
key={`confirm-email-${userEmail.email}`}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
|
signUpDate={signUpDate}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
})}
|
})}
|
||||||
@@ -196,3 +323,4 @@ function ConfirmEmail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ConfirmEmail
|
export default ConfirmEmail
|
||||||
|
export { getEmailDeletionDate }
|
||||||
|
|||||||
@@ -394,6 +394,7 @@
|
|||||||
"confirm_reject_selected_changes": "Are you sure you want to reject the selected change?",
|
"confirm_reject_selected_changes": "Are you sure you want to reject the selected change?",
|
||||||
"confirm_reject_selected_changes_plural": "Are you sure you want to reject the selected __count__ changes?",
|
"confirm_reject_selected_changes_plural": "Are you sure you want to reject the selected __count__ changes?",
|
||||||
"confirm_remove_sso_config_enter_email": "To confirm you want to remove your SSO configuration, enter your email address:",
|
"confirm_remove_sso_config_enter_email": "To confirm you want to remove your SSO configuration, enter your email address:",
|
||||||
|
"confirm_secondary_email": "Confirm secondary email",
|
||||||
"confirm_your_email": "Confirm your email address",
|
"confirm_your_email": "Confirm your email address",
|
||||||
"confirmation_link_broken": "Sorry, something is wrong with your confirmation link. Please try copy and pasting the link from the bottom of your confirmation email.",
|
"confirmation_link_broken": "Sorry, something is wrong with your confirmation link. Please try copy and pasting the link from the bottom of your confirmation email.",
|
||||||
"confirmation_token_invalid": "Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.",
|
"confirmation_token_invalid": "Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.",
|
||||||
@@ -629,6 +630,7 @@
|
|||||||
"email_must_be_linked_to_institution": "As a member of __institutionName__, this email address can only be added via single sign-on on your <0>account settings</0> page. Please add a different recovery email address.",
|
"email_must_be_linked_to_institution": "As a member of __institutionName__, this email address can only be added via single sign-on on your <0>account settings</0> page. Please add a different recovery email address.",
|
||||||
"email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again.",
|
"email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again.",
|
||||||
"email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password</0>.",
|
"email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password</0>.",
|
||||||
|
"email_remove_by_date": "If this is not done by __date__, it will be removed from the account.",
|
||||||
"email_required": "Email required",
|
"email_required": "Email required",
|
||||||
"email_sent": "Email Sent",
|
"email_sent": "Email Sent",
|
||||||
"emails": "Emails",
|
"emails": "Emails",
|
||||||
@@ -648,6 +650,7 @@
|
|||||||
"enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor",
|
"enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor",
|
||||||
"enabling": "Enabling",
|
"enabling": "Enabling",
|
||||||
"end_of_document": "End of document",
|
"end_of_document": "End of document",
|
||||||
|
"ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.",
|
||||||
"enter_6_digit_code": "Enter 6-digit code",
|
"enter_6_digit_code": "Enter 6-digit code",
|
||||||
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
|
"enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command",
|
||||||
"enter_image_url": "Enter image URL",
|
"enter_image_url": "Enter image URL",
|
||||||
@@ -1585,6 +1588,8 @@
|
|||||||
"please_compile_pdf_before_download": "Please compile your project before downloading the PDF",
|
"please_compile_pdf_before_download": "Please compile your project before downloading the PDF",
|
||||||
"please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
|
"please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
|
||||||
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
|
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
|
||||||
|
"please_confirm_primary_email": "Please confirm your primary email address __emailAddress__ by clicking on the link in the confirmation email.",
|
||||||
|
"please_confirm_secondary_email": "Please confirm your secondary email address __emailAddress__ by clicking on the link in the confirmation email.",
|
||||||
"please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
|
"please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
|
||||||
"please_contact_support_to_makes_change_to_your_plan": "Please <0>contact support</0> to make changes to your plan",
|
"please_contact_support_to_makes_change_to_your_plan": "Please <0>contact support</0> to make changes to your plan",
|
||||||
"please_contact_us_if_you_think_this_is_in_error": "Please <0>contact us</0> if you think this is in error.",
|
"please_contact_us_if_you_think_this_is_in_error": "Please <0>contact us</0> if you think this is in error.",
|
||||||
@@ -1719,6 +1724,7 @@
|
|||||||
"reconfirm": "reconfirm",
|
"reconfirm": "reconfirm",
|
||||||
"reconfirm_account": "Reconfirm account",
|
"reconfirm_account": "Reconfirm account",
|
||||||
"reconfirm_explained": "We need to reconfirm your account. Please request a password reset link via the form below to reconfirm your account. If you have any problems reconfirming your account, please contact us at",
|
"reconfirm_explained": "We need to reconfirm your account. Please request a password reset link via the form below to reconfirm your account. If you have any problems reconfirming your account, please contact us at",
|
||||||
|
"reconfirm_secondary_email": "To enhance the security of your __appName__ account, please reconfirm your secondary email address __emailAddress__.",
|
||||||
"reconnect": "Try again",
|
"reconnect": "Try again",
|
||||||
"reconnecting": "Reconnecting",
|
"reconnecting": "Reconnecting",
|
||||||
"reconnecting_in_x_secs": "Reconnecting in __seconds__ secs",
|
"reconnecting_in_x_secs": "Reconnecting in __seconds__ secs",
|
||||||
@@ -1764,6 +1770,7 @@
|
|||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"remove_access": "Remove access",
|
"remove_access": "Remove access",
|
||||||
"remove_add_on": "Remove add-on",
|
"remove_add_on": "Remove add-on",
|
||||||
|
"remove_email_address": "Remove email address",
|
||||||
"remove_from_group": "Remove from group",
|
"remove_from_group": "Remove from group",
|
||||||
"remove_link": "Remove link",
|
"remove_link": "Remove link",
|
||||||
"remove_manager": "Remove manager",
|
"remove_manager": "Remove manager",
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import { merge, cloneDeep } from 'lodash'
|
|||||||
import {
|
import {
|
||||||
professionalUserData,
|
professionalUserData,
|
||||||
unconfirmedUserData,
|
unconfirmedUserData,
|
||||||
|
untrustedUserData,
|
||||||
unconfirmedCommonsUserData,
|
unconfirmedCommonsUserData,
|
||||||
|
confirmedUserData,
|
||||||
} from '../../settings/fixtures/test-user-email-data'
|
} from '../../settings/fixtures/test-user-email-data'
|
||||||
import {
|
import {
|
||||||
notificationDropboxDuplicateProjectNames,
|
notificationDropboxDuplicateProjectNames,
|
||||||
@@ -24,7 +26,9 @@ import {
|
|||||||
} from '../fixtures/notifications-data'
|
} from '../fixtures/notifications-data'
|
||||||
import Common from '../../../../../frontend/js/features/project-list/components/notifications/groups/common'
|
import Common from '../../../../../frontend/js/features/project-list/components/notifications/groups/common'
|
||||||
import Institution from '../../../../../frontend/js/features/project-list/components/notifications/groups/institution'
|
import Institution from '../../../../../frontend/js/features/project-list/components/notifications/groups/institution'
|
||||||
import ConfirmEmail from '../../../../../frontend/js/features/project-list/components/notifications/groups/confirm-email'
|
import ConfirmEmail, {
|
||||||
|
getEmailDeletionDate,
|
||||||
|
} from '../../../../../frontend/js/features/project-list/components/notifications/groups/confirm-email'
|
||||||
import ReconfirmationInfo from '../../../../../frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info'
|
import ReconfirmationInfo from '../../../../../frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info'
|
||||||
import { ProjectListProvider } from '../../../../../frontend/js/features/project-list/context/project-list-context'
|
import { ProjectListProvider } from '../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||||
@@ -597,40 +601,142 @@ describe('<UserNotifications />', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getEmailDeletionDate', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', [
|
||||||
|
confirmedUserData,
|
||||||
|
untrustedUserData,
|
||||||
|
])
|
||||||
|
this.clock = sinon.useFakeTimers(new Date('2025-07-01').getTime())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
this.clock.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns deletion date for unconfirmed email within notification window', function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
|
||||||
|
const signUpDate = '2022-01-01' // Before cutoff '2025-03-03'
|
||||||
|
const emailDeletionDate = getEmailDeletionDate(
|
||||||
|
unconfirmedUserData,
|
||||||
|
signUpDate
|
||||||
|
)
|
||||||
|
expect(emailDeletionDate).to.equal(
|
||||||
|
new Date('2025-09-03').toLocaleDateString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for primary email', function () {
|
||||||
|
const primaryUserData = { ...unconfirmedUserData, default: true }
|
||||||
|
const signUpDate = '2022-01-01'
|
||||||
|
const emailDeletionDate = getEmailDeletionDate(
|
||||||
|
primaryUserData,
|
||||||
|
signUpDate
|
||||||
|
)
|
||||||
|
expect(emailDeletionDate).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for already confirmed email', function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', [confirmedUserData])
|
||||||
|
const signUpDate = '2022-01-01'
|
||||||
|
const emailDeletionDate = getEmailDeletionDate(
|
||||||
|
confirmedUserData,
|
||||||
|
signUpDate
|
||||||
|
)
|
||||||
|
expect(emailDeletionDate).to.be.false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('<ConfirmEmail/>', function () {
|
describe('<ConfirmEmail/>', function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Object.assign(getMeta('ol-ExposedSettings'), {
|
Object.assign(getMeta('ol-ExposedSettings'), {
|
||||||
emailConfirmationDisabled: false,
|
emailConfirmationDisabled: false,
|
||||||
})
|
})
|
||||||
window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
|
window.metaAttributesCache.set('ol-userEmails', [
|
||||||
|
confirmedUserData,
|
||||||
|
untrustedUserData,
|
||||||
|
])
|
||||||
window.metaAttributesCache.set(
|
window.metaAttributesCache.set(
|
||||||
'ol-usersBestSubscription',
|
'ol-usersBestSubscription',
|
||||||
freeSubscription
|
freeSubscription
|
||||||
)
|
)
|
||||||
|
window.metaAttributesCache.set('ol-user', {
|
||||||
|
signUpDate: new Date('2024-01-01').toISOString(),
|
||||||
|
})
|
||||||
|
this.clock = sinon.useFakeTimers(new Date('2025-07-01').getTime())
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
|
this.clock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends successfully', async function () {
|
function testUnconfirmedNotification(
|
||||||
|
userEmails: any[],
|
||||||
|
isPrimary: boolean
|
||||||
|
) {
|
||||||
|
it(`sends unconfirmed notification email successfully when email is ${isPrimary ? 'primary' : 'secondary'}`, async function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', userEmails)
|
||||||
|
|
||||||
|
renderWithinProjectListProvider(ConfirmEmail)
|
||||||
|
await fetchMock.flush(true)
|
||||||
|
fetchMock.post('/user/emails/resend_confirmation', 200)
|
||||||
|
|
||||||
|
const email = userEmails[0].email
|
||||||
|
const notificationBody = screen.getByTestId('pro-notification-body')
|
||||||
|
|
||||||
|
if (isPrimary) {
|
||||||
|
expect(notificationBody.textContent).to.contain(
|
||||||
|
`Please confirm your primary email address ${email} by clicking on the link in the confirmation email.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expect(notificationBody.textContent).to.contain(
|
||||||
|
`Please confirm your secondary email address ${email} by clicking on the link in the confirmation email.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendButton = screen.getByRole('button', { name: /resend/i })
|
||||||
|
fireEvent.click(resendButton)
|
||||||
|
|
||||||
|
await waitForElementToBeRemoved(() =>
|
||||||
|
screen.getByRole('button', { name: /resend/i })
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fetchMock.called()).to.be.true
|
||||||
|
expect(screen.queryByRole('alert')).to.be.null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
testUnconfirmedNotification(
|
||||||
|
[{ email: 'baz@overleaf.com', default: true }],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
testUnconfirmedNotification(
|
||||||
|
[{ email: 'baz@overleaf.com', default: false }],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
it('sends untrusted notification email successfully', async function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', [untrustedUserData])
|
||||||
|
|
||||||
renderWithinProjectListProvider(ConfirmEmail)
|
renderWithinProjectListProvider(ConfirmEmail)
|
||||||
await fetchMock.flush(true)
|
await fetchMock.flush(true)
|
||||||
fetchMock.post('/user/emails/resend_confirmation', 200)
|
fetchMock.post('/user/emails/resend_confirmation', 200)
|
||||||
|
|
||||||
const email = unconfirmedUserData.email
|
const email = untrustedUserData.email
|
||||||
const notificationBody = screen.getByTestId('pro-notification-body')
|
const notificationBody = screen.getByTestId(
|
||||||
|
'not-trusted-notification-body'
|
||||||
|
)
|
||||||
expect(notificationBody.textContent).to.contain(
|
expect(notificationBody.textContent).to.contain(
|
||||||
`Please confirm your email ${email} by clicking on the link in the confirmation email`
|
`To enhance the security of your Overleaf account, please reconfirm your secondary email address ${email}.`
|
||||||
)
|
)
|
||||||
|
|
||||||
const resendButton = screen.getByRole('button', { name: /resend/i })
|
const resendButton = screen.getByRole('button', { name: /resend/i })
|
||||||
fireEvent.click(resendButton)
|
fireEvent.click(resendButton)
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: /resend/i })).to.be.null
|
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() =>
|
await waitForElementToBeRemoved(() =>
|
||||||
screen.getByText(/resending confirmation email/i)
|
screen.getByRole('button', { name: /resend/i })
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(fetchMock.called()).to.be.true
|
expect(fetchMock.called()).to.be.true
|
||||||
@@ -638,15 +744,21 @@ describe('<UserNotifications />', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('fails to send', async function () {
|
it('fails to send', async function () {
|
||||||
|
window.metaAttributesCache.set('ol-userEmails', [unconfirmedUserData])
|
||||||
|
|
||||||
renderWithinProjectListProvider(ConfirmEmail)
|
renderWithinProjectListProvider(ConfirmEmail)
|
||||||
await fetchMock.flush(true)
|
await fetchMock.flush(true)
|
||||||
fetchMock.post('/user/emails/resend_confirmation', 500)
|
fetchMock.post('/user/emails/resend_confirmation', 500)
|
||||||
|
|
||||||
const resendButton = screen.getByRole('button', { name: /resend/i })
|
const resendButtons = screen.getAllByRole('button', { name: /resend/i })
|
||||||
|
const resendButton = resendButtons[0]
|
||||||
fireEvent.click(resendButton)
|
fireEvent.click(resendButton)
|
||||||
|
const notificationBody = screen.getByTestId('pro-notification-body')
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() =>
|
await waitForElementToBeRemoved(() =>
|
||||||
screen.getByText(/resending confirmation email/i)
|
within(notificationBody).getByTestId(
|
||||||
|
'loading-resending-confirmation-email'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(fetchMock.called()).to.be.true
|
expect(fetchMock.called()).to.be.true
|
||||||
@@ -689,9 +801,16 @@ describe('<UserNotifications />', function () {
|
|||||||
const notificationBody = within(alert).getByTestId(
|
const notificationBody = within(alert).getByTestId(
|
||||||
'pro-notification-body'
|
'pro-notification-body'
|
||||||
)
|
)
|
||||||
expect(notificationBody.textContent).to.contain(
|
const isPrimary = unconfirmedCommonsUserData.default
|
||||||
`Please confirm your email ${email} by clicking on the link in the confirmation email`
|
if (isPrimary) {
|
||||||
)
|
expect(notificationBody.textContent).to.contain(
|
||||||
|
`Please confirm your primary email address ${email} by clicking on the link in the confirmation email`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expect(notificationBody.textContent).to.contain(
|
||||||
|
`Please confirm your secondary email address ${email} by clicking on the link in the confirmation email`
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ describe('<ProjectListRoot />', function () {
|
|||||||
it('the email confirmation alert is not displayed', async function () {
|
it('the email confirmation alert is not displayed', async function () {
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(
|
screen.queryByText(
|
||||||
'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email'
|
'Please confirm your primary email address test@overleaf.com by clicking on the link in the confirmation email.'
|
||||||
)
|
)
|
||||||
).to.be.null
|
).to.be.null
|
||||||
})
|
})
|
||||||
@@ -1141,13 +1141,5 @@ describe('<ProjectListRoot />', function () {
|
|||||||
await screen.findByText(copiedProjectName)
|
await screen.findByText(copiedProjectName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('notifications', function () {
|
|
||||||
it('email confirmation alert is displayed', async function () {
|
|
||||||
screen.getByText(
|
|
||||||
'Please confirm your email test@overleaf.com by clicking on the link in the confirmation email'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
unconfirmedUserData,
|
unconfirmedUserData,
|
||||||
fakeUsersData,
|
fakeUsersData,
|
||||||
unconfirmedCommonsUserData,
|
unconfirmedCommonsUserData,
|
||||||
|
untrustedUserData,
|
||||||
} from '../fixtures/test-user-email-data'
|
} from '../fixtures/test-user-email-data'
|
||||||
import localStorage from '@/infrastructure/local-storage'
|
import localStorage from '@/infrastructure/local-storage'
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ describe('UserEmailContext', function () {
|
|||||||
await fetchMock.flush(true)
|
await fetchMock.flush(true)
|
||||||
expect(fetchMock.calls()).to.have.lengthOf(1)
|
expect(fetchMock.calls()).to.have.lengthOf(1)
|
||||||
expect(result.current.state.data.byId).to.deep.equal({
|
expect(result.current.state.data.byId).to.deep.equal({
|
||||||
'bar@overleaf.com': confirmedUserData,
|
'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
|
||||||
'baz@overleaf.com': unconfirmedUserData,
|
'baz@overleaf.com': unconfirmedUserData,
|
||||||
'foo@overleaf.com': professionalUserData,
|
'foo@overleaf.com': professionalUserData,
|
||||||
'qux@overleaf.com': unconfirmedCommonsUserData,
|
'qux@overleaf.com': unconfirmedCommonsUserData,
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export const unconfirmedUserData: UserEmailData = {
|
|||||||
default: false,
|
default: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const untrustedUserData = {
|
||||||
|
...confirmedUserData,
|
||||||
|
lastConfirmedAt: '2024-01-01T10:59:44.139Z',
|
||||||
|
}
|
||||||
|
|
||||||
export const professionalUserData: UserEmailData & {
|
export const professionalUserData: UserEmailData & {
|
||||||
affiliation: Affiliation
|
affiliation: Affiliation
|
||||||
} = {
|
} = {
|
||||||
@@ -112,6 +117,7 @@ export const ssoUserData: UserEmailData = {
|
|||||||
export const fakeUsersData = [
|
export const fakeUsersData = [
|
||||||
{ ...confirmedUserData },
|
{ ...confirmedUserData },
|
||||||
{ ...unconfirmedUserData },
|
{ ...unconfirmedUserData },
|
||||||
|
{ ...untrustedUserData },
|
||||||
{ ...professionalUserData },
|
{ ...professionalUserData },
|
||||||
{ ...unconfirmedCommonsUserData },
|
{ ...unconfirmedCommonsUserData },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Affiliation } from './affiliation'
|
|||||||
export type UserEmailData = {
|
export type UserEmailData = {
|
||||||
affiliation?: Affiliation
|
affiliation?: Affiliation
|
||||||
confirmedAt?: string
|
confirmedAt?: string
|
||||||
|
lastConfirmedAt?: string | null
|
||||||
email: string
|
email: string
|
||||||
default: boolean
|
default: boolean
|
||||||
samlProviderId?: string
|
samlProviderId?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user