diff --git a/frontend/src/hooks/useSettingsData.ts b/frontend/src/hooks/useSettingsData.ts index a376244..12512d3 100644 --- a/frontend/src/hooks/useSettingsData.ts +++ b/frontend/src/hooks/useSettingsData.ts @@ -7,6 +7,8 @@ import { useToast } from '@/contexts/ToastContext'; interface RoutingConfig { enableGlobalRoute: boolean; enableGroupNameRoute: boolean; + enableBearerAuth: boolean; + bearerAuthKey: string; } interface InstallConfig { @@ -21,6 +23,10 @@ interface SystemSettings { }; } +interface TempRoutingConfig { + bearerAuthKey: string; +} + export const useSettingsData = () => { const { t } = useTranslation(); const { showToast } = useToast(); @@ -28,7 +34,14 @@ export const useSettingsData = () => { const [routingConfig, setRoutingConfig] = useState({ enableGlobalRoute: true, enableGroupNameRoute: true, + enableBearerAuth: false, + bearerAuthKey: '', }); + + const [tempRoutingConfig, setTempRoutingConfig] = useState({ + bearerAuthKey: '', + }); + const [installConfig, setInstallConfig] = useState({ pythonIndexUrl: '', npmRegistry: '', @@ -66,6 +79,8 @@ export const useSettingsData = () => { setRoutingConfig({ enableGlobalRoute: data.data.systemConfig.routing.enableGlobalRoute ?? true, enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true, + enableBearerAuth: data.data.systemConfig.routing.enableBearerAuth ?? false, + bearerAuthKey: data.data.systemConfig.routing.bearerAuthKey || '', }); } if (data.success && data.data?.systemConfig?.install) { @@ -84,7 +99,10 @@ export const useSettingsData = () => { }, [t, showToast]); // Update routing configuration - const updateRoutingConfig = async (key: keyof RoutingConfig, value: boolean) => { + const updateRoutingConfig = async ( + key: T, + value: RoutingConfig[T], + ) => { setLoading(true); setError(null); @@ -117,13 +135,13 @@ export const useSettingsData = () => { showToast(t('settings.systemConfigUpdated')); return true; } else { - showToast(t('errors.failedToUpdateSystemConfig')); + showToast(t('errors.failedToUpdateRouteConfig')); return false; } } catch (error) { - console.error('Failed to update system config:', error); - setError(error instanceof Error ? error.message : 'Failed to update system config'); - showToast(t('errors.failedToUpdateSystemConfig')); + console.error('Failed to update routing config:', error); + setError(error instanceof Error ? error.message : 'Failed to update routing config'); + showToast(t('errors.failedToUpdateRouteConfig')); return false; } finally { setLoading(false); @@ -182,8 +200,18 @@ export const useSettingsData = () => { fetchSettings(); }, [fetchSettings, refreshKey]); + useEffect(() => { + if (routingConfig) { + setTempRoutingConfig({ + bearerAuthKey: routingConfig.bearerAuthKey, + }); + } + }, [routingConfig]); + return { routingConfig, + tempRoutingConfig, + setTempRoutingConfig, installConfig, loading, error, diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index b4cb2ec..443aacf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -148,7 +148,7 @@ "account": "Account Settings", "password": "Change Password", "appearance": "Appearance", - "routeConfig": "Route Configuration", + "routeConfig": "Security Configuration", "installConfig": "Installation Configuration" }, "market": { @@ -244,6 +244,11 @@ "enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID", "enableGroupNameRoute": "Enable Group Name Route", "enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs", + "enableBearerAuth": "Enable Bearer Authentication", + "enableBearerAuthDescription": "Require bearer token authentication for MCP requests", + "bearerAuthKey": "Bearer Authentication Key", + "bearerAuthKeyDescription": "The authentication key that will be required in the Bearer token", + "bearerAuthKeyPlaceholder": "Enter bearer authentication key", "pythonIndexUrl": "Python Package Repository URL", "pythonIndexUrlDescription": "Set UV_DEFAULT_INDEX environment variable for Python package installation", "pythonIndexUrlPlaceholder": "e.g. https://pypi.org/simple", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 98a8bc8..ce7e081 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -146,7 +146,7 @@ "account": "账户设置", "password": "修改密码", "appearance": "外观", - "routeConfig": "路由配置", + "routeConfig": "安全配置", "installConfig": "安装配置" }, "groups": { @@ -245,6 +245,11 @@ "enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点", "enableGroupNameRoute": "启用组名路由", "enableGroupNameRouteDescription": "允许使用组名而不仅仅是组 ID 连接到 /sse 端点", + "enableBearerAuth": "启用 Bearer 认证", + "enableBearerAuthDescription": "对 MCP 请求启用 Bearer 令牌认证", + "bearerAuthKey": "Bearer 认证密钥", + "bearerAuthKeyDescription": "Bearer 令牌中需要携带的认证密钥", + "bearerAuthKeyPlaceholder": "请输入 Bearer 认证密钥", "pythonIndexUrl": "Python 包仓库地址", "pythonIndexUrlDescription": "设置 UV_DEFAULT_INDEX 环境变量,用于 Python 包安装", "pythonIndexUrlPlaceholder": "例如: https://mirrors.aliyun.com/pypi/simple", diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index d4df296..86187dc 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -5,6 +5,7 @@ import ChangePasswordForm from '@/components/ChangePasswordForm'; import { Switch } from '@/components/ui/ToggleGroup'; import { useSettingsData } from '@/hooks/useSettingsData'; import { useToast } from '@/contexts/ToastContext'; +import { generateRandomKey } from '@/utils/key'; const SettingsPage: React.FC = () => { const { t, i18n } = useTranslation(); @@ -16,6 +17,7 @@ const SettingsPage: React.FC = () => { useEffect(() => { setCurrentLanguage(i18n.language); }, [i18n.language]); + const [installConfig, setInstallConfig] = useState<{ pythonIndexUrl: string; npmRegistry: string; @@ -26,6 +28,8 @@ const SettingsPage: React.FC = () => { const { routingConfig, + tempRoutingConfig, + setTempRoutingConfig, installConfig: savedInstallConfig, loading, updateRoutingConfig, @@ -52,9 +56,30 @@ const SettingsPage: React.FC = () => { })); }; - const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute', value: boolean) => { + const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey', value: boolean | string) => { await updateRoutingConfig(key, value); + + // If enableBearerAuth is turned on and there's no key, generate one + if (key === 'enableBearerAuth' && value === true) { + if (!tempRoutingConfig.bearerAuthKey) { + const newKey = generateRandomKey(); + handleBearerAuthKeyChange(newKey); + await updateRoutingConfig('bearerAuthKey', newKey); + } + } }; + + const handleBearerAuthKeyChange = (value: string) => { + setTempRoutingConfig(prev => ({ + ...prev, + bearerAuthKey: value + })); + }; + + const saveBearerAuthKey = async () => { + await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey); + }; + const handleInstallConfigChange = (key: 'pythonIndexUrl' | 'npmRegistry', value: string) => { setInstallConfig({ ...installConfig, @@ -122,6 +147,44 @@ const SettingsPage: React.FC = () => { {sectionsVisible.routingConfig && (
+
+
+

{t('settings.enableBearerAuth')}

+

{t('settings.enableBearerAuthDescription')}

+
+ handleRoutingConfigChange('enableBearerAuth', checked)} + /> +
+ + {routingConfig.enableBearerAuth && ( +
+
+

{t('settings.bearerAuthKey')}

+

{t('settings.bearerAuthKeyDescription')}

+
+
+ handleBearerAuthKeyChange(e.target.value)} + placeholder={t('settings.bearerAuthKeyPlaceholder')} + className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + disabled={loading || !routingConfig.enableBearerAuth} + /> + +
+
+ )} +

{t('settings.enableGlobalRoute')}

@@ -145,6 +208,7 @@ const SettingsPage: React.FC = () => { onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)} />
+
)}
diff --git a/frontend/src/utils/key.ts b/frontend/src/utils/key.ts new file mode 100644 index 0000000..4284863 --- /dev/null +++ b/frontend/src/utils/key.ts @@ -0,0 +1,8 @@ +export function generateRandomKey(length: number = 32): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array) + .map((x) => characters.charAt(x % characters.length)) + .join(''); +} diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 6eea77a..33fc9a7 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -288,7 +288,9 @@ export const updateSystemConfig = (req: Request, res: Response): void => { if ( (!routing || (typeof routing.enableGlobalRoute !== 'boolean' && - typeof routing.enableGroupNameRoute !== 'boolean')) && + typeof routing.enableGroupNameRoute !== 'boolean' && + typeof routing.enableBearerAuth !== 'boolean' && + typeof routing.bearerAuthKey !== 'string')) && (!install || (typeof install.pythonIndexUrl !== 'string' && typeof install.npmRegistry !== 'string')) ) { @@ -305,9 +307,12 @@ export const updateSystemConfig = (req: Request, res: Response): void => { routing: { enableGlobalRoute: true, enableGroupNameRoute: true, + enableBearerAuth: false, + bearerAuthKey: '', }, install: { pythonIndexUrl: '', + npmRegistry: '', }, }; } @@ -316,6 +321,8 @@ export const updateSystemConfig = (req: Request, res: Response): void => { settings.systemConfig.routing = { enableGlobalRoute: true, enableGroupNameRoute: true, + enableBearerAuth: false, + bearerAuthKey: '', }; } @@ -334,7 +341,16 @@ export const updateSystemConfig = (req: Request, res: Response): void => { if (typeof routing.enableGroupNameRoute === 'boolean') { settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute; } + + if (typeof routing.enableBearerAuth === 'boolean') { + settings.systemConfig.routing.enableBearerAuth = routing.enableBearerAuth; + } + + if (typeof routing.bearerAuthKey === 'string') { + settings.systemConfig.routing.bearerAuthKey = routing.bearerAuthKey; + } } + if (install) { if (typeof install.pythonIndexUrl === 'string') { settings.systemConfig.install.pythonIndexUrl = install.pythonIndexUrl; diff --git a/src/services/sseService.ts b/src/services/sseService.ts index c18aa15..ade5b60 100644 --- a/src/services/sseService.ts +++ b/src/services/sseService.ts @@ -13,11 +13,42 @@ export const getGroup = (sessionId: string): string => { return transports[sessionId]?.group || ''; }; -export const handleSseConnection = async (req: Request, res: Response): Promise => { +// Helper function to validate bearer auth +const validateBearerAuth = (req: Request): boolean => { const settings = loadSettings(); const routingConfig = settings.systemConfig?.routing || { enableGlobalRoute: true, enableGroupNameRoute: true, + enableBearerAuth: false, + bearerAuthKey: '', + }; + + if (routingConfig.enableBearerAuth) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return false; + } + + const token = authHeader.substring(7); // Remove "Bearer " prefix + return token === routingConfig.bearerAuthKey; + } + + return true; +}; + +export const handleSseConnection = async (req: Request, res: Response): Promise => { + // Check bearer auth + if (!validateBearerAuth(req)) { + res.status(401).send('Bearer authentication required or invalid token'); + return; + } + + const settings = loadSettings(); + const routingConfig = settings.systemConfig?.routing || { + enableGlobalRoute: true, + enableGroupNameRoute: true, + enableBearerAuth: false, + bearerAuthKey: '', }; const group = req.params.group; @@ -43,6 +74,12 @@ export const handleSseConnection = async (req: Request, res: Response): Promise< }; export const handleSseMessage = async (req: Request, res: Response): Promise => { + // Check bearer auth + if (!validateBearerAuth(req)) { + res.status(401).send('Bearer authentication required or invalid token'); + return; + } + const sessionId = req.query.sessionId as string; const { transport, group } = transports[sessionId]; req.params.group = group; @@ -60,6 +97,12 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise const sessionId = req.headers['mcp-session-id'] as string | undefined; const group = req.params.group; console.log(`Handling MCP post request for sessionId: ${sessionId} and group: ${group}`); + // Check bearer auth + if (!validateBearerAuth(req)) { + res.status(401).send('Bearer authentication required or invalid token'); + return; + } + const settings = loadSettings(); const routingConfig = settings.systemConfig?.routing || { enableGlobalRoute: true, @@ -110,6 +153,12 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise export const handleMcpOtherRequest = async (req: Request, res: Response) => { console.log('Handling MCP other request'); + // Check bearer auth + if (!validateBearerAuth(req)) { + res.status(401).send('Bearer authentication required or invalid token'); + return; + } + const sessionId = req.headers['mcp-session-id'] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).send('Invalid or missing session ID'); diff --git a/src/types/index.ts b/src/types/index.ts index 2aa2761..3c4c231 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -83,6 +83,8 @@ export interface McpSettings { routing?: { enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed + enableBearerAuth?: boolean; // Controls whether bearer auth is enabled for group routes + bearerAuthKey?: string; // The bearer auth key to validate against }; install?: { pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX)