mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-06-07 22:09:57 +00:00
✨ feat(oauth2): enhance JWKS manager modal with improved UX and i18n support
- Refactor JWKSManagerModal with tab-based navigation using Card components - Add comprehensive i18n support with English translations for all text - Optimize header actions: refresh button only appears in key list tab - Improve responsive design using ResponsiveModal component - Move cautionary text from bottom to Card titles for better visibility - Update button styles: danger type for delete, circle shape for status tags - Standardize code formatting (single quotes, multiline formatting) - Enhance user workflow: separate Import PEM and Generate PEM operations - Remove redundant cancel buttons as modal already has close icon Breaking changes: None Affects: JWKS key management, OAuth2 settings UI
This commit is contained in:
135
web/src/components/common/ui/ResponsiveModal.jsx
Normal file
135
web/src/components/common/ui/ResponsiveModal.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
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 from 'react';
|
||||
import { Modal, Typography } from '@douyinfe/semi-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
/**
|
||||
* ResponsiveModal 响应式模态框组件
|
||||
*
|
||||
* 特性:
|
||||
* - 响应式布局:移动端和桌面端不同的宽度和布局
|
||||
* - 自定义头部:标题左对齐,操作按钮右对齐,移动端自动换行
|
||||
* - Tailwind CSS 样式支持
|
||||
* - 保持原 Modal 组件的所有功能
|
||||
*/
|
||||
const ResponsiveModal = ({
|
||||
visible,
|
||||
onCancel,
|
||||
title,
|
||||
headerActions = [],
|
||||
children,
|
||||
width = { mobile: '95%', desktop: 600 },
|
||||
className = '',
|
||||
footer = null,
|
||||
titleProps = {},
|
||||
headerClassName = '',
|
||||
actionsClassName = '',
|
||||
...props
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// 自定义 Header 组件
|
||||
const CustomHeader = () => {
|
||||
if (!title && (!headerActions || headerActions.length === 0)) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full gap-3 justify-between ${
|
||||
isMobile ? 'flex-col items-start' : 'flex-row items-center'
|
||||
} ${headerClassName}`}
|
||||
>
|
||||
{title && (
|
||||
<Title heading={5} className='m-0 min-w-fit' {...titleProps}>
|
||||
{title}
|
||||
</Title>
|
||||
)}
|
||||
{headerActions && headerActions.length > 0 && (
|
||||
<div
|
||||
className={`flex flex-wrap gap-2 items-center ${
|
||||
isMobile ? 'w-full justify-start' : 'w-auto justify-end'
|
||||
} ${actionsClassName}`}
|
||||
>
|
||||
{headerActions.map((action, index) => (
|
||||
<React.Fragment key={index}>{action}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 计算模态框宽度
|
||||
const getModalWidth = () => {
|
||||
if (typeof width === 'object') {
|
||||
return isMobile ? width.mobile : width.desktop;
|
||||
}
|
||||
return width;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={<CustomHeader />}
|
||||
onCancel={onCancel}
|
||||
footer={footer}
|
||||
width={getModalWidth()}
|
||||
className={`!top-12 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveModal.propTypes = {
|
||||
// Modal 基础属性
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
|
||||
// 自定义头部
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
headerActions: PropTypes.arrayOf(PropTypes.node),
|
||||
|
||||
// 样式和布局
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
mobile: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
desktop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
}),
|
||||
]),
|
||||
className: PropTypes.string,
|
||||
footer: PropTypes.node,
|
||||
|
||||
// 标题自定义属性
|
||||
titleProps: PropTypes.object,
|
||||
|
||||
// 自定义 CSS 类
|
||||
headerClassName: PropTypes.string,
|
||||
actionsClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ResponsiveModal;
|
||||
@@ -343,13 +343,12 @@ export default function OAuth2ClientSettings() {
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
title={t('暂无OAuth2客户端')}
|
||||
description={t('还没有创建任何客户端,点击下方按钮创建第一个客户端')}
|
||||
description={t(
|
||||
'还没有创建任何客户端,点击下方按钮创建第一个客户端',
|
||||
)}
|
||||
style={{ padding: 30 }}
|
||||
>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<Button type='primary' onClick={() => setShowCreateModal(true)}>
|
||||
{t('创建第一个客户端')}
|
||||
</Button>
|
||||
</Empty>
|
||||
|
||||
@@ -220,7 +220,7 @@ export default function OAuth2ServerSettings(props) {
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Server size={18} className='mr-2' />
|
||||
<Text strong>{t('OAuth2 & SSO 管理')}</Text>
|
||||
<Text strong>{t('OAuth2 服务端管理')}</Text>
|
||||
{isEnabled ? (
|
||||
serverInfo ? (
|
||||
<Badge
|
||||
|
||||
@@ -18,7 +18,6 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
@@ -27,19 +26,28 @@ import {
|
||||
Popconfirm,
|
||||
Toast,
|
||||
Form,
|
||||
TextArea,
|
||||
Divider,
|
||||
Input,
|
||||
Card,
|
||||
Tabs,
|
||||
TabPane,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess } from '../../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ResponsiveModal from '../../../common/ui/ResponsiveModal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 操作模式枚举
|
||||
const OPERATION_MODES = {
|
||||
VIEW: 'view',
|
||||
IMPORT: 'import',
|
||||
GENERATE: 'generate',
|
||||
};
|
||||
|
||||
export default function JWKSManagerModal({ visible, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState(OPERATION_MODES.VIEW);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -84,9 +92,32 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Import PEM state
|
||||
const [pem, setPem] = useState('');
|
||||
const [customKid, setCustomKid] = useState('');
|
||||
|
||||
// Generate PEM file state
|
||||
const [genPath, setGenPath] = useState('/etc/new-api/oauth2-private.pem');
|
||||
const [genKid, setGenKid] = useState('');
|
||||
|
||||
// 重置表单数据
|
||||
const resetForms = () => {
|
||||
setPem('');
|
||||
setCustomKid('');
|
||||
setGenKid('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) load();
|
||||
if (visible) {
|
||||
load();
|
||||
// 重置到主视图
|
||||
setActiveTab(OPERATION_MODES.VIEW);
|
||||
} else {
|
||||
// 模态框关闭时重置表单数据
|
||||
resetForms();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
(async () => {
|
||||
@@ -98,10 +129,7 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
})();
|
||||
}, [visible]);
|
||||
|
||||
// Import PEM state
|
||||
const [showImport, setShowImport] = useState(false);
|
||||
const [pem, setPem] = useState('');
|
||||
const [customKid, setCustomKid] = useState('');
|
||||
// 导入 PEM 私钥
|
||||
const importPem = async () => {
|
||||
if (!pem.trim()) return Toast.warning(t('请粘贴 PEM 私钥'));
|
||||
setLoading(true);
|
||||
@@ -114,9 +142,8 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
Toast.success(
|
||||
t('已导入私钥并切换到 kid={{kid}}', { kid: res.data.kid }),
|
||||
);
|
||||
setPem('');
|
||||
setCustomKid('');
|
||||
setShowImport(false);
|
||||
resetForms();
|
||||
setActiveTab(OPERATION_MODES.VIEW);
|
||||
await load();
|
||||
} else {
|
||||
Toast.error(res?.data?.message || t('导入失败'));
|
||||
@@ -128,10 +155,7 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Generate PEM file state
|
||||
const [showGenerate, setShowGenerate] = useState(false);
|
||||
const [genPath, setGenPath] = useState('/etc/new-api/oauth2-private.pem');
|
||||
const [genKid, setGenKid] = useState('');
|
||||
// 生成 PEM 文件
|
||||
const generatePemFile = async () => {
|
||||
if (!genPath.trim()) return Toast.warning(t('请填写保存路径'));
|
||||
setLoading(true);
|
||||
@@ -142,6 +166,8 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
});
|
||||
if (res?.data?.success) {
|
||||
Toast.success(t('已生成并生效:{{path}}', { path: res.data.path }));
|
||||
resetForms();
|
||||
setActiveTab(OPERATION_MODES.VIEW);
|
||||
await load();
|
||||
} else {
|
||||
Toast.error(res?.data?.message || t('生成失败'));
|
||||
@@ -172,7 +198,13 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
title: t('状态'),
|
||||
dataIndex: 'current',
|
||||
render: (cur) =>
|
||||
cur ? <Tag color='green'>{t('当前')}</Tag> : <Tag>{t('历史')}</Tag>,
|
||||
cur ? (
|
||||
<Tag color='green' shape='circle'>
|
||||
{t('当前')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag shape='circle'>{t('历史')}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -187,7 +219,7 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
okText={t('删除')}
|
||||
onConfirm={() => del(r.kid)}
|
||||
>
|
||||
<Button size='small' theme='borderless'>
|
||||
<Button size='small' type='danger'>
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -197,109 +229,68 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t('JWKS 管理')}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={820}
|
||||
style={{ top: 48 }}
|
||||
>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<Button onClick={load} loading={loading}>
|
||||
// 头部操作按钮 - 根据当前标签页动态生成
|
||||
const getHeaderActions = () => {
|
||||
if (activeTab === OPERATION_MODES.VIEW) {
|
||||
return [
|
||||
<Button key='refresh' onClick={load} loading={loading} size='small'>
|
||||
{t('刷新')}
|
||||
</Button>
|
||||
<Button type='primary' onClick={rotate} loading={loading}>
|
||||
</Button>,
|
||||
<Button
|
||||
key='rotate'
|
||||
type='primary'
|
||||
onClick={rotate}
|
||||
loading={loading}
|
||||
size='small'
|
||||
>
|
||||
{t('轮换密钥')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowImport(!showImport)}>
|
||||
{t('导入 PEM 私钥')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowGenerate(!showGenerate)}>
|
||||
{t('生成 PEM 文件')}
|
||||
</Button>
|
||||
<Button onClick={onClose}>{t('关闭')}</Button>
|
||||
</Space>
|
||||
{showGenerate && (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 6,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
if (activeTab === OPERATION_MODES.IMPORT) {
|
||||
return [
|
||||
<Button
|
||||
key='import'
|
||||
type='primary'
|
||||
onClick={importPem}
|
||||
loading={loading}
|
||||
size='small'
|
||||
>
|
||||
<Form labelPosition='left' labelWidth={120}>
|
||||
<Form.Input
|
||||
field='path'
|
||||
label={t('保存路径')}
|
||||
value={genPath}
|
||||
onChange={setGenPath}
|
||||
placeholder='/secure/path/oauth2-private.pem'
|
||||
/>
|
||||
<Form.Input
|
||||
field='genKid'
|
||||
label={t('自定义 KID')}
|
||||
value={genKid}
|
||||
onChange={setGenKid}
|
||||
placeholder={t('可留空自动生成')}
|
||||
/>
|
||||
</Form>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Button type='primary' onClick={generatePemFile} loading={loading}>
|
||||
{t('生成并生效')}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider margin='12px' />
|
||||
<Text type='tertiary'>
|
||||
{t(
|
||||
'建议:仅在合规要求下使用文件私钥。请确保目录权限安全(建议 0600),并妥善备份。',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{showImport && (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 6,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
{t('导入并生效')}
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
if (activeTab === OPERATION_MODES.GENERATE) {
|
||||
return [
|
||||
<Button
|
||||
key='generate'
|
||||
type='primary'
|
||||
onClick={generatePemFile}
|
||||
loading={loading}
|
||||
size='small'
|
||||
>
|
||||
<Form labelPosition='left' labelWidth={120}>
|
||||
<Form.Input
|
||||
field='kid'
|
||||
label={t('自定义 KID')}
|
||||
placeholder={t('可留空自动生成')}
|
||||
value={customKid}
|
||||
onChange={setCustomKid}
|
||||
/>
|
||||
<Form.TextArea
|
||||
field='pem'
|
||||
label={t('PEM 私钥')}
|
||||
value={pem}
|
||||
onChange={setPem}
|
||||
rows={6}
|
||||
placeholder={
|
||||
'-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Button type='primary' onClick={importPem} loading={loading}>
|
||||
{t('导入并生效')}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider margin='12px' />
|
||||
<Text type='tertiary'>
|
||||
{t(
|
||||
'建议:优先使用内存签名密钥与 JWKS 轮换;仅在有合规要求时导入外部私钥。',
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{t('生成并生效')}
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// 渲染密钥列表视图
|
||||
const renderKeysView = () => (
|
||||
<Card
|
||||
className='!rounded-lg'
|
||||
title={
|
||||
<Text className='text-blue-700 dark:text-blue-300'>
|
||||
{t(
|
||||
'提示:当前密钥用于签发 JWT 令牌。建议定期轮换密钥以提升安全性。只有历史密钥可以删除。',
|
||||
)}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
dataSource={keys}
|
||||
columns={columns}
|
||||
@@ -308,6 +299,100 @@ export default function JWKSManagerModal({ visible, onClose }) {
|
||||
pagination={false}
|
||||
empty={<Text type='tertiary'>{t('暂无密钥')}</Text>}
|
||||
/>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// 渲染导入 PEM 私钥视图
|
||||
const renderImportView = () => (
|
||||
<Card
|
||||
className='!rounded-lg'
|
||||
title={
|
||||
<Text className='text-yellow-700 dark:text-yellow-300'>
|
||||
{t(
|
||||
'建议:优先使用内存签名密钥与 JWKS 轮换;仅在有合规要求时导入外部私钥。请确保私钥来源可信。',
|
||||
)}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Form labelPosition='left' labelWidth={120}>
|
||||
<Form.Input
|
||||
field='kid'
|
||||
label={t('自定义 KID')}
|
||||
placeholder={t('可留空自动生成')}
|
||||
value={customKid}
|
||||
onChange={setCustomKid}
|
||||
/>
|
||||
<Form.TextArea
|
||||
field='pem'
|
||||
label={t('PEM 私钥')}
|
||||
value={pem}
|
||||
onChange={setPem}
|
||||
rows={8}
|
||||
placeholder={
|
||||
'-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
// 渲染生成 PEM 文件视图
|
||||
const renderGenerateView = () => (
|
||||
<Card
|
||||
className='!rounded-lg'
|
||||
title={
|
||||
<Text className='text-orange-700 dark:text-orange-300'>
|
||||
{t(
|
||||
'建议:仅在合规要求下使用文件私钥。请确保目录权限安全(建议 0600),并妥善备份。',
|
||||
)}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Form labelPosition='left' labelWidth={120}>
|
||||
<Form.Input
|
||||
field='path'
|
||||
label={t('保存路径')}
|
||||
value={genPath}
|
||||
onChange={setGenPath}
|
||||
placeholder='/secure/path/oauth2-private.pem'
|
||||
/>
|
||||
<Form.Input
|
||||
field='genKid'
|
||||
label={t('自定义 KID')}
|
||||
value={genKid}
|
||||
onChange={setGenKid}
|
||||
placeholder={t('可留空自动生成')}
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveModal
|
||||
visible={visible}
|
||||
title={t('JWKS 管理')}
|
||||
headerActions={getHeaderActions()}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={{ mobile: '95%', desktop: 800 }}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
type='card'
|
||||
size='medium'
|
||||
className='!-mt-2'
|
||||
>
|
||||
<TabPane tab={t('密钥列表')} itemKey={OPERATION_MODES.VIEW}>
|
||||
{renderKeysView()}
|
||||
</TabPane>
|
||||
<TabPane tab={t('导入 PEM 私钥')} itemKey={OPERATION_MODES.IMPORT}>
|
||||
{renderImportView()}
|
||||
</TabPane>
|
||||
<TabPane tab={t('生成 PEM 文件')} itemKey={OPERATION_MODES.GENERATE}>
|
||||
{renderGenerateView()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</ResponsiveModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,11 @@ const SecretDisplayModal = ({ visible, onClose, secret }) => {
|
||||
)}
|
||||
className='mb-5 !rounded-lg'
|
||||
/>
|
||||
<Text code copyable>
|
||||
{secret}
|
||||
</Text>
|
||||
<div className='flex justify-center items-center'>
|
||||
<Text code copyable>
|
||||
{secret}
|
||||
</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2137,7 +2137,7 @@
|
||||
"客户端密钥已重新生成": "Client secret regenerated",
|
||||
"我已复制保存": "I have copied and saved",
|
||||
"新的客户端密钥如下,请立即复制保存。关闭此窗口后将无法再次查看。": "The new client secret is shown below. Copy and save it now. You will not be able to view it again after closing this window.",
|
||||
"OAuth2 & SSO 管理": "OAuth2 & SSO Management",
|
||||
"OAuth2 服务端管理": "OAuth2 Server",
|
||||
"运行正常": "Healthy",
|
||||
"配置中": "Configuring",
|
||||
"保存配置": "Save Configuration",
|
||||
@@ -2177,5 +2177,34 @@
|
||||
"客户端名称": "Client Name",
|
||||
"暂无描述": "No description",
|
||||
"OAuth2 服务器配置": "OAuth2 Server Configuration",
|
||||
"JWKS 密钥集": "JWKS Key Set"
|
||||
"JWKS 密钥集": "JWKS Key Set",
|
||||
"获取密钥列表失败": "Failed to fetch key list",
|
||||
"签名密钥已轮换:{{kid}}": "Signing key rotated: {{kid}}",
|
||||
"密钥轮换失败": "Key rotation failed",
|
||||
"已删除:{{kid}}": "Deleted: {{kid}}",
|
||||
"请粘贴 PEM 私钥": "Please paste PEM private key",
|
||||
"已导入私钥并切换到 kid={{kid}}": "Private key imported and switched to kid={{kid}}",
|
||||
"导入失败": "Import failed",
|
||||
"请填写保存路径": "Please enter the save path",
|
||||
"已生成并生效:{{path}}": "Generated and applied: {{path}}",
|
||||
"生成失败": "Generation failed",
|
||||
"当前": "Current",
|
||||
"历史": "History",
|
||||
"确定删除密钥 {{kid}} ?": "Confirm delete key {{kid}}?",
|
||||
"删除后使用该 kid 签发的旧令牌仍可被验证(外部 JWKS 缓存可能仍保留)": "Tokens issued with this kid may still be verifiable (external JWKS caches may retain the key)",
|
||||
"轮换密钥": "Rotate key",
|
||||
"导入并生效": "Import and apply",
|
||||
"生成并生效": "Generate and apply",
|
||||
"提示:当前密钥用于签发 JWT 令牌。建议定期轮换密钥以提升安全性。只有历史密钥可以删除。": "Tip: The current key is used to sign JWT tokens. Rotate keys regularly for security. Only historical keys can be deleted.",
|
||||
"暂无密钥": "No keys yet",
|
||||
"建议:优先使用内存签名密钥与 JWKS 轮换;仅在有合规要求时导入外部私钥。请确保私钥来源可信。": "Recommendation: Prefer in-memory signing keys with JWKS rotation; import external private keys only when required for compliance. Ensure the private key source is trusted.",
|
||||
"自定义 KID": "Custom KID",
|
||||
"可留空自动生成": "Optional, auto-generate if empty",
|
||||
"PEM 私钥": "PEM private key",
|
||||
"建议:仅在合规要求下使用文件私钥。请确保目录权限安全(建议 0600),并妥善备份。": "Recommendation: Use file-based private keys only when required for compliance. Ensure directory permissions are secure (0600 recommended) and back up properly.",
|
||||
"保存路径": "Save path",
|
||||
"JWKS 管理": "JWKS Management",
|
||||
"密钥列表": "Key list",
|
||||
"导入 PEM 私钥": "Import PEM private key",
|
||||
"生成 PEM 文件": "Generate PEM file"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user