From 5c7f05bd10c52f1a5dabd419486a53651224dafe Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:14:01 +0800 Subject: [PATCH] fix(web): auth form state management (#37003) --- .../step-definitions/auth/sign-in.steps.ts | 2 +- eslint-suppressions.json | 16 --- .../signin/components/mail-and-code-auth.tsx | 46 +++---- .../components/mail-and-password-auth.tsx | 97 ++++++++------- web/app/signin/normal-form.tsx | 112 +++++++++--------- web/i18n/en-US/login.json | 2 + web/i18n/zh-Hans/login.json | 2 + 7 files changed, 145 insertions(+), 132 deletions(-) diff --git a/e2e/features/step-definitions/auth/sign-in.steps.ts b/e2e/features/step-definitions/auth/sign-in.steps.ts index 8f9e8e765c..095f816407 100644 --- a/e2e/features/step-definitions/auth/sign-in.steps.ts +++ b/e2e/features/step-definitions/auth/sign-in.steps.ts @@ -11,7 +11,7 @@ When('I sign in as the default E2E admin', async function (this: DifyWorld) { const page = this.getPage() await page.getByLabel('Email address').fill(adminCredentials.email) - await page.getByLabel('Password').fill(adminCredentials.password) + await page.getByLabel('Password', { exact: true }).fill(adminCredentials.password) await page.getByRole('button', { name: 'Sign in' }).click() }) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 9e66e96659..7675b4dd77 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -4992,22 +4992,6 @@ "count": 1 } }, - "web/app/signin/components/mail-and-code-auth.tsx": { - "no-restricted-globals": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/signin/components/mail-and-password-auth.tsx": { - "no-restricted-imports": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/signin/invite-settings/page.tsx": { "no-restricted-imports": { "count": 1 diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx index 41cf50854e..eacf1e8acd 100644 --- a/web/app/signin/components/mail-and-code-auth.tsx +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -1,12 +1,13 @@ -import type { FormEvent } from 'react' import { Button } from '@langgenius/dify-ui/button' +import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' +import { Form } from '@langgenius/dify-ui/form' import { toast } from '@langgenius/dify-ui/toast' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' +import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams } from '@/next/navigation' import { sendEMailLoginCode } from '@/service/common' @@ -20,8 +21,9 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) { const searchParams = useSearchParams() const emailFromLink = decodeURIComponent(searchParams.get('email') || '') const [email, setEmail] = useState(emailFromLink) - const [loading, setIsLoading] = useState(false) + const [loading, setLoading] = useState(false) const locale = useLocale() + const setCountdownLeftTime = useSetLocalStorage(COUNT_DOWN_KEY, { raw: true }) const handleGetEMailVerificationCode = async () => { try { @@ -34,10 +36,10 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) { toast.error(t('error.emailInValid', { ns: 'login' })) return } - setIsLoading(true) + setLoading(true) const ret = await sendEMailLoginCode(email, locale) if (ret.result === 'success') { - localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + setCountdownLeftTime(`${COUNT_DOWN_TIME_MS}`) const params = new URLSearchParams(searchParams) params.set('email', encodeURIComponent(email)) params.set('token', encodeURIComponent(ret.data)) @@ -48,27 +50,31 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) { console.error(error) } finally { - setIsLoading(false) + setLoading(false) } } - const handleSubmit = (event: FormEvent) => { - event.preventDefault() - handleGetEMailVerificationCode() - } - return ( -
- -
- -
- setEmail(e.target.value)} /> -
+ { + void handleGetEMailVerificationCode() + }} + > + + {t('email', { ns: 'login' })} +
-
-
+ + ) } diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index e695f72b9a..d5364f46fd 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -1,12 +1,11 @@ -import type { ResponseError } from '@/service/fetch' import { Button } from '@langgenius/dify-ui/button' +import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' +import { Form } from '@langgenius/dify-ui/form' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' -import { noop } from 'es-toolkit/function' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' -import Input from '@/app/components/base/input' import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' import Link from '@/next/link' @@ -20,10 +19,24 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect' type MailAndPasswordAuthProps = { isInvite: boolean isEmailSetup: boolean - allowRegistration: boolean } -export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration: _allowRegistration }: MailAndPasswordAuthProps) { +type LoginRequestBody = { + email: string + password: string + language: string + remember_me: boolean + invite_token?: string +} + +function hasErrorCode(error: unknown, code: string) { + return typeof error === 'object' + && error !== null + && 'code' in error + && error.code === code +} + +export default function MailAndPasswordAuth({ isInvite, isEmailSetup }: MailAndPasswordAuthProps) { const { t } = useTranslation() const locale = useLocale() const router = useRouter() @@ -52,7 +65,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis try { setIsLoading(true) - const loginData: Record = { + const loginData: LoginRequestBody = { email, password: encryptPassword(password), language: locale, @@ -88,7 +101,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis } } catch (error) { - if ((error as ResponseError).code === 'authentication_failed') { + if (hasErrorCode(error, 'authentication_failed')) { toast.error(t('error.invalidEmailOrPassword', { ns: 'login' })) } } @@ -98,28 +111,29 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis } return ( -
-
- -
- setEmail(e.target.value)} - disabled={isInvite} - id="email" - type="email" - autoComplete="email" - placeholder={t('emailPlaceholder', { ns: 'login' }) || ''} - tabIndex={1} - /> -
-
+ + + -
-
+
- + ) } diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx index 511fe75311..e3fbdc1535 100644 --- a/web/app/signin/normal-form.tsx +++ b/web/app/signin/normal-form.tsx @@ -2,8 +2,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' import { useQuery, useSuspenseQuery } from '@tanstack/react-query' -import * as React from 'react' -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { IS_CE_EDITION } from '@/config' import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client' @@ -20,7 +19,9 @@ import SSOAuth from './components/sso-auth' import Split from './split' import { resolvePostLoginRedirect } from './utils/post-login-redirect' -const NormalForm = () => { +type AuthType = 'code' | 'password' + +function NormalForm() { const { t } = useTranslation() const router = useRouter() const searchParams = useSearchParams() @@ -30,55 +31,58 @@ const NormalForm = () => { const { isPending: isCheckLoading, data: userResp, error: probeError } = useQuery({ ...userProfileQueryOptions(), throwOnError: err => !isLegacyBase401(err), + refetchOnWindowFocus: false, }) const isLoggedIn = !!userResp && !probeError const message = decodeURIComponent(searchParams.get('message') || '') - const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') - const [isInitCheckLoading, setInitCheckLoading] = useState(true) - const [isRedirecting, setIsRedirecting] = useState(false) - const isLoading = isCheckLoading || isInitCheckLoading || isRedirecting + const inviteToken = decodeURIComponent(searchParams.get('invite_token') || '') const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const [authType, updateAuthType] = useState<'code' | 'password'>('password') - const [showORLine, setShowORLine] = useState(false) - const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) - const [workspaceName, setWorkSpaceName] = useState('') + const [selectedAuthType, setSelectedAuthType] = useState(null) - const isInviteLink = Boolean(invite_token && invite_token !== 'null') + const isInviteLink = Boolean(inviteToken && inviteToken !== 'null') + const { data: invitationCheckResp, isPending: isInviteCheckLoading, isError: isInviteCheckError } = useQuery({ + queryKey: ['signin', 'invite-check', inviteToken], + queryFn: () => invitationCheck({ + url: '/activate/check', + params: { + token: inviteToken, + }, + }), + enabled: isInviteLink, + retry: false, + refetchOnWindowFocus: false, + }) - const init = useCallback(async () => { - try { - if (isLoggedIn) { - setIsRedirecting(true) - const redirectUrl = resolvePostLoginRedirect(searchParams) - router.replace(redirectUrl || '/') - return - } + const workspaceName = invitationCheckResp?.data?.workspace_name || '' + const hasSocialLogin = systemFeatures.enable_social_oauth_login + const hasSsoLogin = Boolean(systemFeatures.sso_enforced_for_signin) + const hasEmailCodeLogin = systemFeatures.enable_email_code_login + const hasEmailPasswordLogin = systemFeatures.enable_email_password_login + const hasEmailLogin = hasEmailCodeLogin || hasEmailPasswordLogin + const defaultAuthType: AuthType = hasEmailPasswordLogin ? 'password' : 'code' + const authType = selectedAuthType === 'password' && hasEmailPasswordLogin + ? 'password' + : selectedAuthType === 'code' && hasEmailCodeLogin + ? 'code' + : defaultAuthType + const showORLine = (hasSocialLogin || hasSsoLogin) && hasEmailLogin + const noLoginMethodsConfigured = !hasSocialLogin && !hasEmailCodeLogin && !hasEmailPasswordLogin && !hasSsoLogin + const allMethodsAreDisabled = noLoginMethodsConfigured || isInviteCheckError + const isLoading = isCheckLoading || isLoggedIn || (isInviteLink && isInviteCheckLoading) - if (message) { - toast.error(message) - } - setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin) - setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login)) - updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code') - if (isInviteLink) { - const checkRes = await invitationCheck({ - url: '/activate/check', - params: { - token: invite_token, - }, - }) - setWorkSpaceName(checkRes?.data?.workspace_name || '') - } - } - catch (error) { - console.error(error) - setAllMethodsAreDisabled(true) - } - finally { setInitCheckLoading(false) } - }, [isLoggedIn, message, router, searchParams, invite_token, isInviteLink, systemFeatures]) useEffect(() => { - init() - }, [init]) + if (!isLoggedIn) + return + + const redirectUrl = resolvePostLoginRedirect(searchParams) + router.replace(redirectUrl || '/') + }, [isLoggedIn, router, searchParams]) + + useEffect(() => { + if (message) + toast.error(message) + }, [message]) + if (isLoading) { return (
{ )}
- {systemFeatures.enable_social_oauth_login && } - {systemFeatures.sso_enforced_for_signin && ( + {hasSocialLogin && } + {hasSsoLogin && (
@@ -187,23 +191,23 @@ const NormalForm = () => {
)} { - (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && ( + hasEmailLogin && ( <> - {systemFeatures.enable_email_code_login && authType === 'code' && ( + {hasEmailCodeLogin && authType === 'code' && ( <> - {systemFeatures.enable_email_password_login && ( -
{ updateAuthType('password') }}> + {hasEmailPasswordLogin && ( +
{ setSelectedAuthType('password') }}> {t('usePassword', { ns: 'login' })}
)} )} - {systemFeatures.enable_email_password_login && authType === 'password' && ( + {hasEmailPasswordLogin && authType === 'password' && ( <> - - {systemFeatures.enable_email_code_login && ( -
{ updateAuthType('code') }}> + + {hasEmailCodeLogin && ( +
{ setSelectedAuthType('code') }}> {t('useVerificationCode', { ns: 'login' })}
)} diff --git a/web/i18n/en-US/login.json b/web/i18n/en-US/login.json index ec474aa4fb..20c9985ddc 100644 --- a/web/i18n/en-US/login.json +++ b/web/i18n/en-US/login.json @@ -49,6 +49,7 @@ "forgotPasswordDesc": "Please enter your email address to reset your password. We will send you an email with instructions on how to reset your password.", "go": "Go to Dify", "goToInit": "If you have not initialized the account, please go to the initialization page", + "hidePassword": "Hide password", "installBtn": "Set up", "interfaceLanguage": "Interface Language", "invalid": "The link has expired", @@ -92,6 +93,7 @@ "setAdminAccount": "Setting up an admin account", "setAdminAccountDesc": "Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.", "setYourAccount": "Set Your Account", + "showPassword": "Show password", "signBtn": "Sign in", "signup.createAccount": "Create your account", "signup.haveAccount": "Already have an account? ", diff --git a/web/i18n/zh-Hans/login.json b/web/i18n/zh-Hans/login.json index f9f618d536..1c4dd8208b 100644 --- a/web/i18n/zh-Hans/login.json +++ b/web/i18n/zh-Hans/login.json @@ -49,6 +49,7 @@ "forgotPasswordDesc": "请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。", "go": "跳转至 Dify", "goToInit": "如果您还没有初始化账户,请前往初始化页面", + "hidePassword": "隐藏密码", "installBtn": "设置", "interfaceLanguage": "界面语言", "invalid": "链接已失效", @@ -92,6 +93,7 @@ "setAdminAccount": "设置管理员账户", "setAdminAccountDesc": "管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。", "setYourAccount": "设置您的账户", + "showPassword": "显示密码", "signBtn": "登录", "signup.createAccount": "创建您的账户", "signup.haveAccount": "已有账户?",