From c6d7328e15f10b160fc4f42c63485534f92eb72b Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Thu, 24 Jul 2025 11:51:39 +0800 Subject: [PATCH 01/10] feat: revamp tool list page (#22879) --- .../plugins/marketplace/list/index.tsx | 2 +- .../components/tools/marketplace/index.tsx | 154 ++++++++---------- web/app/components/tools/provider-list.tsx | 49 +++++- 3 files changed, 116 insertions(+), 89 deletions(-) diff --git a/web/app/components/plugins/marketplace/list/index.tsx b/web/app/components/plugins/marketplace/list/index.tsx index 5651512f8e..2072e3feed 100644 --- a/web/app/components/plugins/marketplace/list/index.tsx +++ b/web/app/components/plugins/marketplace/list/index.tsx @@ -56,7 +56,7 @@ const List = ({ return ( void + isMarketplaceArrowVisible: boolean + showMarketplacePanel: () => void + marketplaceContext: ReturnType } const Marketplace = ({ searchPluginText, filterPluginTags, - onMarketplaceScroll, + isMarketplaceArrowVisible, + showMarketplacePanel, + marketplaceContext, }: MarketplaceProps) => { const locale = getLocaleOnClient() const { t } = useTranslation() @@ -32,86 +32,76 @@ const Marketplace = ({ marketplaceCollections, marketplaceCollectionPluginsMap, plugins, - handleScroll, page, - } = useMarketplace(searchPluginText, filterPluginTags) - const containerRef = useRef(null) - - useEffect(() => { - const container = containerRef.current - if (container) - container.addEventListener('scroll', handleScroll) - - return () => { - if (container) - container.removeEventListener('scroll', handleScroll) - } - }, [handleScroll]) + } = marketplaceContext return ( -
- onMarketplaceScroll()} - /> -
-
- {t('plugin.marketplace.moreFrom')} -
-
- {t('plugin.marketplace.discover')} - - {t('plugin.category.models')} - - , - - {t('plugin.category.tools')} - - , - - {t('plugin.category.agents')} - - , - - {t('plugin.category.extensions')} - - {t('plugin.marketplace.and')} - - {t('plugin.category.bundles')} - - {t('common.operation.in')} - - {t('plugin.marketplace.difyMarketplace')} - - + <> +
+ {isMarketplaceArrowVisible && ( + + )} +
+
+ {t('plugin.marketplace.moreFrom')} +
+
+ {t('plugin.marketplace.discover')} + + {t('plugin.category.models')} + + , + + {t('plugin.category.tools')} + + , + + {t('plugin.category.agents')} + + , + + {t('plugin.category.extensions')} + + {t('plugin.marketplace.and')} + + {t('plugin.category.bundles')} + + {t('common.operation.in')} + + {t('plugin.marketplace.difyMarketplace')} + + +
- { - isLoading && page === 1 && ( -
- -
- ) - } - { - (!isLoading || page > 1) && ( - - ) - } -
+
+ { + isLoading && page === 1 && ( +
+ +
+ ) + } + { + (!isLoading || page > 1) && ( + + ) + } +
+ ) } diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index ecfa5f6ea2..d267b49c79 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -1,5 +1,5 @@ 'use client' -import { useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import type { Collection } from './types' import Marketplace from './marketplace' @@ -20,6 +20,7 @@ import { useAllToolProviders } from '@/service/use-tools' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useGlobalPublicStore } from '@/context/global-public-context' import { ToolTypeEnum } from '../workflow/block-selector/types' +import { useMarketplace } from './marketplace/hooks' const getToolType = (type: string) => { switch (type) { @@ -37,7 +38,7 @@ const getToolType = (type: string) => { } const ProviderList = () => { // const searchParams = useSearchParams() - // searchParams.get('category') === 'workflow' + // searchParams.get('category') === 'workflow' const { t } = useTranslation() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const containerRef = useRef(null) @@ -83,6 +84,41 @@ const ProviderList = () => { return detail }, [currentProvider?.plugin_id, pluginList?.plugins]) + const toolListTailRef = useRef(null) + const showMarketplacePanel = useCallback(() => { + containerRef.current?.scrollTo({ + top: toolListTailRef.current + ? toolListTailRef.current?.offsetTop - 80 + : 0, + behavior: 'smooth', + }) + }, [toolListTailRef]) + + const marketplaceContext = useMarketplace(keywords, tagFilterValue) + const { + handleScroll, + } = marketplaceContext + + const [isMarketplaceArrowVisible, setIsMarketplaceArrowVisible] = useState(true) + const onContainerScroll = useMemo(() => { + return (e: Event) => { + handleScroll(e) + if (containerRef.current && toolListTailRef.current) + setIsMarketplaceArrowVisible(containerRef.current.scrollTop < (toolListTailRef.current?.offsetTop - 80)) + } + }, [handleScroll, containerRef, toolListTailRef, setIsMarketplaceArrowVisible]) + + useEffect(() => { + const container = containerRef.current + if (container) + container.addEventListener('scroll', onContainerScroll) + + return () => { + if (container) + container.removeEventListener('scroll', onContainerScroll) + } + }, [onContainerScroll]) + return ( <>
@@ -152,15 +188,16 @@ const ProviderList = () => {
)} {!filteredCollectionList.length && activeTab === 'builtin' && ( - + )} +
{enable_marketplace && activeTab === 'builtin' && ( { - containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) - }} searchPluginText={keywords} filterPluginTags={tagFilterValue} + isMarketplaceArrowVisible={isMarketplaceArrowVisible} + showMarketplacePanel={showMarketplacePanel} + marketplaceContext={marketplaceContext} /> )} {activeTab === 'mcp' && ( From 371fe7a700d77bae54cc2c30d408057c69bcfc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=86=E8=90=8C=E9=97=B7=E6=B2=B9=E7=93=B6?= <253605712@qq.com> Date: Thu, 24 Jul 2025 12:21:20 +0800 Subject: [PATCH 02/10] fix: type error in list-operator (#22803) --- api/core/workflow/nodes/list_operator/node.py | 7 +++---- .../base/date-and-time-picker/common/option-list-item.tsx | 1 - .../base/date-and-time-picker/time-picker/index.tsx | 1 - web/app/components/plugins/plugin-page/index.tsx | 1 - web/service/use-plugins.ts | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index ae9401b056..b91fc622f6 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -184,11 +184,10 @@ class ListOperatorNode(BaseNode): value = int(self.graph_runtime_state.variable_pool.convert_template(self._node_data.extract_by.serial).text) if value < 1: raise ValueError(f"Invalid serial index: must be >= 1, got {value}") + if value > len(variable.value): + raise InvalidKeyError(f"Invalid serial index: must be <= {len(variable.value)}, got {value}") value -= 1 - if len(variable.value) > int(value): - result = variable.value[value] - else: - result = "" + result = variable.value[value] return variable.model_copy(update={"value": [result]}) diff --git a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx index 8c2c8d82f1..0144a7c6ec 100644 --- a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx +++ b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx @@ -18,7 +18,6 @@ const OptionListItem: FC = ({ useEffect(() => { if (isSelected && !noAutoScroll) listItemRef.current?.scrollIntoView({ behavior: 'instant' }) - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx index d493106667..8ef10abc2e 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx @@ -52,7 +52,6 @@ const TimePicker = ({ else { setSelectedTime(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [timezone]) const handleClickTrigger = (e: React.MouseEvent) => { diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 894a8b7f45..d326fdf6e4 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -117,7 +117,6 @@ const PluginPage = ({ showInstallFromMarketplace() } })() - // eslint-disable-next-line react-hooks/exhaustive-deps }, [packageId, bundleInfo]) const { diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 86f8134a5d..2877ef15f2 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -519,7 +519,6 @@ export const usePluginTaskList = (category?: PluginType) => { refreshPluginList(category ? { category } as any : undefined, !category) } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRefetching]) const handleRefetch = useCallback(() => { From de611ab344e9fc6eb28699d7011906cfbef0adf9 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Thu, 24 Jul 2025 14:16:39 +0800 Subject: [PATCH 03/10] Feat: add notification for change email completed (#22812) Co-authored-by: Yansong Zhang <916125788@qq.com> --- api/controllers/console/workspace/account.py | 4 + api/libs/email_i18n.py | 13 ++ api/services/account_service.py | 21 ++- api/tasks/mail_change_mail_task.py | 40 +++++- .../change_mail_completed_template_en-US.html | 135 ++++++++++++++++++ .../change_mail_completed_template_zh-CN.html | 135 ++++++++++++++++++ .../change_mail_completed_template_en-US.html | 132 +++++++++++++++++ .../change_mail_completed_template_zh-CN.html | 132 +++++++++++++++++ 8 files changed, 610 insertions(+), 2 deletions(-) create mode 100644 api/templates/change_mail_completed_template_en-US.html create mode 100644 api/templates/change_mail_completed_template_zh-CN.html create mode 100644 api/templates/without-brand/change_mail_completed_template_en-US.html create mode 100644 api/templates/without-brand/change_mail_completed_template_zh-CN.html diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 5cd2e0cd2d..657016e0a8 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -494,6 +494,10 @@ class ChangeEmailResetApi(Resource): updated_account = AccountService.update_account(current_user, email=args["new_email"]) + AccountService.send_change_email_completed_notify_email( + email=args["new_email"], + ) + return updated_account diff --git a/api/libs/email_i18n.py b/api/libs/email_i18n.py index bfbf41a073..b7c9f3ec6c 100644 --- a/api/libs/email_i18n.py +++ b/api/libs/email_i18n.py @@ -25,6 +25,7 @@ class EmailType(Enum): EMAIL_CODE_LOGIN = "email_code_login" CHANGE_EMAIL_OLD = "change_email_old" CHANGE_EMAIL_NEW = "change_email_new" + CHANGE_EMAIL_COMPLETED = "change_email_completed" OWNER_TRANSFER_CONFIRM = "owner_transfer_confirm" OWNER_TRANSFER_OLD_NOTIFY = "owner_transfer_old_notify" OWNER_TRANSFER_NEW_NOTIFY = "owner_transfer_new_notify" @@ -344,6 +345,18 @@ def create_default_email_config() -> EmailI18nConfig: branded_template_path="without-brand/change_mail_confirm_new_template_zh-CN.html", ), }, + EmailType.CHANGE_EMAIL_COMPLETED: { + EmailLanguage.EN_US: EmailTemplate( + subject="Your login email has been changed", + template_path="change_mail_completed_template_en-US.html", + branded_template_path="without-brand/change_mail_completed_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="您的登录邮箱已更改", + template_path="change_mail_completed_template_zh-CN.html", + branded_template_path="without-brand/change_mail_completed_template_zh-CN.html", + ), + }, EmailType.OWNER_TRANSFER_CONFIRM: { EmailLanguage.EN_US: EmailTemplate( subject="Verify Your Request to Transfer Workspace Ownership", diff --git a/api/services/account_service.py b/api/services/account_service.py index 59bffa873c..eb57b675c4 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -54,7 +54,10 @@ from services.errors.workspace import WorkSpaceNotAllowedCreateError, Workspaces from services.feature_service import FeatureService from tasks.delete_account_task import delete_account_task from tasks.mail_account_deletion_task import send_account_deletion_verification_code -from tasks.mail_change_mail_task import send_change_mail_task +from tasks.mail_change_mail_task import ( + send_change_mail_completed_notification_task, + send_change_mail_task, +) from tasks.mail_email_code_login import send_email_code_login_mail_task from tasks.mail_invite_member_task import send_invite_member_mail_task from tasks.mail_owner_transfer_task import ( @@ -461,6 +464,22 @@ class AccountService: cls.change_email_rate_limiter.increment_rate_limit(account_email) return token + @classmethod + def send_change_email_completed_notify_email( + cls, + account: Optional[Account] = None, + email: Optional[str] = None, + language: Optional[str] = "en-US", + ): + account_email = account.email if account else email + if account_email is None: + raise ValueError("Email must be provided.") + + send_change_mail_completed_notification_task.delay( + language=language, + to=account_email, + ) + @classmethod def send_owner_transfer_email( cls, diff --git a/api/tasks/mail_change_mail_task.py b/api/tasks/mail_change_mail_task.py index ea1875901c..6334fb22de 100644 --- a/api/tasks/mail_change_mail_task.py +++ b/api/tasks/mail_change_mail_task.py @@ -5,7 +5,7 @@ import click from celery import shared_task # type: ignore from extensions.ext_mail import mail -from libs.email_i18n import get_email_i18n_service +from libs.email_i18n import EmailType, get_email_i18n_service @shared_task(queue="mail") @@ -40,3 +40,41 @@ def send_change_mail_task(language: str, to: str, code: str, phase: str) -> None ) except Exception: logging.exception("Send change email mail to {} failed".format(to)) + + +@shared_task(queue="mail") +def send_change_mail_completed_notification_task(language: str, to: str) -> None: + """ + Send change email completed notification with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + """ + if not mail.is_inited(): + return + + logging.info(click.style("Start change email completed notify mail to {}".format(to), fg="green")) + start_at = time.perf_counter() + + try: + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.CHANGE_EMAIL_COMPLETED, + language_code=language, + to=to, + template_context={ + "to": to, + "email": to, + }, + ) + + end_at = time.perf_counter() + logging.info( + click.style( + "Send change email completed mail to {} succeeded: latency: {}".format(to, end_at - start_at), + fg="green", + ) + ) + except Exception: + logging.exception("Send change email completed mail to {} failed".format(to)) diff --git a/api/templates/change_mail_completed_template_en-US.html b/api/templates/change_mail_completed_template_en-US.html new file mode 100644 index 0000000000..ecaf35868d --- /dev/null +++ b/api/templates/change_mail_completed_template_en-US.html @@ -0,0 +1,135 @@ + + + + + + + + +
+
+ + Dify Logo +
+

Your login email has been changed

+
+

You can now log into Dify with your new email address:

+
+
+ {{email}} +
+

If you did not make this change, email support@dify.ai.

+
+ + + + diff --git a/api/templates/change_mail_completed_template_zh-CN.html b/api/templates/change_mail_completed_template_zh-CN.html new file mode 100644 index 0000000000..b4fdb4b9ab --- /dev/null +++ b/api/templates/change_mail_completed_template_zh-CN.html @@ -0,0 +1,135 @@ + + + + + + + + +
+
+ + Dify Logo +
+

您的登录邮箱已更改

+
+

您现在可以使用新的电子邮件地址登录 Dify:

+
+
+ {{email}} +
+

如果您没有进行此更改,请发送电子邮件至 support@dify.ai

+
+ + + + diff --git a/api/templates/without-brand/change_mail_completed_template_en-US.html b/api/templates/without-brand/change_mail_completed_template_en-US.html new file mode 100644 index 0000000000..f211cc74d9 --- /dev/null +++ b/api/templates/without-brand/change_mail_completed_template_en-US.html @@ -0,0 +1,132 @@ + + + + + + + + +
+
+

Your login email has been changed

+
+

You can now log into {{application_title}} with your new email address:

+
+
+ {{email}} +
+

If you did not make this change, please ignore this email or contact support immediately.

+
+ + + + diff --git a/api/templates/without-brand/change_mail_completed_template_zh-CN.html b/api/templates/without-brand/change_mail_completed_template_zh-CN.html new file mode 100644 index 0000000000..c96604f0e5 --- /dev/null +++ b/api/templates/without-brand/change_mail_completed_template_zh-CN.html @@ -0,0 +1,132 @@ + + + + + + + + +
+
+

您的登录邮箱已更改

+
+

您现在可以使用新的电子邮件地址登录 {{application_title}}:

+
+
+ {{email}} +
+

如果您没有进行此更改,请忽略此电子邮件或立即联系支持。

+
+ + + + From a8f09ad43fbd6de52234fa8378ba7348024c72f8 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:40:37 +0800 Subject: [PATCH 04/10] =?UTF-8?q?refactor(i18next):=20streamline=20fallbac?= =?UTF-8?q?k=20translation=20handling=20and=20initi=E2=80=A6=20(#22894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/i18n/i18next-config.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index 8c5bd375a7..7af727af7e 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -50,24 +50,35 @@ export const loadLangResources = async (lang: string) => { acc[camelCase(NAMESPACES[index])] = mod return acc }, {} as Record) + return resources +} + +const getFallbackTranslation = () => { + const resources = NAMESPACES.reduce((acc, ns, index) => { + acc[camelCase(NAMESPACES[index])] = require(`./en-US/${ns}`).default + return acc + }, {} as Record) return { translation: resources, } } -i18n.use(initReactI18next) - .init({ - lng: undefined, - fallbackLng: 'en-US', - }) +if (!i18n.isInitialized) { + i18n.use(initReactI18next) + .init({ + lng: undefined, + fallbackLng: 'en-US', + resources: { + 'en-US': getFallbackTranslation(), + }, + }) +} export const changeLanguage = async (lng?: string) => { const resolvedLng = lng ?? 'en-US' - const resources = { - [resolvedLng]: await loadLangResources(resolvedLng), - } + const resource = await loadLangResources(resolvedLng) if (!i18n.hasResourceBundle(resolvedLng, 'translation')) - i18n.addResourceBundle(resolvedLng, 'translation', resources[resolvedLng].translation, true, true) + i18n.addResourceBundle(resolvedLng, 'translation', resource, true, true) await i18n.changeLanguage(resolvedLng) } From aca8b836696167fc47b71c5507876897f8f6dd0b Mon Sep 17 00:00:00 2001 From: NFish Date: Thu, 24 Jul 2025 15:10:15 +0800 Subject: [PATCH 05/10] fix: support authorization using session and user_id in URL. (#22898) --- web/context/web-app-context.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx index 55f95e4811..db1c5158dd 100644 --- a/web/context/web-app-context.tsx +++ b/web/context/web-app-context.tsx @@ -2,6 +2,7 @@ import type { ChatConfig } from '@/app/components/base/chat/types' import Loading from '@/app/components/base/loading' +import { checkOrSetAccessToken } from '@/app/components/share/utils' import { AccessMode } from '@/models/access-control' import type { AppData, AppMeta } from '@/models/share' import { useGetWebAppAccessModeByCode } from '@/service/use-share' @@ -60,6 +61,8 @@ const WebAppStoreProvider: FC = ({ children }) => { const pathname = usePathname() const searchParams = useSearchParams() const redirectUrlParam = searchParams.get('redirect_url') + const session = searchParams.get('session') + const sysUserId = searchParams.get('sys.user_id') const [shareCode, setShareCode] = useState(null) useEffect(() => { const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) @@ -69,11 +72,22 @@ const WebAppStoreProvider: FC = ({ children }) => { updateShareCode(newShareCode) }, [pathname, redirectUrlParam, updateShareCode]) const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) + const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true) useEffect(() => { - if (accessModeResult?.accessMode) + if (accessModeResult?.accessMode) { updateWebAppAccessMode(accessModeResult.accessMode) - }, [accessModeResult, updateWebAppAccessMode]) - if (isFetching) { + if (accessModeResult?.accessMode === AccessMode.PUBLIC && session && sysUserId) { + setIsFetchingAccessToken(true) + checkOrSetAccessToken(shareCode).finally(() => { + setIsFetchingAccessToken(false) + }) + } + else { + setIsFetchingAccessToken(false) + } + } + }, [accessModeResult, updateWebAppAccessMode, setIsFetchingAccessToken, shareCode, session, sysUserId]) + if (isFetching || isFetchingAccessToken) { return
From 952bce4196cca3ad6b2fe6cb276420dda9d270ab Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 24 Jul 2025 15:26:35 +0800 Subject: [PATCH 06/10] check is email is freeze user email --- api/controllers/console/auth/error.py | 5 +++++ api/controllers/console/workspace/account.py | 4 ++++ api/services/account_service.py | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 8c5e23de58..7f5bfa8f45 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -113,3 +113,8 @@ class MemberNotInTenantError(BaseHTTPException): error_code = "member_not_in_tenant" description = "The member is not in the workspace." code = 400 + +class AccountInFreezeError(BaseHTTPException): + error_code = "account_in_freeze" + description = "This email is temporarily unavailable." + code = 400 \ No newline at end of file diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 657016e0a8..961ca202c4 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -14,6 +14,7 @@ from controllers.console.auth.error import ( EmailCodeError, InvalidEmailError, InvalidTokenError, + AccountInFreezeError ) from controllers.console.error import AccountNotFound, EmailSendIpLimitError from controllers.console.workspace.error import ( @@ -488,6 +489,9 @@ class ChangeEmailResetApi(Resource): if not AccountService.check_email_unique(args["new_email"]): raise EmailAlreadyInUseError() + if AccountService.is_account_in_freeze(args["new_email"]): + raise AccountInFreezeError() + old_email = reset_data.get("old_email", "") if current_user.email != old_email: raise AccountNotFound() diff --git a/api/services/account_service.py b/api/services/account_service.py index eb57b675c4..e11f1580e5 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -671,6 +671,12 @@ class AccountService: return account + @classmethod + def is_account_in_freeze(cls, email: str) -> bool: + if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(email): + return True + return False + @staticmethod @redis_fallback(default_return=None) def add_login_error_rate_limit(email: str) -> None: From a1de4fa4286f321e1dd93f5aaa3ff6a63161130b Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 24 Jul 2025 15:26:41 +0800 Subject: [PATCH 07/10] check is email is freeze user email --- api/controllers/console/workspace/account.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 961ca202c4..fb16850540 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -9,12 +9,12 @@ from configs import dify_config from constants.languages import supported_language from controllers.console import api from controllers.console.auth.error import ( + AccountInFreezeError, EmailAlreadyInUseError, EmailChangeLimitError, EmailCodeError, InvalidEmailError, InvalidTokenError, - AccountInFreezeError ) from controllers.console.error import AccountNotFound, EmailSendIpLimitError from controllers.console.workspace.error import ( @@ -480,17 +480,19 @@ class ChangeEmailResetApi(Resource): parser.add_argument("token", type=str, required=True, nullable=False, location="json") args = parser.parse_args() + if AccountService.is_account_in_freeze(args["new_email"]): + raise AccountInFreezeError() + + if not AccountService.check_email_unique(args["new_email"]): + raise EmailAlreadyInUseError() + reset_data = AccountService.get_change_email_data(args["token"]) if not reset_data: raise InvalidTokenError() AccountService.revoke_change_email_token(args["token"]) - if not AccountService.check_email_unique(args["new_email"]): - raise EmailAlreadyInUseError() - if AccountService.is_account_in_freeze(args["new_email"]): - raise AccountInFreezeError() old_email = reset_data.get("old_email", "") if current_user.email != old_email: From c271c65c854f166914842f84a71c6655c4761d72 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Thu, 24 Jul 2025 15:29:23 +0800 Subject: [PATCH 08/10] fix ruff liner --- api/controllers/console/auth/error.py | 3 ++- api/controllers/console/workspace/account.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/auth/error.py b/api/controllers/console/auth/error.py index 7f5bfa8f45..1984339add 100644 --- a/api/controllers/console/auth/error.py +++ b/api/controllers/console/auth/error.py @@ -114,7 +114,8 @@ class MemberNotInTenantError(BaseHTTPException): description = "The member is not in the workspace." code = 400 + class AccountInFreezeError(BaseHTTPException): error_code = "account_in_freeze" description = "This email is temporarily unavailable." - code = 400 \ No newline at end of file + code = 400 diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index fb16850540..9218ddf91d 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -485,15 +485,13 @@ class ChangeEmailResetApi(Resource): if not AccountService.check_email_unique(args["new_email"]): raise EmailAlreadyInUseError() - + reset_data = AccountService.get_change_email_data(args["token"]) if not reset_data: raise InvalidTokenError() AccountService.revoke_change_email_token(args["token"]) - - old_email = reset_data.get("old_email", "") if current_user.email != old_email: raise AccountNotFound() From ae49a2bdd9d833c3b5ed3a15aa1529ed38ac6dc1 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Thu, 24 Jul 2025 16:11:07 +0800 Subject: [PATCH 09/10] add error message of email unavailable --- .../account-page/email-change-modal.tsx | 22 +++++++++++++++---- web/i18n/en-US/common.ts | 1 + web/i18n/ja-JP/common.ts | 1 + web/i18n/zh-Hans/common.ts | 1 + 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/web/app/account/account-page/email-change-modal.tsx b/web/app/account/account-page/email-change-modal.tsx index c3efad104a..bd00f27ac5 100644 --- a/web/app/account/account-page/email-change-modal.tsx +++ b/web/app/account/account-page/email-change-modal.tsx @@ -15,6 +15,8 @@ import { verifyEmail, } from '@/service/common' import { noop } from 'lodash-es' +import { asyncRunSafe } from '@/utils' +import type { ResponseError } from '@/service/fetch' type Props = { show: boolean @@ -39,6 +41,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { const [time, setTime] = useState(0) const [stepToken, setStepToken] = useState('') const [newEmailExited, setNewEmailExited] = useState(false) + const [unAvailableEmail, setUnAvailableEmail] = useState(false) const [isCheckingEmail, setIsCheckingEmail] = useState(false) const startCount = () => { @@ -124,9 +127,17 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { email, }) setNewEmailExited(false) + setUnAvailableEmail(false) } - catch { - setNewEmailExited(true) + catch (e: any) { + if (e.status === 400) { + const [, errRespData] = await asyncRunSafe(e.json()) + const { code } = errRespData || {} + if (code === 'email_already_in_use') + setNewEmailExited(true) + if (code === 'account_in_freeze') + setUnAvailableEmail(true) + } } finally { setIsCheckingEmail(false) @@ -291,15 +302,18 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { placeholder={t('common.account.changeEmail.emailPlaceholder')} value={mail} onChange={e => handleNewEmailValueChange(e.target.value)} - destructive={newEmailExited} + destructive={newEmailExited || unAvailableEmail} /> {newEmailExited && (
{t('common.account.changeEmail.existingEmail')}
)} + {unAvailableEmail && ( +
{t('common.account.changeEmail.unAvailableEmail')}
+ )}