Add OAuth 2.0 authorization server to enable ChatGPT Web integration (#413)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
This commit is contained in:
Copilot
2025-11-21 13:25:02 +08:00
committed by GitHub
parent 1869f283ba
commit 449e6ea4fd
34 changed files with 4930 additions and 103 deletions

View File

@@ -4,6 +4,7 @@ export const PERMISSIONS = {
SETTINGS_SMART_ROUTING: 'settings:smart_routing',
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
SETTINGS_OAUTH_SERVER: 'settings:oauth_server',
SETTINGS_EXPORT_CONFIG: 'settings:export_config',
} as const;

View File

@@ -34,6 +34,21 @@ interface MCPRouterConfig {
baseUrl: string;
}
interface OAuthServerConfig {
enabled: boolean;
accessTokenLifetime: number;
refreshTokenLifetime: number;
authorizationCodeLifetime: number;
requireClientSecret: boolean;
allowedScopes: string[];
requireState: boolean;
dynamicRegistration: {
enabled: boolean;
allowedGrantTypes: string[];
requiresAuthentication: boolean;
};
}
interface SystemSettings {
systemConfig?: {
routing?: RoutingConfig;
@@ -41,6 +56,7 @@ interface SystemSettings {
smartRouting?: SmartRoutingConfig;
mcpRouter?: MCPRouterConfig;
nameSeparator?: string;
oauthServer?: OAuthServerConfig;
enableSessionRebuild?: boolean;
};
}
@@ -49,6 +65,21 @@ interface TempRoutingConfig {
bearerAuthKey: string;
}
const getDefaultOAuthServerConfig = (): OAuthServerConfig => ({
enabled: true,
accessTokenLifetime: 3600,
refreshTokenLifetime: 1209600,
authorizationCodeLifetime: 300,
requireClientSecret: false,
allowedScopes: ['read', 'write'],
requireState: false,
dynamicRegistration: {
enabled: true,
allowedGrantTypes: ['authorization_code', 'refresh_token'],
requiresAuthentication: false,
},
});
export const useSettingsData = () => {
const { t } = useTranslation();
const { showToast } = useToast();
@@ -86,6 +117,10 @@ export const useSettingsData = () => {
baseUrl: 'https://api.mcprouter.to/v1',
});
const [oauthServerConfig, setOAuthServerConfig] = useState<OAuthServerConfig>(
getDefaultOAuthServerConfig(),
);
const [nameSeparator, setNameSeparator] = useState<string>('-');
const [enableSessionRebuild, setEnableSessionRebuild] = useState<boolean>(false);
@@ -140,6 +175,44 @@ export const useSettingsData = () => {
baseUrl: data.data.systemConfig.mcpRouter.baseUrl || 'https://api.mcprouter.to/v1',
});
}
if (data.success) {
if (data.data?.systemConfig?.oauthServer) {
const oauth = data.data.systemConfig.oauthServer;
const defaultOauthConfig = getDefaultOAuthServerConfig();
const defaultDynamic = defaultOauthConfig.dynamicRegistration;
const allowedScopes = Array.isArray(oauth.allowedScopes)
? [...oauth.allowedScopes]
: [...defaultOauthConfig.allowedScopes];
const dynamicAllowedGrantTypes = Array.isArray(
oauth.dynamicRegistration?.allowedGrantTypes,
)
? [...oauth.dynamicRegistration!.allowedGrantTypes!]
: [...defaultDynamic.allowedGrantTypes];
setOAuthServerConfig({
enabled: oauth.enabled ?? defaultOauthConfig.enabled,
accessTokenLifetime:
oauth.accessTokenLifetime ?? defaultOauthConfig.accessTokenLifetime,
refreshTokenLifetime:
oauth.refreshTokenLifetime ?? defaultOauthConfig.refreshTokenLifetime,
authorizationCodeLifetime:
oauth.authorizationCodeLifetime ?? defaultOauthConfig.authorizationCodeLifetime,
requireClientSecret:
oauth.requireClientSecret ?? defaultOauthConfig.requireClientSecret,
requireState: oauth.requireState ?? defaultOauthConfig.requireState,
allowedScopes,
dynamicRegistration: {
enabled: oauth.dynamicRegistration?.enabled ?? defaultDynamic.enabled,
allowedGrantTypes: dynamicAllowedGrantTypes,
requiresAuthentication:
oauth.dynamicRegistration?.requiresAuthentication ??
defaultDynamic.requiresAuthentication,
},
});
} else {
setOAuthServerConfig(getDefaultOAuthServerConfig());
}
}
if (data.success && data.data?.systemConfig?.nameSeparator !== undefined) {
setNameSeparator(data.data.systemConfig.nameSeparator);
}
@@ -395,6 +468,77 @@ export const useSettingsData = () => {
}
};
// Update OAuth server configuration
const updateOAuthServerConfig = async <T extends keyof OAuthServerConfig>(
key: T,
value: OAuthServerConfig[T],
) => {
setLoading(true);
setError(null);
try {
const data = await apiPut('/system-config', {
oauthServer: {
[key]: value,
},
});
if (data.success) {
setOAuthServerConfig((prev) => ({
...prev,
[key]: value,
}));
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
return false;
}
} catch (error) {
console.error('Failed to update OAuth server config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update OAuth server config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Update multiple OAuth server config fields
const updateOAuthServerConfigBatch = async (updates: Partial<OAuthServerConfig>) => {
setLoading(true);
setError(null);
try {
const data = await apiPut('/system-config', {
oauthServer: updates,
});
if (data.success) {
setOAuthServerConfig((prev) => ({
...prev,
...updates,
}));
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
return false;
}
} catch (error) {
console.error('Failed to update OAuth server config:', error);
const errorMessage =
error instanceof Error ? error.message : 'Failed to update OAuth server config';
setError(errorMessage);
showToast(errorMessage);
return false;
} finally {
setLoading(false);
}
};
// Update name separator
const updateNameSeparator = async (value: string) => {
setLoading(true);
@@ -490,6 +634,7 @@ export const useSettingsData = () => {
installConfig,
smartRoutingConfig,
mcpRouterConfig,
oauthServerConfig,
nameSeparator,
enableSessionRebuild,
loading,
@@ -504,6 +649,8 @@ export const useSettingsData = () => {
updateRoutingConfigBatch,
updateMCPRouterConfig,
updateMCPRouterConfigBatch,
updateOAuthServerConfig,
updateOAuthServerConfigBatch,
updateNameSeparator,
updateSessionRebuild,
exportMCPSettings,

View File

@@ -1,11 +1,34 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useState, useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { getToken } from '../services/authService';
import ThemeSwitch from '@/components/ui/ThemeSwitch';
import LanguageSwitch from '@/components/ui/LanguageSwitch';
import DefaultPasswordWarningModal from '@/components/ui/DefaultPasswordWarningModal';
const sanitizeReturnUrl = (value: string | null): string | null => {
if (!value) {
return null;
}
try {
// Support both relative paths and absolute URLs on the same origin
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const url = new URL(value, origin);
if (url.origin !== origin) {
return null;
}
const relativePath = `${url.pathname}${url.search}${url.hash}`;
return relativePath || '/';
} catch {
if (value.startsWith('/') && !value.startsWith('//')) {
return value;
}
return null;
}
};
const LoginPage: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
@@ -14,7 +37,46 @@ const LoginPage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [showDefaultPasswordWarning, setShowDefaultPasswordWarning] = useState(false);
const { login } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const returnUrl = useMemo(() => {
const params = new URLSearchParams(location.search);
return sanitizeReturnUrl(params.get('returnUrl'));
}, [location.search]);
const buildRedirectTarget = useCallback(() => {
if (!returnUrl) {
return '/';
}
// Only attach JWT when returning to the OAuth authorize endpoint
if (!returnUrl.startsWith('/oauth/authorize')) {
return returnUrl;
}
const token = getToken();
if (!token) {
return returnUrl;
}
try {
const origin = window.location.origin;
const url = new URL(returnUrl, origin);
url.searchParams.set('token', token);
return `${url.pathname}${url.search}${url.hash}`;
} catch {
const separator = returnUrl.includes('?') ? '&' : '?';
return `${returnUrl}${separator}token=${encodeURIComponent(token)}`;
}
}, [returnUrl]);
const redirectAfterLogin = useCallback(() => {
if (returnUrl) {
window.location.assign(buildRedirectTarget());
} else {
navigate('/');
}
}, [buildRedirectTarget, navigate, returnUrl]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -35,7 +97,7 @@ const LoginPage: React.FC = () => {
// Show warning modal instead of navigating immediately
setShowDefaultPasswordWarning(true);
} else {
navigate('/');
redirectAfterLogin();
}
} else {
setError(t('auth.loginFailed'));
@@ -49,7 +111,7 @@ const LoginPage: React.FC = () => {
const handleCloseWarning = () => {
setShowDefaultPasswordWarning(false);
navigate('/');
redirectAfterLogin();
};
return (
@@ -160,4 +222,4 @@ const LoginPage: React.FC = () => {
);
};
export default LoginPage;
export default LoginPage;

View File

@@ -49,6 +49,20 @@ const SettingsPage: React.FC = () => {
baseUrl: 'https://api.mcprouter.to/v1',
})
const [tempOAuthServerConfig, setTempOAuthServerConfig] = useState<{
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<string>('-')
const {
@@ -58,6 +72,7 @@ const SettingsPage: React.FC = () => {
installConfig: savedInstallConfig,
smartRoutingConfig,
mcpRouterConfig,
oauthServerConfig,
nameSeparator,
enableSessionRebuild,
loading,
@@ -67,6 +82,7 @@ const SettingsPage: React.FC = () => {
updateSmartRoutingConfig,
updateSmartRoutingConfigBatch,
updateMCPRouterConfig,
updateOAuthServerConfig,
updateNameSeparator,
updateSessionRebuild,
exportMCPSettings,
@@ -103,6 +119,33 @@ const SettingsPage: React.FC = () => {
}
}, [mcpRouterConfig])
useEffect(() => {
if (oauthServerConfig) {
setTempOAuthServerConfig({
accessTokenLifetime:
oauthServerConfig.accessTokenLifetime !== undefined
? String(oauthServerConfig.accessTokenLifetime)
: '',
refreshTokenLifetime:
oauthServerConfig.refreshTokenLifetime !== undefined
? String(oauthServerConfig.refreshTokenLifetime)
: '',
authorizationCodeLifetime:
oauthServerConfig.authorizationCodeLifetime !== undefined
? String(oauthServerConfig.authorizationCodeLifetime)
: '',
allowedScopes:
oauthServerConfig.allowedScopes && oauthServerConfig.allowedScopes.length > 0
? oauthServerConfig.allowedScopes.join(', ')
: '',
dynamicRegistrationAllowedGrantTypes:
oauthServerConfig.dynamicRegistration?.allowedGrantTypes?.length
? oauthServerConfig.dynamicRegistration.allowedGrantTypes.join(', ')
: '',
})
}
}, [oauthServerConfig])
// Update local tempNameSeparator when nameSeparator changes
useEffect(() => {
setTempNameSeparator(nameSeparator)
@@ -112,6 +155,7 @@ const SettingsPage: React.FC = () => {
routingConfig: false,
installConfig: false,
smartRoutingConfig: false,
oauthServerConfig: false,
mcpRouterConfig: false,
nameSeparator: false,
password: false,
@@ -123,6 +167,7 @@ const SettingsPage: React.FC = () => {
| 'routingConfig'
| 'installConfig'
| 'smartRoutingConfig'
| 'oauthServerConfig'
| 'mcpRouterConfig'
| 'nameSeparator'
| 'password'
@@ -224,6 +269,81 @@ const SettingsPage: React.FC = () => {
await updateMCPRouterConfig(key, tempMCPRouterConfig[key])
}
type OAuthServerNumberField =
| 'accessTokenLifetime'
| 'refreshTokenLifetime'
| 'authorizationCodeLifetime'
const handleOAuthServerNumberChange = (key: OAuthServerNumberField, value: string) => {
setTempOAuthServerConfig((prev) => ({
...prev,
[key]: value,
}))
}
const handleOAuthServerTextChange = (
key: 'allowedScopes' | 'dynamicRegistrationAllowedGrantTypes',
value: string,
) => {
setTempOAuthServerConfig((prev) => ({
...prev,
[key]: value,
}))
}
const saveOAuthServerNumberConfig = async (key: OAuthServerNumberField) => {
const rawValue = tempOAuthServerConfig[key]
if (!rawValue || rawValue.trim() === '') {
showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error')
return
}
const parsedValue = Number(rawValue)
if (Number.isNaN(parsedValue) || parsedValue < 0) {
showToast(t('settings.invalidNumberInput') || 'Please enter a valid number', 'error')
return
}
await updateOAuthServerConfig(key, parsedValue)
}
const saveOAuthServerAllowedScopes = async () => {
const scopes = tempOAuthServerConfig.allowedScopes
.split(',')
.map((scope) => scope.trim())
.filter((scope) => scope.length > 0)
await updateOAuthServerConfig('allowedScopes', scopes)
}
const saveOAuthServerGrantTypes = async () => {
const grantTypes = tempOAuthServerConfig.dynamicRegistrationAllowedGrantTypes
.split(',')
.map((grant) => grant.trim())
.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)
}
const handleDynamicRegistrationToggle = async (
updates: Partial<typeof oauthServerConfig.dynamicRegistration>,
) => {
await updateOAuthServerConfig('dynamicRegistration', {
...oauthServerConfig.dynamicRegistration,
...updates,
})
}
const saveNameSeparator = async () => {
await updateNameSeparator(tempNameSeparator)
}
@@ -494,6 +614,266 @@ const SettingsPage: React.FC = () => {
</div>
</PermissionChecker>
{/* OAuth Server Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_OAUTH_SERVER}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('oauthServerConfig')}
>
<h2 className="font-semibold text-gray-800">{t('pages.settings.oauthServer')}</h2>
<span className="text-gray-500">{sectionsVisible.oauthServerConfig ? '▼' : '►'}</span>
</div>
{sectionsVisible.oauthServerConfig && (
<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.enableOauthServer')}</h3>
<p className="text-sm text-gray-500">
{t('settings.enableOauthServerDescription')}
</p>
</div>
<Switch
disabled={loading}
checked={oauthServerConfig.enabled}
onCheckedChange={(checked) => handleOAuthServerToggle('enabled', checked)}
/>
</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.requireClientSecret')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.requireClientSecretDescription')}
</p>
</div>
<Switch
disabled={loading || !oauthServerConfig.enabled}
checked={oauthServerConfig.requireClientSecret}
onCheckedChange={(checked) =>
handleOAuthServerToggle('requireClientSecret', checked)
}
/>
</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.requireState')}</h3>
<p className="text-sm text-gray-500">{t('settings.requireStateDescription')}</p>
</div>
<Switch
disabled={loading || !oauthServerConfig.enabled}
checked={oauthServerConfig.requireState}
onCheckedChange={(checked) => handleOAuthServerToggle('requireState', checked)}
/>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
{t('settings.accessTokenLifetime')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.accessTokenLifetimeDescription')}
</p>
</div>
<div className="flex items-center gap-3">
<input
type="number"
value={tempOAuthServerConfig.accessTokenLifetime}
onChange={(e) =>
handleOAuthServerNumberChange('accessTokenLifetime', e.target.value)
}
placeholder={t('settings.accessTokenLifetimePlaceholder')}
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}
/>
<button
onClick={() => saveOAuthServerNumberConfig('accessTokenLifetime')}
disabled={loading}
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 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
{t('settings.refreshTokenLifetime')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.refreshTokenLifetimeDescription')}
</p>
</div>
<div className="flex items-center gap-3">
<input
type="number"
value={tempOAuthServerConfig.refreshTokenLifetime}
onChange={(e) =>
handleOAuthServerNumberChange('refreshTokenLifetime', e.target.value)
}
placeholder={t('settings.refreshTokenLifetimePlaceholder')}
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}
/>
<button
onClick={() => saveOAuthServerNumberConfig('refreshTokenLifetime')}
disabled={loading}
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 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">
{t('settings.authorizationCodeLifetime')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.authorizationCodeLifetimeDescription')}
</p>
</div>
<div className="flex items-center gap-3">
<input
type="number"
value={tempOAuthServerConfig.authorizationCodeLifetime}
onChange={(e) =>
handleOAuthServerNumberChange('authorizationCodeLifetime', e.target.value)
}
placeholder={t('settings.authorizationCodeLifetimePlaceholder')}
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}
/>
<button
onClick={() => saveOAuthServerNumberConfig('authorizationCodeLifetime')}
disabled={loading}
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 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.allowedScopes')}</h3>
<p className="text-sm text-gray-500">
{t('settings.allowedScopesDescription')}
</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempOAuthServerConfig.allowedScopes}
onChange={(e) => handleOAuthServerTextChange('allowedScopes', e.target.value)}
placeholder={t('settings.allowedScopesPlaceholder')}
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}
/>
<button
onClick={saveOAuthServerAllowedScopes}
disabled={loading}
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 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md space-y-4">
<div className="flex justify-between items-center">
<div>
<h3 className="font-medium text-gray-700">
{t('settings.enableDynamicRegistration')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.dynamicRegistrationDescription')}
</p>
</div>
<Switch
disabled={loading || !oauthServerConfig.enabled}
checked={oauthServerConfig.dynamicRegistration.enabled}
onCheckedChange={(checked) =>
handleDynamicRegistrationToggle({ enabled: checked })
}
/>
</div>
<div>
<div className="mb-2">
<h3 className="font-medium text-gray-700">
{t('settings.dynamicRegistrationAllowedGrantTypes')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.dynamicRegistrationAllowedGrantTypesDescription')}
</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempOAuthServerConfig.dynamicRegistrationAllowedGrantTypes}
onChange={(e) =>
handleOAuthServerTextChange(
'dynamicRegistrationAllowedGrantTypes',
e.target.value,
)
}
placeholder={t('settings.dynamicRegistrationAllowedGrantTypesPlaceholder')}
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 ||
!oauthServerConfig.enabled ||
!oauthServerConfig.dynamicRegistration.enabled
}
/>
<button
onClick={saveOAuthServerGrantTypes}
disabled={
loading ||
!oauthServerConfig.enabled ||
!oauthServerConfig.dynamicRegistration.enabled
}
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 btn-primary"
>
{t('common.save')}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-700">
{t('settings.dynamicRegistrationAuth')}
</h3>
<p className="text-sm text-gray-500">
{t('settings.dynamicRegistrationAuthDescription')}
</p>
</div>
<Switch
disabled={
loading ||
!oauthServerConfig.enabled ||
!oauthServerConfig.dynamicRegistration.enabled
}
checked={oauthServerConfig.dynamicRegistration.requiresAuthentication}
onCheckedChange={(checked) =>
handleDynamicRegistrationToggle({ requiresAuthentication: checked })
}
/>
</div>
</div>
</div>
)}
</div>
</PermissionChecker>
{/* MCPRouter Configuration Settings */}
<PermissionChecker permissions={PERMISSIONS.SETTINGS_INSTALL_CONFIG}>
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">