From cdeb4ea9fbd8a8463b92ca19ebb3a5e36eeffbcd Mon Sep 17 00:00:00 2001 From: nanako <469449812@qq.com> Date: Tue, 11 Nov 2025 16:05:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EAPI=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/handlers.go | 117 +++++++++ backend/main.go | 5 + frontend/src/App.jsx | 5 + frontend/src/features/api-keys/api/index.js | 30 +++ .../api-keys/components/ApiKeyForm.jsx | 128 ++++++++++ .../api-keys/components/ApiKeyList.jsx | 231 ++++++++++++++++++ frontend/src/features/api-keys/index.jsx | 14 ++ 7 files changed, 530 insertions(+) create mode 100644 frontend/src/features/api-keys/api/index.js create mode 100644 frontend/src/features/api-keys/components/ApiKeyForm.jsx create mode 100644 frontend/src/features/api-keys/components/ApiKeyList.jsx create mode 100644 frontend/src/features/api-keys/index.jsx diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 9261ee8..13eb07f 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -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, + }) +} diff --git a/backend/main.go b/backend/main.go index 4110999..a68ce2d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -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) } // 设置优雅关闭信号处理 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cb3a957..e65c632 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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: + }, + { + label: 'API Key管理', + content: } ]; diff --git a/frontend/src/features/api-keys/api/index.js b/frontend/src/features/api-keys/api/index.js new file mode 100644 index 0000000..a2930a4 --- /dev/null +++ b/frontend/src/features/api-keys/api/index.js @@ -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; +}; \ No newline at end of file diff --git a/frontend/src/features/api-keys/components/ApiKeyForm.jsx b/frontend/src/features/api-keys/components/ApiKeyForm.jsx new file mode 100644 index 0000000..a16b23c --- /dev/null +++ b/frontend/src/features/api-keys/components/ApiKeyForm.jsx @@ -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 ( +
+
+ +
+ { + 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 + /> + +
+ {error && ( +

{error}

+ )} +

+ API Key必须以 "sk-" 开头,建议长度不少于32个字符 +

+
+ +
+
+ + + +
+

安全提示:

+
    +
  • API Key是访问AI Gateway的凭证,请妥善保管
  • +
  • 建议定期更换API Key以确保安全
  • +
  • 不要在代码中硬编码API Key,使用环境变量
  • +
+
+
+
+ +
+ + +
+
+ ); +}; + +export default ApiKeyForm; \ No newline at end of file diff --git a/frontend/src/features/api-keys/components/ApiKeyList.jsx b/frontend/src/features/api-keys/components/ApiKeyList.jsx new file mode 100644 index 0000000..79b982e --- /dev/null +++ b/frontend/src/features/api-keys/components/ApiKeyList.jsx @@ -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 ( +
+
加载中...
+
+ ); + } + + return ( + <> + {/* 创建API Key弹窗 */} + setShowCreateForm(false)} + title="创建新API Key" + size="md" + > + setShowCreateForm(false)} + /> + + +
+ {/* 页面标题和操作按钮 */} +
+
+

API Key 管理

+

+ 管理用于访问AI Gateway的API密钥 +

+
+ +
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* API Key统计信息 */} +
+
+ + + +
+ 当前共有 {apiKeys.length} 个API Key + + · 每个API Key都可以独立用于认证请求 + +
+
+
+ + {/* API Key表格 */} +
+ + + + + + + + + + + {apiKeys.length === 0 ? ( + + + + ) : ( + apiKeys.map((apiKey, index) => ( + + + + + + + )) + )} + +
+ ID + + API Key + + 创建时间 + + 操作 +
+
+ + + +

暂无API Key

+

点击"创建API Key"按钮添加新的密钥

+
+
+
+ #{apiKey.id || 'N/A'} +
+
+
+ + {apiKey.key || 'N/A'} + + +
+
+
+ {formatDate(apiKey.created_at)} +
+
+ +
+
+ + {/* 使用说明 */} +
+

使用说明

+
    +
  • API Key用于认证对AI Gateway的所有请求
  • +
  • 在请求头中添加 Authorization: Bearer YOUR_API_KEY
  • +
  • 请妥善保管API Key,避免泄露给未授权人员
  • +
  • 如果API Key泄露,请立即删除并创建新的密钥
  • +
+
+
+ + ); +}; + +export default ApiKeyList; \ No newline at end of file diff --git a/frontend/src/features/api-keys/index.jsx b/frontend/src/features/api-keys/index.jsx new file mode 100644 index 0000000..89a0dfd --- /dev/null +++ b/frontend/src/features/api-keys/index.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ApiKeyList from './components/ApiKeyList'; + +const ApiKeysPage = () => { + return ( +
+
+ +
+
+ ); +}; + +export default ApiKeysPage; \ No newline at end of file