-
-
- {record.name}
-
-
-
-
- {record.id}
-
-
+ title: t('客户端名称'),
+ dataIndex: 'name',
+ render: (name) => (
+
+
+ {name}
),
+ width: 150,
+ },
+ {
+ title: t('客户端ID'),
+ dataIndex: 'id',
+ render: (id) => (
+
+ {id}
+
+ ),
width: 200,
},
+ {
+ title: t('描述'),
+ dataIndex: 'description',
+ render: (description) => (
+
+ {description || '-'}
+
+ ),
+ width: 150,
+ },
{
title: t('类型'),
dataIndex: 'client_type',
- key: 'client_type',
render: (text) => (
-
@@ -224,24 +182,31 @@ export default function OAuth2ClientSettings() {
{
title: t('授权类型'),
dataIndex: 'grant_types',
- key: 'grant_types',
render: (grantTypes) => {
- const types = typeof grantTypes === 'string' ? grantTypes.split(',') : (grantTypes || []);
+ const types =
+ typeof grantTypes === 'string'
+ ? grantTypes.split(',')
+ : grantTypes || [];
const typeMap = {
- 'client_credentials': t('客户端凭证'),
- 'authorization_code': t('授权码'),
- 'refresh_token': t('刷新令牌')
+ client_credentials: t('客户端凭证'),
+ authorization_code: t('授权码'),
+ refresh_token: t('刷新令牌'),
};
return (
-
- {types.slice(0, 2).map(type => (
-
+
+ {types.slice(0, 2).map((type) => (
+
{typeMap[type] || type}
))}
{types.length > 2 && (
-
typeMap[t] || t).join(', ')}>
-
+ typeMap[t] || t)
+ .join(', ')}
+ >
+
+{types.length - 2}
@@ -254,9 +219,8 @@ export default function OAuth2ClientSettings() {
{
title: t('状态'),
dataIndex: 'status',
- key: 'status',
render: (status) => (
-
@@ -268,78 +232,88 @@ export default function OAuth2ClientSettings() {
{
title: t('创建时间'),
dataIndex: 'created_time',
- key: 'created_time',
render: (time) => new Date(time * 1000).toLocaleString(),
width: 150,
},
{
title: t('操作'),
- key: 'action',
render: (_, record) => (
-
-
- }
- onClick={() => {
- setEditingClient(record);
- setShowEditModal(true);
- }}
- />
-
+
+
{record.client_type === 'confidential' && (
- {t('客户端')}:{record.name}
-
- ⚠️ {t('操作不可撤销,旧密钥将立即失效。')}
+
+
+ {t('客户端')}:
+ {record.name}
+
+
+
+ ⚠️ {t('操作不可撤销,旧密钥将立即失效。')}
+
}
onConfirm={() => handleRegenerateSecret(record)}
okText={t('确认')}
cancelText={t('取消')}
+ position='bottomLeft'
>
-
- }
- />
-
+
)}
- {t('客户端')}:{record.name}
-
- 🗑️ {t('删除后无法恢复,相关 API 调用将立即失效。')}
+
+
+ {t('客户端')}:
+ {record.name}
+
+
+
+ 🗑️ {t('删除后无法恢复,相关 API 调用将立即失效。')}
+
}
onConfirm={() => handleDelete(record)}
okText={t('确定删除')}
cancelText={t('取消')}
+ position='bottomLeft'
>
-
- }
- />
-
+
),
- width: 120,
+ width: 140,
fixed: 'right',
},
];
@@ -349,94 +323,94 @@ export default function OAuth2ClientSettings() {
}, []);
return (
-
-
- {t('OAuth2 客户端管理')}
-
- }
- >
-
-
- {t('管理OAuth2客户端应用程序,每个客户端代表一个可以访问API的应用程序。机密客户端用于服务器端应用,公开客户端用于移动应用或单页应用。')}
-
-
-
- {/* 工具栏 */}
-
-
- }
- placeholder={t('搜索客户端名称、ID或描述')}
- value={searchKeyword}
- onChange={handleSearch}
- showClear
- style={{ width: '100%' }}
- />
-
-
-
-
}
- onClick={loadClients}
- size="default"
- >
-
{t('刷新')}
+
+
+
+ {t('OAuth2 客户端管理')}
+
+ {filteredClients.length} {t('个客户端')}
+
+
+
+ }
+ placeholder={t('搜索客户端名称、ID或描述')}
+ value={searchKeyword}
+ onChange={handleSearch}
+ showClear
+ size='small'
+ style={{ width: 300 }}
+ />
+
- }
- onClick={showServerInfo}
- size="default"
- >
- {t('服务器信息')}
+
- }
- onClick={showJWKS}
- size="default"
- >
- {t('查看JWKS')}
+
}
+ type='primary'
onClick={() => setShowCreateModal(true)}
- size="default"
+ size='small'
>
{t('创建客户端')}
-
-
+
+ }
+ >
+
+
+ {t(
+ '管理OAuth2客户端应用程序,每个客户端代表一个可以访问API的应用程序。机密客户端用于服务器端应用,公开客户端用于移动应用或单页应用。',
+ )}
+
+
{/* 客户端表格 */}
t('第 {{start}}-{{end}} 条,共 {{total}} 条', { start: range[0], end: range[1], total }),
+ showTotal: (total, range) =>
+ t('第 {{start}}-{{end}} 条,共 {{total}} 条', {
+ start: range[0],
+ end: range[1],
+ total,
+ }),
pageSize: 10,
- size: 'small'
+ size: 'small',
+ style: { marginTop: 16 },
}}
- scroll={{ x: 800 }}
empty={
}
+ image={}
title={t('暂无OAuth2客户端')}
- description={t('还没有创建任何客户端,点击下方按钮创建第一个客户端')}
+ description={
+
+ {t('还没有创建任何客户端,点击下方按钮创建第一个客户端')}
+
+ }
>
}
+ type='primary'
onClick={() => setShowCreateModal(true)}
- style={{ marginTop: 16 }}
+ className='mt-4'
>
{t('创建第一个客户端')}
@@ -470,35 +444,23 @@ export default function OAuth2ClientSettings() {
/>
{/* 密钥显示模态框 */}
- setShowSecretModal(false)}
- onOk={() => setShowSecretModal(false)}
- cancelText=""
- okText={t('我已复制保存')}
- width={600}
- >
-
-
-
-
- {currentSecret}
-
-
-
-
+ onClose={() => setShowSecretModal(false)}
+ secret={currentSecret}
+ />
+
+ {/* 服务器信息模态框 */}
+ setShowServerInfoModal(false)}
+ />
+
+ {/* JWKS信息模态框 */}
+ setShowJWKSModal(false)}
+ />
);
}
diff --git a/web/src/components/settings/oauth2/OAuth2ServerSettings.jsx b/web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
index 4f13b8207..d75ba3281 100644
--- a/web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
+++ b/web/src/components/settings/oauth2/OAuth2ServerSettings.jsx
@@ -26,22 +26,10 @@ import {
Row,
Card,
Typography,
- Space,
- Tag
+ Badge,
+ Divider,
} from '@douyinfe/semi-ui';
-import {
- Server,
- Key,
- Shield,
- Settings,
- CheckCircle,
- AlertTriangle,
- PlayCircle,
- Wrench,
- BookOpen
-} from 'lucide-react';
-import OAuth2ToolsModal from './modals/OAuth2ToolsModal';
-import OAuth2QuickStartModal from './modals/OAuth2QuickStartModal';
+import { Server } from 'lucide-react';
import JWKSManagerModal from './modals/JWKSManagerModal';
import {
compareObjects,
@@ -52,7 +40,7 @@ import {
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
-const { Title, Text } = Typography;
+const { Text } = Typography;
export default function OAuth2ServerSettings(props) {
const { t } = useTranslation();
@@ -64,7 +52,11 @@ export default function OAuth2ServerSettings(props) {
'oauth2.refresh_token_ttl': 720,
'oauth2.jwt_signing_algorithm': 'RS256',
'oauth2.jwt_key_id': 'oauth2-key-1',
- 'oauth2.allowed_grant_types': ['client_credentials', 'authorization_code', 'refresh_token'],
+ 'oauth2.allowed_grant_types': [
+ 'client_credentials',
+ 'authorization_code',
+ 'refresh_token',
+ ],
'oauth2.require_pkce': true,
'oauth2.max_jwks_keys': 3,
});
@@ -73,11 +65,10 @@ export default function OAuth2ServerSettings(props) {
const [keysReady, setKeysReady] = useState(true);
const [keysLoading, setKeysLoading] = useState(false);
const [serverInfo, setServerInfo] = useState(null);
+ const enabledRef = useRef(inputs['oauth2.enabled']);
// 模态框状态
- const [qsVisible, setQsVisible] = useState(false);
const [jwksVisible, setJwksVisible] = useState(false);
- const [toolsVisible, setToolsVisible] = useState(false);
function handleFieldChange(fieldName) {
return (value) => {
@@ -124,18 +115,28 @@ export default function OAuth2ServerSettings(props) {
});
}
- // 测试OAuth2连接
- const testOAuth2 = async () => {
+ // 测试OAuth2连接(默认静默,仅用户点击时弹提示)
+ const testOAuth2 = async (silent = true) => {
+ // 未启用时不触发测试,避免 404
+ if (!enabledRef.current) return;
try {
- const res = await API.get('/api/oauth/server-info');
- if (res.status === 200 && (res.data.issuer || res.data.authorization_endpoint)) {
- showSuccess('OAuth2服务器运行正常');
+ const res = await API.get('/api/oauth/server-info', {
+ skipErrorHandler: true,
+ });
+ if (!enabledRef.current) return;
+ if (
+ res.status === 200 &&
+ (res.data.issuer || res.data.authorization_endpoint)
+ ) {
+ if (!silent) showSuccess('OAuth2服务器运行正常');
setServerInfo(res.data);
} else {
- showError('OAuth2服务器测试失败');
+ if (!enabledRef.current) return;
+ if (!silent) showError('OAuth2服务器测试失败');
}
} catch (error) {
- showError('OAuth2服务器连接测试失败');
+ if (!enabledRef.current) return;
+ if (!silent) showError('OAuth2服务器连接测试失败');
}
};
@@ -146,9 +147,16 @@ export default function OAuth2ServerSettings(props) {
if (Object.keys(inputs).includes(key)) {
if (key === 'oauth2.allowed_grant_types') {
try {
- currentInputs[key] = JSON.parse(props.options[key] || '["client_credentials","authorization_code","refresh_token"]');
+ currentInputs[key] = JSON.parse(
+ props.options[key] ||
+ '["client_credentials","authorization_code","refresh_token"]',
+ );
} catch {
- currentInputs[key] = ['client_credentials', 'authorization_code', 'refresh_token'];
+ currentInputs[key] = [
+ 'client_credentials',
+ 'authorization_code',
+ 'refresh_token',
+ ];
}
} else if (typeof inputs[key] === 'boolean') {
currentInputs[key] = props.options[key] === 'true';
@@ -167,11 +175,17 @@ export default function OAuth2ServerSettings(props) {
}
}, [props]);
+ useEffect(() => {
+ enabledRef.current = inputs['oauth2.enabled'];
+ }, [inputs['oauth2.enabled']]);
+
useEffect(() => {
const loadKeys = async () => {
try {
setKeysLoading(true);
- const res = await API.get('/api/oauth/keys', { skipErrorHandler: true });
+ const res = await API.get('/api/oauth/keys', {
+ skipErrorHandler: true,
+ });
const list = res?.data?.data || [];
setKeysReady(list.length > 0);
} catch {
@@ -182,7 +196,12 @@ export default function OAuth2ServerSettings(props) {
};
if (inputs['oauth2.enabled']) {
loadKeys();
- testOAuth2();
+ testOAuth2(true);
+ } else {
+ // 禁用时清理状态,避免残留状态与不必要的请求
+ setKeysReady(true);
+ setServerInfo(null);
+ setKeysLoading(false);
}
}, [inputs['oauth2.enabled']]);
@@ -190,73 +209,62 @@ export default function OAuth2ServerSettings(props) {
return (
- {/* OAuth2 & SSO 管理 */}
+ {/* OAuth2 服务端管理 */}
-
- {t('OAuth2 & SSO 管理')}
+
+
+
+ {t('OAuth2 & SSO 管理')}
+ {isEnabled ? (
+ serverInfo ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+
+ {isEnabled && (
+
+ )}
+
}
>
-
-
- {t('OAuth2 是一个开放标准的授权框架,允许用户授权第三方应用访问他们的资源,而无需分享他们的凭据。支持标准的 API 认证与授权流程。')}
-
-
-
- {!isEnabled && (
- }
- description={t('OAuth2 功能尚未启用,建议使用一键初始化向导完成基础配置。')}
- style={{ marginBottom: 16 }}
- />
- )}
-
- {/* 快捷操作按钮 */}
-
-
- }
- onClick={() => setQsVisible(true)}
- style={{ width: '100%' }}
- >
- {t('一键初始化')}
-
-
-
- }
- onClick={() => setJwksVisible(true)}
- style={{ width: '100%' }}
- >
- {t('密钥管理')}
-
-
-
- }
- onClick={() => setToolsVisible(true)}
- style={{ width: '100%' }}
- >
- {t('调试助手')}
-
-
-
- }
- onClick={() => window.open('/oauth-demo.html', '_blank')}
- style={{ width: '100%' }}
- >
- {t('前端演示')}
-
-
-
-
- {/* 高级配置 */}
- {isEnabled && (
- <>
- {/* 令牌配置 */}
-
-
- {t('令牌配置')}
-
- }
- footer={
-
-
-
• {t('OAuth2 服务器提供标准的 API 认证与授权')}
-
• {t('支持 Client Credentials、Authorization Code + PKCE 等标准流程')}
-
• {t('配置保存后多数项即时生效;签名密钥轮换与 JWKS 发布为即时操作')}
-
• {t('生产环境务必启用 HTTPS,并妥善管理 JWT 签名密钥')}
-
-
- }
- >
-
-
-
-
- {/* 授权配置 */}
-
-
- {t('授权配置')}
-
- }
- >
-
-
-
-
- >
- )}
-
{/* 模态框 */}
- setQsVisible(false)}
- onDone={() => { props?.refresh && props.refresh(); }}
- />
setJwksVisible(false)}
/>
- setToolsVisible(false)}
- />
);
}
diff --git a/web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx b/web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx
index e6d7d8069..771c5e206 100644
--- a/web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx
+++ b/web/src/components/settings/oauth2/modals/CreateOAuth2ClientModal.jsx
@@ -31,7 +31,6 @@ import {
Row,
Col,
} from '@douyinfe/semi-ui';
-import { Plus, Trash2 } from 'lucide-react';
import { API, showError, showSuccess } from '../../../../helpers';
import { useTranslation } from 'react-i18next';
@@ -119,7 +118,9 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
// 仅允许本地开发时使用 http
const host = u.hostname;
const isLocal =
- host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local');
+ host === 'localhost' ||
+ host === '127.0.0.1' ||
+ host.endsWith('.local');
if (!isLocal) return false;
}
return true;
@@ -145,10 +146,15 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
// 校验是否包含不被允许的授权类型
const invalids = grantTypes.filter((g) => !allowedGrantTypes.includes(g));
if (invalids.length) {
- showError(t('不被允许的授权类型: {{types}}', { types: invalids.join(', ') }));
+ showError(
+ t('不被允许的授权类型: {{types}}', { types: invalids.join(', ') }),
+ );
return;
}
- if (clientType === 'public' && grantTypes.includes('client_credentials')) {
+ if (
+ clientType === 'public' &&
+ grantTypes.includes('client_credentials')
+ ) {
showError(t('公开客户端不允许使用client_credentials授权类型'));
return;
}
@@ -173,17 +179,23 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
const res = await API.post('/api/oauth_clients/', payload);
const { success, message, client_id, client_secret } = res.data;
-
+
if (success) {
showSuccess(t('OAuth2客户端创建成功'));
-
+
// 显示客户端信息
Modal.info({
title: t('客户端创建成功'),
content: (
{t('请妥善保存以下信息:')}
-
+
{t('客户端ID')}:
@@ -201,11 +213,10 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
)}
-
- {client_secret
- ? t('客户端密钥仅显示一次,请立即复制保存。')
- : t('公开客户端无需密钥。')
- }
+
+ {client_secret
+ ? t('客户端密钥仅显示一次,请立即复制保存。')
+ : t('公开客户端无需密钥。')}
),
@@ -213,7 +224,7 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
onOk: () => {
resetForm();
onSuccess();
- }
+ },
});
} else {
showError(message);
@@ -280,13 +291,13 @@ const CreateOAuth2ClientModal = ({ visible, onCancel, onSuccess }) => {
okText={t('创建')}
cancelText={t('取消')}
confirmLoading={loading}
- width="90vw"
- style={{
- top: 20,
+ width='90vw'
+ style={{
+ top: 20,
maxWidth: '800px',
'@media (min-width: 768px)': {
- width: '600px'
- }
+ width: '600px',
+ },
}}
>