mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-06-07 22:09:57 +00:00
wip sso
This commit is contained in:
420
web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx
Normal file
420
web/src/pages/Setting/OAuth2/OAuth2ClientSettings.jsx
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Input,
|
||||
Popconfirm,
|
||||
Modal,
|
||||
Form,
|
||||
Banner,
|
||||
Row,
|
||||
Col
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconSearch, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { API, showError, showSuccess, showInfo } from '../../../helpers';
|
||||
import CreateOAuth2ClientModal from '../../../components/modals/oauth2/CreateOAuth2ClientModal';
|
||||
import EditOAuth2ClientModal from '../../../components/modals/oauth2/EditOAuth2ClientModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function OAuth2ClientSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [clients, setClients] = useState([]);
|
||||
const [filteredClients, setFilteredClients] = useState([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingClient, setEditingClient] = useState(null);
|
||||
const [showSecretModal, setShowSecretModal] = useState(false);
|
||||
const [currentSecret, setCurrentSecret] = useState('');
|
||||
|
||||
// 加载客户端列表
|
||||
const loadClients = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.get('/api/oauth_clients/');
|
||||
if (res.data.success) {
|
||||
setClients(res.data.data || []);
|
||||
setFilteredClients(res.data.data || []);
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('加载OAuth2客户端失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索过滤
|
||||
const handleSearch = (value) => {
|
||||
setSearchKeyword(value);
|
||||
if (!value) {
|
||||
setFilteredClients(clients);
|
||||
} else {
|
||||
const filtered = clients.filter(client =>
|
||||
client.name?.toLowerCase().includes(value.toLowerCase()) ||
|
||||
client.id?.toLowerCase().includes(value.toLowerCase()) ||
|
||||
client.description?.toLowerCase().includes(value.toLowerCase())
|
||||
);
|
||||
setFilteredClients(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除客户端
|
||||
const handleDelete = async (client) => {
|
||||
try {
|
||||
const res = await API.delete(`/api/oauth_clients/${client.id}`);
|
||||
if (res.data.success) {
|
||||
showSuccess('删除成功');
|
||||
loadClients();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成密钥
|
||||
const handleRegenerateSecret = async (client) => {
|
||||
try {
|
||||
const res = await API.post(`/api/oauth_clients/${client.id}/regenerate_secret`);
|
||||
if (res.data.success) {
|
||||
setCurrentSecret(res.data.client_secret);
|
||||
setShowSecretModal(true);
|
||||
loadClients();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('重新生成密钥失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '客户端名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Text strong>{text}</Text>
|
||||
<br />
|
||||
<Text type="tertiary" size="small">{record.id}</Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'client_type',
|
||||
key: 'client_type',
|
||||
render: (text) => (
|
||||
<Tag color={text === 'confidential' ? 'blue' : 'green'}>
|
||||
{text === 'confidential' ? '机密客户端' : '公开客户端'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '授权类型',
|
||||
dataIndex: 'grant_types',
|
||||
key: 'grant_types',
|
||||
render: (grantTypes) => {
|
||||
const types = typeof grantTypes === 'string' ? grantTypes.split(',') : (grantTypes || []);
|
||||
return (
|
||||
<div>
|
||||
{types.map(type => (
|
||||
<Tag key={type} size="small" style={{ margin: '2px' }}>
|
||||
{type === 'client_credentials' ? '客户端凭证' :
|
||||
type === 'authorization_code' ? '授权码' :
|
||||
type === 'refresh_token' ? '刷新令牌' : type}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status) => (
|
||||
<Tag color={status === 1 ? 'green' : 'red'}>
|
||||
{status === 1 ? '启用' : '禁用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_time',
|
||||
key: 'created_time',
|
||||
render: (time) => new Date(time * 1000).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditingClient(record);
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{record.client_type === 'confidential' && (
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={() => handleRegenerateSecret(record)}
|
||||
>
|
||||
重新生成密钥
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除这个OAuth2客户端吗?"
|
||||
content="删除后无法恢复,相关的API访问将失效。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
size="small"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadClients();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginTop: 10 }}>
|
||||
<Form.Section text={'OAuth2 客户端管理'}>
|
||||
<Banner
|
||||
type="info"
|
||||
description="管理OAuth2客户端应用程序,每个客户端代表一个可以访问API的应用程序。机密客户端用于服务器端应用,公开客户端用于移动应用或单页应用。"
|
||||
style={{ marginBottom: 15 }}
|
||||
/>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={8} xl={8}>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder="搜索客户端名称、ID或描述"
|
||||
value={searchKeyword}
|
||||
onChange={handleSearch}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={16} xl={16} style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button onClick={loadClients}>刷新</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
创建OAuth2客户端
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredClients}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||
pageSize: 10,
|
||||
}}
|
||||
empty={
|
||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||
<Text type="tertiary">暂无OAuth2客户端</Text>
|
||||
<br />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
创建第一个客户端
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<div style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
<Text strong>快速操作</Text>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await API.get('/api/oauth/jwks');
|
||||
Modal.info({
|
||||
title: 'JWKS信息',
|
||||
content: (
|
||||
<div>
|
||||
<Text>JSON Web Key Set:</Text>
|
||||
<pre style={{
|
||||
background: '#f8f9fa',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '8px',
|
||||
fontSize: '12px',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{JSON.stringify(res.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
width: 600
|
||||
});
|
||||
} catch (error) {
|
||||
showError('获取JWKS失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
查看JWKS
|
||||
</Button>
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await API.get('/api/oauth/server-info');
|
||||
Modal.info({
|
||||
title: 'OAuth2服务器信息',
|
||||
content: (
|
||||
<div>
|
||||
<Text>授权服务器配置:</Text>
|
||||
<pre style={{
|
||||
background: '#f8f9fa',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '8px',
|
||||
fontSize: '12px',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{JSON.stringify(res.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
width: 600
|
||||
});
|
||||
} catch (error) {
|
||||
showError('获取服务器信息失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
查看服务器信息
|
||||
</Button>
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={() => showInfo('OAuth2集成文档功能开发中,请参考相关API文档')}
|
||||
>
|
||||
集成文档
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
{/* 创建客户端模态框 */}
|
||||
<CreateOAuth2ClientModal
|
||||
visible={showCreateModal}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowCreateModal(false);
|
||||
loadClients();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 编辑客户端模态框 */}
|
||||
<EditOAuth2ClientModal
|
||||
visible={showEditModal}
|
||||
client={editingClient}
|
||||
onCancel={() => {
|
||||
setShowEditModal(false);
|
||||
setEditingClient(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setShowEditModal(false);
|
||||
setEditingClient(null);
|
||||
loadClients();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 密钥显示模态框 */}
|
||||
<Modal
|
||||
title="客户端密钥已重新生成"
|
||||
visible={showSecretModal}
|
||||
onCancel={() => setShowSecretModal(false)}
|
||||
onOk={() => setShowSecretModal(false)}
|
||||
cancelText=""
|
||||
okText="我已复制保存"
|
||||
width={600}
|
||||
>
|
||||
<div>
|
||||
<Text>新的客户端密钥如下,请立即复制保存。关闭此窗口后将无法再次查看。</Text>
|
||||
<div style={{
|
||||
background: '#f8f9fa',
|
||||
padding: '16px',
|
||||
borderRadius: '6px',
|
||||
marginTop: '16px',
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all'
|
||||
}}>
|
||||
<Text code copyable>{currentSecret}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
351
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx
Normal file
351
web/src/pages/Setting/OAuth2/OAuth2ServerSettings.jsx
Normal file
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Banner, Button, Col, Form, Row, Card } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function OAuth2ServerSettings(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
'oauth2.enabled': false,
|
||||
'oauth2.issuer': '',
|
||||
'oauth2.access_token_ttl': 10,
|
||||
'oauth2.refresh_token_ttl': 720,
|
||||
'oauth2.jwt_signing_algorithm': 'RS256',
|
||||
'oauth2.jwt_key_id': 'oauth2-key-1',
|
||||
'oauth2.jwt_private_key_file': '',
|
||||
'oauth2.allowed_grant_types': ['client_credentials', 'authorization_code'],
|
||||
'oauth2.require_pkce': true,
|
||||
'oauth2.auto_create_user': false,
|
||||
'oauth2.default_user_role': 1,
|
||||
'oauth2.default_user_group': 'default',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
|
||||
function handleFieldChange(fieldName) {
|
||||
return (value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
let value = '';
|
||||
if (typeof inputs[item.key] === 'boolean') {
|
||||
value = String(inputs[item.key]);
|
||||
} else if (Array.isArray(inputs[item.key])) {
|
||||
value = JSON.stringify(inputs[item.key]);
|
||||
} else {
|
||||
value = inputs[item.key];
|
||||
}
|
||||
return API.put('/api/option/', {
|
||||
key: item.key,
|
||||
value,
|
||||
});
|
||||
});
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
if (props && props.refresh) {
|
||||
props.refresh();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
// 测试OAuth2连接
|
||||
const testOAuth2 = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/oauth/server-info');
|
||||
if (res.data.success) {
|
||||
showSuccess('OAuth2服务器运行正常');
|
||||
} else {
|
||||
showError('OAuth2服务器测试失败: ' + res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('OAuth2服务器连接测试失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props && props.options) {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
if (Object.keys(inputs).includes(key)) {
|
||||
if (key === 'oauth2.allowed_grant_types') {
|
||||
try {
|
||||
currentInputs[key] = JSON.parse(props.options[key] || '["client_credentials","authorization_code"]');
|
||||
} catch {
|
||||
currentInputs[key] = ['client_credentials', 'authorization_code'];
|
||||
}
|
||||
} else if (typeof inputs[key] === 'boolean') {
|
||||
currentInputs[key] = props.options[key] === 'true';
|
||||
} else if (typeof inputs[key] === 'number') {
|
||||
currentInputs[key] = parseInt(props.options[key]) || inputs[key];
|
||||
} else {
|
||||
currentInputs[key] = props.options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
setInputs({...inputs, ...currentInputs});
|
||||
setInputsRow(structuredClone({...inputs, ...currentInputs}));
|
||||
if (refForm.current) {
|
||||
refForm.current.setValues({...inputs, ...currentInputs});
|
||||
}
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
>
|
||||
<Form.Section text={'OAuth2 服务器设置'}>
|
||||
<Banner
|
||||
type="info"
|
||||
description={
|
||||
<div>
|
||||
<p>• OAuth2服务器提供标准的API认证和授权功能</p>
|
||||
<p>• 支持Client Credentials、Authorization Code + PKCE等标准流程</p>
|
||||
<p>• 更改配置后需要重启服务才能生效</p>
|
||||
<p>• 生产环境务必配置HTTPS和安全的JWT签名密钥</p>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: 15 }}
|
||||
/>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='oauth2.enabled'
|
||||
label={t('启用OAuth2服务器')}
|
||||
checkedText='开'
|
||||
uncheckedText='关'
|
||||
value={inputs['oauth2.enabled']}
|
||||
onChange={handleFieldChange('oauth2.enabled')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<Form.Input
|
||||
field='oauth2.issuer'
|
||||
label={t('签发者标识(Issuer)')}
|
||||
placeholder="https://your-domain.com"
|
||||
extraText="OAuth2令牌的签发者,通常是您的域名"
|
||||
value={inputs['oauth2.issuer']}
|
||||
onChange={handleFieldChange('oauth2.issuer')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onSubmit} loading={loading}>{t('更新服务器设置')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginTop: 10 }}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
>
|
||||
<Form.Section text={'令牌配置'}>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='oauth2.access_token_ttl'
|
||||
label={t('访问令牌有效期')}
|
||||
suffix="分钟"
|
||||
min={1}
|
||||
max={1440}
|
||||
value={inputs['oauth2.access_token_ttl']}
|
||||
onChange={handleFieldChange('oauth2.access_token_ttl')}
|
||||
extraText="访问令牌的有效时间,建议较短(10-60分钟)"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='oauth2.refresh_token_ttl'
|
||||
label={t('刷新令牌有效期')}
|
||||
suffix="小时"
|
||||
min={1}
|
||||
max={8760}
|
||||
value={inputs['oauth2.refresh_token_ttl']}
|
||||
onChange={handleFieldChange('oauth2.refresh_token_ttl')}
|
||||
extraText="刷新令牌的有效时间,建议较长(12-720小时)"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<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="用于标识JWT签名密钥,支持密钥轮换"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Select
|
||||
field='oauth2.jwt_signing_algorithm'
|
||||
label={t('JWT签名算法')}
|
||||
value={inputs['oauth2.jwt_signing_algorithm']}
|
||||
onChange={handleFieldChange('oauth2.jwt_signing_algorithm')}
|
||||
extraText="JWT令牌的签名算法,推荐使用RS256"
|
||||
>
|
||||
<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} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Input
|
||||
field='oauth2.jwt_private_key_file'
|
||||
label={t('JWT私钥文件路径')}
|
||||
placeholder="/path/to/oauth2-private-key.pem"
|
||||
value={inputs['oauth2.jwt_private_key_file']}
|
||||
onChange={handleFieldChange('oauth2.jwt_private_key_file')}
|
||||
extraText="RSA私钥文件路径,留空将使用内存生成的密钥"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onSubmit} loading={loading}>{t('更新令牌配置')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginTop: 10 }}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
>
|
||||
<Form.Section text={'授权配置'}>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Select
|
||||
field='oauth2.allowed_grant_types'
|
||||
label={t('允许的授权类型')}
|
||||
multiple
|
||||
value={inputs['oauth2.allowed_grant_types']}
|
||||
onChange={handleFieldChange('oauth2.allowed_grant_types')}
|
||||
extraText="选择允许的OAuth2授权流程"
|
||||
>
|
||||
<Form.Select.Option value="client_credentials">Client Credentials(客户端凭证)</Form.Select.Option>
|
||||
<Form.Select.Option value="authorization_code">Authorization Code(授权码)</Form.Select.Option>
|
||||
<Form.Select.Option value="refresh_token">Refresh Token(刷新令牌)</Form.Select.Option>
|
||||
</Form.Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
<Form.Switch
|
||||
field='oauth2.require_pkce'
|
||||
label={t('强制PKCE验证')}
|
||||
checkedText='开'
|
||||
uncheckedText='关'
|
||||
value={inputs['oauth2.require_pkce']}
|
||||
onChange={handleFieldChange('oauth2.require_pkce')}
|
||||
extraText="为授权码流程强制启用PKCE,提高安全性"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onSubmit} loading={loading}>{t('更新授权配置')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginTop: 10 }}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
>
|
||||
<Form.Section text={'用户配置'}>
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='oauth2.auto_create_user'
|
||||
label={t('自动创建用户')}
|
||||
checkedText='开'
|
||||
uncheckedText='关'
|
||||
value={inputs['oauth2.auto_create_user']}
|
||||
onChange={handleFieldChange('oauth2.auto_create_user')}
|
||||
extraText="首次OAuth2登录时自动创建用户账户"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Select
|
||||
field='oauth2.default_user_role'
|
||||
label={t('默认用户角色')}
|
||||
value={inputs['oauth2.default_user_role']}
|
||||
onChange={handleFieldChange('oauth2.default_user_role')}
|
||||
extraText="自动创建用户时的默认角色"
|
||||
>
|
||||
<Form.Select.Option value={1}>普通用户</Form.Select.Option>
|
||||
<Form.Select.Option value={10}>管理员</Form.Select.Option>
|
||||
<Form.Select.Option value={100}>超级管理员</Form.Select.Option>
|
||||
</Form.Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='oauth2.default_user_group'
|
||||
label={t('默认用户分组')}
|
||||
placeholder="default"
|
||||
value={inputs['oauth2.default_user_group']}
|
||||
onChange={handleFieldChange('oauth2.default_user_group')}
|
||||
extraText="自动创建用户时的默认分组"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onSubmit} loading={loading}>{t('更新用户配置')}</Button>
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={testOAuth2}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{t('测试连接')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user