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: {},
}),
})
)