feat: add bearer authentication support for MCP requests (#79)

This commit is contained in:
samanhappy
2025-05-13 13:02:41 +08:00
committed by GitHub
parent 59454ca250
commit 37c3fd9e06
8 changed files with 187 additions and 10 deletions

View File

@@ -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<RoutingConfig>({
enableGlobalRoute: true,
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
});
const [tempRoutingConfig, setTempRoutingConfig] = useState<TempRoutingConfig>({
bearerAuthKey: '',
});
const [installConfig, setInstallConfig] = useState<InstallConfig>({
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 <T extends keyof RoutingConfig>(
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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableBearerAuth')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableBearerAuthDescription')}</p>
</div>
<Switch
disabled={loading}
checked={routingConfig.enableBearerAuth}
onCheckedChange={(checked) => handleRoutingConfigChange('enableBearerAuth', checked)}
/>
</div>
{routingConfig.enableBearerAuth && (
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.bearerAuthKey')}</h3>
<p className="text-sm text-gray-500">{t('settings.bearerAuthKeyDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempRoutingConfig.bearerAuthKey}
onChange={(e) => 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}
/>
<button
onClick={saveBearerAuthKey}
disabled={loading || !routingConfig.enableBearerAuth}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
)}
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
@@ -145,6 +208,7 @@ const SettingsPage: React.FC = () => {
onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
/>
</div>
</div>
)}
</div>

View File

@@ -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('');
}