From eaaf55149735891678301eb3bd261c047ba054d1 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 28 May 2025 16:36:08 +0800 Subject: [PATCH 01/85] fix: Instance is not bound to a Session (#20347) Signed-off-by: -LAN- --- .../app/task_pipeline/easy_ui_based_generate_task_pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index a98a42f5df..6c768fd86c 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -455,8 +455,6 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline, MessageCycleMan agent_thought: Optional[MessageAgentThought] = ( db.session.query(MessageAgentThought).filter(MessageAgentThought.id == event.agent_thought_id).first() ) - db.session.refresh(agent_thought) - db.session.close() if agent_thought: return AgentThoughtStreamResponse( From 42505010583fa8936b0a091aa4e019159682c225 Mon Sep 17 00:00:00 2001 From: "Junjie.M" <118170653@qq.com> Date: Wed, 28 May 2025 16:36:32 +0800 Subject: [PATCH 02/85] fix: reset password page dark style (#20350) --- web/app/reset-password/set-password/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx index dd1c4ef1f4..ee4c114a77 100644 --- a/web/app/reset-password/set-password/page.tsx +++ b/web/app/reset-password/set-password/page.tsx @@ -105,7 +105,7 @@ const ChangePasswordForm = () => {
-
+
{/* Password */}
)}
diff --git a/web/app/components/base/logo/dify-logo.tsx b/web/app/components/base/logo/dify-logo.tsx index 9e8f077372..5369144e1c 100644 --- a/web/app/components/base/logo/dify-logo.tsx +++ b/web/app/components/base/logo/dify-logo.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import classNames from '@/utils/classnames' import useTheme from '@/hooks/use-theme' import { basePath } from '@/utils/var' -import { useGlobalPublicStore } from '@/context/global-public-context' export type LogoStyle = 'default' | 'monochromeWhite' export const logoPathMap: Record = { @@ -32,18 +31,12 @@ const DifyLogo: FC = ({ }) => { const { theme } = useTheme() const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style - const { systemFeatures } = useGlobalPublicStore() - const hasBrandingLogo = Boolean(systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo) - - let src = `${basePath}${logoPathMap[themedStyle]}` - if (hasBrandingLogo) - src = systemFeatures.branding.workspace_logo return ( {hasBrandingLogo ) } diff --git a/web/app/components/custom/custom-web-app-brand/index.tsx b/web/app/components/custom/custom-web-app-brand/index.tsx index 444df98f24..f6f617be85 100644 --- a/web/app/components/custom/custom-web-app-brand/index.tsx +++ b/web/app/components/custom/custom-web-app-brand/index.tsx @@ -24,6 +24,7 @@ import { } from '@/service/common' import { useAppContext } from '@/context/app-context' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' const ALLOW_FILE_EXTENSIONS = ['svg', 'png'] @@ -39,6 +40,7 @@ const CustomWebAppBrand = () => { const [fileId, setFileId] = useState('') const [imgKey, setImgKey] = useState(Date.now()) const [uploadProgress, setUploadProgress] = useState(0) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const isSandbox = enableBilling && plan.type === Plan.sandbox const uploading = uploadProgress > 0 && uploadProgress < 100 const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || '' @@ -244,9 +246,12 @@ const CustomWebAppBrand = () => { {!webappBrandRemoved && ( <>
POWERED BY
- {webappLogo - ? logo - : + { + systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : webappLogo + ? logo + : } )} @@ -303,9 +308,12 @@ const CustomWebAppBrand = () => { {!webappBrandRemoved && ( <>
POWERED BY
- {webappLogo - ? logo - : + { + systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : webappLogo + ? logo + : } )} diff --git a/web/app/components/header/account-about/index.tsx b/web/app/components/header/account-about/index.tsx index 6129b48dce..280e276be9 100644 --- a/web/app/components/header/account-about/index.tsx +++ b/web/app/components/header/account-about/index.tsx @@ -9,6 +9,7 @@ import type { LangGeniusVersionResponse } from '@/models/common' import { IS_CE_EDITION } from '@/config' import DifyLogo from '@/app/components/base/logo/dify-logo' import { noop } from 'lodash-es' +import { useGlobalPublicStore } from '@/context/global-public-context' type IAccountSettingProps = { langeniusVersionInfo: LangGeniusVersionResponse @@ -21,6 +22,7 @@ export default function AccountAbout({ }: IAccountSettingProps) { const { t } = useTranslation() const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return (
- + {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : } +
Version {langeniusVersionInfo?.current_version}
© {dayjs().year()} LangGenius, Inc., Contributors.
diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 6e8d1704dd..a9c26e0070 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -21,6 +21,7 @@ import { useModalContext } from '@/context/modal-context' import PlanBadge from './plan-badge' import LicenseNav from './license-env' import { Plan } from '../billing/type' +import { useGlobalPublicStore } from '@/context/global-public-context' const navClassName = ` flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl @@ -36,6 +37,7 @@ const Header = () => { const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false) const { enableBilling, plan } = useProviderContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const isFreePlan = plan.type === Plan.sandbox const handlePlanClick = useCallback(() => { if (isFreePlan) @@ -61,7 +63,13 @@ const Header = () => { !isMobile &&
- + {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : }
/
@@ -76,7 +84,13 @@ const Header = () => { {isMobile && (
- + {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : }
/
{enableBilling ? : } diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 5450fa7ce6..6fd6d17278 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -641,11 +641,13 @@ const TextGeneration: FC = ({ !isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular', )}>
{t('share.chat.poweredBy')}
- {systemFeatures.branding.enabled ? ( - logo - ) : ( - - )} + { + systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? logo + : customConfig?.replace_webapp_logo + ? logo + : + }
)}
diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx index 5e85a8d306..0efd30b6cc 100644 --- a/web/app/signin/_header.tsx +++ b/web/app/signin/_header.tsx @@ -7,6 +7,7 @@ import { languages } from '@/i18n/language' import type { Locale } from '@/i18n' import I18n from '@/context/i18n' import dynamic from 'next/dynamic' +import { useGlobalPublicStore } from '@/context/global-public-context' // Avoid rendering the logo and theme selector on the server const DifyLogo = dynamic(() => import('@/app/components/base/logo/dify-logo'), { @@ -20,10 +21,17 @@ const ThemeSelector = dynamic(() => import('@/app/components/base/theme-selector const Header = () => { const { locale, setLocaleOnClient } = useContext(I18n) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return (
- + {systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo + ? logo + : }
+ + setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> + + + +
+
+
+
router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'> +
+ +
+ {t('login.back')} +
+
+} diff --git a/web/app/(shareLayout)/webapp-reset-password/layout.tsx b/web/app/(shareLayout)/webapp-reset-password/layout.tsx new file mode 100644 index 0000000000..e0ac6b9ad6 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/layout.tsx @@ -0,0 +1,30 @@ +'use client' +import Header from '@/app/signin/_header' + +import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' + +export default function SignInLayout({ children }: any) { + const { systemFeatures } = useGlobalPublicStore() + return <> +
+
+
+
+
+ {children} +
+
+ {!systemFeatures.branding.enabled &&
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. +
} +
+
+ +} diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx new file mode 100644 index 0000000000..96cd4c5805 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -0,0 +1,104 @@ +'use client' +import Link from 'next/link' +import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' +import { emailRegex } from '@/config' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import { sendResetPasswordCode } from '@/service/common' +import I18NContext from '@/context/i18n' +import { noop } from 'lodash-es' +import useDocumentTitle from '@/hooks/use-document-title' + +export default function CheckCode() { + const { t } = useTranslation() + useDocumentTitle('') + const searchParams = useSearchParams() + const router = useRouter() + const [email, setEmail] = useState('') + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + + const handleGetEMailVerificationCode = async () => { + try { + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + setIsLoading(true) + const res = await sendResetPasswordCode(email, locale) + if (res.result === 'success') { + localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(res.data)) + params.set('email', encodeURIComponent(email)) + router.push(`/webapp-reset-password/check-code?${params.toString()}`) + } + else if (res.code === 'account_not_found') { + Toast.notify({ + type: 'error', + message: t('login.error.registrationNotAllowed'), + }) + } + else { + Toast.notify({ + type: 'error', + message: res.data, + }) + } + } + catch (error) { + console.error(error) + } + finally { + setIsLoading(false) + } + } + + return
+
+ +
+
+

{t('login.resetPassword')}

+

+ {t('login.resetPasswordDesc')} +

+
+ +
+ +
+ +
+ setEmail(e.target.value)} /> +
+
+ +
+
+
+
+
+
+ +
+ +
+ {t('login.backToLogin')} + +
+} diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx new file mode 100644 index 0000000000..9f9a8ad4e3 --- /dev/null +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -0,0 +1,188 @@ +'use client' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import cn from 'classnames' +import { RiCheckboxCircleFill } from '@remixicon/react' +import { useCountDown } from 'ahooks' +import Button from '@/app/components/base/button' +import { changeWebAppPasswordWithToken } from '@/service/common' +import Toast from '@/app/components/base/toast' +import Input from '@/app/components/base/input' + +const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ + +const ChangePasswordForm = () => { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const token = decodeURIComponent(searchParams.get('token') || '') + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showSuccess, setShowSuccess] = useState(false) + const [showPassword, setShowPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) + + const showErrorMessage = useCallback((message: string) => { + Toast.notify({ + type: 'error', + message, + }) + }, []) + + const getSignInUrl = () => { + return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}` + } + + const AUTO_REDIRECT_TIME = 5000 + const [leftTime, setLeftTime] = useState(undefined) + const [countdown] = useCountDown({ + leftTime, + onEnd: () => { + router.replace(getSignInUrl()) + }, + }) + + const valid = useCallback(() => { + if (!password.trim()) { + showErrorMessage(t('login.error.passwordEmpty')) + return false + } + if (!validPassword.test(password)) { + showErrorMessage(t('login.error.passwordInvalid')) + return false + } + if (password !== confirmPassword) { + showErrorMessage(t('common.account.notEqual')) + return false + } + return true + }, [password, confirmPassword, showErrorMessage, t]) + + const handleChangePassword = useCallback(async () => { + if (!valid()) + return + try { + await changeWebAppPasswordWithToken({ + url: '/forgot-password/resets', + body: { + token, + new_password: password, + password_confirm: confirmPassword, + }, + }) + setShowSuccess(true) + setLeftTime(AUTO_REDIRECT_TIME) + } + catch (error) { + console.error(error) + } + }, [password, token, valid, confirmPassword]) + + return ( +
+ {!showSuccess && ( +
+
+

+ {t('login.changePassword')} +

+

+ {t('login.changePasswordTip')} +

+
+ +
+
+ {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder={t('login.passwordPlaceholder') || ''} + /> + +
+ +
+
+
{t('login.error.passwordInvalid')}
+
+ {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder={t('login.confirmPasswordPlaceholder') || ''} + /> +
+ +
+
+
+
+ +
+
+
+
+ )} + {showSuccess && ( +
+
+
+ +
+

+ {t('login.passwordChangedTip')} +

+
+
+ +
+
+ )} +
+ ) +} + +export default ChangePasswordForm diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx new file mode 100644 index 0000000000..1b8f18c98f --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -0,0 +1,115 @@ +'use client' +import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { useCallback, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Countdown from '@/app/components/signin/countdown' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common' +import I18NContext from '@/context/i18n' +import { setAccessToken } from '@/app/components/share/utils' +import { fetchAccessToken } from '@/service/share' + +export default function CheckCode() { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const email = decodeURIComponent(searchParams.get('email') as string) + const token = decodeURIComponent(searchParams.get('token') as string) + const [code, setVerifyCode] = useState('') + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + const redirectUrl = searchParams.get('redirect_url') + + const getAppCodeFromRedirectUrl = useCallback(() => { + const appCode = redirectUrl?.split('/').pop() + if (!appCode) + return null + + return appCode + }, [redirectUrl]) + + const verify = async () => { + try { + const appCode = getAppCodeFromRedirectUrl() + if (!code.trim()) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.emptyCode'), + }) + return + } + if (!/\d{6}/.test(code)) { + Toast.notify({ + type: 'error', + message: t('login.checkCode.invalidCode'), + }) + return + } + if (!redirectUrl || !appCode) { + Toast.notify({ + type: 'error', + message: t('login.error.redirectUrlMissing'), + }) + return + } + setIsLoading(true) + const ret = await webAppEmailLoginWithCode({ email, code, token }) + if (ret.result === 'success') { + localStorage.setItem('webapp_access_token', ret.data.access_token) + const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token }) + await setAccessToken(appCode, tokenResp.access_token) + router.replace(redirectUrl) + } + } + catch (error) { console.error(error) } + finally { + setIsLoading(false) + } + } + + const resendCode = async () => { + try { + const ret = await sendWebAppEMailLoginCode(email, locale) + if (ret.result === 'success') { + const params = new URLSearchParams(searchParams) + params.set('token', encodeURIComponent(ret.data)) + router.replace(`/webapp-signin/check-code?${params.toString()}`) + } + } + catch (error) { console.error(error) } + } + + return
+
+ +
+
+

{t('login.checkCode.checkYourEmail')}

+

+ +
+ {t('login.checkCode.validTime')} +

+
+ +
+ + setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} /> + + + +
+
+
+
router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'> +
+ +
+ {t('login.back')} +
+
+} diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx new file mode 100644 index 0000000000..e9b15ae331 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx @@ -0,0 +1,80 @@ +'use client' +import { useRouter, useSearchParams } from 'next/navigation' +import React, { useCallback, useEffect } from 'react' +import Toast from '@/app/components/base/toast' +import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { SSOProtocol } from '@/types/feature' +import Loading from '@/app/components/base/loading' +import AppUnavailable from '@/app/components/base/app-unavailable' + +const ExternalMemberSSOAuth = () => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + const searchParams = useSearchParams() + const router = useRouter() + + const redirectUrl = searchParams.get('redirect_url') + + const showErrorToast = (message: string) => { + Toast.notify({ + type: 'error', + message, + }) + } + + const getAppCodeFromRedirectUrl = useCallback(() => { + const appCode = redirectUrl?.split('/').pop() + if (!appCode) + return null + + return appCode + }, [redirectUrl]) + + const handleSSOLogin = useCallback(async () => { + const appCode = getAppCodeFromRedirectUrl() + if (!appCode || !redirectUrl) { + showErrorToast('redirect url or app code is invalid.') + return + } + + switch (systemFeatures.webapp_auth.sso_config.protocol) { + case SSOProtocol.SAML: { + const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) + router.push(samlRes.url) + break + } + case SSOProtocol.OIDC: { + const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) + router.push(oidcRes.url) + break + } + case SSOProtocol.OAuth2: { + const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) + router.push(oauth2Res.url) + break + } + case '': + break + default: + showErrorToast('SSO protocol is not supported.') + } + }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol]) + + useEffect(() => { + handleSSOLogin() + }, [handleSSOLogin]) + + if (!systemFeatures.webapp_auth.sso_config.protocol) { + return
+ +
+ } + + return ( +
+ +
+ ) +} + +export default React.memo(ExternalMemberSSOAuth) diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx new file mode 100644 index 0000000000..29af3e3a57 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Input from '@/app/components/base/input' +import Button from '@/app/components/base/button' +import { emailRegex } from '@/config' +import Toast from '@/app/components/base/toast' +import { sendWebAppEMailLoginCode } from '@/service/common' +import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' +import I18NContext from '@/context/i18n' +import { noop } from 'lodash-es' + +export default function MailAndCodeAuth() { + const { t } = useTranslation() + const router = useRouter() + const searchParams = useSearchParams() + const emailFromLink = decodeURIComponent(searchParams.get('email') || '') + const [email, setEmail] = useState(emailFromLink) + const [loading, setIsLoading] = useState(false) + const { locale } = useContext(I18NContext) + + const handleGetEMailVerificationCode = async () => { + try { + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + setIsLoading(true) + const ret = await sendWebAppEMailLoginCode(email, locale) + if (ret.result === 'success') { + localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) + const params = new URLSearchParams(searchParams) + params.set('email', encodeURIComponent(email)) + params.set('token', encodeURIComponent(ret.data)) + router.push(`/webapp-signin/check-code?${params.toString()}`) + } + } + catch (error) { + console.error(error) + } + finally { + setIsLoading(false) + } + } + + return (
+ +
+ +
+ setEmail(e.target.value)} /> +
+
+ +
+
+
+ ) +} diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx new file mode 100644 index 0000000000..d9e56af1b8 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -0,0 +1,171 @@ +import Link from 'next/link' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useRouter, useSearchParams } from 'next/navigation' +import { useContext } from 'use-context-selector' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { emailRegex } from '@/config' +import { webAppLogin } from '@/service/common' +import Input from '@/app/components/base/input' +import I18NContext from '@/context/i18n' +import { noop } from 'lodash-es' +import { setAccessToken } from '@/app/components/share/utils' +import { fetchAccessToken } from '@/service/share' + +type MailAndPasswordAuthProps = { + isEmailSetup: boolean +} + +const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ + +export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) { + const { t } = useTranslation() + const { locale } = useContext(I18NContext) + const router = useRouter() + const searchParams = useSearchParams() + const [showPassword, setShowPassword] = useState(false) + const emailFromLink = decodeURIComponent(searchParams.get('email') || '') + const [email, setEmail] = useState(emailFromLink) + const [password, setPassword] = useState('') + + const [isLoading, setIsLoading] = useState(false) + const redirectUrl = searchParams.get('redirect_url') + + const getAppCodeFromRedirectUrl = useCallback(() => { + const appCode = redirectUrl?.split('/').pop() + if (!appCode) + return null + + return appCode + }, [redirectUrl]) + const handleEmailPasswordLogin = async () => { + const appCode = getAppCodeFromRedirectUrl() + if (!email) { + Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) + return + } + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + return + } + if (!password?.trim()) { + Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') }) + return + } + if (!passwordRegex.test(password)) { + Toast.notify({ + type: 'error', + message: t('login.error.passwordInvalid'), + }) + return + } + if (!redirectUrl || !appCode) { + Toast.notify({ + type: 'error', + message: t('login.error.redirectUrlMissing'), + }) + return + } + try { + setIsLoading(true) + const loginData: Record = { + email, + password, + language: locale, + remember_me: true, + } + + const res = await webAppLogin({ + url: '/login', + body: loginData, + }) + if (res.result === 'success') { + localStorage.setItem('webapp_access_token', res.data.access_token) + const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token }) + await setAccessToken(appCode, tokenResp.access_token) + router.replace(redirectUrl) + } + else { + Toast.notify({ + type: 'error', + message: res.data, + }) + } + } + + finally { + setIsLoading(false) + } + } + + return
+
+ +
+ setEmail(e.target.value)} + id="email" + type="email" + autoComplete="email" + placeholder={t('login.emailPlaceholder') || ''} + tabIndex={1} + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') + handleEmailPasswordLogin() + }} + type={showPassword ? 'text' : 'password'} + autoComplete="current-password" + placeholder={t('login.passwordPlaceholder') || ''} + tabIndex={2} + /> +
+ +
+
+
+ +
+ +
+
+} diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx new file mode 100644 index 0000000000..5d649322ba --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx @@ -0,0 +1,88 @@ +'use client' +import { useRouter, useSearchParams } from 'next/navigation' +import type { FC } from 'react' +import { useCallback } from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' +import Toast from '@/app/components/base/toast' +import Button from '@/app/components/base/button' +import { SSOProtocol } from '@/types/feature' +import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share' + +type SSOAuthProps = { + protocol: SSOProtocol | '' +} + +const SSOAuth: FC = ({ + protocol, +}) => { + const router = useRouter() + const { t } = useTranslation() + const searchParams = useSearchParams() + + const redirectUrl = searchParams.get('redirect_url') + const getAppCodeFromRedirectUrl = useCallback(() => { + const appCode = redirectUrl?.split('/').pop() + if (!appCode) + return null + + return appCode + }, [redirectUrl]) + + const [isLoading, setIsLoading] = useState(false) + + const handleSSOLogin = () => { + const appCode = getAppCodeFromRedirectUrl() + if (!redirectUrl || !appCode) { + Toast.notify({ + type: 'error', + message: 'invalid redirect URL or app code', + }) + return + } + setIsLoading(true) + if (protocol === SSOProtocol.SAML) { + fetchMembersSAMLSSOUrl(appCode, redirectUrl).then((res) => { + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else if (protocol === SSOProtocol.OIDC) { + fetchMembersOIDCSSOUrl(appCode, redirectUrl).then((res) => { + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else if (protocol === SSOProtocol.OAuth2) { + fetchMembersOAuth2SSOUrl(appCode, redirectUrl).then((res) => { + router.push(res.url) + }).finally(() => { + setIsLoading(false) + }) + } + else { + Toast.notify({ + type: 'error', + message: 'invalid SSO protocol', + }) + setIsLoading(false) + } + } + + return ( + + ) +} + +export default SSOAuth diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx new file mode 100644 index 0000000000..a03364d326 --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -0,0 +1,25 @@ +'use client' + +import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' +import useDocumentTitle from '@/hooks/use-document-title' + +export default function SignInLayout({ children }: any) { + const { systemFeatures } = useGlobalPublicStore() + useDocumentTitle('') + return <> +
+
+ {/*
*/} +
+
+ {children} +
+
+ {systemFeatures.branding.enabled === false &&
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. +
} +
+
+ +} diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx new file mode 100644 index 0000000000..d6bdf607ba --- /dev/null +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -0,0 +1,176 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Link from 'next/link' +import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' +import Loading from '@/app/components/base/loading' +import MailAndCodeAuth from './components/mail-and-code-auth' +import MailAndPasswordAuth from './components/mail-and-password-auth' +import SSOAuth from './components/sso-auth' +import cn from '@/utils/classnames' +import { LicenseStatus } from '@/types/feature' +import { IS_CE_EDITION } from '@/config' +import { useGlobalPublicStore } from '@/context/global-public-context' + +const NormalForm = () => { + const { t } = useTranslation() + + const [isLoading, setIsLoading] = useState(true) + const { systemFeatures } = useGlobalPublicStore() + const [authType, updateAuthType] = useState<'code' | 'password'>('password') + const [showORLine, setShowORLine] = useState(false) + const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) + + const init = useCallback(async () => { + try { + 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') + } + catch (error) { + console.error(error) + setAllMethodsAreDisabled(true) + } + finally { setIsLoading(false) } + }, [systemFeatures]) + useEffect(() => { + init() + }, [init]) + if (isLoading) { + return
+ +
+ } + if (systemFeatures.license?.status === LicenseStatus.LOST) { + return
+
+
+
+ + +
+

{t('login.licenseLost')}

+

{t('login.licenseLostTip')}

+
+
+
+ } + if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { + return
+
+
+
+ + +
+

{t('login.licenseExpired')}

+

{t('login.licenseExpiredTip')}

+
+
+
+ } + if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { + return
+
+
+
+ + +
+

{t('login.licenseInactive')}

+

{t('login.licenseInactiveTip')}

+
+
+
+ } + + return ( + <> +
+
+

{t('login.pageTitle')}

+ {!systemFeatures.branding.enabled &&

{t('login.welcome')}

} +
+
+
+ {systemFeatures.sso_enforced_for_signin &&
+ +
} +
+ + {showORLine &&
+ +
+ {t('login.or')} +
+
} + { + (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <> + {systemFeatures.enable_email_code_login && authType === 'code' && <> + + {systemFeatures.enable_email_password_login &&
{ updateAuthType('password') }}> + {t('login.usePassword')} +
} + } + {systemFeatures.enable_email_password_login && authType === 'password' && <> + + {systemFeatures.enable_email_code_login &&
{ updateAuthType('code') }}> + {t('login.useVerificationCode')} +
} + } + + } + {allMethodsAreDisabled && <> +
+
+ +
+

{t('login.noLoginMethod')}

+

{t('login.noLoginMethodTip')}

+
+
+ +
+ } + {!systemFeatures.branding.enabled && <> +
+ {t('login.tosDesc')} +   + {t('login.tos')} +  &  + {t('login.pp')} +
+ {IS_CE_EDITION &&
+ {t('login.goToInit')} +   + {t('login.setAdminAccount')} +
} + } + +
+
+ + ) +} + +export default NormalForm diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 668c3f312c..c12fde38dd 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -3,19 +3,20 @@ import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { RiDoorLockLine } from '@remixicon/react' -import cn from '@/utils/classnames' import Toast from '@/app/components/base/toast' -import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' -import { setAccessToken } from '@/app/components/share/utils' +import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' import { useGlobalPublicStore } from '@/context/global-public-context' -import { SSOProtocol } from '@/types/feature' import Loading from '@/app/components/base/loading' import AppUnavailable from '@/app/components/base/app-unavailable' +import NormalForm from './normalForm' +import { AccessMode } from '@/models/access-control' +import ExternalMemberSsoAuth from './components/external-member-sso-auth' +import { fetchAccessToken } from '@/service/share' const WebSSOForm: FC = () => { const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) const searchParams = useSearchParams() const router = useRouter() @@ -23,10 +24,22 @@ const WebSSOForm: FC = () => { const tokenFromUrl = searchParams.get('web_sso_token') const message = searchParams.get('message') - const showErrorToast = (message: string) => { + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + return `/webapp-signin?${params.toString()}` + }, [searchParams]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + const showErrorToast = (msg: string) => { Toast.notify({ type: 'error', - message, + message: msg, }) } @@ -38,102 +51,73 @@ const WebSSOForm: FC = () => { return appCode }, [redirectUrl]) - const processTokenAndRedirect = useCallback(async () => { - const appCode = getAppCodeFromRedirectUrl() - if (!appCode || !tokenFromUrl || !redirectUrl) { - showErrorToast('redirect url or app code or token is invalid.') - return - } - - await setAccessToken(appCode, tokenFromUrl) - router.push(redirectUrl) - }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl]) - - const handleSSOLogin = useCallback(async () => { - const appCode = getAppCodeFromRedirectUrl() - if (!appCode || !redirectUrl) { - showErrorToast('redirect url or app code is invalid.') - return - } - - switch (systemFeatures.webapp_auth.sso_config.protocol) { - case SSOProtocol.SAML: { - const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) - router.push(samlRes.url) - break - } - case SSOProtocol.OIDC: { - const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) - router.push(oidcRes.url) - break - } - case SSOProtocol.OAuth2: { - const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) - router.push(oauth2Res.url) - break - } - case '': - break - default: - showErrorToast('SSO protocol is not supported.') - } - }, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol]) - useEffect(() => { - const init = async () => { - if (message) { - showErrorToast(message) + (async () => { + if (message) return - } - if (!tokenFromUrl) { - await handleSSOLogin() + const appCode = getAppCodeFromRedirectUrl() + if (appCode && tokenFromUrl && redirectUrl) { + localStorage.setItem('webapp_access_token', tokenFromUrl) + const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) + await setAccessToken(appCode, tokenResp.access_token) + router.replace(redirectUrl) return } + if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { + const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) + await setAccessToken(appCode, tokenResp.access_token) + router.replace(redirectUrl) + } + })() + }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) - await processTokenAndRedirect() - } + useEffect(() => { + if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) + router.replace(redirectUrl) + }, [webAppAccessMode, router, redirectUrl]) - init() - }, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin]) - if (tokenFromUrl) - return
- if (message) { + if (tokenFromUrl) { return
- +
} - if (systemFeatures.webapp_auth.enabled) { - if (systemFeatures.webapp_auth.allow_sso) { - return ( -
-
- -
-
- ) - } - return
-
-
- -
-

{t('login.webapp.noLoginMethod')}

-

{t('login.webapp.noLoginMethodTip')}

-
-
- -
+ if (message) { + return
+ + {t('share.login.backToHome')} +
+ } + if (!redirectUrl) { + showErrorToast('redirect url is invalid.') + return
+ +
+ } + if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { + return
+
} - else { + if (!systemFeatures.webapp_auth.enabled) { return

{t('login.webapp.disabled')}

} + if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) { + return
+ +
+ } + + if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS) + return + + return
+ + {t('share.login.backToHome')} +
} export default React.memo(WebSSOForm) diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 2f15c8ec48..13faaea957 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -1,6 +1,6 @@ 'use client' -import { Dialog } from '@headlessui/react' -import { RiBuildingLine, RiGlobalLine } from '@remixicon/react' +import { Description as DialogDescription, DialogTitle } from '@headlessui/react' +import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useCallback, useEffect } from 'react' import Button from '../../base/button' @@ -67,8 +67,8 @@ export default function AccessControl(props: AccessControlProps) { return
- {t('app.accessControlDialog.title')} - {t('app.accessControlDialog.description')} + {t('app.accessControlDialog.title')} + {t('app.accessControlDialog.description')}
@@ -80,12 +80,20 @@ export default function AccessControl(props: AccessControlProps) {

{t('app.accessControlDialog.accessItems.organization')}

- {!hideTip && }
+ +
+
+ +

{t('app.accessControlDialog.accessItems.external')}

+
+ {!hideTip && } +
+
diff --git a/web/app/components/app/app-access-control/specific-groups-or-members.tsx b/web/app/components/app/app-access-control/specific-groups-or-members.tsx index f4872f8c99..b30c8f1ba3 100644 --- a/web/app/components/app/app-access-control/specific-groups-or-members.tsx +++ b/web/app/components/app/app-access-control/specific-groups-or-members.tsx @@ -3,12 +3,10 @@ import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from import { useTranslation } from 'react-i18next' import { useCallback, useEffect } from 'react' import Avatar from '../../base/avatar' -import Divider from '../../base/divider' import Tooltip from '../../base/tooltip' import Loading from '../../base/loading' import useAccessControlStore from '../../../../context/access-control-store' import AddMemberOrGroupDialog from './add-member-or-group-pop' -import { useGlobalPublicStore } from '@/context/global-public-context' import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control' import { AccessMode } from '@/models/access-control' import { useAppWhiteListSubjects } from '@/service/access-control' @@ -19,11 +17,6 @@ export default function SpecificGroupsOrMembers() { const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) const { t } = useTranslation() - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const hideTip = systemFeatures.webapp_auth.enabled - && (systemFeatures.webapp_auth.allow_sso - || systemFeatures.webapp_auth.allow_email_password_login - || systemFeatures.webapp_auth.allow_email_code_login) const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) useEffect(() => { @@ -37,7 +30,6 @@ export default function SpecificGroupsOrMembers() {

{t('app.accessControlDialog.accessItems.specific')}

- {!hideTip && }
} @@ -48,10 +40,6 @@ export default function SpecificGroupsOrMembers() {

{t('app.accessControlDialog.accessItems.specific')}

- {!hideTip && <> - - - }
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 8d0028c7d7..5825bb72ee 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -9,11 +9,14 @@ import dayjs from 'dayjs' import { RiArrowDownSLine, RiArrowRightSLine, + RiBuildingLine, + RiGlobalLine, RiLockLine, RiPlanetLine, RiPlayCircleLine, RiPlayList2Line, RiTerminalBoxLine, + RiVerifiedBadgeLine, } from '@remixicon/react' import { useKeyPress } from 'ahooks' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' @@ -276,10 +279,30 @@ const AppPublisher = ({ setShowAppAccessControl(true) }}>
- - {appDetail?.access_mode === AccessMode.ORGANIZATION &&

{t('app.accessControlDialog.accessItems.organization')}

} - {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&

{t('app.accessControlDialog.accessItems.specific')}

} - {appDetail?.access_mode === AccessMode.PUBLIC &&

{t('app.accessControlDialog.accessItems.anyone')}

} + {appDetail?.access_mode === AccessMode.ORGANIZATION + && <> + +

{t('app.accessControlDialog.accessItems.organization')}

+ + } + {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS + && <> + +

{t('app.accessControlDialog.accessItems.specific')}

+ + } + {appDetail?.access_mode === AccessMode.PUBLIC + && <> + +

{t('app.accessControlDialog.accessItems.anyone')}

+ + } + {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS + && <> + +

{t('app.accessControlDialog.accessItems.external')}

+ + }
{!isAppAccessSet &&

{t('app.publishApp.notSet')}

}
diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 9b283cdf5e..9f3b3ac4a6 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -5,10 +5,13 @@ import { useTranslation } from 'react-i18next' import { RiArrowRightSLine, RiBookOpenLine, + RiBuildingLine, RiEqualizer2Line, RiExternalLinkLine, + RiGlobalLine, RiLockLine, RiPaintBrushLine, + RiVerifiedBadgeLine, RiWindowLine, } from '@remixicon/react' import SettingsModal from './settings' @@ -248,11 +251,30 @@ function AppCard({
- - {appDetail?.access_mode === AccessMode.ORGANIZATION &&

{t('app.accessControlDialog.accessItems.organization')}

} - {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&

{t('app.accessControlDialog.accessItems.specific')}

} - {appDetail?.access_mode === AccessMode.PUBLIC &&

{t('app.accessControlDialog.accessItems.anyone')}

} -
+ {appDetail?.access_mode === AccessMode.ORGANIZATION + && <> + +

{t('app.accessControlDialog.accessItems.organization')}

+ + } + {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS + && <> + +

{t('app.accessControlDialog.accessItems.specific')}

+ + } + {appDetail?.access_mode === AccessMode.PUBLIC + && <> + +

{t('app.accessControlDialog.accessItems.anyone')}

+ + } + {appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS + && <> + +

{t('app.accessControlDialog.accessItems.external')}

+ + }
{!isAppAccessSet &&

{t('app.publishApp.notSet')}

}
diff --git a/web/app/components/base/app-unavailable.tsx b/web/app/components/base/app-unavailable.tsx index 4e835cbfcf..928c850262 100644 --- a/web/app/components/base/app-unavailable.tsx +++ b/web/app/components/base/app-unavailable.tsx @@ -1,4 +1,5 @@ 'use client' +import classNames from '@/utils/classnames' import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' @@ -7,17 +8,19 @@ type IAppUnavailableProps = { code?: number | string isUnknownReason?: boolean unknownReason?: string + className?: string } const AppUnavailable: FC = ({ code = 404, isUnknownReason, unknownReason, + className, }) => { const { t } = useTranslation() return ( -
+

({ - accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, userCanAccess: false, currentConversationId: '', appPrevChatTree: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index dd7cb14b25..32f74e6457 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -43,9 +43,8 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { noop } from 'lodash-es' -import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { useGetUserCanAccessApp } from '@/service/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' -import { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -77,11 +76,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) - const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ - appId: installedAppInfo?.app.id || appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp, @@ -492,8 +486,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return { appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), - accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index de023e7f58..1fd1383196 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -124,7 +124,6 @@ const ChatWithHistoryWrap: FC = ({ const { appInfoError, appInfoLoading, - accessMode, userCanAccess, appData, appParams, @@ -169,7 +168,6 @@ const ChatWithHistoryWrap: FC = ({ appInfoError, appInfoLoading, appData, - accessMode, userCanAccess, appParams, appMeta, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index fd317ccf91..4e50c1cb79 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -19,7 +19,6 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re import DifyLogo from '@/app/components/base/logo/dify-logo' import type { ConversationItem } from '@/models/share' import cn from '@/utils/classnames' -import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { @@ -30,7 +29,6 @@ const Sidebar = ({ isPanel }: Props) => { const { t } = useTranslation() const { isInstalledApp, - accessMode, appData, handleNewConversation, pinnedConversationList, @@ -140,7 +138,7 @@ const Sidebar = ({ isPanel }: Props) => { )}

- + {/* powered by */}
{!appData?.custom_config?.remove_webapp_brand && ( diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index 5964efd806..d24265ed9e 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -15,10 +15,8 @@ import type { ConversationItem, } from '@/models/share' import { noop } from 'lodash-es' -import { AccessMode } from '@/models/access-control' export type EmbeddedChatbotContextValue = { - accessMode?: AccessMode userCanAccess?: boolean appInfoError?: any appInfoLoading?: boolean @@ -58,7 +56,6 @@ export type EmbeddedChatbotContextValue = { export const EmbeddedChatbotContext = createContext({ userCanAccess: false, - accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, currentConversationId: '', appPrevChatList: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 40c56eca7b..0158e8d041 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -36,9 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { noop } from 'lodash-es' -import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { useGetUserCanAccessApp } from '@/service/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' -import { AccessMode } from '@/models/access-control' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -70,11 +69,6 @@ export const useEmbeddedChatbot = () => { const isInstalledApp = false const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) - const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ - appId: appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp, @@ -385,8 +379,7 @@ export const useEmbeddedChatbot = () => { return { appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)), - accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC, + appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, allowResetChat, diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index 19b660b083..adb926c7ca 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -6,9 +6,8 @@ import type { Placement } from '@floating-ui/react' import { RiEqualizer2Line, } from '@remixicon/react' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import Divider from '../../base/divider' -import { removeAccessToken } from '../utils' import InfoModal from './info-modal' import ActionButton from '@/app/components/base/action-button' import { @@ -19,6 +18,8 @@ import { import ThemeSwitcher from '@/app/components/base/theme-switcher' import type { SiteInfo } from '@/models/share' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' type Props = { data?: SiteInfo @@ -31,7 +32,9 @@ const MenuDropdown: FC = ({ placement, hideLogout, }) => { + const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) const router = useRouter() + const pathname = usePathname() const { t } = useTranslation() const [open, doSetOpen] = useState(false) const openRef = useRef(open) @@ -45,9 +48,10 @@ const MenuDropdown: FC = ({ }, [setOpen]) const handleLogout = useCallback(() => { - removeAccessToken() - router.replace(`/webapp-signin?redirect_url=${window.location.href}`) - }, [router]) + localStorage.removeItem('token') + localStorage.removeItem('webapp_access_token') + router.replace(`/webapp-signin?redirect_url=${pathname}`) + }, [router, pathname]) const [show, setShow] = useState(false) @@ -92,6 +96,16 @@ const MenuDropdown: FC = ({ className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' >{t('common.userProfile.about')}
+ {!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && ( +
+
+ {t('common.userProfile.logout')} +
+
+ )}
diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts index 9ce891a50c..d793d48b48 100644 --- a/web/app/components/share/utils.ts +++ b/web/app/components/share/utils.ts @@ -10,8 +10,8 @@ export const getInitialTokenV2 = (): Record => ({ version: 2, }) -export const checkOrSetAccessToken = async () => { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] +export const checkOrSetAccessToken = async (appCode?: string) => { + const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) let accessTokenJson = getInitialTokenV2() @@ -23,8 +23,10 @@ export const checkOrSetAccessToken = async () => { catch { } + if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) { - const res = await fetchAccessToken(sharedToken, userId) + const webAppAccessToken = localStorage.getItem('webapp_access_token') + const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken }) accessTokenJson[sharedToken] = { ...accessTokenJson[sharedToken], [userId || 'DEFAULT']: res.access_token, @@ -33,7 +35,7 @@ export const checkOrSetAccessToken = async () => { } } -export const setAccessToken = async (sharedToken: string, token: string, user_id?: string) => { +export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => { const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) let accessTokenJson = getInitialTokenV2() try { @@ -69,6 +71,7 @@ export const removeAccessToken = () => { } localStorage.removeItem(CONVERSATION_ID_INFO) + localStorage.removeItem('webapp_access_token') delete accessTokenJson[sharedToken] localStorage.setItem('token', JSON.stringify(accessTokenJson)) diff --git a/web/app/signin/LoginLogo.tsx b/web/app/signin/LoginLogo.tsx index 0753d1f98a..73dfb88205 100644 --- a/web/app/signin/LoginLogo.tsx +++ b/web/app/signin/LoginLogo.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' import classNames from '@/utils/classnames' -import { useSelector } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' +import { useTheme } from 'next-themes' type LoginLogoProps = { className?: string @@ -12,11 +12,7 @@ const LoginLogo: FC = ({ className, }) => { const { systemFeatures } = useGlobalPublicStore() - const { theme } = useSelector((s) => { - return { - theme: s.theme, - } - }) + const { theme } = useTheme() let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` if (systemFeatures.branding.enabled) diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx index 5aa5e7a302..26ad84be65 100644 --- a/web/context/global-public-context.tsx +++ b/web/context/global-public-context.tsx @@ -7,19 +7,24 @@ import type { SystemFeatures } from '@/types/feature' import { defaultSystemFeatures } from '@/types/feature' import { getSystemFeatures } from '@/service/common' import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' type GlobalPublicStore = { - isPending: boolean - setIsPending: (isPending: boolean) => void + isGlobalPending: boolean + setIsGlobalPending: (isPending: boolean) => void systemFeatures: SystemFeatures setSystemFeatures: (systemFeatures: SystemFeatures) => void + webAppAccessMode: AccessMode, + setWebAppAccessMode: (webAppAccessMode: AccessMode) => void } export const useGlobalPublicStore = create(set => ({ - isPending: true, - setIsPending: (isPending: boolean) => set(() => ({ isPending })), + isGlobalPending: true, + setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), systemFeatures: defaultSystemFeatures, setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), + webAppAccessMode: AccessMode.PUBLIC, + setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), })) const GlobalPublicStoreProvider: FC = ({ @@ -29,7 +34,7 @@ const GlobalPublicStoreProvider: FC = ({ queryKey: ['systemFeatures'], queryFn: getSystemFeatures, }) - const { setSystemFeatures, setIsPending } = useGlobalPublicStore() + const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore() useEffect(() => { if (data) setSystemFeatures({ ...defaultSystemFeatures, ...data }) diff --git a/web/hooks/use-document-title.spec.ts b/web/hooks/use-document-title.spec.ts index 88239ffbdf..a8d3d56cff 100644 --- a/web/hooks/use-document-title.spec.ts +++ b/web/hooks/use-document-title.spec.ts @@ -11,7 +11,7 @@ describe('title should be empty if systemFeatures is pending', () => { act(() => { useGlobalPublicStore.setState({ systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, - isPending: true, + isGlobalPending: true, }) }) it('document title should be empty if set title', () => { @@ -28,7 +28,7 @@ describe('use default branding', () => { beforeEach(() => { act(() => { useGlobalPublicStore.setState({ - isPending: false, + isGlobalPending: false, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } }, }) }) @@ -48,7 +48,7 @@ describe('use specific branding', () => { beforeEach(() => { act(() => { useGlobalPublicStore.setState({ - isPending: false, + isGlobalPending: false, systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } }, }) }) diff --git a/web/hooks/use-document-title.ts b/web/hooks/use-document-title.ts index 10275a196f..2c848a1f56 100644 --- a/web/hooks/use-document-title.ts +++ b/web/hooks/use-document-title.ts @@ -3,7 +3,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context' import { useFavicon, useTitle } from 'ahooks' export default function useDocumentTitle(title: string) { - const isPending = useGlobalPublicStore(s => s.isPending) + const isPending = useGlobalPublicStore(s => s.isGlobalPending) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const prefix = title ? `${title} - ` : '' let titleStr = '' diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 20a80ba4cd..ccfe23ead6 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -197,9 +197,10 @@ const translation = { }, accessControl: 'Web App Access Control', accessItemsDescription: { - anyone: 'Anyone can access the web app', - specific: 'Only specific groups or members can access the web app', - organization: 'Anyone in the organization can access the web app', + anyone: 'Anyone can access the web app (no login required)', + specific: 'Only specific members within the platform can access the Web application', + organization: 'All members within the platform can access the Web application', + external: 'Only authenticated external users can access the Web application', }, accessControlDialog: { title: 'Web App Access Control', @@ -207,15 +208,16 @@ const translation = { accessLabel: 'Who has access', accessItems: { anyone: 'Anyone with the link', - specific: 'Specific groups or members', - organization: 'Only members within the enterprise', + specific: 'Specific members within the platform', + organization: 'All members within the platform', + external: 'Authenticated external users', }, groups_one: '{{count}} GROUP', groups_other: '{{count}} GROUPS', members_one: '{{count}} MEMBER', members_other: '{{count}} MEMBERS', noGroupsOrMembers: 'No groups or members selected', - webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.', + webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the Web application.', operateGroupAndMember: { searchPlaceholder: 'Search groups and members', allMembers: 'All members', diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share-app.ts index bf99005d71..ab589ffb76 100644 --- a/web/i18n/en-US/share-app.ts +++ b/web/i18n/en-US/share-app.ts @@ -77,6 +77,9 @@ const translation = { atLeastOne: 'Please input at least one row in the uploaded file.', }, }, + login: { + backToHome: 'Back to Home', + }, } export default translation diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index b4fc8d4d82..b501bc129e 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -210,30 +210,27 @@ const translation = { }, accessControl: 'Web アプリアクセス制御', accessItemsDescription: { - anyone: '誰でも Web アプリにアクセス可能', - specific: '特定のグループまたはメンバーのみが Web アプリにアクセス可能', - organization: '組織内の誰でも Web アプリにアクセス可能', + anyone: '誰でもこの web アプリにアクセスできます(ログイン不要)', + specific: '特定のプラットフォーム内メンバーのみがこの Web アプリにアクセスできます', + organization: 'プラットフォーム内の全メンバーがこの Web アプリにアクセスできます', + external: '認証済みの外部ユーザーのみがこの Web アプリにアクセスできます', }, accessControlDialog: { title: 'アクセス権限', description: 'Web アプリのアクセス権限を設定します', accessLabel: '誰がアクセスできますか', - accessItemsDescription: { - anyone: '誰でも Web アプリにアクセス可能です', - specific: '特定のグループやメンバーが Web アプリにアクセス可能です', - organization: '組織内の誰でも Web アプリにアクセス可能です', - }, accessItems: { - anyone: 'すべてのユーザー', - specific: '特定のグループメンバー', - organization: 'グループ内の全員', + anyone: 'リンクを知っているすべてのユーザー', + specific: '特定のプラットフォーム内メンバー', + organization: 'プラットフォーム内の全メンバー', + external: '認証済みの外部ユーザー', }, groups_one: '{{count}} グループ', groups_other: '{{count}} グループ', members_one: '{{count}} メンバー', members_other: '{{count}} メンバー', noGroupsOrMembers: 'グループまたはメンバーが選択されていません', - webAppSSONotEnabledTip: 'Web アプリの認証方式設定については、企業管理者へご連絡ください。', + webAppSSONotEnabledTip: 'Web アプリの外部認証方式を設定するには、組織の管理者にお問い合わせください。', operateGroupAndMember: { searchPlaceholder: 'グループやメンバーを検索', allMembers: 'すべてのメンバー', diff --git a/web/i18n/ja-JP/share-app.ts b/web/i18n/ja-JP/share-app.ts index 9e76f6518a..20dad7faec 100644 --- a/web/i18n/ja-JP/share-app.ts +++ b/web/i18n/ja-JP/share-app.ts @@ -73,6 +73,9 @@ const translation = { atLeastOne: '1 行以上のデータが必要です', }, }, + login: { + backToHome: 'ホームに戻る', + }, } export default translation diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index bdd7d98d9b..4ec1e65059 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -198,30 +198,27 @@ const translation = { }, accessControl: 'Web 应用访问控制', accessItemsDescription: { - anyone: '任何人可以访问 web 应用', - specific: '特定组或成员可以访问 web 应用', - organization: '组织内任何人可以访问 web 应用', + anyone: '任何人都可以访问该 web 应用(无需登录)', + specific: '仅指定的平台内成员可访问该 Web 应用', + organization: '平台内所有成员均可访问该 Web 应用', + external: '仅经认证的外部用户可访问该 Web 应用', }, accessControlDialog: { title: 'Web 应用访问权限', description: '设置 web 应用访问权限。', accessLabel: '谁可以访问', - accessItemsDescription: { - anyone: '任何人可以访问 web 应用', - specific: '特定组或成员可以访问 web 应用', - organization: '组织内任何人可以访问 web 应用', - }, accessItems: { anyone: '任何人', - specific: '特定组或成员', - organization: '组织内任何人', + specific: '平台内指定成员', + organization: '平台内所有成员', + external: '经认证的外部用户', }, groups_one: '{{count}} 个组', groups_other: '{{count}} 个组', members_one: '{{count}} 个成员', members_other: '{{count}} 个成员', noGroupsOrMembers: '未选择分组或成员', - webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。', + webAppSSONotEnabledTip: '请联系企业管理员配置 Web 应用外部认证方式。', operateGroupAndMember: { searchPlaceholder: '搜索组或成员', allMembers: '所有成员', diff --git a/web/i18n/zh-Hans/share-app.ts b/web/i18n/zh-Hans/share-app.ts index 4ea2ad6f49..ce1270dae8 100644 --- a/web/i18n/zh-Hans/share-app.ts +++ b/web/i18n/zh-Hans/share-app.ts @@ -73,6 +73,9 @@ const translation = { atLeastOne: '上传文件的内容不能少于一条', }, }, + login: { + backToHome: '返回首页', + }, } export default translation diff --git a/web/models/access-control.ts b/web/models/access-control.ts index 8ad9cc6491..911662b5c4 100644 --- a/web/models/access-control.ts +++ b/web/models/access-control.ts @@ -7,6 +7,7 @@ export enum AccessMode { PUBLIC = 'public', SPECIFIC_GROUPS_MEMBERS = 'private', ORGANIZATION = 'private_all', + EXTERNAL_MEMBERS = 'sso_verified', } export type AccessControlGroup = { diff --git a/web/service/base.ts b/web/service/base.ts index 4b08736288..c3cafe600b 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -109,6 +109,7 @@ function unicodeToChar(text: string) { } function requiredWebSSOLogin(message?: string) { + removeAccessToken() const params = new URLSearchParams() params.append('redirect_url', globalThis.location.pathname) if (message) diff --git a/web/service/common.ts b/web/service/common.ts index e76cfb4196..700cd4bf51 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -52,6 +52,9 @@ type LoginResponse = LoginSuccess | LoginFail export const login: Fetcher }> = ({ url, body }) => { return post(url, { body }) as Promise } +export const webAppLogin: Fetcher }> = ({ url, body }) => { + return post(url, { body }, { isPublicAPI: true }) as Promise +} export const fetchNewToken: Fetcher }> = ({ body }) => { return post('/refresh-token', { body }) as Promise @@ -324,6 +327,16 @@ export const verifyForgotPasswordToken: Fetcher = ({ url, body }) => post(url, { body }) +export const sendWebAppForgotPasswordEmail: Fetcher = ({ url, body }) => + post(url, { body }, { isPublicAPI: true }) + +export const verifyWebAppForgotPasswordToken: Fetcher = ({ url, body }) => { + return post(url, { body }, { isPublicAPI: true }) as Promise +} + +export const changeWebAppPasswordWithToken: Fetcher = ({ url, body }) => + post(url, { body }, { isPublicAPI: true }) + export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) } @@ -340,6 +353,18 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') => export const verifyResetPasswordCode = (body: { email: string; code: string; token: string }) => post('/forgot-password/validity', { body }) +export const sendWebAppEMailLoginCode = (email: string, language = 'en-US') => + post('/email-code-login', { body: { email, language } }, { isPublicAPI: true }) + +export const webAppEmailLoginWithCode = (data: { email: string; code: string; token: string }) => + post('/email-code-login/validity', { body: data }, { isPublicAPI: true }) + +export const sendWebAppResetPasswordCode = (email: string, language = 'en-US') => + post('/forgot-password', { body: { email, language } }, { isPublicAPI: true }) + +export const verifyWebAppResetPasswordCode = (body: { email: string; code: string; token: string }) => + post('/forgot-password/validity', { body }, { isPublicAPI: true }) + export const sendDeleteAccountCode = () => get('/account/delete/verify') diff --git a/web/service/share.ts b/web/service/share.ts index 7fb1562185..6a2a7e5b16 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -214,6 +214,34 @@ export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) }) as Promise<{ url: string }> } +export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + }) as Promise<{ url: string }> +} + +export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + + }) as Promise<{ url: string }> +} + +export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { + return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), { + params: { + app_code: appCode, + redirect_url: redirectUrl, + }, + }) as Promise<{ url: string }> +} + export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise } @@ -258,10 +286,13 @@ export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { c return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) } -export const fetchAccessToken = async (appCode: string, userId?: string) => { +export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { appCode: string, userId?: string, webAppAccessToken?: string | null }) => { const headers = new Headers() headers.append('X-App-Code', appCode) - const url = userId ? `/passport?user_id=${encodeURIComponent(userId)}` : '/passport' + const params = new URLSearchParams() + webAppAccessToken && params.append('web_app_access_token', webAppAccessToken) + userId && params.append('user_id', userId) + const url = `/passport?${params.toString()}` return get(url, { headers }) as Promise<{ access_token: string }> } @@ -278,3 +309,7 @@ export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`) } + +export const getAppAccessModeByAppCode = (appCode: string) => { + return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appCode=${appCode}`) +} diff --git a/web/service/use-share.ts b/web/service/use-share.ts new file mode 100644 index 0000000000..b8f96f6cc5 --- /dev/null +++ b/web/service/use-share.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { getAppAccessModeByAppCode } from './share' + +const NAME_SPACE = 'webapp' + +export const useAppAccessModeByCode = (code: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appAccessMode', code], + queryFn: () => { + if (!code) + return null + + return getAppAccessModeByAppCode(code) + }, + enabled: !!code, + }) +} From 1fbbbb735db1237aa36dcc0d976fda617ab5c71b Mon Sep 17 00:00:00 2001 From: XiaoBa <94062266+XiaoBa-Yu@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:07:54 +0800 Subject: [PATCH 81/85] fix: the locale format(#20662) (#20665) Co-authored-by: Xiaoba Yu --- web/app/components/header/maintenance-notice.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx index 78715bb53e..f9c00dd01e 100644 --- a/web/app/components/header/maintenance-notice.tsx +++ b/web/app/components/header/maintenance-notice.tsx @@ -1,11 +1,10 @@ import { useState } from 'react' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' import { X } from '@/app/components/base/icons/src/vender/line/general' import { NOTICE_I18N } from '@/i18n/language' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' const MaintenanceNotice = () => { - const { locale } = useContext(I18n) + const locale = useLanguage() const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1') const handleJumpNotice = () => { From de9c7f2ea44b55191a01e1c4148e201f72392f4d Mon Sep 17 00:00:00 2001 From: geosmart Date: Thu, 5 Jun 2025 12:11:11 +0800 Subject: [PATCH 82/85] Update template.zh.mdx-fix document update metadata body param (#20659) --- web/app/(commonLayout)/datasets/template/template.zh.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index 08ef5d562a..d121a93df2 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -2223,7 +2223,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi - document_id (string) 文档 ID - metadata_list (list) 元数据列表 - id (string) 元数据 ID - - type (string) 元数据类型 + - value (string) 元数据值 - name (string) 元数据名称 From d608be6e7f17c60109cf0d7f53ce1892ffac5aff Mon Sep 17 00:00:00 2001 From: GuanMu Date: Thu, 5 Jun 2025 13:35:32 +0800 Subject: [PATCH 83/85] Add vscode debugger (#20668) --- .gitignore | 7 ++-- .vscode/README.md | 14 ++++++++ .vscode/launch.json.template | 68 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .vscode/README.md create mode 100644 .vscode/launch.json.template diff --git a/.gitignore b/.gitignore index 8818ab6f65..74a9ef63ef 100644 --- a/.gitignore +++ b/.gitignore @@ -192,12 +192,12 @@ sdks/python-client/dist sdks/python-client/dify_client.egg-info .vscode/* -!.vscode/launch.json +!.vscode/launch.json.template +!.vscode/README.md pyrightconfig.json api/.vscode .idea/ -.vscode # pnpm /.pnpm-store @@ -207,3 +207,6 @@ plugins.jsonl # mise mise.toml + +# Next.js build output +.next/ diff --git a/.vscode/README.md b/.vscode/README.md new file mode 100644 index 0000000000..26516f0540 --- /dev/null +++ b/.vscode/README.md @@ -0,0 +1,14 @@ +# Debugging with VS Code + +This `launch.json.template` file provides various debug configurations for the Dify project within VS Code / Cursor. To use these configurations, you should copy the contents of this file into a new file named `launch.json` in the same `.vscode` directory. + +## How to Use + +1. **Create `launch.json`**: If you don't have one, create a file named `launch.json` inside the `.vscode` directory. +2. **Copy Content**: Copy the entire content from `launch.json.template` into your newly created `launch.json` file. +3. **Select Debug Configuration**: Go to the Run and Debug view in VS Code / Cursor (Ctrl+Shift+D or Cmd+Shift+D). +4. **Start Debugging**: Select the desired configuration from the dropdown menu and click the green play button. + +## Tips + +- If you need to debug with Edge browser instead of Chrome, modify the `serverReadyAction` configuration in the "Next.js: debug full stack" section, change `"debugWithChrome"` to `"debugWithEdge"` to use Microsoft Edge for debugging. diff --git a/.vscode/launch.json.template b/.vscode/launch.json.template new file mode 100644 index 0000000000..f5a7f0893b --- /dev/null +++ b/.vscode/launch.json.template @@ -0,0 +1,68 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Flask API", + "type": "debugpy", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py", + "FLASK_ENV": "development", + "GEVENT_SUPPORT": "True" + }, + "args": [ + "run", + "--host=0.0.0.0", + "--port=5001", + "--no-debugger", + "--no-reload" + ], + "jinja": true, + "justMyCode": true, + "cwd": "${workspaceFolder}/api", + "python": "${workspaceFolder}/api/.venv/bin/python" + }, + { + "name": "Python: Celery Worker (Solo)", + "type": "debugpy", + "request": "launch", + "module": "celery", + "env": { + "GEVENT_SUPPORT": "True" + }, + "args": [ + "-A", + "app.celery", + "worker", + "-P", + "solo", + "-c", + "1", + "-Q", + "dataset,generation,mail,ops_trace", + "--loglevel", + "INFO" + ], + "justMyCode": false, + "cwd": "${workspaceFolder}/api", + "python": "${workspaceFolder}/api/.venv/bin/python" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/web/node_modules/next/dist/bin/next", + "runtimeArgs": ["--inspect"], + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithChrome", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}/web" + }, + "cwd": "${workspaceFolder}/web" + } + ] +} From 3367d4258d75079afc3e505b909184945adfa535 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:35:40 +0800 Subject: [PATCH 84/85] chore: translate i18n files (#20664) Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> --- web/i18n/de-DE/app.ts | 2 ++ web/i18n/de-DE/share-app.ts | 3 +++ web/i18n/es-ES/app.ts | 2 ++ web/i18n/es-ES/common.ts | 1 + web/i18n/es-ES/share-app.ts | 3 +++ web/i18n/fa-IR/app.ts | 2 ++ web/i18n/fa-IR/share-app.ts | 3 +++ web/i18n/fr-FR/app.ts | 3 +++ web/i18n/fr-FR/share-app.ts | 3 +++ web/i18n/hi-IN/app.ts | 2 ++ web/i18n/hi-IN/share-app.ts | 3 +++ web/i18n/it-IT/app.ts | 2 ++ web/i18n/it-IT/share-app.ts | 3 +++ web/i18n/ko-KR/app.ts | 2 ++ web/i18n/ko-KR/share-app.ts | 3 +++ web/i18n/pl-PL/app.ts | 2 ++ web/i18n/pl-PL/share-app.ts | 3 +++ web/i18n/pt-BR/app.ts | 2 ++ web/i18n/pt-BR/share-app.ts | 3 +++ web/i18n/ro-RO/app.ts | 2 ++ web/i18n/ro-RO/share-app.ts | 3 +++ web/i18n/ru-RU/app.ts | 2 ++ web/i18n/ru-RU/share-app.ts | 3 +++ web/i18n/sl-SI/app.ts | 2 ++ web/i18n/sl-SI/share-app.ts | 3 +++ web/i18n/th-TH/app.ts | 2 ++ web/i18n/th-TH/share-app.ts | 3 +++ web/i18n/tr-TR/app.ts | 2 ++ web/i18n/tr-TR/share-app.ts | 3 +++ web/i18n/uk-UA/app.ts | 2 ++ web/i18n/uk-UA/share-app.ts | 3 +++ web/i18n/vi-VN/app.ts | 2 ++ web/i18n/vi-VN/share-app.ts | 3 +++ web/i18n/zh-Hant/app.ts | 2 ++ web/i18n/zh-Hant/share-app.ts | 3 +++ 35 files changed, 87 insertions(+) diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index b9fdde58ff..1373dd611b 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -220,12 +220,14 @@ const translation = { anyone: 'Jeder kann auf die Webanwendung zugreifen.', specific: 'Nur bestimmte Gruppen oder Mitglieder können auf die Webanwendung zugreifen.', organization: 'Jeder in der Organisation kann auf die Webanwendung zugreifen.', + external: 'Nur authentifizierte externe Benutzer können auf die Webanwendung zugreifen.', }, accessControlDialog: { accessItems: { anyone: 'Jeder mit dem Link', specific: 'Spezifische Gruppen oder Mitglieder', organization: 'Nur Mitglieder innerhalb des Unternehmens', + external: 'Authentifizierte externe Benutzer', }, operateGroupAndMember: { searchPlaceholder: 'Gruppen und Mitglieder suchen', diff --git a/web/i18n/de-DE/share-app.ts b/web/i18n/de-DE/share-app.ts index 462286fa23..33c40917dd 100644 --- a/web/i18n/de-DE/share-app.ts +++ b/web/i18n/de-DE/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} HINRICHTUNGEN', execution: 'AUSFÜHRUNG', }, + login: { + backToHome: 'Zurück zur Startseite', + }, } export default translation diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index c183485294..c1147720e7 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -212,12 +212,14 @@ const translation = { anyone: 'Cualquiera puede acceder a la aplicación web.', specific: 'Solo grupos o miembros específicos pueden acceder a la aplicación web', organization: 'Cualquiera en la organización puede acceder a la aplicación web', + external: 'Solo los usuarios externos autenticados pueden acceder a la aplicación web.', }, accessControlDialog: { accessItems: { anyone: 'Cualquiera con el enlace', specific: 'Grupos o miembros específicos', organization: 'Solo miembros dentro de la empresa', + external: 'Usuarios externos autenticados', }, operateGroupAndMember: { searchPlaceholder: 'Buscar grupos y miembros', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 22c70f6bff..82ed315f1c 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -654,6 +654,7 @@ const translation = { auto: 'sistema', light: 'luz', theme: 'Tema', + dark: 'noche', }, compliance: { iso27001: 'Certificación ISO 27001:2022', diff --git a/web/i18n/es-ES/share-app.ts b/web/i18n/es-ES/share-app.ts index 41aa35c43e..caeb056d89 100644 --- a/web/i18n/es-ES/share-app.ts +++ b/web/i18n/es-ES/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'EJECUCIÓN', executions: '{{num}} EJECUCIONES', }, + login: { + backToHome: 'Volver a Inicio', + }, } export default translation diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index d37f4e8f90..13473d21f5 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -213,12 +213,14 @@ const translation = { specific: 'فقط گروه‌ها یا اعضای خاصی می‌توانند به اپلیکیشن وب دسترسی پیدا کنند.', anyone: 'هر کسی می‌تواند به وب‌اپلیکیشن دسترسی پیدا کند', organization: 'هر کسی در سازمان می‌تواند به اپلیکیشن وب دسترسی پیدا کند.', + external: 'تنها کاربران خارجی تأیید شده می‌توانند به برنامه وب دسترسی پیدا کنند.', }, accessControlDialog: { accessItems: { specific: 'گروه‌ها یا اعضای خاص', organization: 'فقط اعضای داخل سازمان', anyone: 'هر کسی که لینک را داشته باشد', + external: 'کاربران خارجی تأیید شده', }, operateGroupAndMember: { searchPlaceholder: 'گروه‌ها و اعضا را جستجو کنید', diff --git a/web/i18n/fa-IR/share-app.ts b/web/i18n/fa-IR/share-app.ts index bf1c0dec50..03ed4e8ea9 100644 --- a/web/i18n/fa-IR/share-app.ts +++ b/web/i18n/fa-IR/share-app.ts @@ -73,6 +73,9 @@ const translation = { executions: '{{num}} اعدام', execution: 'اجرا', }, + login: { + backToHome: 'بازگشت به خانه', + }, } export default translation diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index ffa00c758a..5c0965815e 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -207,17 +207,20 @@ const translation = { modelNotSupported: 'Modèle non pris en charge', moreFillTip: 'Affichage d\'un maximum de 10 niveaux d\'imbrication', configure: 'Configurer', + structured: 'systématique', }, accessItemsDescription: { anyone: 'Tout le monde peut accéder à l\'application web.', specific: 'Seules des groupes ou membres spécifiques peuvent accéder à l\'application web.', organization: 'Toute personne dans l\'organisation peut accéder à l\'application web.', + external: 'Seuls les utilisateurs externes authentifiés peuvent accéder à l\'application Web.', }, accessControlDialog: { accessItems: { anyone: 'Quiconque avec le lien', specific: 'Groupes ou membres spécifiques', organization: 'Seuls les membres au sein de l\'entreprise', + external: 'Utilisateurs externes authentifiés', }, operateGroupAndMember: { searchPlaceholder: 'Rechercher des groupes et des membres', diff --git a/web/i18n/fr-FR/share-app.ts b/web/i18n/fr-FR/share-app.ts index d0b3a5047e..2374da70e6 100644 --- a/web/i18n/fr-FR/share-app.ts +++ b/web/i18n/fr-FR/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} EXÉCUTIONS', execution: 'EXÉCUTION', }, + login: { + backToHome: 'Retour à l\'accueil', + }, } export default translation diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index f9486b93ec..3929dfeb6a 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'कोई भी वेब ऐप तक पहुँच सकता है', organization: 'संस्थान के किसी भी व्यक्ति को वेब ऐप तक पहुंच प्राप्त है', specific: 'केवल विशेष समूह या सदस्य ही वेब ऐप तक पहुंच सकते हैं', + external: 'केवल प्रमाणित बाहरी उपयोगकर्ता वेब अनुप्रयोग तक पहुँच सकते हैं', }, accessControlDialog: { accessItems: { anyone: 'लिंक के साथ कोई भी', specific: 'विशिष्ट समूह या सदस्य', organization: 'केवल उद्यम के भीतर के सदस्य', + external: 'प्रमाणित बाहरी उपयोगकर्ता', }, operateGroupAndMember: { searchPlaceholder: 'समूहों और सदस्यों की खोज करें', diff --git a/web/i18n/hi-IN/share-app.ts b/web/i18n/hi-IN/share-app.ts index e0296fda83..a1e716b5bc 100644 --- a/web/i18n/hi-IN/share-app.ts +++ b/web/i18n/hi-IN/share-app.ts @@ -80,6 +80,9 @@ const translation = { execution: 'अनु执行', executions: '{{num}} फाँसी', }, + login: { + backToHome: 'होम पर वापस', + }, } export default translation diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index f6855873db..2bd5069b6c 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -224,12 +224,14 @@ const translation = { anyone: 'Chiunque può accedere all\'app web', specific: 'Solo gruppi o membri specifici possono accedere all\'app web.', organization: 'Qualsiasi persona nell\'organizzazione può accedere all\'app web', + external: 'Solo gli utenti esterni autenticati possono accedere all\'applicazione Web', }, accessControlDialog: { accessItems: { anyone: 'Chiunque con il link', specific: 'Gruppi o membri specifici', organization: 'Solo i membri all\'interno dell\'impresa', + external: 'Utenti esterni autenticati', }, operateGroupAndMember: { searchPlaceholder: 'Cerca gruppi e membri', diff --git a/web/i18n/it-IT/share-app.ts b/web/i18n/it-IT/share-app.ts index 2e1c96a396..4c6c18ff33 100644 --- a/web/i18n/it-IT/share-app.ts +++ b/web/i18n/it-IT/share-app.ts @@ -79,6 +79,9 @@ const translation = { execution: 'ESECUZIONE', executions: '{{num}} ESECUZIONI', }, + login: { + backToHome: 'Torna alla home', + }, } export default translation diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 0ab08251fb..7227fd3171 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -209,12 +209,14 @@ const translation = { anyone: '누구나 웹 앱에 접근할 수 있습니다.', specific: '특정 그룹이나 회원만 웹 앱에 접근할 수 있습니다.', organization: '조직 내 모든 사람이 웹 애플리케이션에 접근할 수 있습니다.', + external: '인증된 외부 사용자만 웹 애플리케이션에 접근할 수 있습니다.', }, accessControlDialog: { accessItems: { anyone: '링크가 있는 누구나', specific: '특정 그룹 또는 구성원', organization: '기업 내의 회원만', + external: '인증된 외부 사용자', }, operateGroupAndMember: { searchPlaceholder: '그룹 및 구성원 검색', diff --git a/web/i18n/ko-KR/share-app.ts b/web/i18n/ko-KR/share-app.ts index 1ee44f2816..3958b4f93e 100644 --- a/web/i18n/ko-KR/share-app.ts +++ b/web/i18n/ko-KR/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: '실행', executions: '{{num}} 처형', }, + login: { + backToHome: '홈으로 돌아가기', + }, } export default translation diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 54759154ca..856a64c868 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -220,12 +220,14 @@ const translation = { anyone: 'Każdy może uzyskać dostęp do aplikacji webowej', specific: 'Tylko określone grupy lub członkowie mogą uzyskać dostęp do aplikacji internetowej', organization: 'Każdy w organizacji ma dostęp do aplikacji internetowej.', + external: 'Tylko uwierzytelnieni zewnętrzni użytkownicy mogą uzyskać dostęp do aplikacji internetowej.', }, accessControlDialog: { accessItems: { anyone: 'Każdy z linkiem', specific: 'Specyficzne grupy lub członkowie', organization: 'Tylko członkowie w obrębie przedsiębiorstwa', + external: 'Uwierzytelnieni użytkownicy zewnętrzni', }, operateGroupAndMember: { searchPlaceholder: 'Szukaj grup i członków', diff --git a/web/i18n/pl-PL/share-app.ts b/web/i18n/pl-PL/share-app.ts index 80619cf4fc..617f66d994 100644 --- a/web/i18n/pl-PL/share-app.ts +++ b/web/i18n/pl-PL/share-app.ts @@ -78,6 +78,9 @@ const translation = { executions: '{{num}} EGZEKUCJI', execution: 'WYKONANIE', }, + login: { + backToHome: 'Powrót do strony głównej', + }, } export default translation diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 5dd1753cac..766053456a 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Qualquer pessoa pode acessar o aplicativo web', specific: 'Apenas grupos ou membros específicos podem acessar o aplicativo web', organization: 'Qualquer pessoa na organização pode acessar o aplicativo web', + external: 'Apenas usuários externos autenticados podem acessar o aplicativo Web.', }, accessControlDialog: { accessItems: { anyone: 'Qualquer pessoa com o link', specific: 'Grupos específicos ou membros', organization: 'Apenas membros dentro da empresa', + external: 'Usuários externos autenticados', }, operateGroupAndMember: { searchPlaceholder: 'Pesquisar grupos e membros', diff --git a/web/i18n/pt-BR/share-app.ts b/web/i18n/pt-BR/share-app.ts index d8bca03089..9a9d7db632 100644 --- a/web/i18n/pt-BR/share-app.ts +++ b/web/i18n/pt-BR/share-app.ts @@ -77,6 +77,9 @@ const translation = { executions: '{{num}} EXECUÇÕES', execution: 'EXECUÇÃO', }, + login: { + backToHome: 'Voltar para a página inicial', + }, } export default translation diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index adf82aa38e..cd267e7b66 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -213,12 +213,14 @@ const translation = { specific: 'Numai grupuri sau membri specifici pot accesa aplicația web.', organization: 'Oricine din organizație poate accesa aplicația web', anyone: 'Oricine poate accesa aplicația web', + external: 'Numai utilizatorii externi autentificați pot accesa aplicația web', }, accessControlDialog: { accessItems: { anyone: 'Oricine are linkul', specific: 'Grupuri sau membri specifici', organization: 'Numai membrii din cadrul întreprinderii', + external: 'Utilizatori extern autentificați', }, operateGroupAndMember: { searchPlaceholder: 'Caută grupuri și membri', diff --git a/web/i18n/ro-RO/share-app.ts b/web/i18n/ro-RO/share-app.ts index 2cb39a0485..41e38812c5 100644 --- a/web/i18n/ro-RO/share-app.ts +++ b/web/i18n/ro-RO/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'EXECUȚIE', executions: '{{num}} EXECUȚII', }, + login: { + backToHome: 'Înapoi la Acasă', + }, } export default translation diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index fa73e33197..428d4c4e57 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Любой может получить доступ к веб-приложению', specific: 'Только определенные группы или участники могут получить доступ к веб-приложению.', organization: 'Любой в организации может получить доступ к веб-приложению', + external: 'Только аутентифицированные внешние пользователи могут получить доступ к веб-приложению.', }, accessControlDialog: { accessItems: { anyone: 'Кто угодно с ссылкой', specific: 'Конкретные группы или члены', organization: 'Только члены внутри предприятия', + external: 'Аутентифицированные внешние пользователи', }, operateGroupAndMember: { searchPlaceholder: 'Искать группы и участников', diff --git a/web/i18n/ru-RU/share-app.ts b/web/i18n/ru-RU/share-app.ts index b2850fa276..dafbe9d6b1 100644 --- a/web/i18n/ru-RU/share-app.ts +++ b/web/i18n/ru-RU/share-app.ts @@ -77,6 +77,9 @@ const translation = { execution: 'ИСПОЛНЕНИЕ', executions: '{{num}} ВЫПОЛНЕНИЯ', }, + login: { + backToHome: 'Назад на главную', + }, } export default translation diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index 6241d40f30..4ac445872d 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Vsakdo lahko dostopa do spletne aplikacije', specific: 'Samo določenim skupinam ali članom je omogočen dostop do spletne aplikacije', organization: 'Vsakdo v organizaciji lahko dostopa do spletne aplikacije', + external: 'Samo avtentificirani zunanji uporabniki lahko dostopajo do spletne aplikacije.', }, accessControlDialog: { accessItems: { anyone: 'Kdorkoli s povezavo', specific: 'Specifične skupine ali člani', organization: 'Samo člani znotraj podjetja', + external: 'Avtorizirani zunanji uporabniki', }, operateGroupAndMember: { searchPlaceholder: 'Išči skupine in člane', diff --git a/web/i18n/sl-SI/share-app.ts b/web/i18n/sl-SI/share-app.ts index 28d62b2336..8b7fe87cbd 100644 --- a/web/i18n/sl-SI/share-app.ts +++ b/web/i18n/sl-SI/share-app.ts @@ -74,6 +74,9 @@ const translation = { execution: 'IZVEDBA', executions: '{{num}} IZVRŠITEV', }, + login: { + backToHome: 'Nazaj na začetno stran', + }, } export default translation diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 9204c71d32..0979d07f51 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -209,12 +209,14 @@ const translation = { anyone: 'ใครก็สามารถเข้าถึงเว็บแอปได้', specific: 'สมาชิกหรือกลุ่มเฉพาะเท่านั้นที่สามารถเข้าถึงแอปเว็บได้', organization: 'ใครก็ได้ในองค์กรสามารถเข้าถึงแอปเว็บได้', + external: 'ผู้ใช้งานภายนอกที่ได้รับการยืนยันตัวตนเท่านั้นที่สามารถเข้าถึงแอปพลิเคชันเว็บได้', }, accessControlDialog: { accessItems: { specific: 'กลุ่มหรือสมาชิกเฉพาะ', organization: 'เฉพาะสมาชิกภายในองค์กร', anyone: 'ใครก็ตามที่มีลิงก์', + external: 'ผู้ใช้ภายนอกที่ได้รับการตรวจสอบแล้ว', }, operateGroupAndMember: { searchPlaceholder: 'ค้นหากลุ่มและสมาชิก', diff --git a/web/i18n/th-TH/share-app.ts b/web/i18n/th-TH/share-app.ts index fd4a8f386c..eca049b9a2 100644 --- a/web/i18n/th-TH/share-app.ts +++ b/web/i18n/th-TH/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'การดำเนินการ', executions: '{{num}} การประหารชีวิต', }, + login: { + backToHome: 'กลับไปที่หน้าแรก', + }, } export default translation diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 995cc9c795..5e55ffa349 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -209,12 +209,14 @@ const translation = { anyone: 'Herkes web uygulamasına erişebilir', organization: 'Kuruluşta herkes web uygulamasına erişebilir.', specific: 'Sadece belirli gruplar veya üyeler web uygulamasına erişebilir.', + external: 'Sadece kimliği doğrulanmış dış kullanıcılar Web uygulamasına erişebilir', }, accessControlDialog: { accessItems: { anyone: 'Bağlantıya sahip olan herkes', organization: 'Sadece işletme içindeki üyeler', specific: 'Belirli gruplar veya üyeler', + external: 'Kimliği onaylanmış harici kullanıcılar', }, operateGroupAndMember: { searchPlaceholder: 'Grupları ve üyeleri ara', diff --git a/web/i18n/tr-TR/share-app.ts b/web/i18n/tr-TR/share-app.ts index 184f44e147..e7ad4fcd68 100644 --- a/web/i18n/tr-TR/share-app.ts +++ b/web/i18n/tr-TR/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'İFRAZAT', executions: '{{num}} İDAM', }, + login: { + backToHome: 'Ana Sayfaya Dön', + }, } export default translation diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 4bbb0dcbf1..6e5ff5dc74 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Будь-хто може отримати доступ до веб-додатку', specific: 'Тільки окремі групи або члени можуть отримати доступ до веб-додатку.', organization: 'Будь-хто в організації може отримати доступ до веб-додатку.', + external: 'Тільки перевірені зовнішні користувачі можуть отримати доступ до веб-застосунку.', }, accessControlDialog: { accessItems: { anyone: 'Кожен, у кого є посилання', specific: 'Конкретні групи або члени', organization: 'Тільки члени підприємства', + external: 'Аутентифіковані зовнішні користувачі', }, operateGroupAndMember: { searchPlaceholder: 'Шукати групи та учасників', diff --git a/web/i18n/uk-UA/share-app.ts b/web/i18n/uk-UA/share-app.ts index 058925ff15..92f25545d9 100644 --- a/web/i18n/uk-UA/share-app.ts +++ b/web/i18n/uk-UA/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: 'ВИКОНАННЯ', executions: '{{num}} ВИКОНАНЬ', }, + login: { + backToHome: 'Повернутися на головну', + }, } export default translation diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 243454d011..c5f1a7496d 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -213,12 +213,14 @@ const translation = { anyone: 'Mọi người đều có thể truy cập ứng dụng web.', specific: 'Chỉ những nhóm hoặc thành viên cụ thể mới có thể truy cập ứng dụng web.', organization: 'Bất kỳ ai trong tổ chức đều có thể truy cập ứng dụng web.', + external: 'Chỉ những người dùng bên ngoài đã xác thực mới có thể truy cập vào ứng dụng Web.', }, accessControlDialog: { accessItems: { anyone: 'Ai có liên kết', specific: 'Các nhóm hoặc thành viên cụ thể', organization: 'Chỉ các thành viên trong doanh nghiệp', + external: 'Người dùng bên ngoài được xác thực', }, operateGroupAndMember: { searchPlaceholder: 'Tìm kiếm nhóm và thành viên', diff --git a/web/i18n/vi-VN/share-app.ts b/web/i18n/vi-VN/share-app.ts index a55f9b8476..12a31bd40b 100644 --- a/web/i18n/vi-VN/share-app.ts +++ b/web/i18n/vi-VN/share-app.ts @@ -73,6 +73,9 @@ const translation = { executions: '{{num}} ÁN TỬ HÌNH', execution: 'THI HÀNH', }, + login: { + backToHome: 'Trở về Trang Chủ', + }, } export default translation diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index c393fc949e..c43b2ee308 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -212,12 +212,14 @@ const translation = { anyone: '任何人都可以訪問這個網絡應用程式', specific: '只有特定的群體或成員可以訪問這個網絡應用程序', organization: '組織中的任何人都可以訪問該網絡應用程序', + external: '只有經過身份驗證的外部用戶才能訪問該網絡應用程序', }, accessControlDialog: { accessItems: { anyone: '擁有鏈接的人', specific: '特定群體或成員', organization: '只有企業內部成員', + external: '經過驗證的外部用戶', }, operateGroupAndMember: { searchPlaceholder: '搜尋群組和成員', diff --git a/web/i18n/zh-Hant/share-app.ts b/web/i18n/zh-Hant/share-app.ts index 54d2ff98b6..e25aa0c0de 100644 --- a/web/i18n/zh-Hant/share-app.ts +++ b/web/i18n/zh-Hant/share-app.ts @@ -73,6 +73,9 @@ const translation = { execution: '執行', executions: '{{num}} 執行', }, + login: { + backToHome: '返回首頁', + }, } export default translation From 837f769960932b67bf1157bdbe1e0c682ea5e177 Mon Sep 17 00:00:00 2001 From: minglu7 <1347866672@qq.com> Date: Thu, 5 Jun 2025 14:33:24 +0800 Subject: [PATCH 85/85] fix: update text_to_audio method to send data as JSON (#20663) --- sdks/python-client/dify_client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/python-client/dify_client/client.py b/sdks/python-client/dify_client/client.py index ee1b5c57e1..d885dc6fb7 100644 --- a/sdks/python-client/dify_client/client.py +++ b/sdks/python-client/dify_client/client.py @@ -47,7 +47,7 @@ class DifyClient: def text_to_audio(self, text: str, user: str, streaming: bool = False): data = {"text": text, "user": user, "streaming": streaming} - return self._send_request("POST", "/text-to-audio", data=data) + return self._send_request("POST", "/text-to-audio", json=data) def get_meta(self, user): params = {"user": user}