♻️ refactor(oauth2): restructure OAuth2 client settings UI and extract modal components

- **UI Restructuring:**
  - Separate client info into individual table columns (name, ID, description)
  - Replace icon-only action buttons with text labels for better UX
  - Adjust table scroll width from 1000px to 1200px for new column layout
  - Remove unnecessary Tooltip wrappers and Lucide icons (Edit, Key, Trash2)

- **Component Architecture:**
  - Extract all modal dialogs into separate reusable components:
    * SecretDisplayModal.jsx - for displaying regenerated client secrets
    * ServerInfoModal.jsx - for OAuth2 server configuration info
    * JWKSInfoModal.jsx - for JWKS key set information
  - Simplify main component by removing ~60 lines of inline modal code
  - Implement proper state management for each modal component

- **Code Quality:**
  - Remove unused imports and clean up component dependencies
  - Consolidate modal logic into dedicated components with error handling
  - Improve code maintainability and reusability across the application

- **Internationalization:**
  - Add English translation for '客户端名称': 'Client Name'
  - Remove duplicate translation keys to fix linter warnings
  - Ensure all new components support full i18n functionality

- **User Experience:**
  - Enhance table readability with dedicated columns for each data type
  - Maintain copyable client ID functionality in separate column
  - Improve action button accessibility with clear text labels
  - Add loading states and proper error handling in modal components

This refactoring improves code organization, enhances user experience, and follows React best practices for component composition and separation of concerns.
This commit is contained in:
t0ng7u
2025-09-20 22:52:50 +08:00
parent 926cad87b3
commit 81272da9ac
22 changed files with 2105 additions and 1556 deletions

View File

@@ -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 (
<div>
{/* OAuth2 & SSO 管理 */}
{/* OAuth2 服务端管理 */}
<Card
className='!rounded-2xl shadow-sm border-0'
style={{ marginTop: 10 }}
title={
<div className='flex items-center'>
<Server size={18} className='mr-2' />
<Text strong>{t('OAuth2 & SSO 管理')}</Text>
<div
className='flex flex-col sm:flex-row sm:items-center sm:justify-between w-full gap-3 sm:gap-0'
style={{ paddingRight: '8px' }}
>
<div className='flex items-center'>
<Server size={18} className='mr-2' />
<Text strong>{t('OAuth2 & SSO 管理')}</Text>
{isEnabled ? (
serverInfo ? (
<Badge
count={t('运行正常')}
type='success'
style={{ marginLeft: 8 }}
/>
) : (
<Badge
count={t('配置中')}
type='warning'
style={{ marginLeft: 8 }}
/>
)
) : (
<Badge
count={t('未启用')}
type='tertiary'
style={{ marginLeft: 8 }}
/>
)}
</div>
<div className='flex items-center gap-2 sm:flex-shrink-0'>
<Button
type='primary'
onClick={onSubmit}
loading={loading}
size='small'
>
{t('保存配置')}
</Button>
{isEnabled && (
<Button
type='secondary'
onClick={() => setJwksVisible(true)}
size='small'
>
{t('密钥管理')}
</Button>
)}
</div>
</div>
}
>
<div style={{ marginBottom: 16 }}>
<Text type="tertiary">
{t('OAuth2 是一个开放标准的授权框架,允许用户授权第三方应用访问他们的资源,而无需分享他们的凭据。支持标准的 API 认证与授权流程。')}
</Text>
</div>
{!isEnabled && (
<Banner
type="info"
icon={<Settings size={16} />}
description={t('OAuth2 功能尚未启用,建议使用一键初始化向导完成基础配置。')}
style={{ marginBottom: 16 }}
/>
)}
{/* 快捷操作按钮 */}
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
<Col xs={12} sm={6} md={6} lg={6}>
<Button
type="primary"
icon={<PlayCircle size={16} />}
onClick={() => setQsVisible(true)}
style={{ width: '100%' }}
>
<span className="hidden sm:inline">{t('一键初始化')}</span>
</Button>
</Col>
<Col xs={12} sm={6} md={6} lg={6}>
<Button
icon={<Key size={16} />}
onClick={() => setJwksVisible(true)}
style={{ width: '100%' }}
>
<span className="hidden sm:inline">{t('密钥管理')}</span>
</Button>
</Col>
<Col xs={12} sm={6} md={6} lg={6}>
<Button
icon={<Wrench size={16} />}
onClick={() => setToolsVisible(true)}
style={{ width: '100%' }}
>
<span className="hidden sm:inline">{t('调试助手')}</span>
</Button>
</Col>
<Col xs={12} sm={6} md={6} lg={6}>
<Button
icon={<BookOpen size={16} />}
onClick={() => window.open('/oauth-demo.html', '_blank')}
style={{ width: '100%' }}
>
<span className="hidden sm:inline">{t('前端演示')}</span>
</Button>
</Col>
</Row>
<Form
initValues={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
@@ -264,17 +272,18 @@ export default function OAuth2ServerSettings(props) {
{!keysReady && isEnabled && (
<Banner
type='warning'
icon={<AlertTriangle size={16} />}
description={
<div>
<div> 尚未准备签名密钥建议立即初始化或轮换以发布 JWKS</div>
<div>
尚未准备签名密钥建议立即初始化或轮换以发布 JWKS
</div>
<div>签名密钥用于 JWT 令牌的安全签发</div>
</div>
}
actions={
<Button
size='small'
type="primary"
type='primary'
onClick={() => setJwksVisible(true)}
loading={keysLoading}
>
@@ -289,18 +298,10 @@ export default function OAuth2ServerSettings(props) {
<Col xs={24} lg={12}>
<Form.Switch
field='oauth2.enabled'
label={
<span style={{ display: 'flex', alignItems: 'center' }}>
<Shield size={16} style={{ marginRight: 4 }} />
{t('启用 OAuth2 & SSO')}
</span>
}
checkedText={t('开')}
uncheckedText={t('关')}
label={t('启用 OAuth2 & SSO')}
value={inputs['oauth2.enabled']}
onChange={handleFieldChange('oauth2.enabled')}
extraText={t("开启后将允许以 OAuth2/OIDC 标准进行授权与登录")}
size="large"
extraText={t('开启后将允许以 OAuth2/OIDC 标准进行授权与登录')}
/>
</Col>
<Col xs={24} lg={12}>
@@ -310,227 +311,177 @@ export default function OAuth2ServerSettings(props) {
placeholder={window.location.origin}
value={inputs['oauth2.issuer']}
onChange={handleFieldChange('oauth2.issuer')}
extraText={t("为空则按请求自动推断(含 X-Forwarded-Proto")}
extraText={t('为空则按请求自动推断(含 X-Forwarded-Proto')}
/>
</Col>
</Row>
{/* 服务器状态 */}
{isEnabled && serverInfo && (
<div style={{
marginTop: 16,
padding: '12px 16px',
backgroundColor: 'var(--semi-color-success-light-default)',
borderRadius: '8px',
border: '1px solid var(--semi-color-success-light-active)'
}}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<CheckCircle size={16} style={{ marginRight: 6, color: 'var(--semi-color-success)' }} />
<Text strong style={{ color: 'var(--semi-color-success)' }}>{t('服务器运行正常')}</Text>
</div>
<Space wrap>
<Tag color="green">{t('发行人')}: {serverInfo.issuer}</Tag>
{serverInfo.authorization_endpoint && <Tag>{t('授权端点')}: {t('已配置')}</Tag>}
{serverInfo.token_endpoint && <Tag>{t('令牌端点')}: {t('已配置')}</Tag>}
{serverInfo.jwks_uri && <Tag>JWKS: {t('已配置')}</Tag>}
</Space>
</div>
)}
{/* 令牌配置 */}
<Divider margin='24px'>{t('令牌配置')}</Divider>
<Row gutter={[16, 24]}>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.access_token_ttl'
label={t('访问令牌有效期')}
suffix={t('分钟')}
min={1}
max={1440}
value={inputs['oauth2.access_token_ttl']}
onChange={handleFieldChange('oauth2.access_token_ttl')}
extraText={t('访问令牌的有效时间建议较短10-60分钟')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.refresh_token_ttl'
label={t('刷新令牌有效期')}
suffix={t('小时')}
min={1}
max={8760}
value={inputs['oauth2.refresh_token_ttl']}
onChange={handleFieldChange('oauth2.refresh_token_ttl')}
extraText={t('刷新令牌的有效时间建议较长12-720小时')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.max_jwks_keys'
label={t('JWKS历史保留上限')}
min={1}
max={10}
value={inputs['oauth2.max_jwks_keys']}
onChange={handleFieldChange('oauth2.max_jwks_keys')}
extraText={t('轮换后最多保留的历史签名密钥数量')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
/>
</Col>
</Row>
<Row gutter={[16, 24]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<Form.Select
field='oauth2.jwt_signing_algorithm'
label={t('JWT签名算法')}
value={inputs['oauth2.jwt_signing_algorithm']}
onChange={handleFieldChange('oauth2.jwt_signing_algorithm')}
extraText={t('JWT令牌的签名算法推荐使用RS256')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
>
<Form.Select.Option value='RS256'>
RS256 (RSA with SHA-256)
</Form.Select.Option>
<Form.Select.Option value='HS256'>
HS256 (HMAC with SHA-256)
</Form.Select.Option>
</Form.Select>
</Col>
<Col xs={24} lg={12}>
<Form.Input
field='oauth2.jwt_key_id'
label={t('JWT密钥ID')}
placeholder='oauth2-key-1'
value={inputs['oauth2.jwt_key_id']}
onChange={handleFieldChange('oauth2.jwt_key_id')}
extraText={t('用于标识JWT签名密钥支持密钥轮换')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
/>
</Col>
</Row>
{/* 授权配置 */}
<Divider margin='24px'>{t('授权配置')}</Divider>
<Row gutter={[16, 24]}>
<Col xs={24} lg={12}>
<Form.Select
field='oauth2.allowed_grant_types'
label={t('允许的授权类型')}
multiple
value={inputs['oauth2.allowed_grant_types']}
onChange={handleFieldChange('oauth2.allowed_grant_types')}
extraText={t('选择允许的OAuth2授权流程')}
style={{
width: '100%',
opacity: isEnabled ? 1 : 0.5,
}}
disabled={!isEnabled}
>
<Form.Select.Option value='client_credentials'>
{t('Client Credentials客户端凭证')}
</Form.Select.Option>
<Form.Select.Option value='authorization_code'>
{t('Authorization Code授权码')}
</Form.Select.Option>
<Form.Select.Option value='refresh_token'>
{t('Refresh Token刷新令牌')}
</Form.Select.Option>
</Form.Select>
</Col>
<Col xs={24} lg={12}>
<Form.Switch
field='oauth2.require_pkce'
label={t('强制PKCE验证')}
value={inputs['oauth2.require_pkce']}
onChange={handleFieldChange('oauth2.require_pkce')}
extraText={t('为授权码流程强制启用PKCE提高安全性')}
disabled={!isEnabled}
/>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button type="primary" onClick={onSubmit} loading={loading}>
{t('保存基础配置')}
</Button>
{isEnabled && (
<Button
type="secondary"
onClick={testOAuth2}
style={{ marginLeft: 8 }}
>
测试连接
</Button>
)}
<Text type='tertiary' size='small'>
<div className='space-y-1'>
<div> {t('OAuth2 服务器提供标准的 API 认证与授权')}</div>
<div>
{' '}
{t(
'支持 Client Credentials、Authorization Code + PKCE 等标准流程',
)}
</div>
<div>
{' '}
{t(
'配置保存后多数项即时生效;签名密钥轮换与 JWKS 发布为即时操作',
)}
</div>
<div>
{t('生产环境务必启用 HTTPS并妥善管理 JWT 签名密钥')}
</div>
</div>
</Text>
</div>
</Form>
</Card>
{/* 高级配置 */}
{isEnabled && (
<>
{/* 令牌配置 */}
<Card
className='!rounded-2xl shadow-sm border-0'
style={{ marginTop: 10 }}
title={
<div className='flex items-center'>
<Key size={18} className='mr-2' />
<Text strong>{t('令牌配置')}</Text>
</div>
}
footer={
<Text type='tertiary' size='small'>
<div className='space-y-1'>
<div> {t('OAuth2 服务器提供标准的 API 认证与授权')}</div>
<div> {t('支持 Client Credentials、Authorization Code + PKCE 等标准流程')}</div>
<div> {t('配置保存后多数项即时生效;签名密钥轮换与 JWKS 发布为即时操作')}</div>
<div> {t('生产环境务必启用 HTTPS并妥善管理 JWT 签名密钥')}</div>
</div>
</Text>
}
>
<Form initValues={inputs}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.access_token_ttl'
label={t('访问令牌有效期')}
suffix={t("分钟")}
min={1}
max={1440}
value={inputs['oauth2.access_token_ttl']}
onChange={handleFieldChange('oauth2.access_token_ttl')}
extraText={t("访问令牌的有效时间建议较短10-60分钟")}
style={{ width: '100%' }}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.refresh_token_ttl'
label={t('刷新令牌有效期')}
suffix={t("小时")}
min={1}
max={8760}
value={inputs['oauth2.refresh_token_ttl']}
onChange={handleFieldChange('oauth2.refresh_token_ttl')}
extraText={t("刷新令牌的有效时间建议较长12-720小时")}
style={{ width: '100%' }}
/>
</Col>
<Col xs={24} sm={12} lg={8}>
<Form.InputNumber
field='oauth2.max_jwks_keys'
label={t('JWKS历史保留上限')}
min={1}
max={10}
value={inputs['oauth2.max_jwks_keys']}
onChange={handleFieldChange('oauth2.max_jwks_keys')}
extraText={t("轮换后最多保留的历史签名密钥数量")}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Row gutter={[16, 24]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<Form.Select
field='oauth2.jwt_signing_algorithm'
label={t('JWT签名算法')}
value={inputs['oauth2.jwt_signing_algorithm']}
onChange={handleFieldChange('oauth2.jwt_signing_algorithm')}
extraText={t("JWT令牌的签名算法推荐使用RS256")}
style={{ width: '100%' }}
>
<Form.Select.Option value="RS256">RS256 (RSA with SHA-256)</Form.Select.Option>
<Form.Select.Option value="HS256">HS256 (HMAC with SHA-256)</Form.Select.Option>
</Form.Select>
</Col>
<Col xs={24} lg={12}>
<Form.Input
field='oauth2.jwt_key_id'
label={t('JWT密钥ID')}
placeholder="oauth2-key-1"
value={inputs['oauth2.jwt_key_id']}
onChange={handleFieldChange('oauth2.jwt_key_id')}
extraText={t("用于标识JWT签名密钥支持密钥轮换")}
style={{ width: '100%' }}
/>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button type="primary" onClick={onSubmit} loading={loading}>
{t('更新令牌配置')}
</Button>
<Button
type='secondary'
onClick={() => setJwksVisible(true)}
style={{ marginLeft: 8 }}
>
密钥管理
</Button>
</div>
</Form>
</Card>
{/* 授权配置 */}
<Card
className='!rounded-2xl shadow-sm border-0'
style={{ marginTop: 10 }}
title={
<div className='flex items-center'>
<Settings size={18} className='mr-2' />
<Text strong>{t('授权配置')}</Text>
</div>
}
>
<Form initValues={inputs}>
<Row gutter={[16, 24]}>
<Col xs={24} lg={12}>
<Form.Select
field='oauth2.allowed_grant_types'
label={t('允许的授权类型')}
multiple
value={inputs['oauth2.allowed_grant_types']}
onChange={handleFieldChange('oauth2.allowed_grant_types')}
extraText={t("选择允许的OAuth2授权流程")}
style={{ width: '100%' }}
>
<Form.Select.Option value="client_credentials">{t('Client Credentials客户端凭证')}</Form.Select.Option>
<Form.Select.Option value="authorization_code">{t('Authorization Code授权码')}</Form.Select.Option>
<Form.Select.Option value="refresh_token">{t('Refresh Token刷新令牌')}</Form.Select.Option>
</Form.Select>
</Col>
<Col xs={24} lg={12}>
<Form.Switch
field='oauth2.require_pkce'
label={t('强制PKCE验证')}
checkedText={t('开')}
uncheckedText={t('关')}
value={inputs['oauth2.require_pkce']}
onChange={handleFieldChange('oauth2.require_pkce')}
extraText={t("为授权码流程强制启用PKCE提高安全性")}
size="large"
/>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button type="primary" onClick={onSubmit} loading={loading}>
{t('更新授权配置')}
</Button>
</div>
</Form>
</Card>
</>
)}
{/* 模态框 */}
<OAuth2QuickStartModal
visible={qsVisible}
onClose={() => setQsVisible(false)}
onDone={() => { props?.refresh && props.refresh(); }}
/>
<JWKSManagerModal
visible={jwksVisible}
onClose={() => setJwksVisible(false)}
/>
<OAuth2ToolsModal
visible={toolsVisible}
onClose={() => setToolsVisible(false)}
/>
</div>
);
}