diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 3d08e8c5..47bd4333 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -56,21 +56,25 @@ const authenticateApiKey = async (req, res, next) => { let clientAllowed = false; let matchedClient = null; + // 获取预定义客户端列表,如果配置不存在则使用默认值 + const predefinedClients = config.clientRestrictions?.predefinedClients || []; + const allowCustomClients = config.clientRestrictions?.allowCustomClients || false; + // 遍历允许的客户端列表 for (const allowedClientId of validation.keyData.allowedClients) { // 在预定义客户端列表中查找 - const predefinedClient = config.clientRestrictions.predefinedClients.find( + const predefinedClient = predefinedClients.find( client => client.id === allowedClientId ); if (predefinedClient) { // 使用预定义的正则表达式匹配 User-Agent - if (predefinedClient.userAgentPattern.test(userAgent)) { + if (predefinedClient.userAgentPattern && predefinedClient.userAgentPattern.test(userAgent)) { clientAllowed = true; matchedClient = predefinedClient.name; break; } - } else if (config.clientRestrictions.allowCustomClients) { + } else if (allowCustomClients) { // 如果允许自定义客户端,这里可以添加自定义客户端的验证逻辑 // 目前暂时跳过自定义客户端 continue; diff --git a/src/routes/admin.js b/src/routes/admin.js index 07412291..bf7e566c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -291,11 +291,26 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { // 获取支持的客户端列表 router.get('/supported-clients', authenticateAdmin, async (req, res) => { try { - const clients = config.clientRestrictions.predefinedClients.map(client => ({ + // 检查配置是否存在,如果不存在则使用默认值 + const predefinedClients = config.clientRestrictions?.predefinedClients || [ + { + id: 'claude_code', + name: 'ClaudeCode', + description: 'Official Claude Code CLI' + }, + { + id: 'gemini_cli', + name: 'Gemini-CLI', + description: 'Gemini Command Line Interface' + } + ]; + + const clients = predefinedClients.map(client => ({ id: client.id, name: client.name, description: client.description })); + res.json({ success: true, data: clients }); } catch (error) { logger.error('❌ Failed to get supported clients:', error); @@ -439,6 +454,114 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { } }); +// 批量创建API Keys +router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { + try { + const { + baseName, + count, + description, + tokenLimit, + expiresAt, + claudeAccountId, + claudeConsoleAccountId, + geminiAccountId, + permissions, + concurrencyLimit, + rateLimitWindow, + rateLimitRequests, + enableModelRestriction, + restrictedModels, + enableClientRestriction, + allowedClients, + dailyCostLimit, + tags + } = req.body; + + // 输入验证 + if (!baseName || typeof baseName !== 'string' || baseName.trim().length === 0) { + return res.status(400).json({ error: 'Base name is required and must be a non-empty string' }); + } + + if (!count || !Number.isInteger(count) || count < 2 || count > 500) { + return res.status(400).json({ error: 'Count must be an integer between 2 and 500' }); + } + + if (baseName.length > 90) { + return res.status(400).json({ error: 'Base name must be less than 90 characters to allow for numbering' }); + } + + // 生成批量API Keys + const createdKeys = []; + const errors = []; + + for (let i = 1; i <= count; i++) { + try { + const name = `${baseName}_${i}`; + const newKey = await apiKeyService.generateApiKey({ + name, + description, + tokenLimit, + expiresAt, + claudeAccountId, + claudeConsoleAccountId, + geminiAccountId, + permissions, + concurrencyLimit, + rateLimitWindow, + rateLimitRequests, + enableModelRestriction, + restrictedModels, + enableClientRestriction, + allowedClients, + dailyCostLimit, + tags + }); + + // 保留原始 API Key 供返回 + createdKeys.push({ + ...newKey, + apiKey: newKey.apiKey + }); + } catch (error) { + errors.push({ + index: i, + name: `${baseName}_${i}`, + error: error.message + }); + } + } + + // 如果有部分失败,返回部分成功的结果 + if (errors.length > 0 && createdKeys.length === 0) { + return res.status(400).json({ + success: false, + error: 'Failed to create any API keys', + errors + }); + } + + // 返回创建的keys(包含完整的apiKey) + res.json({ + success: true, + data: createdKeys, + errors: errors.length > 0 ? errors : undefined, + summary: { + requested: count, + created: createdKeys.length, + failed: errors.length + } + }); + } catch (error) { + logger.error('Failed to batch create API keys:', error); + res.status(500).json({ + success: false, + error: 'Failed to batch create API keys', + message: error.message + }); + } +}); + // 更新API Key router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { diff --git a/web/admin-spa/src/assets/styles/components.css b/web/admin-spa/src/assets/styles/components.css index a2c83973..7f5bd632 100644 --- a/web/admin-spa/src/assets/styles/components.css +++ b/web/admin-spa/src/assets/styles/components.css @@ -297,6 +297,114 @@ transition: transform 0.3s ease-in-out; } +/* 响应式设计 - 移动端优化 */ +@media (max-width: 640px) { + /* 玻璃态容器 */ + .glass, + .glass-strong { + margin: 12px; + border-radius: 16px; + padding: 16px; + } + + /* 统计卡片 */ + .stat-card { + padding: 12px; + border-radius: 12px; + } + + .stat-icon { + width: 40px; + height: 40px; + font-size: 16px; + } + + /* 标签按钮 */ + .tab-btn { + font-size: 12px; + padding: 10px 6px; + } + + /* 模态框 */ + .modal-content { + margin: 8px; + max-width: calc(100vw - 24px); + padding: 16px; + } + + .modal-scroll-content { + max-height: calc(90vh - 100px); + } + + /* 卡片 */ + .card { + border-radius: 12px; + } + + /* 表单元素 */ + .form-input, + .form-select, + .form-textarea { + font-size: 14px; + padding: 8px 12px; + } + + /* 按钮 */ + .btn { + font-size: 14px; + padding: 8px 16px; + } + + /* 表格 */ + .table-container table { + font-size: 12px; + } + + .table-container th, + .table-container td { + padding: 8px 12px; + } + + /* Toast通知 */ + .toast { + min-width: 280px; + max-width: calc(100vw - 40px); + right: 12px; + top: 60px; + } + + /* 加载动画 */ + .loading-spinner { + width: 16px; + height: 16px; + } +} + +@media (max-width: 768px) { + /* 玻璃态容器 */ + .glass, + .glass-strong { + margin: 16px; + border-radius: 20px; + } + + /* 统计卡片 */ + .stat-card { + padding: 16px; + } + + /* 标签按钮 */ + .tab-btn { + font-size: 14px; + padding: 12px 8px; + } + + /* 模态框滚动内容 */ + .modal-scroll-content { + max-height: calc(85vh - 120px); + } +} + .toast.show { transform: translateX(0); } diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 57123955..cf696d59 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -2,50 +2,50 @@