diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx
index 42967b96f..31b9ed87c 100644
--- a/web/app/(commonLayout)/apps/AppCard.tsx
+++ b/web/app/(commonLayout)/apps/AppCard.tsx
@@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react'
+import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm'
@@ -338,7 +338,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx
index 83adbd3ca..8db336a17 100644
--- a/web/app/(shareLayout)/layout.tsx
+++ b/web/app/(shareLayout)/layout.tsx
@@ -1,14 +1,42 @@
-import React from 'react'
+'use client'
+import React, { useEffect, useState } from 'react'
import type { FC } from 'react'
-import type { Metadata } from 'next'
-
-export const metadata: Metadata = {
- icons: 'data:,', // prevent browser from using default favicon
-}
+import { usePathname, useSearchParams } from 'next/navigation'
+import Loading from '../components/base/loading'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+import { AccessMode } from '@/models/access-control'
+import { getAppAccessModeByAppCode } from '@/service/share'
const Layout: FC<{
children: React.ReactNode
}> = ({ children }) => {
+ const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
+ const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode)
+ const pathname = usePathname()
+ const searchParams = useSearchParams()
+ const redirectUrl = searchParams.get('redirect_url')
+ const [isLoading, setIsLoading] = useState(true)
+ useEffect(() => {
+ (async () => {
+ let appCode: string | null = null
+ if (redirectUrl)
+ appCode = redirectUrl?.split('/').pop() || null
+ else
+ appCode = pathname.split('/').pop() || null
+
+ if (!appCode)
+ return
+ setIsLoading(true)
+ const ret = await getAppAccessModeByAppCode(appCode)
+ setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC)
+ setIsLoading(false)
+ })()
+ }, [pathname, redirectUrl, setWebAppAccessMode])
+ if (isLoading || isGlobalPending) {
+ return
+
+
+ }
return (
{children}
diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
new file mode 100644
index 000000000..da754794b
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
@@ -0,0 +1,96 @@
+'use client'
+import { RiArrowLeftLine, RiMailSendFill } 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 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 { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+
+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 verify = async () => {
+ try {
+ 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
+ }
+ setIsLoading(true)
+ const ret = await verifyWebAppResetPasswordCode({ email, code, token })
+ if (ret.is_valid) {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(ret.token))
+ router.push(`/webapp-reset-password/set-password?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ const resendCode = async () => {
+ try {
+ const res = await sendWebAppResetPasswordCode(email, locale)
+ if (res.result === 'success') {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(res.data))
+ router.replace(`/webapp-reset-password/check-code?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ }
+
+ return
+
+
+
+
+
{t('login.checkCode.checkYourEmail')}
+
+
+
+ {t('login.checkCode.validTime')}
+
+
+
+
+
+
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 000000000..e0ac6b9ad
--- /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 <>
+
+
+
+
+ {!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 000000000..96cd4c580
--- /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')}
+
+
+
+
+
+
+
+
+
+
{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 000000000..9f9a8ad4e
--- /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 000000000..1b8f18c98
--- /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')}
+
+
+
+
+
+
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 000000000..e9b15ae33
--- /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 000000000..29af3e3a5
--- /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 (
+ )
+}
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 000000000..d9e56af1b
--- /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
+}
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 000000000..5d649322b
--- /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 000000000..a03364d32
--- /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 <>
+
+
+ {/*
*/}
+
+ {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 000000000..d6bdf607b
--- /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 668c3f312..c12fde38d 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
- }
+ useEffect(() => {
+ (async () => {
+ if (message)
+ 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
+ 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
}
- case SSOProtocol.OIDC: {
- const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
- router.push(oidcRes.url)
- break
+ 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)
}
- 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])
+ })()
+ }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message])
useEffect(() => {
- const init = async () => {
- if (message) {
- showErrorToast(message)
- return
- }
+ if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl)
+ router.replace(redirectUrl)
+ }, [webAppAccessMode, router, redirectUrl])
- if (!tokenFromUrl) {
- await handleSSOLogin()
- return
- }
-
- await processTokenAndRedirect()
- }
-
- 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')}
}
- else {
+ if (!redirectUrl) {
+ showErrorToast('redirect url is invalid.')
+ return
+ }
+ if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) {
+ return
+
+
+ }
+ 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 2f15c8ec4..13faaea95 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 f4872f8c9..b30c8f1ba 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 8d0028c7d..5825bb72e 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 9b283cdf5..9f3b3ac4a 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 4e835cbfc..928c85026 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 dd7cb14b2..32f74e645 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 de023e7f5..1fd138319 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 fd317ccf9..4e50c1cb7 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 5964efd80..d24265ed9 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 40c56eca7..0158e8d04 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 19b660b08..adb926c7c 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 9ce891a50..d793d48b4 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 0753d1f98..73dfb8820 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 5aa5e7a30..26ad84be6 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 88239ffbd..a8d3d56cf 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 10275a196..2c848a1f5 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 20a80ba4c..ccfe23ead 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 bf99005d7..ab589ffb7 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 b4fc8d4d8..b501bc129 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 9e76f6518..20dad7fae 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 bdd7d98d9..4ec1e6505 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 4ea2ad6f4..ce1270dae 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 8ad9cc649..911662b5c 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 4b0873628..c3cafe600 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 e76cfb419..700cd4bf5 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 7fb156218..6a2a7e5b1 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 000000000..b8f96f6cc
--- /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,
+ })
+}