diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs index b93d37c8b3..69bb1f3adb 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.mjs +++ b/services/web/app/src/Features/Email/EmailBuilder.mjs @@ -271,32 +271,6 @@ templates.passwordResetRequested = ctaTemplate({ }, }) -templates.confirmEmail = ctaTemplate({ - subject() { - return `Confirm email - ${settings.appName}` - }, - title() { - return 'Confirm email' - }, - message(opts) { - return [ - `Please confirm that you have added a new email, ${opts.to}, to your ${settings.appName} account.`, - ] - }, - secondaryMessage() { - return [ - `If you did not request this, please let us know at ${settings.adminEmail}.`, - `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`, - ] - }, - ctaText() { - return 'Confirm email' - }, - ctaURL(opts) { - return opts.confirmEmailUrl - }, -}) - templates.confirmCode = NoCTAEmailTemplate({ greeting(opts) { return '' @@ -391,32 +365,6 @@ templates.projectInvite = ctaTemplate({ }, }) -templates.reconfirmEmail = ctaTemplate({ - subject() { - return `Reconfirm Email - ${settings.appName}` - }, - title() { - return 'Reconfirm Email' - }, - message(opts) { - return [ - `Please reconfirm your email address, ${opts.to}, on your ${settings.appName} account.`, - ] - }, - secondaryMessage() { - return [ - 'If you did not request this, you can simply ignore this message.', - `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`, - ] - }, - ctaText() { - return 'Reconfirm email' - }, - ctaURL(opts) { - return opts.confirmEmailUrl - }, -}) - templates.verifyEmailToJoinTeam = ctaTemplate({ subject(opts) { return `${opts.reminder ? 'Reminder: ' : ''}${_.escape( diff --git a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.mjs b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.mjs index 444b4dda56..57df9cdf95 100644 --- a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.mjs +++ b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.mjs @@ -1,7 +1,6 @@ import EmailHelper from '../Helpers/EmailHelper.mjs' import EmailHandler from '../Email/EmailHandler.mjs' import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs' -import settings from '@overleaf/settings' import Errors from '../Errors/Errors.js' import UserUpdater from './UserUpdater.mjs' import UserGetter from './UserGetter.mjs' @@ -10,33 +9,9 @@ import crypto from 'node:crypto' import SessionManager from '../Authentication/SessionManager.mjs' // Reject email confirmation tokens after 90 days -const TOKEN_EXPIRY_IN_S = 90 * 24 * 60 * 60 const TOKEN_USE = 'email_confirmation' const CONFIRMATION_CODE_EXPIRY_IN_S = 10 * 60 -async function sendConfirmationEmail( - userId, - email, - emailTemplate = 'confirmEmail' -) { - email = EmailHelper.parseEmail(email) - if (!email) { - throw new Error('invalid email') - } - const data = { user_id: userId, email } - const token = await OneTimeTokenHandler.promises.getNewToken( - TOKEN_USE, - data, - { expiresIn: TOKEN_EXPIRY_IN_S } - ) - const emailOptions = { - to: email, - confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`, - sendingUser_id: userId, - } - await EmailHandler.promises.sendEmail(emailTemplate, emailOptions) -} - async function sendConfirmationCode(email, welcomeUser) { if (!EmailHelper.parseEmail(email)) { throw new Error('invalid email') @@ -95,11 +70,9 @@ async function confirmEmailFromToken(req, token) { const UserEmailsConfirmationHandler = { confirmEmailFromToken: callbackify(confirmEmailFromToken), - sendConfirmationEmail: callbackify(sendConfirmationEmail), } UserEmailsConfirmationHandler.promises = { - sendConfirmationEmail, confirmEmailFromToken, sendConfirmationCode, } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 627649bf52..becf381a1d 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -594,6 +594,7 @@ "email_address": "", "email_address_is_invalid": "", "email_already_registered": "", + "email_already_registered_sso": "", "email_attribute": "", "email_does_not_belong_to_university": "", "email_limit_reached": "", diff --git a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx index 5f51195551..d9b90dc2f9 100644 --- a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx +++ b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx @@ -144,7 +144,6 @@ export function ConfirmEmailForm({ postJSON(resendEndpoint) .then(data => { - setIsResending(false) if (data?.message?.key) { setFeedback({ type: 'alert', @@ -334,7 +333,7 @@ function Title({ return outerErrorDisplay ? (
) : ( -

{outerErrorDisplay ? null : t('we_sent_code')}

+

{t('we_sent_code')}

) if (interstitial) return

{t('confirm_your_email')}

@@ -416,6 +415,9 @@ function ErrorMessage({ error }: { error: string }) { case 'please_enter_confirmation_code': return {t('please_enter_confirmation_code')} + case 'email_already_registered_sso': + return {t('email_already_registered_sso')} + default: return {t('generic_something_went_wrong')} } diff --git a/services/web/frontend/js/shared/components/ds/ds-form-text.tsx b/services/web/frontend/js/shared/components/ds/ds-form-text.tsx index 37674a4a4e..5dae58ad61 100644 --- a/services/web/frontend/js/shared/components/ds/ds-form-text.tsx +++ b/services/web/frontend/js/shared/components/ds/ds-form-text.tsx @@ -26,9 +26,9 @@ export const getFormTextClass = (type?: TextType) => function FormTextIcon({ type }: { type?: TextType }) { switch (type) { case 'success': - return + return case 'error': - return + return default: return null } diff --git a/services/web/locales/da.json b/services/web/locales/da.json index cc848df0b5..591e726a7b 100644 --- a/services/web/locales/da.json +++ b/services/web/locales/da.json @@ -1445,7 +1445,6 @@ "register_with_another_email": "Bliv registreret hos __appName__ med en anden e-mailadresse.", "registered": "Registreret", "registering": "Registrerer", - "registration_error": "Registreringsfejl", "reject": "Afvis", "reject_change": "Afslå ændring", "related_tags": "Relaterede tags", diff --git a/services/web/locales/de.json b/services/web/locales/de.json index 41ef741740..b65979d4e5 100644 --- a/services/web/locales/de.json +++ b/services/web/locales/de.json @@ -1245,7 +1245,6 @@ "register_with_another_email": "Registriere Dich bei __appName__ mit einer anderen E-Mail-Adresse.", "registered": "Registriert", "registering": "Registrieren", - "registration_error": "Registrierungs-Fehler", "reject": "Verwerfen", "related_tags": "Ähnliche Stichwörter", "relink_your_account": "Verknüpfe dein Konto neu", diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 5b511196cc..c0bdfa53be 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -485,6 +485,7 @@ "confirm_remove_sso_config_enter_email": "To confirm you want to remove your SSO configuration, enter your email address:", "confirm_remove_user_type_email_address": "To confirm you want to remove __userName__ please type the email address associated with their account.", "confirm_secondary_email": "Confirm secondary email", + "confirm_your_account": "Confirm your account", "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_token_invalid": "Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.", @@ -807,6 +808,7 @@ "english_british": "English (British)", "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_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command", + "enter_email_to_get_code": "Enter your email address to get a 6-digit confirmation code.", "enter_emails_separated_by_commas": "Enter emails separated by commas", "enter_manually": "Enter manually", "enter_tax_id_number": "Enter tax ID number", @@ -2110,7 +2112,6 @@ "register_with_another_email": "Register with __appName__ using another email.", "registered": "Registered", "registering": "Registering", - "registration_error": "Registration error", "reject": "Reject", "reject_change": "Reject change", "reject_selected_changes": "Reject selected changes", diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json index 5cc46ca3d7..0af0c0d536 100644 --- a/services/web/locales/fr.json +++ b/services/web/locales/fr.json @@ -899,7 +899,6 @@ "register_with_another_email": "Inscrivez-vous avec __appName__ en utilisant une autre adresse courriel.", "registered": "Inscrit·e", "registering": "Inscription en cours", - "registration_error": "Erreur d’inscription", "reject": "Rejeter", "related_tags": "Étiquettes associées", "reload_editor": "Actualiser l’éditeur", diff --git a/services/web/locales/pt.json b/services/web/locales/pt.json index 4e615d99e2..5402506228 100644 --- a/services/web/locales/pt.json +++ b/services/web/locales/pt.json @@ -474,7 +474,6 @@ "register_to_edit_template": "Por favor, registre-se para editar o modelo __templateName__", "registered": "Registrado", "registering": "Registrando", - "registration_error": "Erro de Registro", "reject": "Rejeitar", "remove": "remover", "remove_from_group": "Remover do grupo", diff --git a/services/web/locales/sv.json b/services/web/locales/sv.json index ea3b86d5a3..a4f25cc1cc 100644 --- a/services/web/locales/sv.json +++ b/services/web/locales/sv.json @@ -668,7 +668,6 @@ "register_to_edit_template": "Vänligen registrera dig för att redigera __templateName__ mallen", "registered": "Registrerad", "registering": "Registrerar", - "registration_error": "Registreringsfel", "reject": "Neka", "reload_editor": "Ladda om redigeraren", "remote_service_error": "Fjärrtjänsten producerade ett fel", diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json index 57d076fc67..8441c9726b 100644 --- a/services/web/locales/zh-CN.json +++ b/services/web/locales/zh-CN.json @@ -1652,7 +1652,6 @@ "register_with_another_email": "使用另一个邮件地址注册 __appName__", "registered": "已注册", "registering": "正在注册", - "registration_error": "注册错误", "reject": "不要", "reject_change": "拒绝修改", "reject_selected_changes": "拒绝选定的更改", diff --git a/services/web/test/acceptance/src/mocks/MockOrcidApi.mjs b/services/web/test/acceptance/src/mocks/MockOrcidApi.mjs new file mode 100644 index 0000000000..42c2e75ef9 --- /dev/null +++ b/services/web/test/acceptance/src/mocks/MockOrcidApi.mjs @@ -0,0 +1,39 @@ +import AbstractMockApi from './AbstractMockApi.mjs' + +class MockOrcidApi extends AbstractMockApi { + reset() { + this.profiles = {} + this.tokens = {} + } + + // profile: { orcid, name } + addProfile(profile, token, authorizationCode) { + this.profiles[token] = { ...profile } + this.tokens[authorizationCode] = token + } + + applyRoutes() { + this.app.post('/oauth/token', (req, res) => { + const token = this.tokens[req.body.code] + if (!token) { + return res.sendStatus(400) + } + const profile = this.profiles[token] + res.json({ + access_token: token, + token_type: 'bearer', + scope: '/authenticate', + ...profile, + }) + }) + } +} + +export default MockOrcidApi + +/** + * @function instance + * @memberOf MockOrcidApi + * @static + * @returns {MockOrcidApi} + */ diff --git a/services/web/test/unit/src/Email/EmailBuilder.test.mjs b/services/web/test/unit/src/Email/EmailBuilder.test.mjs index 4d376ab264..1ade94eda8 100644 --- a/services/web/test/unit/src/Email/EmailBuilder.test.mjs +++ b/services/web/test/unit/src/Email/EmailBuilder.test.mjs @@ -371,41 +371,6 @@ describe('EmailBuilder', function () { }) }) - describe('confirmEmail', function () { - beforeEach(function (ctx) { - ctx.emailAddress = 'example@overleaf.com' - ctx.userId = 'abc123' - ctx.opts = { - to: ctx.emailAddress, - confirmEmailUrl: `${ctx.settings.siteUrl}/user/emails/confirm?token=aToken123`, - sendingUser_id: ctx.userId, - } - ctx.email = ctx.EmailBuilder.buildEmail('confirmEmail', ctx.opts) - }) - - it('should build the email', function (ctx) { - expect(ctx.email.html).to.exist - expect(ctx.email.text).to.exist - }) - - describe('HTML email', function () { - it('should include a CTA button and a fallback CTA link', function (ctx) { - const dom = cheerio.load(ctx.email.html) - const buttonLink = dom('a:contains("Confirm email")') - expect(buttonLink.length).to.equal(1) - expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl) - expect(ctx.email.html).to.contain('copy and paste this link') - expect(ctx.email.html).to.contain(ctx.opts.confirmEmailUrl) - }) - }) - - describe('plain text email', function () { - it('should contain the CTA link', function (ctx) { - expect(ctx.email.text).to.contain(ctx.opts.confirmEmailUrl) - }) - }) - }) - describe('ownershipTransferConfirmationNewOwner', function () { beforeEach(function (ctx) { ctx.emailAddress = 'example@overleaf.com' @@ -489,41 +454,6 @@ describe('EmailBuilder', function () { }) }) - describe('reconfirmEmail', function () { - beforeEach(function (ctx) { - ctx.emailAddress = 'example@overleaf.com' - ctx.userId = 'abc123' - ctx.opts = { - to: ctx.emailAddress, - confirmEmailUrl: `${ctx.settings.siteUrl}/user/emails/confirm?token=aToken123`, - sendingUser_id: ctx.userId, - } - ctx.email = ctx.EmailBuilder.buildEmail('reconfirmEmail', ctx.opts) - }) - - it('should build the email', function (ctx) { - expect(ctx.email.html).to.exist - expect(ctx.email.text).to.exist - }) - - describe('HTML email', function () { - it('should include a CTA button and a fallback CTA link', function (ctx) { - const dom = cheerio.load(ctx.email.html) - const buttonLink = dom('a:contains("Reconfirm email")') - expect(buttonLink.length).to.equal(1) - expect(buttonLink.attr('href')).to.equal(ctx.opts.confirmEmailUrl) - expect(ctx.email.html).to.contain('copy and paste this link') - expect(ctx.email.html).to.contain(ctx.opts.confirmEmailUrl) - }) - }) - - describe('plain text email', function () { - it('should contain the CTA link', function (ctx) { - expect(ctx.email.text).to.contain(ctx.opts.confirmEmailUrl) - }) - }) - }) - describe('verifyEmailToJoinTeam', function () { beforeEach(function (ctx) { ctx.emailAddress = 'example@overleaf.com' diff --git a/services/web/test/unit/src/User/UserEmailsConfirmationHandler.test.mjs b/services/web/test/unit/src/User/UserEmailsConfirmationHandler.test.mjs index 6ed79d7874..3526049044 100644 --- a/services/web/test/unit/src/User/UserEmailsConfirmationHandler.test.mjs +++ b/services/web/test/unit/src/User/UserEmailsConfirmationHandler.test.mjs @@ -30,12 +30,6 @@ describe('UserEmailsConfirmationHandler', function () { ctx.email = ctx.mockUser.email ctx.req = {} - vi.doMock('@overleaf/settings', () => ({ - default: (ctx.settings = { - siteUrl: 'https://emails.example.com', - }), - })) - vi.doMock( '../../../../app/src/Features/Security/OneTimeTokenHandler', () => ({ @@ -83,72 +77,6 @@ describe('UserEmailsConfirmationHandler', function () { return (ctx.callback = sinon.stub()) }) - describe('sendConfirmationEmail', function () { - beforeEach(function (ctx) { - ctx.OneTimeTokenHandler.promises.getNewToken = sinon - .stub() - .resolves((ctx.token = 'new-token')) - return (ctx.EmailHandler.promises.sendEmail = sinon.stub().resolves()) - }) - - describe('successfully', function () { - beforeEach(async function (ctx) { - await ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail( - ctx.user_id, - ctx.email - ) - }) - - it('should generate a token for the user which references their id and email', function (ctx) { - return ctx.OneTimeTokenHandler.promises.getNewToken - .calledWith( - 'email_confirmation', - { user_id: ctx.user_id, email: ctx.email }, - { expiresIn: 90 * 24 * 60 * 60 } - ) - .should.equal(true) - }) - - it('should send an email to the user', function (ctx) { - return ctx.EmailHandler.promises.sendEmail - .calledWith('confirmEmail', { - to: ctx.email, - confirmEmailUrl: - 'https://emails.example.com/user/emails/confirm?token=new-token', - sendingUser_id: ctx.user_id, - }) - .should.equal(true) - }) - }) - - describe('with invalid email', function () { - it('should reject with an error', async function (ctx) { - await expect( - ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail( - ctx.user_id, - '!"£$%^&*()' - ) - ).to.be.rejectedWith(Error) - }) - }) - - describe('a custom template', function () { - beforeEach(async function (ctx) { - await ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail( - ctx.user_id, - ctx.email, - 'myCustomTemplate' - ) - }) - - it('should send an email with the given template', function (ctx) { - return ctx.EmailHandler.promises.sendEmail - .calledWith('myCustomTemplate') - .should.equal(true) - }) - }) - }) - describe('confirmEmailFromToken', function () { beforeEach(function (ctx) { ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon diff --git a/services/web/test/unit/src/User/UserEmailsController.test.mjs b/services/web/test/unit/src/User/UserEmailsController.test.mjs index 384a1873bc..4171257078 100644 --- a/services/web/test/unit/src/User/UserEmailsController.test.mjs +++ b/services/web/test/unit/src/User/UserEmailsController.test.mjs @@ -133,9 +133,7 @@ describe('UserEmailsController', function () { '../../../../app/src/Features/User/UserEmailsConfirmationHandler', () => ({ default: (ctx.UserEmailsConfirmationHandler = { - promises: { - sendConfirmationEmail: vi.fn().mockResolvedValue(undefined), - }, + promises: {}, }), }) )