From 891c7473ca222a99811a3b3e7e4d7b15d8ec6d1c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 2 Apr 2026 09:58:20 +0200 Subject: [PATCH] fix(login): invite flow instead of email verification for users with no primary method, improve sending behaviour (#11837) Closes #10929 # Which Problems Are Solved When a human user without a primary authentication method enters their email on the login screen, the login historically auto-sent an email code (send=true) and redirected them to the `/verify` flow. If the user was newly created via the API and already received an initial verify-email, navigating to the login page would trigger a new invite code, silently invalidating the code they received in their first email. Additionally instead of resending the same type of email (invitation) a regular email verification mail was sent # How the Problems Are Solved - Resend an invitation email instead of a email verification if a user has no method set (still in invitation state) - Conditional Code Sending: Updated `loginname.ts` to check `humanUser?.email?.isVerified`. We only auto-send a new code (`send=true`) if the user's email is already verified. Unverified users will be redirected with `send=false`, allowing them to safely enter the code they already have. - UI State Fix: Fixed an issue in `verify/page.tsx` where the send URL parameter was being checked directly as a string ("false" is truthy). By using the properly evaluated doSend boolean, the "Code Sent" alert now correctly hides itself when a new code is not explicitly sent. - Translation Updates: Refined the codeSent messaging across all locales to specify "A new code has been sent..." to provide better context to the user when they do explicitly request a resend. --- apps/login/locales/ar.json | 2 +- apps/login/locales/de.json | 2 +- apps/login/locales/en.json | 4 ++-- apps/login/locales/es.json | 2 +- apps/login/locales/fr.json | 2 +- apps/login/locales/it.json | 2 +- apps/login/locales/ja.json | 2 +- apps/login/locales/nl.json | 2 +- apps/login/locales/pl.json | 2 +- apps/login/locales/ru.json | 2 +- apps/login/locales/tr.json | 2 +- apps/login/locales/uk.json | 2 +- apps/login/locales/zh.json | 2 +- apps/login/src/lib/server/loginname.test.ts | 25 ++++++++++++++++++++- apps/login/src/lib/server/loginname.ts | 10 +++++++-- 15 files changed, 46 insertions(+), 17 deletions(-) diff --git a/apps/login/locales/ar.json b/apps/login/locales/ar.json index 4c32edf54..159afca41 100644 --- a/apps/login/locales/ar.json +++ b/apps/login/locales/ar.json @@ -84,7 +84,7 @@ "set": { "title": "تعيين كلمة المرور", "description": "تعيين كلمة المرور لحسابك", - "codeSent": "تم إرسال رمز إلى عنوان بريدك الإلكتروني.", + "codeSent": "تم إرسال رمز جديد إلى عنوان بريدك الإلكتروني.", "noCodeReceived": "لم تستلم رمزاً؟", "resend": "إعادة إرسال الرمز", "submit": "متابعة", diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 3fccb474f..5ab2233d5 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -83,7 +83,7 @@ "set": { "title": "Passwort festlegen", "description": "Legen Sie das Passwort für Ihr Konto fest", - "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.", + "codeSent": "Ein neuer Code wurde an Ihre E-Mail-Adresse gesendet.", "noCodeReceived": "Keinen Code erhalten?", "resend": "Erneut senden", "submit": "Weiter", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 735828fe7..4d4c327ae 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -83,7 +83,7 @@ "set": { "title": "Set Password", "description": "Set the password for your account", - "codeSent": "A code has been sent to your email address.", + "codeSent": "A new code has been sent to your email address.", "noCodeReceived": "Didn't receive a code?", "resend": "Resend code", "submit": "Continue", @@ -389,7 +389,7 @@ "description": "Enter the Code provided in the verification email.", "noCodeReceived": "Didn't receive a code?", "resendCode": "Resend code", - "codeSent": "A code has just been sent to your email address.", + "codeSent": "A new code has just been sent to your email address.", "submit": "Continue", "labels": { "code": "Code" diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index af1c6c5e7..99f6c5793 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -83,7 +83,7 @@ "set": { "title": "Establecer Contraseña", "description": "Establece la contraseña para tu cuenta", - "codeSent": "Se ha enviado un código a su correo electrónico.", + "codeSent": "Se ha enviado un nuevo código a su dirección de correo electrónico.", "noCodeReceived": "¿No recibiste un código?", "resend": "Reenviar código", "submit": "Continuar", diff --git a/apps/login/locales/fr.json b/apps/login/locales/fr.json index 4b8682992..fbd868be0 100644 --- a/apps/login/locales/fr.json +++ b/apps/login/locales/fr.json @@ -83,7 +83,7 @@ "set": { "title": "Définir le mot de passe", "description": "Définissez le mot de passe de votre compte", - "codeSent": "Un code a été envoyé à votre adresse e-mail.", + "codeSent": "Un nouveau code a été envoyé à votre adresse e-mail.", "noCodeReceived": "Vous n'avez pas reçu de code ?", "resend": "Renvoyer le code", "submit": "Continuer", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 92ac3fcd6..d07976905 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -83,7 +83,7 @@ "set": { "title": "Imposta Password", "description": "Imposta la password per il tuo account", - "codeSent": "Un codice è stato inviato al tuo indirizzo email.", + "codeSent": "Un nuovo codice è stato inviato al tuo indirizzo email.", "noCodeReceived": "Non hai ricevuto un codice?", "resend": "Invia di nuovo", "submit": "Continua", diff --git a/apps/login/locales/ja.json b/apps/login/locales/ja.json index fd5893824..25b5f940c 100644 --- a/apps/login/locales/ja.json +++ b/apps/login/locales/ja.json @@ -76,7 +76,7 @@ "set": { "title": "パスワード設定", "description": "アカウントのパスワードを設定してください", - "codeSent": "確認コードをメールアドレスに送信しました。", + "codeSent": "新しいコードがメールアドレスに送信されました。", "noCodeReceived": "コードが届きませんか?", "resend": "コードを再送信", "submit": "続行", diff --git a/apps/login/locales/nl.json b/apps/login/locales/nl.json index 797bf269c..8fc3b1803 100644 --- a/apps/login/locales/nl.json +++ b/apps/login/locales/nl.json @@ -83,7 +83,7 @@ "set": { "title": "Stel Wachtwoord in", "description": "Stel het wachtwoord voor jouw account in", - "codeSent": "Er is een code naar jouw e-mailadres verzonden.", + "codeSent": "Er is een nieuwe code naar uw e-mailadres verzonden.", "noCodeReceived": "Geen code ontvangen?", "resend": "Code opnieuw versturen", "submit": "Verder", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index f45547eca..8a3d33d65 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -83,7 +83,7 @@ "set": { "title": "Ustaw hasło", "description": "Ustaw hasło dla swojego konta", - "codeSent": "Kod został wysłany na twój adres e-mail.", + "codeSent": "Nowy kod został wysłany na Twój adres e-mail.", "noCodeReceived": "Nie otrzymałeś kodu?", "resend": "Wyślij kod ponownie", "submit": "Kontynuuj", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index a4b269b33..f7d7db2db 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -83,7 +83,7 @@ "set": { "title": "Установить пароль", "description": "Установите пароль для вашего аккаунта", - "codeSent": "Код отправлен на ваш адрес электронной почты.", + "codeSent": "Новый код был отправлен на ваш адрес электронной почты.", "noCodeReceived": "Не получили код?", "resend": "Отправить код повторно", "submit": "Продолжить", diff --git a/apps/login/locales/tr.json b/apps/login/locales/tr.json index 8fd5c4710..6dc1983ee 100644 --- a/apps/login/locales/tr.json +++ b/apps/login/locales/tr.json @@ -83,7 +83,7 @@ "set": { "title": "Şifre Belirle", "description": "Hesabınız için şifre belirleyin", - "codeSent": "E-posta adresinize bir kod gönderildi.", + "codeSent": "E-posta adresinize yeni bir kod gönderildi.", "noCodeReceived": "Kod almadınız mı?", "resend": "Kodu tekrar gönder", "submit": "Devam", diff --git a/apps/login/locales/uk.json b/apps/login/locales/uk.json index 92e9f052b..f8c028f9f 100644 --- a/apps/login/locales/uk.json +++ b/apps/login/locales/uk.json @@ -83,7 +83,7 @@ "set": { "title": "Встановити пароль", "description": "Встановіть пароль для свого облікового запису", - "codeSent": "Код було надіслано на вашу електронну адресу.", + "codeSent": "Новий код надіслано на вашу електронну адресу.", "noCodeReceived": "Не отримали код?", "resend": "Надіслати код повторно", "submit": "Продовжити", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index d2ae3278d..a9ff5fcee 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -83,7 +83,7 @@ "set": { "title": "设置密码", "description": "为您的账户设置密码", - "codeSent": "验证码已发送到您的邮箱。", + "codeSent": "新代码已发送到您的电子邮件地址。", "noCodeReceived": "没有收到验证码?", "resend": "重发验证码", "submit": "继续", diff --git a/apps/login/src/lib/server/loginname.test.ts b/apps/login/src/lib/server/loginname.test.ts index b2addfa8f..e5122e6ea 100644 --- a/apps/login/src/lib/server/loginname.test.ts +++ b/apps/login/src/lib/server/loginname.test.ts @@ -198,12 +198,35 @@ describe("sendLoginname", () => { mockCreateSessionAndUpdateCookie.mockResolvedValue({ session: mockSession, sessionCookie: {} }); }); - test("should redirect to verify when user has no authentication methods", async () => { + test("should redirect to verify with send=false when user has no authentication methods and email is unverified", async () => { mockListAuthenticationMethodTypes.mockResolvedValue({ authMethodTypes: [] }); const result = await sendLoginname({ loginName: "user@example.com", requestId: "req123", + organization: "org123", + }); + + expect(result).toHaveProperty("redirect"); + expect((result as any).redirect).toMatch(/^\/verify\?/); + expect((result as any).redirect).toContain("loginName=user%40example.com"); + expect((result as any).redirect).toContain("send=false"); + expect((result as any).redirect).toContain("invite=true"); + expect((result as any).redirect).toContain("requestId=req123"); + }); + + test("should redirect to verify with send=true when user has no authentication methods and email is already verified", async () => { + const verifiedEmailUser = { + ...mockUser, + type: { case: "human", value: { email: { email: "user@example.com", isVerified: true } } }, + }; + mockSearchUsers.mockResolvedValue({ result: [verifiedEmailUser] }); + mockListAuthenticationMethodTypes.mockResolvedValue({ authMethodTypes: [] }); + + const result = await sendLoginname({ + loginName: "user@example.com", + requestId: "req123", + organization: "org123", }); expect(result).toHaveProperty("redirect"); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index abc10a384..e46112b48 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -338,10 +338,16 @@ export async function sendLoginname(command: SendLoginnameCommand) { logger.debug("humanUser.email?.isVerified", { isVerified: humanUser?.email?.isVerified, }); + + // If the user's email is not verified, they likely already have a code from the + // initial verification email. Auto-sending a new one here invalidates their existing code + // and causes confusion. Only auto-send (`send=true`) if the email is already verified. + const shouldSend = humanUser?.email?.isVerified === true; + const params = new URLSearchParams({ loginName: (session?.factors?.user?.loginName ?? user.preferredLoginName) as string, - send: "true", // set this to true to request a new code immediately - invite: humanUser?.email?.isVerified ? "false" : "true", // sendInviteEmailCode results in an error if user is already initialized + send: shouldSend ? "true" : "false", + invite: "true", // always send invite code if user has no primary auth method }); if (command.requestId) {