Merge branch 'feat/change-user-email-freezes-limit' into deploy/dev

deploy/dev
JzoNg 7 months ago
commit f03afaf052

@ -113,3 +113,9 @@ 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

@ -9,6 +9,7 @@ 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,
@ -479,15 +480,18 @@ 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()
old_email = reset_data.get("old_email", "")
if current_user.email != old_email:
raise AccountNotFound()
@ -507,6 +511,8 @@ class CheckEmailUnique(Resource):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
args = parser.parse_args()
if AccountService.is_account_in_freeze(args["new_email"]):
raise AccountInFreezeError()
if not AccountService.check_email_unique(args["email"]):
raise EmailAlreadyInUseError()
return {"result": "success"}

@ -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]})

@ -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:

@ -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<number>(0)
const [stepToken, setStepToken] = useState<string>('')
const [newEmailExited, setNewEmailExited] = useState<boolean>(false)
const [unAvailableEmail, setUnAvailableEmail] = useState<boolean>(false)
const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(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<ResponseError>(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 && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
)}
{unAvailableEmail && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.unAvailableEmail')}</div>
)}
</div>
<div className='mt-3 space-y-2'>
<Button
disabled={!mail || newEmailExited || isCheckingEmail || !isValidEmail(mail)}
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
className='!w-full'
variant='primary'
onClick={sendCodeToNewEmail}

@ -18,7 +18,6 @@ const OptionListItem: FC<OptionListItemProps> = ({
useEffect(() => {
if (isSelected && !noAutoScroll)
listItemRef.current?.scrollIntoView({ behavior: 'instant' })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (

@ -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) => {

@ -56,7 +56,7 @@ const List = ({
return (
<CardWrapper
key={plugin.name}
key={`${plugin.org}/${plugin.name}`}
plugin={plugin}
showInstallButton={showInstallButton}
locale={locale}

@ -117,7 +117,6 @@ const PluginPage = ({
showInstallFromMarketplace()
}
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [packageId, bundleInfo])
const {

@ -1,14 +1,10 @@
import {
useEffect,
useRef,
} from 'react'
import { useTheme } from 'next-themes'
import {
RiArrowRightUpLine,
RiArrowUpDoubleLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMarketplace } from './hooks'
import type { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n'
@ -17,12 +13,16 @@ import { getMarketplaceUrl } from '@/utils/var'
type MarketplaceProps = {
searchPluginText: string
filterPluginTags: string[]
onMarketplaceScroll: () => void
isMarketplaceArrowVisible: boolean
showMarketplacePanel: () => void
marketplaceContext: ReturnType<typeof useMarketplace>
}
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<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current
if (container)
container.addEventListener('scroll', handleScroll)
return () => {
if (container)
container.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
} = marketplaceContext
return (
<div
ref={containerRef}
className='sticky bottom-[-442px] flex h-[530px] shrink-0 grow flex-col overflow-y-auto bg-background-default-subtle px-12 py-2 pt-0'
>
<RiArrowUpDoubleLine
className='absolute left-1/2 top-2 h-4 w-4 -translate-x-1/2 cursor-pointer text-text-quaternary'
onClick={() => onMarketplaceScroll()}
/>
<div className='sticky top-0 z-10 bg-background-default-subtle pb-3 pt-5'>
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>
{t('plugin.marketplace.moreFrom')}
</div>
<div className='body-md-regular flex items-center text-center text-text-tertiary'>
{t('plugin.marketplace.discover')}
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.models')}
</span>
,
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.tools')}
</span>
,
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.agents')}
</span>
,
<span className="body-md-medium relative ml-1 mr-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.extensions')}
</span>
{t('plugin.marketplace.and')}
<span className="body-md-medium relative ml-1 mr-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.bundles')}
</span>
{t('common.operation.in')}
<a
href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })}
className='system-sm-medium ml-1 flex items-center text-text-accent'
target='_blank'
>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' />
</a>
<>
<div className='sticky bottom-0 flex shrink-0 flex-col bg-background-default-subtle px-12 pb-[14px] pt-2'>
{isMarketplaceArrowVisible && (
<RiArrowUpDoubleLine
className='absolute left-1/2 top-2 z-10 h-4 w-4 -translate-x-1/2 cursor-pointer text-text-quaternary'
onClick={showMarketplacePanel}
/>
)}
<div className='pb-3 pt-4'>
<div className='title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent'>
{t('plugin.marketplace.moreFrom')}
</div>
<div className='body-md-regular flex items-center text-center text-text-tertiary'>
{t('plugin.marketplace.discover')}
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.models')}
</span>
,
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.tools')}
</span>
,
<span className="body-md-medium relative ml-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.agents')}
</span>
,
<span className="body-md-medium relative ml-1 mr-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.extensions')}
</span>
{t('plugin.marketplace.and')}
<span className="body-md-medium relative ml-1 mr-1 text-text-secondary after:absolute after:bottom-[1.5px] after:left-0 after:h-2 after:w-full after:bg-text-text-selected after:content-['']">
{t('plugin.category.bundles')}
</span>
{t('common.operation.in')}
<a
href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })}
className='system-sm-medium ml-1 flex items-center text-text-accent'
target='_blank'
>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' />
</a>
</div>
</div>
</div>
{
isLoading && page === 1 && (
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'>
<Loading />
</div>
)
}
{
(!isLoading || page > 1) && (
<List
marketplaceCollections={marketplaceCollections || []}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
plugins={plugins}
showInstallButton
locale={locale}
/>
)
}
</div>
<div className='mt-[-14px] shrink-0 grow bg-background-default-subtle px-12 pb-2'>
{
isLoading && page === 1 && (
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'>
<Loading />
</div>
)
}
{
(!isLoading || page > 1) && (
<List
marketplaceCollections={marketplaceCollections || []}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
plugins={plugins}
showInstallButton
locale={locale}
/>
)
}
</div>
</>
)
}

@ -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<HTMLDivElement>(null)
@ -83,6 +84,41 @@ const ProviderList = () => {
return detail
}, [currentProvider?.plugin_id, pluginList?.plugins])
const toolListTailRef = useRef<HTMLDivElement>(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 (
<>
<div className='relative flex h-0 shrink-0 grow overflow-hidden'>
@ -152,15 +188,16 @@ const ProviderList = () => {
</div>
)}
{!filteredCollectionList.length && activeTab === 'builtin' && (
<Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' />
<Empty lightCard text={t('tools.noTools')} className='h-[224px] shrink-0 px-12' />
)}
<div ref={toolListTailRef} />
{enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
isMarketplaceArrowVisible={isMarketplaceArrowVisible}
showMarketplacePanel={showMarketplacePanel}
marketplaceContext={marketplaceContext}
/>
)}
{activeTab === 'mcp' && (

@ -248,6 +248,7 @@ const translation = {
emailLabel: 'New email',
emailPlaceholder: 'Enter a new email',
existingEmail: 'A user with this email already exists.',
unAvailableEmail: 'This email is temporarily unavailable.',
sendVerifyCode: 'Send verification code',
continue: 'Continue',
changeTo: 'Change to {{email}}',

@ -50,24 +50,35 @@ export const loadLangResources = async (lang: string) => {
acc[camelCase(NAMESPACES[index])] = mod
return acc
}, {} as Record<string, any>)
return resources
}
const getFallbackTranslation = () => {
const resources = NAMESPACES.reduce((acc, ns, index) => {
acc[camelCase(NAMESPACES[index])] = require(`./en-US/${ns}`).default
return acc
}, {} as Record<string, any>)
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)
}

@ -249,6 +249,7 @@ const translation = {
emailLabel: '新しいメール',
emailPlaceholder: '新しいメールを入力してください',
existingEmail: 'このメールアドレスのユーザーは既に存在します',
unAvailableEmail: 'このメールアドレスは現在使用できません。',
sendVerifyCode: '確認コードを送信',
continue: '続行',
changeTo: '{{email}} に変更',

@ -248,6 +248,7 @@ const translation = {
emailLabel: '新邮箱',
emailPlaceholder: '输入新邮箱',
existingEmail: '该邮箱已存在',
unAvailableEmail: '该邮箱暂时无法使用。',
sendVerifyCode: '发送验证码',
continue: '继续',
changeTo: '更改为 {{email}}',

@ -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(() => {

Loading…
Cancel
Save