新增API管理页面

This commit is contained in:
2025-11-11 16:05:58 +08:00
parent fdf0c1442c
commit cdeb4ea9fb
7 changed files with 530 additions and 0 deletions

View File

@@ -555,3 +555,120 @@ func (h *APIHandler) ForceLogCleanupHandler(c *gin.Context) {
"success": report.Success,
})
}
// ============ API Key 管理相关处理器 ============
// APIKeyListResponse API Key列表响应
type APIKeyListResponse struct {
ID uint `json:"id"`
Key string `json:"key"`
CreatedAt time.Time `json:"created_at"`
}
// GetAPIKeysHandler 获取所有API Key列表
func (h *APIHandler) GetAPIKeysHandler(c *gin.Context) {
var apiKeys []models.APIKey
// 查询所有API Key
if err := h.DB.Order("created_at DESC").Find(&apiKeys).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch API keys"})
return
}
// 转换为响应格式
response := make([]APIKeyListResponse, len(apiKeys))
for i, key := range apiKeys {
response[i] = APIKeyListResponse{
ID: key.ID,
Key: key.Key,
CreatedAt: key.CreatedAt,
}
}
c.JSON(http.StatusOK, gin.H{
"api_keys": response,
"total": len(response),
})
}
// CreateAPIKeyRequest 创建API Key的请求结构
type CreateAPIKeyRequest struct {
Key string `json:"key" binding:"required"`
}
// CreateAPIKeyHandler 创建新的API Key
func (h *APIHandler) CreateAPIKeyHandler(c *gin.Context) {
var req CreateAPIKeyRequest
// 解析请求
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()})
return
}
// 验证Key不为空
if req.Key == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "API key cannot be empty"})
return
}
// 检查Key是否已存在
var existingKey models.APIKey
if err := h.DB.Where("key = ?", req.Key).First(&existingKey).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "API key already exists"})
return
}
// 创建新的API Key
newAPIKey := models.APIKey{
Key: req.Key,
}
if err := h.DB.Create(&newAPIKey).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create API key"})
return
}
// 返回创建的API Key
c.JSON(http.StatusCreated, gin.H{
"message": "API key created successfully",
"api_key": APIKeyListResponse{
ID: newAPIKey.ID,
Key: newAPIKey.Key,
CreatedAt: newAPIKey.CreatedAt,
},
})
}
// DeleteAPIKeyHandler 删除指定的API Key
func (h *APIHandler) DeleteAPIKeyHandler(c *gin.Context) {
// 获取API Key ID
keyID := c.Param("id")
if keyID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "API key ID is required"})
return
}
// 查找API Key
var apiKey models.APIKey
if err := h.DB.First(&apiKey, keyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "API key not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find API key"})
return
}
// 删除API Key
if err := h.DB.Delete(&apiKey).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete API key"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "API key deleted successfully",
"id": apiKey.ID,
})
}

View File

@@ -118,6 +118,11 @@ func main() {
// Log Cleaner Management
api_.GET("/log-cleaner/status", handler.GetLogCleanerStatusHandler)
api_.POST("/log-cleaner/force-cleanup", handler.ForceLogCleanupHandler)
// API Keys Management
api_.GET("/api-keys", handler.GetAPIKeysHandler)
api_.POST("/api-keys", handler.CreateAPIKeyHandler)
api_.DELETE("/api-keys/:id", handler.DeleteAPIKeyHandler)
}
// 设置优雅关闭信号处理

View File

@@ -3,6 +3,7 @@ import PageLayout from './components/layout/PageLayout';
import ProvidersPage from './features/providers';
import VirtualModelsPage from './features/virtual-models';
import LogsPage from './features/logs';
import ApiKeysPage from './features/api-keys';
import './App.css';
function App() {
@@ -18,6 +19,10 @@ function App() {
{
label: '请求日志',
content: <LogsPage />
},
{
label: 'API Key管理',
content: <ApiKeysPage />
}
];

View File

@@ -0,0 +1,30 @@
import api from '../../../lib/api';
/**
* 获取所有API Key列表
* @returns {Promise} API响应
*/
export const fetchAPIKeys = async () => {
const response = await api.get('/api/api-keys');
return response.data;
};
/**
* 创建新的API Key
* @param {string} key - API Key字符串
* @returns {Promise} API响应
*/
export const createAPIKey = async (key) => {
const response = await api.post('/api/api-keys', { key });
return response.data;
};
/**
* 删除指定的API Key
* @param {number} id - API Key的ID
* @returns {Promise} API响应
*/
export const deleteAPIKey = async (id) => {
const response = await api.delete(`/api/api-keys/${id}`);
return response.data;
};

View File

@@ -0,0 +1,128 @@
import { useState } from 'react';
const ApiKeyForm = ({ onSave, onCancel }) => {
const [key, setKey] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// 生成随机API Key
const generateRandomKey = () => {
const prefix = 'sk-';
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomPart = '';
for (let i = 0; i < 32; i++) {
randomPart += chars.charAt(Math.floor(Math.random() * chars.length));
}
return prefix + randomPart;
};
const handleGenerate = () => {
setKey(generateRandomKey());
setError('');
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!key.trim()) {
setError('请输入API Key');
return;
}
// 验证API Key格式
if (!key.startsWith('sk-')) {
setError('API Key必须以 "sk-" 开头');
return;
}
if (key.length < 10) {
setError('API Key长度不能少于10个字符');
return;
}
setLoading(true);
setError('');
try {
await onSave({ key: key.trim() });
// 成功后会由父组件处理关闭弹窗
} catch (err) {
setError(err.message || '创建API Key失败');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key
</label>
<div className="flex space-x-2">
<input
type="text"
value={key}
onChange={(e) => {
setKey(e.target.value);
setError('');
}}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
/>
<button
type="button"
onClick={handleGenerate}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors text-sm"
disabled={loading}
>
生成
</button>
</div>
{error && (
<p className="mt-1 text-sm text-red-600">{error}</p>
)}
<p className="mt-1 text-xs text-gray-500">
API Key必须以 "sk-" 开头建议长度不少于32个字符
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start">
<svg className="w-5 h-5 text-blue-600 mr-2 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-sm text-blue-800">
<p className="font-semibold mb-1">安全提示</p>
<ul className="space-y-1 list-disc list-inside">
<li>API Key是访问AI Gateway的凭证请妥善保管</li>
<li>建议定期更换API Key以确保安全</li>
<li>不要在代码中硬编码API Key使用环境变量</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors"
disabled={loading}
>
取消
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading || !key.trim()}
>
{loading ? '创建中...' : '创建'}
</button>
</div>
</form>
);
};
export default ApiKeyForm;

View File

@@ -0,0 +1,231 @@
import { useState, useEffect } from 'react';
import { fetchAPIKeys, createAPIKey, deleteAPIKey } from '../api';
import ApiKeyForm from './ApiKeyForm';
import Modal from '../../../components/ui/Modal';
const ApiKeyList = () => {
const [apiKeys, setApiKeys] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const fetchKeys = async () => {
try {
setLoading(true);
const data = await fetchAPIKeys();
setApiKeys(data.api_keys || []);
setError(null);
} catch (err) {
setError(err.message || '获取API Key列表失败');
console.error('Error fetching API keys:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchKeys();
}, []);
const handleCreate = async (keyData) => {
try {
await createAPIKey(keyData.key);
setShowCreateForm(false);
await fetchKeys();
// 显示成功消息
alert('API Key创建成功');
} catch (err) {
setError(err.message || '创建API Key失败');
throw err; // 向表单组件传递错误
}
};
const handleDelete = async (id) => {
if (!window.confirm('确定要删除这个API Key吗删除后将无法使用该Key进行API调用。')) {
return;
}
try {
await deleteAPIKey(id);
await fetchKeys();
alert('API Key删除成功');
} catch (err) {
setError(err.message || '删除API Key失败');
}
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
alert('API Key已复制到剪贴板');
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动复制');
});
};
if (loading) {
return (
<div className="flex justify-center items-center py-8">
<div className="text-gray-600">加载中...</div>
</div>
);
}
return (
<>
{/* 创建API Key弹窗 */}
<Modal
isOpen={showCreateForm}
onClose={() => setShowCreateForm(false)}
title="创建新API Key"
size="md"
>
<ApiKeyForm
onSave={handleCreate}
onCancel={() => setShowCreateForm(false)}
/>
</Modal>
<div className="space-y-4">
{/* 页面标题和操作按钮 */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-gray-900">API Key 管理</h2>
<p className="mt-1 text-sm text-gray-500">
管理用于访问AI Gateway的API密钥
</p>
</div>
<button
onClick={() => setShowCreateForm(true)}
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 transition-colors"
>
创建API Key
</button>
</div>
{/* 错误提示 */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
{/* API Key统计信息 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center">
<svg className="w-5 h-5 text-blue-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-sm text-blue-800">
<span className="font-semibold">当前共有 {apiKeys.length} 个API Key</span>
<span className="ml-2 text-blue-600">
· 每个API Key都可以独立用于认证请求
</span>
</div>
</div>
</div>
{/* API Key表格 */}
<div className="overflow-x-auto bg-white rounded-lg shadow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
API Key
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
创建时间
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{apiKeys.length === 0 ? (
<tr>
<td colSpan="4" className="px-6 py-8 text-center">
<div className="flex flex-col items-center justify-center">
<svg className="w-12 h-12 text-gray-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<p className="text-gray-500 text-sm">暂无API Key</p>
<p className="text-gray-400 text-xs mt-1">点击"创建API Key"按钮添加新的密钥</p>
</div>
</td>
</tr>
) : (
apiKeys.map((apiKey, index) => (
<tr key={apiKey.id || index} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
#{apiKey.id || 'N/A'}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
<code className="px-3 py-1 bg-gray-100 text-gray-800 rounded text-sm font-mono break-all">
{apiKey.key || 'N/A'}
</code>
<button
onClick={() => copyToClipboard(apiKey.key)}
className="flex-shrink-0 text-blue-600 hover:text-blue-800 transition-colors"
title="复制到剪贴板"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{formatDate(apiKey.created_at)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
className="text-red-600 hover:text-red-900 transition-colors"
onClick={() => handleDelete(apiKey.id)}
>
删除
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* 使用说明 */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">使用说明</h3>
<ul className="text-sm text-gray-600 space-y-1 list-disc list-inside">
<li>API Key用于认证对AI Gateway的所有请求</li>
<li>在请求头中添加 <code className="px-1 bg-gray-200 rounded">Authorization: Bearer YOUR_API_KEY</code></li>
<li>请妥善保管API Key避免泄露给未授权人员</li>
<li>如果API Key泄露请立即删除并创建新的密钥</li>
</ul>
</div>
</div>
</>
);
};
export default ApiKeyList;

View File

@@ -0,0 +1,14 @@
import React from 'react';
import ApiKeyList from './components/ApiKeyList';
const ApiKeysPage = () => {
return (
<div className="space-y-6">
<div className="max-w-7xl mx-auto px-4">
<ApiKeyList />
</div>
</div>
);
};
export default ApiKeysPage;