Kaynağa Gözat

Web app now supports SSO config (#7137)

NFish 8 ay önce
ebeveyn
işleme
23cedc3f1c

+ 10 - 3
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx

@@ -15,13 +15,14 @@ import {
 } from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
 import { useShallow } from 'zustand/react/shallow'
+import { useContextSelector } from 'use-context-selector'
 import s from './style.module.css'
 import cn from '@/utils/classnames'
 import { useStore } from '@/app/components/app/store'
 import AppSideBar from '@/app/components/app-sidebar'
 import type { NavIcon } from '@/app/components/app-sidebar/navLink'
-import { fetchAppDetail } from '@/service/apps'
-import { useAppContext } from '@/context/app-context'
+import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
+import AppContext, { useAppContext } from '@/context/app-context'
 import Loading from '@/app/components/base/loading'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 
@@ -52,6 +53,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
     icon: NavIcon
     selectedIcon: NavIcon
   }>>([])
+  const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
 
   const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
     const navs = [
@@ -114,8 +116,13 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
         router.replace(`/app/${appId}/configuration`)
       }
       else {
-        setAppDetail(res)
+        setAppDetail({ ...res, enable_sso: false })
         setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
+        if (systemFeatures.enable_web_sso_switch_component) {
+          fetchAppSSO({ appId }).then((ssoRes) => {
+            setAppDetail({ ...res, enable_sso: ssoRes.enabled })
+          })
+        }
       }
     }).catch((e: any) => {
       if (e.status === 404)

+ 27 - 5
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx

@@ -2,22 +2,25 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import { useContext } from 'use-context-selector'
+import { useContext, useContextSelector } from 'use-context-selector'
 import AppCard from '@/app/components/app/overview/appCard'
 import Loading from '@/app/components/base/loading'
 import { ToastContext } from '@/app/components/base/toast'
 import {
   fetchAppDetail,
+  fetchAppSSO,
+  updateAppSSO,
   updateAppSiteAccessToken,
   updateAppSiteConfig,
   updateAppSiteStatus,
 } from '@/service/apps'
-import type { App } from '@/types/app'
+import type { App, AppSSO } from '@/types/app'
 import type { UpdateAppSiteCodeResponse } from '@/models/app'
 import { asyncRunSafe } from '@/utils'
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import type { IAppCardProps } from '@/app/components/app/overview/appCard'
 import { useStore as useAppStore } from '@/app/components/app/store'
+import AppContext from '@/context/app-context'
 
 export type ICardViewProps = {
   appId: string
@@ -28,11 +31,20 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
   const { notify } = useContext(ToastContext)
   const appDetail = useAppStore(state => state.appDetail)
   const setAppDetail = useAppStore(state => state.setAppDetail)
+  const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
 
   const updateAppDetail = async () => {
-    fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
-      setAppDetail(res)
-    })
+    try {
+      const res = await fetchAppDetail({ url: '/apps', id: appId })
+      if (systemFeatures.enable_web_sso_switch_component) {
+        const ssoRes = await fetchAppSSO({ appId })
+        setAppDetail({ ...res, enable_sso: ssoRes.enabled })
+      }
+      else {
+        setAppDetail({ ...res })
+      }
+    }
+    catch (error) { console.error(error) }
   }
 
   const handleCallbackResult = (err: Error | null, message?: string) => {
@@ -81,6 +93,16 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
     if (!err)
       localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
 
+    if (systemFeatures.enable_web_sso_switch_component) {
+      const [sso_err] = await asyncRunSafe<AppSSO>(
+        updateAppSSO({ id: appId, enabled: params.enable_sso }) as Promise<AppSSO>,
+      )
+      if (sso_err) {
+        handleCallbackResult(sso_err)
+        return
+      }
+    }
+
     handleCallbackResult(err)
   }
 

+ 2 - 1
web/app/components/app/overview/appCard.tsx

@@ -27,10 +27,11 @@ import ShareQRCode from '@/app/components/base/qrcode'
 import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
 import type { AppDetailResponse } from '@/models/app'
 import { useAppContext } from '@/context/app-context'
+import type { AppSSO } from '@/types/app'
 
 export type IAppCardProps = {
   className?: string
-  appInfo: AppDetailResponse
+  appInfo: AppDetailResponse & Partial<AppSSO>
   cardType?: 'api' | 'webapp'
   customBgColor?: string
   onChangeStatus: (val: boolean) => Promise<void>

+ 22 - 3
web/app/components/app/overview/settings/index.tsx

@@ -4,21 +4,25 @@ import React, { useEffect, useState } from 'react'
 import { ChevronRightIcon } from '@heroicons/react/20/solid'
 import Link from 'next/link'
 import { Trans, useTranslation } from 'react-i18next'
+import { useContextSelector } from 'use-context-selector'
 import s from './style.module.css'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
 import AppIcon from '@/app/components/base/app-icon'
+import Switch from '@/app/components/base/switch'
 import { SimpleSelect } from '@/app/components/base/select'
 import type { AppDetailResponse } from '@/models/app'
-import type { AppIconType, Language } from '@/types/app'
+import type { AppIconType, AppSSO, Language } from '@/types/app'
 import { useToastContext } from '@/app/components/base/toast'
 import { languages } from '@/i18n/language'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+import AppContext from '@/context/app-context'
 import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
 import AppIconPicker from '@/app/components/base/app-icon-picker'
 
 export type ISettingsModalProps = {
   isChat: boolean
-  appInfo: AppDetailResponse
+  appInfo: AppDetailResponse & Partial<AppSSO>
   isShow: boolean
   defaultValue?: string
   onClose: () => void
@@ -39,6 +43,7 @@ export type ConfigParams = {
   icon: string
   icon_background?: string
   show_workflow_steps: boolean
+  enable_sso?: boolean
 }
 
 const prefixSettings = 'appOverview.overview.appInfo.settings'
@@ -50,6 +55,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
   onClose,
   onSave,
 }) => {
+  const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
   const { notify } = useToastContext()
   const [isShowMore, setIsShowMore] = useState(false)
   const {
@@ -76,6 +82,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
     privacyPolicy: privacy_policy,
     customDisclaimer: custom_disclaimer,
     show_workflow_steps,
+    enable_sso: appInfo.enable_sso,
   })
   const [language, setLanguage] = useState(default_language)
   const [saveLoading, setSaveLoading] = useState(false)
@@ -98,6 +105,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       privacyPolicy: privacy_policy,
       customDisclaimer: custom_disclaimer,
       show_workflow_steps,
+      enable_sso: appInfo.enable_sso,
     })
     setLanguage(default_language)
     setAppIcon(icon_type === 'image'
@@ -149,6 +157,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
       icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
       show_workflow_steps: inputInfo.show_workflow_steps,
+      enable_sso: inputInfo.enable_sso,
     }
     await onSave?.(params)
     setSaveLoading(false)
@@ -219,9 +228,19 @@ const SettingsModal: FC<ISettingsModalProps> = ({
           <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
             value={inputInfo.chatColorTheme ?? ''}
             onChange={onChange('chatColorTheme')}
-            placeholder= 'E.g #A020F0'
+            placeholder='E.g #A020F0'
           />
         </>}
+        {systemFeatures.enable_web_sso_switch_component && <div className='w-full mt-8'>
+          <p className='system-xs-medium text-gray-500'>{t(`${prefixSettings}.sso.label`)}</p>
+          <div className='flex justify-between items-center'>
+            <div className='font-medium system-sm-semibold flex-grow text-gray-900'>{t(`${prefixSettings}.sso.title`)}</div>
+            <TooltipPlus disabled={systemFeatures.sso_enforced_for_web} popupContent={<div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>}>
+              <Switch disabled={!systemFeatures.sso_enforced_for_web} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
+            </TooltipPlus>
+          </div>
+          <p className='body-xs-regular text-gray-500'>{t(`${prefixSettings}.sso.description`)}</p>
+        </div>}
         {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
           <div className='flex justify-between'>
             <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>

+ 3 - 3
web/app/components/app/store.ts

@@ -1,9 +1,9 @@
 import { create } from 'zustand'
-import type { App } from '@/types/app'
+import type { App, AppSSO } from '@/types/app'
 import type { IChatItem } from '@/app/components/base/chat/chat/type'
 
 type State = {
-  appDetail?: App
+  appDetail?: App & Partial<AppSSO>
   appSidebarExpand: string
   currentLogItem?: IChatItem
   currentLogModalActiveTab: string
@@ -13,7 +13,7 @@ type State = {
 }
 
 type Action = {
-  setAppDetail: (appDetail?: App) => void
+  setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
   setAppSiderbarExpand: (state: string) => void
   setCurrentLogItem: (item?: IChatItem) => void
   setCurrentLogModalActiveTab: (tab: string) => void

+ 10 - 1
web/context/app-context.tsx

@@ -6,16 +6,19 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
 import type { FC, ReactNode } from 'react'
 import { fetchAppList } from '@/service/apps'
 import Loading from '@/app/components/base/loading'
-import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
+import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common'
 import type { App } from '@/types/app'
 import { Theme } from '@/types/app'
 import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
 import MaintenanceNotice from '@/app/components/header/maintenance-notice'
+import type { SystemFeatures } from '@/types/feature'
+import { defaultSystemFeatures } from '@/types/feature'
 
 export type AppContextValue = {
   theme: Theme
   setTheme: (theme: Theme) => void
   apps: App[]
+  systemFeatures: SystemFeatures
   mutateApps: VoidFunction
   userProfile: UserProfileResponse
   mutateUserProfile: VoidFunction
@@ -53,6 +56,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
 
 const AppContext = createContext<AppContextValue>({
   theme: Theme.light,
+  systemFeatures: defaultSystemFeatures,
   setTheme: () => { },
   apps: [],
   mutateApps: () => { },
@@ -90,6 +94,10 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
   const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
   const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
 
+  const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, {
+    fallbackData: defaultSystemFeatures,
+  })
+
   const [userProfile, setUserProfile] = useState<UserProfileResponse>()
   const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
   const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
@@ -136,6 +144,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
       theme,
       setTheme: handleSetTheme,
       apps: appList.data,
+      systemFeatures,
       mutateApps,
       userProfile,
       mutateUserProfile,

+ 6 - 0
web/i18n/en-US/app-overview.ts

@@ -53,6 +53,12 @@ const translation = {
         chatColorThemeDesc: 'Set the color theme of the chatbot',
         chatColorThemeInverted: 'Inverted',
         invalidHexMessage: 'Invalid hex value',
+        sso: {
+          label: 'SSO Authentication',
+          title: 'WebApp SSO',
+          description: 'All users are required to login with SSO before using WebApp',
+          tooltip: 'Contact the administrator to enable WebApp SSO',
+        },
         more: {
           entry: 'Show more settings',
           copyright: 'Copyright',

+ 6 - 0
web/i18n/zh-Hans/app-overview.ts

@@ -53,6 +53,12 @@ const translation = {
         chatColorThemeDesc: '设置聊天机器人的颜色主题',
         chatColorThemeInverted: '反转',
         invalidHexMessage: '无效的十六进制值',
+        sso: {
+          label: '单点登录认证',
+          title: 'WebApp SSO 认证',
+          description: '启用后,所有用户都需要先进行 SSO 认证才能访问',
+          tooltip: '联系管理员以开启 WebApp SSO 认证',
+        },
         more: {
           entry: '展示更多设置',
           copyright: '版权',

+ 2 - 1
web/models/app.ts

@@ -1,5 +1,5 @@
 import type { LangFuseConfig, LangSmithConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
-import type { App, AppTemplate, SiteConfig } from '@/types/app'
+import type { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app'
 
 /* export type App = {
   id: string
@@ -67,6 +67,7 @@ export type AppListResponse = {
 }
 
 export type AppDetailResponse = App
+export type AppSSOResponse = { enabled: AppSSO['enable_sso'] }
 
 export type AppTemplatesResponse = {
   data: AppTemplate[]

+ 8 - 1
web/service/apps.ts

@@ -1,6 +1,6 @@
 import type { Fetcher } from 'swr'
 import { del, get, patch, post, put } from './base'
-import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
+import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
 import type { CommonResponse } from '@/models/common'
 import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
 import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
@@ -13,6 +13,13 @@ export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => {
   return get<AppDetailResponse>(`${url}/${id}`)
 }
 
+export const fetchAppSSO = async ({ appId }: { appId: string }) => {
+  return get<AppSSOResponse>(`/enterprise/app-setting/sso?appID=${appId}`)
+}
+export const updateAppSSO = async ({ id, enabled }: { id: string; enabled: boolean }) => {
+  return post('/enterprise/app-setting/sso', { body: { app_id: id, enabled } })
+}
+
 export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
   return get<AppTemplatesResponse>(url)
 }

+ 4 - 0
web/types/app.ts

@@ -348,6 +348,10 @@ export type App = {
   tags: Tag[]
 }
 
+export type AppSSO = {
+  enable_sso: boolean
+}
+
 /**
  * App Template
  */

+ 2 - 0
web/types/feature.ts

@@ -3,6 +3,7 @@ export type SystemFeatures = {
   sso_enforced_for_signin_protocol: string
   sso_enforced_for_web: boolean
   sso_enforced_for_web_protocol: string
+  enable_web_sso_switch_component: boolean
 }
 
 export const defaultSystemFeatures: SystemFeatures = {
@@ -10,4 +11,5 @@ export const defaultSystemFeatures: SystemFeatures = {
   sso_enforced_for_signin_protocol: '',
   sso_enforced_for_web: false,
   sso_enforced_for_web_protocol: '',
+  enable_web_sso_switch_component: false,
 }