From 23cedc3f1ccdda4fef42eb763a51dbde7c2ff444 Mon Sep 17 00:00:00 2001 From: NFish Date: Sun, 25 Aug 2024 18:47:16 +0800 Subject: [PATCH] Web app now supports SSO config (#7137) --- .../app/(appDetailLayout)/[appId]/layout.tsx | 13 ++++++-- .../[appId]/overview/cardView.tsx | 32 ++++++++++++++++--- web/app/components/app/overview/appCard.tsx | 3 +- .../app/overview/settings/index.tsx | 25 +++++++++++++-- web/app/components/app/store.ts | 6 ++-- web/context/app-context.tsx | 11 ++++++- web/i18n/en-US/app-overview.ts | 6 ++++ web/i18n/zh-Hans/app-overview.ts | 6 ++++ web/models/app.ts | 3 +- web/service/apps.ts | 9 +++++- web/types/app.ts | 4 +++ web/types/feature.ts | 2 ++ 12 files changed, 102 insertions(+), 18 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx index 09569df8bf..84ec157323 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +++ b/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 = (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 = (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) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx index 5fa9a2e406..3584e13733 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx +++ b/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 = ({ 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 = ({ appId }) => { if (!err) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') + if (systemFeatures.enable_web_sso_switch_component) { + const [sso_err] = await asyncRunSafe( + updateAppSSO({ id: appId, enabled: params.enable_sso }) as Promise, + ) + if (sso_err) { + handleCallbackResult(sso_err) + return + } + } + handleCallbackResult(err) } diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index ea0b793857..a7564120a2 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/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 cardType?: 'api' | 'webapp' customBgColor?: string onChangeStatus: (val: boolean) => Promise diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 8da9b9864f..a67093183a 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/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 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ } + {systemFeatures.enable_web_sso_switch_component &&
+

{t(`${prefixSettings}.sso.label`)}

+
+
{t(`${prefixSettings}.sso.title`)}
+ {t(`${prefixSettings}.sso.tooltip`)}
}> + setInputInfo({ ...inputInfo, enable_sso: v })}> + +
+

{t(`${prefixSettings}.sso.description`)}

+ } {!isShowMore &&
setIsShowMore(true)}>
{t(`${prefixSettings}.more.entry`)}
diff --git a/web/app/components/app/store.ts b/web/app/components/app/store.ts index a89b96d65d..0209102372 100644 --- a/web/app/components/app/store.ts +++ b/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 appSidebarExpand: string currentLogItem?: IChatItem currentLogModalActiveTab: string @@ -13,7 +13,7 @@ type State = { } type Action = { - setAppDetail: (appDetail?: App) => void + setAppDetail: (appDetail?: App & Partial) => void setAppSiderbarExpand: (state: string) => void setCurrentLogItem: (item?: IChatItem) => void setCurrentLogModalActiveTab: (tab: string) => void diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index db28d28518..78ac1c9848 100644 --- a/web/context/app-context.tsx +++ b/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({ theme: Theme.light, + systemFeatures: defaultSystemFeatures, setTheme: () => { }, apps: [], mutateApps: () => { }, @@ -90,6 +94,10 @@ export const AppContextProvider: FC = ({ 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() const [langeniusVersionInfo, setLangeniusVersionInfo] = useState(initialLangeniusVersionInfo) const [currentWorkspace, setCurrentWorkspace] = useState(initialWorkspaceInfo) @@ -136,6 +144,7 @@ export const AppContextProvider: FC = ({ children }) => theme, setTheme: handleSetTheme, apps: appList.data, + systemFeatures, mutateApps, userProfile, mutateUserProfile, diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index 5bf0b44563..d5f619887d 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/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', diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index 556dfe539e..7b4f61fc13 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/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: '版权', diff --git a/web/models/app.ts b/web/models/app.ts index 80d121c7a3..c0fc532a37 100644 --- a/web/models/app.ts +++ b/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[] diff --git a/web/service/apps.ts b/web/service/apps.ts index 7049af82cf..6beba7bc61 100644 --- a/web/service/apps.ts +++ b/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(`${url}/${id}`) } +export const fetchAppSSO = async ({ appId }: { appId: string }) => { + return get(`/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 = ({ url }) => { return get(url) } diff --git a/web/types/app.ts b/web/types/app.ts index ed3c24234d..fb8a407dd2 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -348,6 +348,10 @@ export type App = { tags: Tag[] } +export type AppSSO = { + enable_sso: boolean +} + /** * App Template */ diff --git a/web/types/feature.ts b/web/types/feature.ts index 89af9d21ab..b129da3aee 100644 --- a/web/types/feature.ts +++ b/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, }