feat: Update permissions and settings structure for improved configuration management (#447)

This commit is contained in:
samanhappy
2025-11-23 20:43:24 +08:00
committed by GitHub
parent 6de3221974
commit a736398cd5
6 changed files with 333 additions and 323 deletions

View File

@@ -2,8 +2,9 @@
export const PERMISSIONS = { export const PERMISSIONS = {
// Settings page permissions // Settings page permissions
SETTINGS_SMART_ROUTING: 'settings:smart_routing', 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_INSTALL_CONFIG: 'settings:install_config',
SETTINGS_SYSTEM_CONFIG: 'settings:system_config',
SETTINGS_OAUTH_SERVER: 'settings:oauth_server', SETTINGS_OAUTH_SERVER: 'settings:oauth_server',
SETTINGS_EXPORT_CONFIG: 'settings:export_config', SETTINGS_EXPORT_CONFIG: 'settings:export_config',
} as const; } as const;

View File

@@ -1,69 +1,69 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom';
import ChangePasswordForm from '@/components/ChangePasswordForm' import ChangePasswordForm from '@/components/ChangePasswordForm';
import { Switch } from '@/components/ui/ToggleGroup' import { Switch } from '@/components/ui/ToggleGroup';
import { useSettingsData } from '@/hooks/useSettingsData' import { useSettingsData } from '@/hooks/useSettingsData';
import { useToast } from '@/contexts/ToastContext' import { useToast } from '@/contexts/ToastContext';
import { generateRandomKey } from '@/utils/key' import { generateRandomKey } from '@/utils/key';
import { PermissionChecker } from '@/components/PermissionChecker' import { PermissionChecker } from '@/components/PermissionChecker';
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions';
import { Copy, Check, Download } from 'lucide-react' import { Copy, Check, Download } from 'lucide-react';
const SettingsPage: React.FC = () => { const SettingsPage: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { showToast } = useToast() const { showToast } = useToast();
const [installConfig, setInstallConfig] = useState<{ const [installConfig, setInstallConfig] = useState<{
pythonIndexUrl: string pythonIndexUrl: string;
npmRegistry: string npmRegistry: string;
baseUrl: string baseUrl: string;
}>({ }>({
pythonIndexUrl: '', pythonIndexUrl: '',
npmRegistry: '', npmRegistry: '',
baseUrl: 'http://localhost:3000', baseUrl: 'http://localhost:3000',
}) });
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{ const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
dbUrl: string dbUrl: string;
openaiApiBaseUrl: string openaiApiBaseUrl: string;
openaiApiKey: string openaiApiKey: string;
openaiApiEmbeddingModel: string openaiApiEmbeddingModel: string;
}>({ }>({
dbUrl: '', dbUrl: '',
openaiApiBaseUrl: '', openaiApiBaseUrl: '',
openaiApiKey: '', openaiApiKey: '',
openaiApiEmbeddingModel: '', openaiApiEmbeddingModel: '',
}) });
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{ const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
apiKey: string apiKey: string;
referer: string referer: string;
title: string title: string;
baseUrl: string baseUrl: string;
}>({ }>({
apiKey: '', apiKey: '',
referer: 'https://www.mcphubx.com', referer: 'https://www.mcphubx.com',
title: 'MCPHub', title: 'MCPHub',
baseUrl: 'https://api.mcprouter.to/v1', baseUrl: 'https://api.mcprouter.to/v1',
}) });
const [tempOAuthServerConfig, setTempOAuthServerConfig] = useState<{ const [tempOAuthServerConfig, setTempOAuthServerConfig] = useState<{
accessTokenLifetime: string accessTokenLifetime: string;
refreshTokenLifetime: string refreshTokenLifetime: string;
authorizationCodeLifetime: string authorizationCodeLifetime: string;
allowedScopes: string allowedScopes: string;
dynamicRegistrationAllowedGrantTypes: string dynamicRegistrationAllowedGrantTypes: string;
}>({ }>({
accessTokenLifetime: '3600', accessTokenLifetime: '3600',
refreshTokenLifetime: '1209600', refreshTokenLifetime: '1209600',
authorizationCodeLifetime: '300', authorizationCodeLifetime: '300',
allowedScopes: 'read, write', allowedScopes: 'read, write',
dynamicRegistrationAllowedGrantTypes: 'authorization_code, refresh_token', dynamicRegistrationAllowedGrantTypes: 'authorization_code, refresh_token',
}) });
const [tempNameSeparator, setTempNameSeparator] = useState<string>('-') const [tempNameSeparator, setTempNameSeparator] = useState<string>('-');
const { const {
routingConfig, routingConfig,
@@ -86,14 +86,14 @@ const SettingsPage: React.FC = () => {
updateNameSeparator, updateNameSeparator,
updateSessionRebuild, updateSessionRebuild,
exportMCPSettings, exportMCPSettings,
} = useSettingsData() } = useSettingsData();
// Update local installConfig when savedInstallConfig changes // Update local installConfig when savedInstallConfig changes
useEffect(() => { useEffect(() => {
if (savedInstallConfig) { if (savedInstallConfig) {
setInstallConfig(savedInstallConfig) setInstallConfig(savedInstallConfig);
} }
}, [savedInstallConfig]) }, [savedInstallConfig]);
// Update local tempSmartRoutingConfig when smartRoutingConfig changes // Update local tempSmartRoutingConfig when smartRoutingConfig changes
useEffect(() => { useEffect(() => {
@@ -103,9 +103,9 @@ const SettingsPage: React.FC = () => {
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '', openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
openaiApiKey: smartRoutingConfig.openaiApiKey || '', openaiApiKey: smartRoutingConfig.openaiApiKey || '',
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '', openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
}) });
} }
}, [smartRoutingConfig]) }, [smartRoutingConfig]);
// Update local tempMCPRouterConfig when mcpRouterConfig changes // Update local tempMCPRouterConfig when mcpRouterConfig changes
useEffect(() => { useEffect(() => {
@@ -115,9 +115,9 @@ const SettingsPage: React.FC = () => {
referer: mcpRouterConfig.referer || 'https://www.mcphubx.com', referer: mcpRouterConfig.referer || 'https://www.mcphubx.com',
title: mcpRouterConfig.title || 'MCPHub', title: mcpRouterConfig.title || 'MCPHub',
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1', baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
}) });
} }
}, [mcpRouterConfig]) }, [mcpRouterConfig]);
useEffect(() => { useEffect(() => {
if (oauthServerConfig) { if (oauthServerConfig) {
@@ -138,18 +138,18 @@ const SettingsPage: React.FC = () => {
oauthServerConfig.allowedScopes && oauthServerConfig.allowedScopes.length > 0 oauthServerConfig.allowedScopes && oauthServerConfig.allowedScopes.length > 0
? oauthServerConfig.allowedScopes.join(', ') ? oauthServerConfig.allowedScopes.join(', ')
: '', : '',
dynamicRegistrationAllowedGrantTypes: dynamicRegistrationAllowedGrantTypes: oauthServerConfig.dynamicRegistration
oauthServerConfig.dynamicRegistration?.allowedGrantTypes?.length ?.allowedGrantTypes?.length
? oauthServerConfig.dynamicRegistration.allowedGrantTypes.join(', ') ? oauthServerConfig.dynamicRegistration.allowedGrantTypes.join(', ')
: '', : '',
}) });
} }
}, [oauthServerConfig]) }, [oauthServerConfig]);
// Update local tempNameSeparator when nameSeparator changes // Update local tempNameSeparator when nameSeparator changes
useEffect(() => { useEffect(() => {
setTempNameSeparator(nameSeparator) setTempNameSeparator(nameSeparator);
}, [nameSeparator]) }, [nameSeparator]);
const [sectionsVisible, setSectionsVisible] = useState({ const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false, routingConfig: false,
@@ -160,7 +160,7 @@ const SettingsPage: React.FC = () => {
nameSeparator: false, nameSeparator: false,
password: false, password: false,
exportConfig: false, exportConfig: false,
}) });
const toggleSection = ( const toggleSection = (
section: section:
@@ -176,8 +176,8 @@ const SettingsPage: React.FC = () => {
setSectionsVisible((prev) => ({ setSectionsVisible((prev) => ({
...prev, ...prev,
[section]: !prev[section], [section]: !prev[section],
})) }));
} };
const handleRoutingConfigChange = async ( const handleRoutingConfigChange = async (
key: key:
@@ -191,39 +191,39 @@ const SettingsPage: React.FC = () => {
// If enableBearerAuth is turned on and there's no key, generate one first // If enableBearerAuth is turned on and there's no key, generate one first
if (key === 'enableBearerAuth' && value === true) { if (key === 'enableBearerAuth' && value === true) {
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) { if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
const newKey = generateRandomKey() const newKey = generateRandomKey();
handleBearerAuthKeyChange(newKey) handleBearerAuthKeyChange(newKey);
// Update both enableBearerAuth and bearerAuthKey in a single call // Update both enableBearerAuth and bearerAuthKey in a single call
const success = await updateRoutingConfigBatch({ const success = await updateRoutingConfigBatch({
enableBearerAuth: true, enableBearerAuth: true,
bearerAuthKey: newKey, bearerAuthKey: newKey,
}) });
if (success) { if (success) {
// Update tempRoutingConfig to reflect the saved values // Update tempRoutingConfig to reflect the saved values
setTempRoutingConfig((prev) => ({ setTempRoutingConfig((prev) => ({
...prev, ...prev,
bearerAuthKey: newKey, bearerAuthKey: newKey,
})) }));
} }
return return;
} }
} }
await updateRoutingConfig(key, value) await updateRoutingConfig(key, value);
} };
const handleBearerAuthKeyChange = (value: string) => { const handleBearerAuthKeyChange = (value: string) => {
setTempRoutingConfig((prev) => ({ setTempRoutingConfig((prev) => ({
...prev, ...prev,
bearerAuthKey: value, bearerAuthKey: value,
})) }));
} };
const saveBearerAuthKey = async () => { const saveBearerAuthKey = async () => {
await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey) await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey);
} };
const handleInstallConfigChange = ( const handleInstallConfigChange = (
key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl', key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl',
@@ -232,12 +232,12 @@ const SettingsPage: React.FC = () => {
setInstallConfig({ setInstallConfig({
...installConfig, ...installConfig,
[key]: value, [key]: value,
}) });
} };
const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => { const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => {
await updateInstallConfig(key, installConfig[key]) await updateInstallConfig(key, installConfig[key]);
} };
const handleSmartRoutingConfigChange = ( const handleSmartRoutingConfigChange = (
key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
@@ -246,14 +246,14 @@ const SettingsPage: React.FC = () => {
setTempSmartRoutingConfig({ setTempSmartRoutingConfig({
...tempSmartRoutingConfig, ...tempSmartRoutingConfig,
[key]: value, [key]: value,
}) });
} };
const saveSmartRoutingConfig = async ( const saveSmartRoutingConfig = async (
key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel', key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
) => { ) => {
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]) await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
} };
const handleMCPRouterConfigChange = ( const handleMCPRouterConfigChange = (
key: 'apiKey' | 'referer' | 'title' | 'baseUrl', key: 'apiKey' | 'referer' | 'title' | 'baseUrl',
@@ -262,24 +262,24 @@ const SettingsPage: React.FC = () => {
setTempMCPRouterConfig({ setTempMCPRouterConfig({
...tempMCPRouterConfig, ...tempMCPRouterConfig,
[key]: value, [key]: value,
}) });
} };
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => { const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
await updateMCPRouterConfig(key, tempMCPRouterConfig[key]) await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
} };
type OAuthServerNumberField = type OAuthServerNumberField =
| 'accessTokenLifetime' | 'accessTokenLifetime'
| 'refreshTokenLifetime' | 'refreshTokenLifetime'
| 'authorizationCodeLifetime' | 'authorizationCodeLifetime';
const handleOAuthServerNumberChange = (key: OAuthServerNumberField, value: string) => { const handleOAuthServerNumberChange = (key: OAuthServerNumberField, value: string) => {
setTempOAuthServerConfig((prev) => ({ setTempOAuthServerConfig((prev) => ({
...prev, ...prev,
[key]: value, [key]: value,
})) }));
} };
const handleOAuthServerTextChange = ( const handleOAuthServerTextChange = (
key: 'allowedScopes' | 'dynamicRegistrationAllowedGrantTypes', key: 'allowedScopes' | 'dynamicRegistrationAllowedGrantTypes',
@@ -288,52 +288,52 @@ const SettingsPage: React.FC = () => {
setTempOAuthServerConfig((prev) => ({ setTempOAuthServerConfig((prev) => ({
...prev, ...prev,
[key]: value, [key]: value,
})) }));
} };
const saveOAuthServerNumberConfig = async (key: OAuthServerNumberField) => { const saveOAuthServerNumberConfig = async (key: OAuthServerNumberField) => {
const rawValue = tempOAuthServerConfig[key] const rawValue = tempOAuthServerConfig[key];
if (!rawValue || rawValue.trim() === '') { if (!rawValue || rawValue.trim() === '') {
showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error') showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error');
return return;
} }
const parsedValue = Number(rawValue) const parsedValue = Number(rawValue);
if (Number.isNaN(parsedValue) || parsedValue < 0) { if (Number.isNaN(parsedValue) || parsedValue < 0) {
showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error') showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error');
return return;
} }
await updateOAuthServerConfig(key, parsedValue) await updateOAuthServerConfig(key, parsedValue);
} };
const saveOAuthServerAllowedScopes = async () => { const saveOAuthServerAllowedScopes = async () => {
const scopes = tempOAuthServerConfig.allowedScopes const scopes = tempOAuthServerConfig.allowedScopes
.split(',') .split(',')
.map((scope) => scope.trim()) .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 saveOAuthServerGrantTypes = async () => {
const grantTypes = tempOAuthServerConfig.dynamicRegistrationAllowedGrantTypes const grantTypes = tempOAuthServerConfig.dynamicRegistrationAllowedGrantTypes
.split(',') .split(',')
.map((grant) => grant.trim()) .map((grant) => grant.trim())
.filter((grant) => grant.length > 0) .filter((grant) => grant.length > 0);
await updateOAuthServerConfig('dynamicRegistration', { await updateOAuthServerConfig('dynamicRegistration', {
...oauthServerConfig.dynamicRegistration, ...oauthServerConfig.dynamicRegistration,
allowedGrantTypes: grantTypes, allowedGrantTypes: grantTypes,
}) });
} };
const handleOAuthServerToggle = async ( const handleOAuthServerToggle = async (
key: 'enabled' | 'requireClientSecret' | 'requireState', key: 'enabled' | 'requireClientSecret' | 'requireState',
value: boolean, value: boolean,
) => { ) => {
await updateOAuthServerConfig(key, value) await updateOAuthServerConfig(key, value);
} };
const handleDynamicRegistrationToggle = async ( const handleDynamicRegistrationToggle = async (
updates: Partial<typeof oauthServerConfig.dynamicRegistration>, updates: Partial<typeof oauthServerConfig.dynamicRegistration>,
@@ -341,137 +341,137 @@ const SettingsPage: React.FC = () => {
await updateOAuthServerConfig('dynamicRegistration', { await updateOAuthServerConfig('dynamicRegistration', {
...oauthServerConfig.dynamicRegistration, ...oauthServerConfig.dynamicRegistration,
...updates, ...updates,
}) });
} };
const saveNameSeparator = async () => { const saveNameSeparator = async () => {
await updateNameSeparator(tempNameSeparator) await updateNameSeparator(tempNameSeparator);
} };
const handleSmartRoutingEnabledChange = async (value: boolean) => { const handleSmartRoutingEnabledChange = async (value: boolean) => {
// If enabling Smart Routing, validate required fields and save any unsaved changes // If enabling Smart Routing, validate required fields and save any unsaved changes
if (value) { if (value) {
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl;
const currentOpenaiApiKey = const currentOpenaiApiKey =
tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey;
if (!currentDbUrl || !currentOpenaiApiKey) { if (!currentDbUrl || !currentOpenaiApiKey) {
const missingFields = [] const missingFields = [];
if (!currentDbUrl) missingFields.push(t('settings.dbUrl')) if (!currentDbUrl) missingFields.push(t('settings.dbUrl'));
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey')) if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'));
showToast( showToast(
t('settings.smartRoutingValidationError', { t('settings.smartRoutingValidationError', {
fields: missingFields.join(', '), fields: missingFields.join(', '),
}), }),
) );
return return;
} }
// Prepare updates object with unsaved changes and enabled status // 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 // Check for unsaved changes and include them in the batch update
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) { if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
updates.dbUrl = tempSmartRoutingConfig.dbUrl updates.dbUrl = tempSmartRoutingConfig.dbUrl;
} }
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) { if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
} }
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) { if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
} }
if ( if (
tempSmartRoutingConfig.openaiApiEmbeddingModel !== tempSmartRoutingConfig.openaiApiEmbeddingModel !==
smartRoutingConfig.openaiApiEmbeddingModel smartRoutingConfig.openaiApiEmbeddingModel
) { ) {
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
} }
// Save all changes in a single batch update // Save all changes in a single batch update
await updateSmartRoutingConfigBatch(updates) await updateSmartRoutingConfigBatch(updates);
} else { } else {
// If disabling, just update the enabled status // If disabling, just update the enabled status
await updateSmartRoutingConfig('enabled', value) await updateSmartRoutingConfig('enabled', value);
}
} }
};
const handlePasswordChangeSuccess = () => { const handlePasswordChangeSuccess = () => {
setTimeout(() => { setTimeout(() => {
navigate('/') navigate('/');
}, 2000) }, 2000);
} };
const [copiedConfig, setCopiedConfig] = useState(false) const [copiedConfig, setCopiedConfig] = useState(false);
const [mcpSettingsJson, setMcpSettingsJson] = useState<string>('') const [mcpSettingsJson, setMcpSettingsJson] = useState<string>('');
const fetchMcpSettings = async () => { const fetchMcpSettings = async () => {
try { try {
const result = await exportMCPSettings() const result = await exportMCPSettings();
console.log('Fetched MCP settings:', result) console.log('Fetched MCP settings:', result);
const configJson = JSON.stringify(result.data, null, 2) const configJson = JSON.stringify(result.data, null, 2);
setMcpSettingsJson(configJson) setMcpSettingsJson(configJson);
} catch (error) { } catch (error) {
console.error('Error fetching MCP settings:', error) console.error('Error fetching MCP settings:', error);
showToast(t('settings.exportError') || 'Failed to fetch settings', 'error') showToast(t('settings.exportError') || 'Failed to fetch settings', 'error');
}
} }
};
useEffect(() => { useEffect(() => {
if (sectionsVisible.exportConfig && !mcpSettingsJson) { if (sectionsVisible.exportConfig && !mcpSettingsJson) {
fetchMcpSettings() fetchMcpSettings();
} }
}, [sectionsVisible.exportConfig]) }, [sectionsVisible.exportConfig]);
const handleCopyConfig = async () => { const handleCopyConfig = async () => {
if (!mcpSettingsJson) return if (!mcpSettingsJson) return;
try { try {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(mcpSettingsJson) await navigator.clipboard.writeText(mcpSettingsJson);
setCopiedConfig(true) setCopiedConfig(true);
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
setTimeout(() => setCopiedConfig(false), 2000) setTimeout(() => setCopiedConfig(false), 2000);
} else { } else {
// Fallback for HTTP or unsupported clipboard API // Fallback for HTTP or unsupported clipboard API
const textArea = document.createElement('textarea') const textArea = document.createElement('textarea');
textArea.value = mcpSettingsJson textArea.value = mcpSettingsJson;
textArea.style.position = 'fixed' textArea.style.position = 'fixed';
textArea.style.left = '-9999px' textArea.style.left = '-9999px';
document.body.appendChild(textArea) document.body.appendChild(textArea);
textArea.focus() textArea.focus();
textArea.select() textArea.select();
try { try {
document.execCommand('copy') document.execCommand('copy');
setCopiedConfig(true) setCopiedConfig(true);
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success') showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
setTimeout(() => setCopiedConfig(false), 2000) setTimeout(() => setCopiedConfig(false), 2000);
} catch (err) { } catch (err) {
showToast(t('common.copyFailed') || 'Copy failed', 'error') showToast(t('common.copyFailed') || 'Copy failed', 'error');
console.error('Copy to clipboard failed:', err) console.error('Copy to clipboard failed:', err);
} }
document.body.removeChild(textArea) document.body.removeChild(textArea);
} }
} catch (error) { } catch (error) {
console.error('Error copying configuration:', error) console.error('Error copying configuration:', error);
showToast(t('common.copyFailed') || 'Copy failed', 'error') showToast(t('common.copyFailed') || 'Copy failed', 'error');
}
} }
};
const handleDownloadConfig = () => { const handleDownloadConfig = () => {
if (!mcpSettingsJson) return if (!mcpSettingsJson) return;
const blob = new Blob([mcpSettingsJson], { type: 'application/json' }) const blob = new Blob([mcpSettingsJson], { type: 'application/json' });
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob);
const link = document.createElement('a') const link = document.createElement('a');
link.href = url link.href = url;
link.download = 'mcp_settings.json' link.download = 'mcp_settings.json';
document.body.appendChild(link) document.body.appendChild(link);
link.click() link.click();
document.body.removeChild(link) document.body.removeChild(link);
URL.revokeObjectURL(url) URL.revokeObjectURL(url);
showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success') showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success');
} };
return ( return (
<div className="container mx-auto"> <div className="container mx-auto">
@@ -643,9 +643,7 @@ const SettingsPage: React.FC = () => {
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div> <div>
<h3 className="font-medium text-gray-700"> <h3 className="font-medium text-gray-700">{t('settings.requireClientSecret')}</h3>
{t('settings.requireClientSecret')}
</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t('settings.requireClientSecretDescription')} {t('settings.requireClientSecretDescription')}
</p> </p>
@@ -673,9 +671,7 @@ const SettingsPage: React.FC = () => {
<div className="p-3 bg-gray-50 rounded-md"> <div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2"> <div className="mb-2">
<h3 className="font-medium text-gray-700"> <h3 className="font-medium text-gray-700">{t('settings.accessTokenLifetime')}</h3>
{t('settings.accessTokenLifetime')}
</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t('settings.accessTokenLifetimeDescription')} {t('settings.accessTokenLifetimeDescription')}
</p> </p>
@@ -764,9 +760,7 @@ const SettingsPage: React.FC = () => {
<div className="p-3 bg-gray-50 rounded-md"> <div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2"> <div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.allowedScopes')}</h3> <h3 className="font-medium text-gray-700">{t('settings.allowedScopes')}</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">{t('settings.allowedScopesDescription')}</p>
{t('settings.allowedScopesDescription')}
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@@ -946,6 +940,7 @@ const SettingsPage: React.FC = () => {
</PermissionChecker> </PermissionChecker>
{/* System Settings */} {/* System Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SYSTEM_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card"> <div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div <div
className="flex justify-between items-center cursor-pointer" className="flex justify-between items-center cursor-pointer"
@@ -984,8 +979,12 @@ const SettingsPage: React.FC = () => {
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div> <div>
<h3 className="font-medium text-gray-700">{t('settings.enableSessionRebuild')}</h3> <h3 className="font-medium text-gray-700">
<p className="text-sm text-gray-500">{t('settings.enableSessionRebuildDescription')}</p> {t('settings.enableSessionRebuild')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.enableSessionRebuildDescription')}
</p>
</div> </div>
<Switch <Switch
disabled={loading} disabled={loading}
@@ -996,8 +995,10 @@ const SettingsPage: React.FC = () => {
</div> </div>
)} )}
</div> </div>
</PermissionChecker>
{/* Route Configuration Settings */} {/* Route Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_ROUTE_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card"> <div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div <div
className="flex justify-between items-center cursor-pointer" className="flex justify-between items-center cursor-pointer"
@@ -1012,7 +1013,9 @@ const SettingsPage: React.FC = () => {
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div> <div>
<h3 className="font-medium text-gray-700">{t('settings.enableBearerAuth')}</h3> <h3 className="font-medium text-gray-700">{t('settings.enableBearerAuth')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableBearerAuthDescription')}</p> <p className="text-sm text-gray-500">
{t('settings.enableBearerAuthDescription')}
</p>
</div> </div>
<Switch <Switch
disabled={loading} disabled={loading}
@@ -1027,7 +1030,9 @@ const SettingsPage: React.FC = () => {
<div className="p-3 bg-gray-50 rounded-md"> <div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2"> <div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.bearerAuthKey')}</h3> <h3 className="font-medium text-gray-700">{t('settings.bearerAuthKey')}</h3>
<p className="text-sm text-gray-500">{t('settings.bearerAuthKeyDescription')}</p> <p className="text-sm text-gray-500">
{t('settings.bearerAuthKeyDescription')}
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@@ -1067,7 +1072,9 @@ const SettingsPage: React.FC = () => {
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div> <div>
<h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3> <h3 className="font-medium text-gray-700">
{t('settings.enableGroupNameRoute')}
</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{t('settings.enableGroupNameRouteDescription')} {t('settings.enableGroupNameRouteDescription')}
</p> </p>
@@ -1081,7 +1088,6 @@ const SettingsPage: React.FC = () => {
/> />
</div> </div>
<PermissionChecker permissions={PERMISSIONS.SETTINGS_SKIP_AUTH}>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md"> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div> <div>
<h3 className="font-medium text-gray-700">{t('settings.skipAuth')}</h3> <h3 className="font-medium text-gray-700">{t('settings.skipAuth')}</h3>
@@ -1093,10 +1099,10 @@ const SettingsPage: React.FC = () => {
onCheckedChange={(checked) => handleRoutingConfigChange('skipAuth', checked)} onCheckedChange={(checked) => handleRoutingConfigChange('skipAuth', checked)}
/> />
</div> </div>
</PermissionChecker>
</div> </div>
)} )}
</div> </div>
</PermissionChecker>
{/* Installation Configuration Settings */} {/* Installation Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}> <PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
@@ -1188,7 +1194,10 @@ const SettingsPage: React.FC = () => {
</PermissionChecker> </PermissionChecker>
{/* Change Password */} {/* Change Password */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card" data-section="password"> <div
className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card"
data-section="password"
>
<div <div
className="flex justify-between items-center cursor-pointer" className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('password')} onClick={() => toggleSection('password')}
@@ -1258,7 +1267,7 @@ const SettingsPage: React.FC = () => {
</div> </div>
</PermissionChecker> </PermissionChecker>
</div> </div>
) );
} };
export default SettingsPage export default SettingsPage;

View File

@@ -26,7 +26,8 @@ export class DataServicex implements DataService {
return result; return result;
} else { } else {
const result = { ...settings }; 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; delete result.userConfigs;
return result; return result;
} }
@@ -53,10 +54,7 @@ export class DataServicex implements DataService {
const userConfig: UserConfig = { const userConfig: UserConfig = {
routing: systemConfig.routing routing: systemConfig.routing
? { ? {
enableGlobalRoute: systemConfig.routing.enableGlobalRoute, // TODO: only allow modifying certain fields based on userConfig permissions
enableGroupNameRoute: systemConfig.routing.enableGroupNameRoute,
enableBearerAuth: systemConfig.routing.enableBearerAuth,
bearerAuthKey: systemConfig.routing.bearerAuthKey,
} }
: undefined, : undefined,
}; };

View File

@@ -1,5 +1,5 @@
import { createRequire } from 'module';
import { join } from 'path'; import { join } from 'path';
import { pathToFileURL } from 'url';
type Class<T> = new (...args: any[]) => T; type Class<T> = new (...args: any[]) => T;
@@ -11,7 +11,24 @@ interface Service<T> {
const registry = new Map<string, Service<any>>(); const registry = new Map<string, Service<any>>();
const instances = new Map<string, unknown>(); const instances = new Map<string, unknown>();
export function registerService<T>(key: string, entry: Service<T>) { async function tryLoadOverride<T>(key: string, overridePath: string): Promise<Class<T> | 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<T>;
}
} 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<T>(key: string, entry: Service<T>) {
// Try to load override immediately during registration // Try to load override immediately during registration
// Try multiple paths and file extensions in order // Try multiple paths and file extensions in order
const serviceDirs = ['src/services', 'dist/services']; const serviceDirs = ['src/services', 'dist/services'];
@@ -22,19 +39,11 @@ export function registerService<T>(key: string, entry: Service<T>) {
for (const fileExt of fileExts) { for (const fileExt of fileExts) {
const overridePath = join(process.cwd(), serviceDir, overrideFileName + fileExt); const overridePath = join(process.cwd(), serviceDir, overrideFileName + fileExt);
try { const override = await tryLoadOverride<T>(key, overridePath);
// Use createRequire with a stable path reference if (override) {
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; entry.override = override;
break; // Found override, exit both loops break; // Found override, exit both loops
} }
} catch (error) {
// Continue trying next path/extension combination
continue;
}
} }
// If override was found, break out of outer loop too // If override was found, break out of outer loop too

View File

@@ -1,7 +1,7 @@
import { registerService, getService } from './registry.js'; import { registerService, getService } from './registry.js';
import { DataService, DataServiceImpl } from './dataService.js'; import { DataService, DataServiceImpl } from './dataService.js';
registerService('dataService', { await registerService('dataService', {
defaultImpl: DataServiceImpl, defaultImpl: DataServiceImpl,
}); });

View File

@@ -175,14 +175,7 @@ export interface SystemConfig {
enableSessionRebuild?: boolean; // Controls whether server session rebuild is enabled enableSessionRebuild?: boolean; // Controls whether server session rebuild is enabled
} }
export interface UserConfig { 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
};
}
// OAuth Client for MCPHub's own authorization server // OAuth Client for MCPHub's own authorization server
export interface IOAuthClient { export interface IOAuthClient {