diff --git a/frontend/src/constants/permissions.ts b/frontend/src/constants/permissions.ts index c004288..8a6f283 100644 --- a/frontend/src/constants/permissions.ts +++ b/frontend/src/constants/permissions.ts @@ -2,8 +2,9 @@ export const PERMISSIONS = { // Settings page permissions SETTINGS_SMART_ROUTING: 'settings:smart_routing', - SETTINGS_SKIP_AUTH: 'settings:skip_auth', + SETTINGS_ROUTE_CONFIG: 'settings:route_config', SETTINGS_INSTALL_CONFIG: 'settings:install_config', + SETTINGS_SYSTEM_CONFIG: 'settings:system_config', SETTINGS_OAUTH_SERVER: 'settings:oauth_server', SETTINGS_EXPORT_CONFIG: 'settings:export_config', } as const; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index fd8e1ce..839940c 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,69 +1,69 @@ -import React, { useState, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' -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' -import { PermissionChecker } from '@/components/PermissionChecker' -import { PERMISSIONS } from '@/constants/permissions' -import { Copy, Check, Download } from 'lucide-react' +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +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'; +import { PermissionChecker } from '@/components/PermissionChecker'; +import { PERMISSIONS } from '@/constants/permissions'; +import { Copy, Check, Download } from 'lucide-react'; const SettingsPage: React.FC = () => { - const { t } = useTranslation() - const navigate = useNavigate() - const { showToast } = useToast() + const { t } = useTranslation(); + const navigate = useNavigate(); + const { showToast } = useToast(); const [installConfig, setInstallConfig] = useState<{ - pythonIndexUrl: string - npmRegistry: string - baseUrl: string + pythonIndexUrl: string; + npmRegistry: string; + baseUrl: string; }>({ pythonIndexUrl: '', npmRegistry: '', baseUrl: 'http://localhost:3000', - }) + }); const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{ - dbUrl: string - openaiApiBaseUrl: string - openaiApiKey: string - openaiApiEmbeddingModel: string + dbUrl: string; + openaiApiBaseUrl: string; + openaiApiKey: string; + openaiApiEmbeddingModel: string; }>({ dbUrl: '', openaiApiBaseUrl: '', openaiApiKey: '', openaiApiEmbeddingModel: '', - }) + }); const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{ - apiKey: string - referer: string - title: string - baseUrl: string + apiKey: string; + referer: string; + title: string; + baseUrl: string; }>({ apiKey: '', referer: 'https://www.mcphubx.com', title: 'MCPHub', baseUrl: 'https://api.mcprouter.to/v1', - }) + }); const [tempOAuthServerConfig, setTempOAuthServerConfig] = useState<{ - accessTokenLifetime: string - refreshTokenLifetime: string - authorizationCodeLifetime: string - allowedScopes: string - dynamicRegistrationAllowedGrantTypes: string + accessTokenLifetime: string; + refreshTokenLifetime: string; + authorizationCodeLifetime: string; + allowedScopes: string; + dynamicRegistrationAllowedGrantTypes: string; }>({ accessTokenLifetime: '3600', refreshTokenLifetime: '1209600', authorizationCodeLifetime: '300', allowedScopes: 'read, write', dynamicRegistrationAllowedGrantTypes: 'authorization_code, refresh_token', - }) + }); - const [tempNameSeparator, setTempNameSeparator] = useState('-') + const [tempNameSeparator, setTempNameSeparator] = useState('-'); const { routingConfig, @@ -86,14 +86,14 @@ const SettingsPage: React.FC = () => { updateNameSeparator, updateSessionRebuild, exportMCPSettings, - } = useSettingsData() + } = useSettingsData(); // Update local installConfig when savedInstallConfig changes useEffect(() => { if (savedInstallConfig) { - setInstallConfig(savedInstallConfig) + setInstallConfig(savedInstallConfig); } - }, [savedInstallConfig]) + }, [savedInstallConfig]); // Update local tempSmartRoutingConfig when smartRoutingConfig changes useEffect(() => { @@ -103,9 +103,9 @@ const SettingsPage: React.FC = () => { openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '', openaiApiKey: smartRoutingConfig.openaiApiKey || '', openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '', - }) + }); } - }, [smartRoutingConfig]) + }, [smartRoutingConfig]); // Update local tempMCPRouterConfig when mcpRouterConfig changes useEffect(() => { @@ -115,9 +115,9 @@ const SettingsPage: React.FC = () => { referer: mcpRouterConfig.referer || 'https://www.mcphubx.com', title: mcpRouterConfig.title || 'MCPHub', baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1', - }) + }); } - }, [mcpRouterConfig]) + }, [mcpRouterConfig]); useEffect(() => { if (oauthServerConfig) { @@ -138,18 +138,18 @@ const SettingsPage: React.FC = () => { oauthServerConfig.allowedScopes && oauthServerConfig.allowedScopes.length > 0 ? oauthServerConfig.allowedScopes.join(', ') : '', - dynamicRegistrationAllowedGrantTypes: - oauthServerConfig.dynamicRegistration?.allowedGrantTypes?.length - ? oauthServerConfig.dynamicRegistration.allowedGrantTypes.join(', ') - : '', - }) + dynamicRegistrationAllowedGrantTypes: oauthServerConfig.dynamicRegistration + ?.allowedGrantTypes?.length + ? oauthServerConfig.dynamicRegistration.allowedGrantTypes.join(', ') + : '', + }); } - }, [oauthServerConfig]) + }, [oauthServerConfig]); // Update local tempNameSeparator when nameSeparator changes useEffect(() => { - setTempNameSeparator(nameSeparator) - }, [nameSeparator]) + setTempNameSeparator(nameSeparator); + }, [nameSeparator]); const [sectionsVisible, setSectionsVisible] = useState({ routingConfig: false, @@ -160,7 +160,7 @@ const SettingsPage: React.FC = () => { nameSeparator: false, password: false, exportConfig: false, - }) + }); const toggleSection = ( section: @@ -176,8 +176,8 @@ const SettingsPage: React.FC = () => { setSectionsVisible((prev) => ({ ...prev, [section]: !prev[section], - })) - } + })); + }; const handleRoutingConfigChange = async ( key: @@ -191,39 +191,39 @@ const SettingsPage: React.FC = () => { // If enableBearerAuth is turned on and there's no key, generate one first if (key === 'enableBearerAuth' && value === true) { if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) { - const newKey = generateRandomKey() - handleBearerAuthKeyChange(newKey) + const newKey = generateRandomKey(); + handleBearerAuthKeyChange(newKey); // Update both enableBearerAuth and bearerAuthKey in a single call const success = await updateRoutingConfigBatch({ enableBearerAuth: true, bearerAuthKey: newKey, - }) + }); if (success) { // Update tempRoutingConfig to reflect the saved values setTempRoutingConfig((prev) => ({ ...prev, bearerAuthKey: newKey, - })) + })); } - return + return; } } - await updateRoutingConfig(key, value) - } + await updateRoutingConfig(key, value); + }; const handleBearerAuthKeyChange = (value: string) => { setTempRoutingConfig((prev) => ({ ...prev, bearerAuthKey: value, - })) - } + })); + }; const saveBearerAuthKey = async () => { - await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey) - } + await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey); + }; const handleInstallConfigChange = ( key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl', @@ -232,12 +232,12 @@ const SettingsPage: React.FC = () => { setInstallConfig({ ...installConfig, [key]: value, - }) - } + }); + }; const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => { - await updateInstallConfig(key, installConfig[key]) - } + await updateInstallConfig(key, installConfig[key]); + }; const handleSmartRoutingConfigChange = ( key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', @@ -246,14 +246,14 @@ const SettingsPage: React.FC = () => { setTempSmartRoutingConfig({ ...tempSmartRoutingConfig, [key]: value, - }) - } + }); + }; const saveSmartRoutingConfig = async ( key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', ) => { - await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]) - } + await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]); + }; const handleMCPRouterConfigChange = ( key: 'apiKey' | 'referer' | 'title' | 'baseUrl', @@ -262,24 +262,24 @@ const SettingsPage: React.FC = () => { setTempMCPRouterConfig({ ...tempMCPRouterConfig, [key]: value, - }) - } + }); + }; const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => { - await updateMCPRouterConfig(key, tempMCPRouterConfig[key]) - } + await updateMCPRouterConfig(key, tempMCPRouterConfig[key]); + }; type OAuthServerNumberField = | 'accessTokenLifetime' | 'refreshTokenLifetime' - | 'authorizationCodeLifetime' + | 'authorizationCodeLifetime'; const handleOAuthServerNumberChange = (key: OAuthServerNumberField, value: string) => { setTempOAuthServerConfig((prev) => ({ ...prev, [key]: value, - })) - } + })); + }; const handleOAuthServerTextChange = ( key: 'allowedScopes' | 'dynamicRegistrationAllowedGrantTypes', @@ -288,52 +288,52 @@ const SettingsPage: React.FC = () => { setTempOAuthServerConfig((prev) => ({ ...prev, [key]: value, - })) - } + })); + }; const saveOAuthServerNumberConfig = async (key: OAuthServerNumberField) => { - const rawValue = tempOAuthServerConfig[key] + const rawValue = tempOAuthServerConfig[key]; if (!rawValue || rawValue.trim() === '') { - showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error') - return + showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error'); + return; } - const parsedValue = Number(rawValue) + const parsedValue = Number(rawValue); if (Number.isNaN(parsedValue) || parsedValue < 0) { - showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error') - return + showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error'); + return; } - await updateOAuthServerConfig(key, parsedValue) - } + await updateOAuthServerConfig(key, parsedValue); + }; const saveOAuthServerAllowedScopes = async () => { const scopes = tempOAuthServerConfig.allowedScopes .split(',') .map((scope) => scope.trim()) - .filter((scope) => scope.length > 0) + .filter((scope) => scope.length > 0); - await updateOAuthServerConfig('allowedScopes', scopes) - } + await updateOAuthServerConfig('allowedScopes', scopes); + }; const saveOAuthServerGrantTypes = async () => { const grantTypes = tempOAuthServerConfig.dynamicRegistrationAllowedGrantTypes .split(',') .map((grant) => grant.trim()) - .filter((grant) => grant.length > 0) + .filter((grant) => grant.length > 0); await updateOAuthServerConfig('dynamicRegistration', { ...oauthServerConfig.dynamicRegistration, allowedGrantTypes: grantTypes, - }) - } + }); + }; const handleOAuthServerToggle = async ( key: 'enabled' | 'requireClientSecret' | 'requireState', value: boolean, ) => { - await updateOAuthServerConfig(key, value) - } + await updateOAuthServerConfig(key, value); + }; const handleDynamicRegistrationToggle = async ( updates: Partial, @@ -341,137 +341,137 @@ const SettingsPage: React.FC = () => { await updateOAuthServerConfig('dynamicRegistration', { ...oauthServerConfig.dynamicRegistration, ...updates, - }) - } + }); + }; const saveNameSeparator = async () => { - await updateNameSeparator(tempNameSeparator) - } + await updateNameSeparator(tempNameSeparator); + }; const handleSmartRoutingEnabledChange = async (value: boolean) => { // If enabling Smart Routing, validate required fields and save any unsaved changes if (value) { - const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl + const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl; const currentOpenaiApiKey = - tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey + tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey; if (!currentDbUrl || !currentOpenaiApiKey) { - const missingFields = [] - if (!currentDbUrl) missingFields.push(t('settings.dbUrl')) - if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey')) + const missingFields = []; + if (!currentDbUrl) missingFields.push(t('settings.dbUrl')); + if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey')); showToast( t('settings.smartRoutingValidationError', { fields: missingFields.join(', '), }), - ) - return + ); + return; } // Prepare updates object with unsaved changes and enabled status - const updates: any = { enabled: value } + const updates: any = { enabled: value }; // Check for unsaved changes and include them in the batch update if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) { - updates.dbUrl = tempSmartRoutingConfig.dbUrl + updates.dbUrl = tempSmartRoutingConfig.dbUrl; } if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) { - updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl + updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl; } if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) { - updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey + updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey; } if ( tempSmartRoutingConfig.openaiApiEmbeddingModel !== smartRoutingConfig.openaiApiEmbeddingModel ) { - updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel + updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel; } // Save all changes in a single batch update - await updateSmartRoutingConfigBatch(updates) + await updateSmartRoutingConfigBatch(updates); } else { // If disabling, just update the enabled status - await updateSmartRoutingConfig('enabled', value) + await updateSmartRoutingConfig('enabled', value); } - } + }; const handlePasswordChangeSuccess = () => { setTimeout(() => { - navigate('/') - }, 2000) - } + navigate('/'); + }, 2000); + }; - const [copiedConfig, setCopiedConfig] = useState(false) - const [mcpSettingsJson, setMcpSettingsJson] = useState('') + const [copiedConfig, setCopiedConfig] = useState(false); + const [mcpSettingsJson, setMcpSettingsJson] = useState(''); const fetchMcpSettings = async () => { try { - const result = await exportMCPSettings() - console.log('Fetched MCP settings:', result) - const configJson = JSON.stringify(result.data, null, 2) - setMcpSettingsJson(configJson) + const result = await exportMCPSettings(); + console.log('Fetched MCP settings:', result); + const configJson = JSON.stringify(result.data, null, 2); + setMcpSettingsJson(configJson); } catch (error) { - console.error('Error fetching MCP settings:', error) - showToast(t('settings.exportError') || 'Failed to fetch settings', 'error') + console.error('Error fetching MCP settings:', error); + showToast(t('settings.exportError') || 'Failed to fetch settings', 'error'); } - } + }; useEffect(() => { if (sectionsVisible.exportConfig && !mcpSettingsJson) { - fetchMcpSettings() + fetchMcpSettings(); } - }, [sectionsVisible.exportConfig]) + }, [sectionsVisible.exportConfig]); const handleCopyConfig = async () => { - if (!mcpSettingsJson) return + if (!mcpSettingsJson) return; try { if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(mcpSettingsJson) - setCopiedConfig(true) - showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') - setTimeout(() => setCopiedConfig(false), 2000) + await navigator.clipboard.writeText(mcpSettingsJson); + setCopiedConfig(true); + showToast(t('common.copySuccess') || 'Copied to clipboard', 'success'); + setTimeout(() => setCopiedConfig(false), 2000); } else { // Fallback for HTTP or unsupported clipboard API - const textArea = document.createElement('textarea') - textArea.value = mcpSettingsJson - textArea.style.position = 'fixed' - textArea.style.left = '-9999px' - document.body.appendChild(textArea) - textArea.focus() - textArea.select() + const textArea = document.createElement('textarea'); + textArea.value = mcpSettingsJson; + textArea.style.position = 'fixed'; + textArea.style.left = '-9999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); try { - document.execCommand('copy') - setCopiedConfig(true) - showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') - setTimeout(() => setCopiedConfig(false), 2000) + document.execCommand('copy'); + setCopiedConfig(true); + showToast(t('common.copySuccess') || 'Copied to clipboard', 'success'); + setTimeout(() => setCopiedConfig(false), 2000); } catch (err) { - showToast(t('common.copyFailed') || 'Copy failed', 'error') - console.error('Copy to clipboard failed:', err) + showToast(t('common.copyFailed') || 'Copy failed', 'error'); + console.error('Copy to clipboard failed:', err); } - document.body.removeChild(textArea) + document.body.removeChild(textArea); } } catch (error) { - console.error('Error copying configuration:', error) - showToast(t('common.copyFailed') || 'Copy failed', 'error') + console.error('Error copying configuration:', error); + showToast(t('common.copyFailed') || 'Copy failed', 'error'); } - } + }; const handleDownloadConfig = () => { - if (!mcpSettingsJson) return + if (!mcpSettingsJson) return; - const blob = new Blob([mcpSettingsJson], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = 'mcp_settings.json' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) - showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success') - } + const blob = new Blob([mcpSettingsJson], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'mcp_settings.json'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success'); + }; return (
@@ -643,9 +643,7 @@ const SettingsPage: React.FC = () => {
-

- {t('settings.requireClientSecret')} -

+

{t('settings.requireClientSecret')}

{t('settings.requireClientSecretDescription')}

@@ -673,9 +671,7 @@ const SettingsPage: React.FC = () => {
-

- {t('settings.accessTokenLifetime')} -

+

{t('settings.accessTokenLifetime')}

{t('settings.accessTokenLifetimeDescription')}

@@ -764,9 +760,7 @@ const SettingsPage: React.FC = () => {

{t('settings.allowedScopes')}

-

- {t('settings.allowedScopesDescription')} -

+

{t('settings.allowedScopesDescription')}

{ {/* System Settings */} -
-
toggleSection('nameSeparator')} - > -

{t('settings.systemSettings')}

- {sectionsVisible.nameSeparator ? '▼' : '►'} -
- - {sectionsVisible.nameSeparator && ( -
-
-
-

{t('settings.nameSeparatorLabel')}

-

{t('settings.nameSeparatorDescription')}

-
-
- setTempNameSeparator(e.target.value)} - placeholder="-" - 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 form-input" - disabled={loading} - maxLength={5} - /> - -
-
- -
-
-

{t('settings.enableSessionRebuild')}

-

{t('settings.enableSessionRebuildDescription')}

-
- updateSessionRebuild(checked)} - /> -
+ +
+
toggleSection('nameSeparator')} + > +

{t('settings.systemSettings')}

+ {sectionsVisible.nameSeparator ? '▼' : '►'}
- )} -
- {/* Route Configuration Settings */} -
-
toggleSection('routingConfig')} - > -

{t('pages.settings.routeConfig')}

- {sectionsVisible.routingConfig ? '▼' : '►'} -
- - {sectionsVisible.routingConfig && ( -
-
-
-

{t('settings.enableBearerAuth')}

-

{t('settings.enableBearerAuthDescription')}

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

{t('settings.bearerAuthKey')}

-

{t('settings.bearerAuthKeyDescription')}

+

{t('settings.nameSeparatorLabel')}

+

{t('settings.nameSeparatorDescription')}

handleBearerAuthKeyChange(e.target.value)} - placeholder={t('settings.bearerAuthKeyPlaceholder')} + value={tempNameSeparator} + onChange={(e) => setTempNameSeparator(e.target.value)} + placeholder="-" 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 form-input" - disabled={loading || !routingConfig.enableBearerAuth} + disabled={loading} + maxLength={5} />
- )} -
-
-

{t('settings.enableGlobalRoute')}

-

- {t('settings.enableGlobalRouteDescription')} -

+
+
+

+ {t('settings.enableSessionRebuild')} +

+

+ {t('settings.enableSessionRebuildDescription')} +

+
+ updateSessionRebuild(checked)} + />
- - handleRoutingConfigChange('enableGlobalRoute', checked) - } - />
+ )} +
+ -
-
-

{t('settings.enableGroupNameRoute')}

-

- {t('settings.enableGroupNameRouteDescription')} -

+ {/* Route Configuration Settings */} + +
+
toggleSection('routingConfig')} + > +

{t('pages.settings.routeConfig')}

+ {sectionsVisible.routingConfig ? '▼' : '►'} +
+ + {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 form-input" + disabled={loading || !routingConfig.enableBearerAuth} + /> + +
+
+ )} + +
+
+

{t('settings.enableGlobalRoute')}

+

+ {t('settings.enableGlobalRouteDescription')} +

+
+ + handleRoutingConfigChange('enableGlobalRoute', checked) + } + /> +
+ +
+
+

+ {t('settings.enableGroupNameRoute')} +

+

+ {t('settings.enableGroupNameRouteDescription')} +

+
+ + handleRoutingConfigChange('enableGroupNameRoute', checked) + } + />
- - handleRoutingConfigChange('enableGroupNameRoute', checked) - } - /> -
-

{t('settings.skipAuth')}

@@ -1093,10 +1099,10 @@ const SettingsPage: React.FC = () => { onCheckedChange={(checked) => handleRoutingConfigChange('skipAuth', checked)} />
- -
- )} -
+
+ )} +
+ {/* Installation Configuration Settings */} @@ -1188,7 +1194,10 @@ const SettingsPage: React.FC = () => { {/* Change Password */} -
+
toggleSection('password')} @@ -1258,7 +1267,7 @@ const SettingsPage: React.FC = () => {
- ) -} + ); +}; -export default SettingsPage +export default SettingsPage; diff --git a/src/services/dataServicex.ts b/src/services/dataServicex.ts index 8cd3ff3..01a4af4 100644 --- a/src/services/dataServicex.ts +++ b/src/services/dataServicex.ts @@ -26,7 +26,8 @@ export class DataServicex implements DataService { return result; } else { const result = { ...settings }; - result.systemConfig = settings.userConfigs?.[currentUser?.username || ''] || {}; + // TODO: apply userConfig to filter settings as needed + // const userConfig = settings.userConfigs?.[currentUser?.username || '']; delete result.userConfigs; return result; } @@ -53,10 +54,7 @@ export class DataServicex implements DataService { const userConfig: UserConfig = { routing: systemConfig.routing ? { - enableGlobalRoute: systemConfig.routing.enableGlobalRoute, - enableGroupNameRoute: systemConfig.routing.enableGroupNameRoute, - enableBearerAuth: systemConfig.routing.enableBearerAuth, - bearerAuthKey: systemConfig.routing.bearerAuthKey, + // TODO: only allow modifying certain fields based on userConfig permissions } : undefined, }; diff --git a/src/services/registry.ts b/src/services/registry.ts index c962ecd..d2e4139 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -1,5 +1,5 @@ -import { createRequire } from 'module'; import { join } from 'path'; +import { pathToFileURL } from 'url'; type Class = new (...args: any[]) => T; @@ -11,7 +11,24 @@ interface Service { const registry = new Map>(); const instances = new Map(); -export function registerService(key: string, entry: Service) { +async function tryLoadOverride(key: string, overridePath: string): Promise | undefined> { + try { + const moduleUrl = pathToFileURL(overridePath).href; + const mod = await import(moduleUrl); + const override = mod[key.charAt(0).toUpperCase() + key.slice(1) + 'x']; + if (typeof override === 'function') { + return override as Class; + } + } catch (error: any) { + // Ignore not-found errors and keep trying other paths; surface other errors for visibility + if (error?.code !== 'ERR_MODULE_NOT_FOUND' && error?.code !== 'MODULE_NOT_FOUND') { + console.warn(`Failed to load service override from ${overridePath}:`, error); + } + } + return undefined; +} + +export async function registerService(key: string, entry: Service) { // Try to load override immediately during registration // Try multiple paths and file extensions in order const serviceDirs = ['src/services', 'dist/services']; @@ -22,18 +39,10 @@ export function registerService(key: string, entry: Service) { for (const fileExt of fileExts) { const overridePath = join(process.cwd(), serviceDir, overrideFileName + fileExt); - try { - // Use createRequire with a stable path reference - const require = createRequire(join(process.cwd(), 'package.json')); - const mod = require(overridePath); - const override = mod[key.charAt(0).toUpperCase() + key.slice(1) + 'x']; - if (typeof override === 'function') { - entry.override = override; - break; // Found override, exit both loops - } - } catch (error) { - // Continue trying next path/extension combination - continue; + const override = await tryLoadOverride(key, overridePath); + if (override) { + entry.override = override; + break; // Found override, exit both loops } } diff --git a/src/services/services.ts b/src/services/services.ts index 4c1738f..0e64d9e 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,7 +1,7 @@ import { registerService, getService } from './registry.js'; import { DataService, DataServiceImpl } from './dataService.js'; -registerService('dataService', { +await registerService('dataService', { defaultImpl: DataServiceImpl, }); diff --git a/src/types/index.ts b/src/types/index.ts index 116aa92..0c280ec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -175,14 +175,7 @@ export interface SystemConfig { enableSessionRebuild?: boolean; // Controls whether server session rebuild is enabled } -export interface UserConfig { - 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 - }; -} +export interface UserConfig {} // OAuth Client for MCPHub's own authorization server export interface IOAuthClient {