mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 增加APIKey 客户端限制功能
This commit is contained in:
@@ -54,4 +54,7 @@ WEB_LOGO_URL=/assets/logo.png
|
||||
# 🛠️ 开发配置
|
||||
DEBUG=false
|
||||
ENABLE_CORS=true
|
||||
TRUST_PROXY=true
|
||||
TRUST_PROXY=true
|
||||
|
||||
# 🔒 客户端限制(可选)
|
||||
# ALLOW_CUSTOM_CLIENTS=false
|
||||
65
README.md
65
README.md
@@ -106,7 +106,7 @@
|
||||
- 🔄 **智能切换**: 账户出问题自动换下一个
|
||||
- 🚀 **性能优化**: 连接池、缓存,减少延迟
|
||||
- 📊 **监控面板**: Web界面查看所有数据
|
||||
- 🛡️ **安全控制**: 访问限制、速率控制
|
||||
- 🛡️ **安全控制**: 访问限制、速率控制、客户端限制
|
||||
- 🌐 **代理支持**: 支持HTTP/SOCKS5代理
|
||||
|
||||
---
|
||||
@@ -398,7 +398,11 @@ docker-compose.yml 已包含:
|
||||
1. 点击「API Keys」标签
|
||||
2. 点击「创建新Key」
|
||||
3. 给Key起个名字,比如「张三的Key」
|
||||
4. 设置使用限制(可选)
|
||||
4. 设置使用限制(可选):
|
||||
- **速率限制**: 限制每个时间窗口的请求次数和Token使用量
|
||||
- **并发限制**: 限制同时处理的请求数
|
||||
- **模型限制**: 限制可访问的模型列表
|
||||
- **客户端限制**: 限制只允许特定客户端使用(如ClaudeCode、Gemini-CLI等)
|
||||
5. 保存,记下生成的Key
|
||||
|
||||
### 4. 开始使用Claude code
|
||||
@@ -498,6 +502,63 @@ npm run service:status
|
||||
- 查看更新日志了解是否有破坏性变更
|
||||
- 如果有数据库结构变更,会自动迁移
|
||||
|
||||
---
|
||||
|
||||
## 🔒 客户端限制功能
|
||||
|
||||
### 功能说明
|
||||
|
||||
客户端限制功能允许你控制每个API Key可以被哪些客户端使用,通过User-Agent识别客户端,提高API的安全性。
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **在创建或编辑API Key时启用客户端限制**:
|
||||
- 勾选"启用客户端限制"
|
||||
- 选择允许的客户端(支持多选)
|
||||
|
||||
2. **预定义客户端**:
|
||||
- **ClaudeCode**: 官方Claude CLI(匹配 `claude-cli/x.x.x (external, cli)` 格式)
|
||||
- **Gemini-CLI**: Gemini命令行工具(匹配 `GeminiCLI/vx.x.x (platform; arch)` 格式)
|
||||
|
||||
3. **调试和诊断**:
|
||||
- 系统会在日志中记录所有请求的User-Agent
|
||||
- 客户端验证失败时会返回403错误并记录详细信息
|
||||
- 通过日志可以查看实际的User-Agent格式,方便配置自定义客户端
|
||||
|
||||
### 自定义客户端配置
|
||||
|
||||
如需添加自定义客户端,可以修改 `config/config.js` 文件:
|
||||
|
||||
```javascript
|
||||
clientRestrictions: {
|
||||
predefinedClients: [
|
||||
// ... 现有客户端配置
|
||||
{
|
||||
id: 'my_custom_client',
|
||||
name: 'My Custom Client',
|
||||
description: '我的自定义客户端',
|
||||
userAgentPattern: /^MyClient\/[\d\.]+/i
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 日志示例
|
||||
|
||||
认证成功时的日志:
|
||||
```
|
||||
🔓 Authenticated request from key: 测试Key (key-id) in 5ms
|
||||
User-Agent: "claude-cli/1.0.58 (external, cli)"
|
||||
```
|
||||
|
||||
客户端限制检查日志:
|
||||
```
|
||||
🔍 Checking client restriction for key: key-id (测试Key)
|
||||
User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
|
||||
Allowed clients: claude_code, gemini_cli
|
||||
🚫 Client restriction failed for key: key-id (测试Key) from 127.0.0.1, User-Agent: Mozilla/5.0...
|
||||
```
|
||||
|
||||
### 常见问题处理
|
||||
|
||||
**Redis连不上?**
|
||||
|
||||
@@ -2,6 +2,7 @@ const apiKeyService = require('../services/apiKeyService');
|
||||
const logger = require('../utils/logger');
|
||||
const redis = require('../models/redis');
|
||||
const { RateLimiterRedis } = require('rate-limiter-flexible');
|
||||
const config = require('../../config/config');
|
||||
|
||||
// 🔑 API Key验证中间件(优化版)
|
||||
const authenticateApiKey = async (req, res, next) => {
|
||||
@@ -42,6 +43,52 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 🔒 检查客户端限制
|
||||
if (validation.keyData.enableClientRestriction && validation.keyData.allowedClients?.length > 0) {
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown';
|
||||
|
||||
// 记录客户端限制检查开始
|
||||
logger.api(`🔍 Checking client restriction for key: ${validation.keyData.id} (${validation.keyData.name})`);
|
||||
logger.api(` User-Agent: "${userAgent}"`);
|
||||
logger.api(` Allowed clients: ${validation.keyData.allowedClients.join(', ')}`);
|
||||
|
||||
let clientAllowed = false;
|
||||
let matchedClient = null;
|
||||
|
||||
// 遍历允许的客户端列表
|
||||
for (const allowedClientId of validation.keyData.allowedClients) {
|
||||
// 在预定义客户端列表中查找
|
||||
const predefinedClient = config.clientRestrictions.predefinedClients.find(
|
||||
client => client.id === allowedClientId
|
||||
);
|
||||
|
||||
if (predefinedClient) {
|
||||
// 使用预定义的正则表达式匹配 User-Agent
|
||||
if (predefinedClient.userAgentPattern.test(userAgent)) {
|
||||
clientAllowed = true;
|
||||
matchedClient = predefinedClient.name;
|
||||
break;
|
||||
}
|
||||
} else if (config.clientRestrictions.allowCustomClients) {
|
||||
// 如果允许自定义客户端,这里可以添加自定义客户端的验证逻辑
|
||||
// 目前暂时跳过自定义客户端
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clientAllowed) {
|
||||
logger.security(`🚫 Client restriction failed for key: ${validation.keyData.id} (${validation.keyData.name}) from ${clientIP}, User-Agent: ${userAgent}`);
|
||||
return res.status(403).json({
|
||||
error: 'Client not allowed',
|
||||
message: 'Your client is not authorized to use this API key',
|
||||
allowedClients: validation.keyData.allowedClients
|
||||
});
|
||||
}
|
||||
|
||||
logger.api(`✅ Client validated: ${matchedClient} for key: ${validation.keyData.id} (${validation.keyData.name})`);
|
||||
logger.api(` Matched client: ${matchedClient} with User-Agent: "${userAgent}"`);
|
||||
}
|
||||
|
||||
// 检查并发限制
|
||||
const concurrencyLimit = validation.keyData.concurrencyLimit || 0;
|
||||
@@ -205,12 +252,16 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
rateLimitRequests: validation.keyData.rateLimitRequests,
|
||||
enableModelRestriction: validation.keyData.enableModelRestriction,
|
||||
restrictedModels: validation.keyData.restrictedModels,
|
||||
enableClientRestriction: validation.keyData.enableClientRestriction,
|
||||
allowedClients: validation.keyData.allowedClients,
|
||||
usage: validation.keyData.usage
|
||||
};
|
||||
req.usage = validation.keyData.usage;
|
||||
|
||||
const authDuration = Date.now() - startTime;
|
||||
const userAgent = req.headers['user-agent'] || 'No User-Agent';
|
||||
logger.api(`🔓 Authenticated request from key: ${validation.keyData.name} (${validation.keyData.id}) in ${authDuration}ms`);
|
||||
logger.api(` User-Agent: "${userAgent}"`);
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,6 +12,7 @@ const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('../../config/config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -236,6 +237,21 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取支持的客户端列表
|
||||
router.get('/supported-clients', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const clients = config.clientRestrictions.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);
|
||||
res.status(500).json({ error: 'Failed to get supported clients', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新的API Key
|
||||
router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
@@ -251,7 +267,9 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
rateLimitWindow,
|
||||
rateLimitRequests,
|
||||
enableModelRestriction,
|
||||
restrictedModels
|
||||
restrictedModels,
|
||||
enableClientRestriction,
|
||||
allowedClients
|
||||
} = req.body;
|
||||
|
||||
// 输入验证
|
||||
@@ -293,6 +311,15 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Restricted models must be an array' });
|
||||
}
|
||||
|
||||
// 验证客户端限制字段
|
||||
if (enableClientRestriction !== undefined && typeof enableClientRestriction !== 'boolean') {
|
||||
return res.status(400).json({ error: 'Enable client restriction must be a boolean' });
|
||||
}
|
||||
|
||||
if (allowedClients !== undefined && !Array.isArray(allowedClients)) {
|
||||
return res.status(400).json({ error: 'Allowed clients must be an array' });
|
||||
}
|
||||
|
||||
const newKey = await apiKeyService.generateApiKey({
|
||||
name,
|
||||
description,
|
||||
@@ -305,7 +332,9 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
rateLimitWindow,
|
||||
rateLimitRequests,
|
||||
enableModelRestriction,
|
||||
restrictedModels
|
||||
restrictedModels,
|
||||
enableClientRestriction,
|
||||
allowedClients
|
||||
});
|
||||
|
||||
logger.success(`🔑 Admin created new API key: ${name}`);
|
||||
@@ -320,7 +349,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { keyId } = req.params;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, expiresAt } = req.body;
|
||||
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, geminiAccountId, permissions, enableModelRestriction, restrictedModels, enableClientRestriction, allowedClients, expiresAt } = req.body;
|
||||
|
||||
// 只允许更新指定字段
|
||||
const updates = {};
|
||||
@@ -386,6 +415,21 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.restrictedModels = restrictedModels;
|
||||
}
|
||||
|
||||
// 处理客户端限制字段
|
||||
if (enableClientRestriction !== undefined) {
|
||||
if (typeof enableClientRestriction !== 'boolean') {
|
||||
return res.status(400).json({ error: 'Enable client restriction must be a boolean' });
|
||||
}
|
||||
updates.enableClientRestriction = enableClientRestriction;
|
||||
}
|
||||
|
||||
if (allowedClients !== undefined) {
|
||||
if (!Array.isArray(allowedClients)) {
|
||||
return res.status(400).json({ error: 'Allowed clients must be an array' });
|
||||
}
|
||||
updates.allowedClients = allowedClients;
|
||||
}
|
||||
|
||||
// 处理过期时间字段
|
||||
if (expiresAt !== undefined) {
|
||||
if (expiresAt === null) {
|
||||
|
||||
@@ -24,7 +24,9 @@ class ApiKeyService {
|
||||
rateLimitWindow = null,
|
||||
rateLimitRequests = null,
|
||||
enableModelRestriction = false,
|
||||
restrictedModels = []
|
||||
restrictedModels = [],
|
||||
enableClientRestriction = false,
|
||||
allowedClients = []
|
||||
} = options;
|
||||
|
||||
// 生成简单的API Key (64字符十六进制)
|
||||
@@ -47,6 +49,8 @@ class ApiKeyService {
|
||||
permissions: permissions || 'all',
|
||||
enableModelRestriction: String(enableModelRestriction),
|
||||
restrictedModels: JSON.stringify(restrictedModels || []),
|
||||
enableClientRestriction: String(enableClientRestriction || false),
|
||||
allowedClients: JSON.stringify(allowedClients || []),
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: '',
|
||||
expiresAt: expiresAt || '',
|
||||
@@ -73,6 +77,8 @@ class ApiKeyService {
|
||||
permissions: keyData.permissions,
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
restrictedModels: JSON.parse(keyData.restrictedModels),
|
||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
|
||||
createdAt: keyData.createdAt,
|
||||
expiresAt: keyData.expiresAt,
|
||||
createdBy: keyData.createdBy
|
||||
@@ -122,6 +128,14 @@ class ApiKeyService {
|
||||
restrictedModels = [];
|
||||
}
|
||||
|
||||
// 解析允许的客户端
|
||||
let allowedClients = [];
|
||||
try {
|
||||
allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : [];
|
||||
} catch (e) {
|
||||
allowedClients = [];
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
keyData: {
|
||||
@@ -136,6 +150,8 @@ class ApiKeyService {
|
||||
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
restrictedModels: restrictedModels,
|
||||
enableClientRestriction: keyData.enableClientRestriction === 'true',
|
||||
allowedClients: allowedClients,
|
||||
usage
|
||||
}
|
||||
};
|
||||
@@ -160,12 +176,18 @@ class ApiKeyService {
|
||||
key.currentConcurrency = await redis.getConcurrency(key.id);
|
||||
key.isActive = key.isActive === 'true';
|
||||
key.enableModelRestriction = key.enableModelRestriction === 'true';
|
||||
key.enableClientRestriction = key.enableClientRestriction === 'true';
|
||||
key.permissions = key.permissions || 'all'; // 兼容旧数据
|
||||
try {
|
||||
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : [];
|
||||
} catch (e) {
|
||||
key.restrictedModels = [];
|
||||
}
|
||||
try {
|
||||
key.allowedClients = key.allowedClients ? JSON.parse(key.allowedClients) : [];
|
||||
} catch (e) {
|
||||
key.allowedClients = [];
|
||||
}
|
||||
delete key.apiKey; // 不返回哈希后的key
|
||||
}
|
||||
|
||||
@@ -185,15 +207,15 @@ class ApiKeyService {
|
||||
}
|
||||
|
||||
// 允许更新的字段
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'geminiAccountId', 'permissions', 'expiresAt', 'enableModelRestriction', 'restrictedModels', 'enableClientRestriction', 'allowedClients'];
|
||||
const updatedData = { ...keyData };
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
if (allowedUpdates.includes(field)) {
|
||||
if (field === 'restrictedModels') {
|
||||
// 特殊处理 restrictedModels 数组
|
||||
if (field === 'restrictedModels' || field === 'allowedClients') {
|
||||
// 特殊处理数组字段
|
||||
updatedData[field] = JSON.stringify(value || []);
|
||||
} else if (field === 'enableModelRestriction') {
|
||||
} else if (field === 'enableModelRestriction' || field === 'enableClientRestriction') {
|
||||
// 布尔值转字符串
|
||||
updatedData[field] = String(value);
|
||||
} else {
|
||||
|
||||
@@ -127,6 +127,8 @@ const app = createApp({
|
||||
enableModelRestriction: false,
|
||||
restrictedModels: [],
|
||||
modelInput: '',
|
||||
enableClientRestriction: false,
|
||||
allowedClients: [],
|
||||
expireDuration: '', // 过期时长选择
|
||||
customExpireDate: '', // 自定义过期日期
|
||||
expiresAt: null // 实际的过期时间戳
|
||||
@@ -186,9 +188,14 @@ const app = createApp({
|
||||
permissions: 'all',
|
||||
enableModelRestriction: false,
|
||||
restrictedModels: [],
|
||||
modelInput: ''
|
||||
modelInput: '',
|
||||
enableClientRestriction: false,
|
||||
allowedClients: []
|
||||
},
|
||||
|
||||
// 支持的客户端列表
|
||||
supportedClients: [],
|
||||
|
||||
// 账户
|
||||
accounts: [],
|
||||
accountsLoading: false,
|
||||
@@ -346,10 +353,11 @@ const app = createApp({
|
||||
// 初始化日期筛选器和图表数据
|
||||
this.initializeDateFilter();
|
||||
|
||||
// 预加载账号列表和API Keys,以便正确显示绑定关系
|
||||
// 预加载账号列表、API Keys和支持的客户端,以便正确显示绑定关系
|
||||
Promise.all([
|
||||
this.loadAccounts(),
|
||||
this.loadApiKeys()
|
||||
this.loadApiKeys(),
|
||||
this.loadSupportedClients()
|
||||
]).then(() => {
|
||||
// 根据当前活跃标签页加载数据
|
||||
this.loadCurrentTabData();
|
||||
@@ -1778,6 +1786,18 @@ const app = createApp({
|
||||
}
|
||||
},
|
||||
|
||||
async loadSupportedClients() {
|
||||
try {
|
||||
const data = await this.apiRequest('/admin/supported-clients');
|
||||
if (data && data.success) {
|
||||
this.supportedClients = data.data || [];
|
||||
console.log('Loaded supported clients:', this.supportedClients);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load supported clients:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadApiKeys() {
|
||||
this.apiKeysLoading = true;
|
||||
console.log('Loading API Keys with time range:', this.apiKeyStatsTimeRange);
|
||||
@@ -1916,6 +1936,8 @@ const app = createApp({
|
||||
permissions: this.apiKeyForm.permissions || 'all',
|
||||
enableModelRestriction: this.apiKeyForm.enableModelRestriction,
|
||||
restrictedModels: this.apiKeyForm.restrictedModels,
|
||||
enableClientRestriction: this.apiKeyForm.enableClientRestriction,
|
||||
allowedClients: this.apiKeyForm.allowedClients,
|
||||
expiresAt: this.apiKeyForm.expiresAt
|
||||
})
|
||||
});
|
||||
@@ -1950,6 +1972,8 @@ const app = createApp({
|
||||
enableModelRestriction: false,
|
||||
restrictedModels: [],
|
||||
modelInput: '',
|
||||
enableClientRestriction: false,
|
||||
allowedClients: [],
|
||||
expireDuration: '',
|
||||
customExpireDate: '',
|
||||
expiresAt: null
|
||||
@@ -2117,7 +2141,9 @@ const app = createApp({
|
||||
permissions: key.permissions || 'all',
|
||||
enableModelRestriction: key.enableModelRestriction || false,
|
||||
restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [],
|
||||
modelInput: ''
|
||||
modelInput: '',
|
||||
enableClientRestriction: key.enableClientRestriction || false,
|
||||
allowedClients: key.allowedClients ? [...key.allowedClients] : []
|
||||
};
|
||||
this.showEditApiKeyModal = true;
|
||||
},
|
||||
@@ -2136,7 +2162,9 @@ const app = createApp({
|
||||
permissions: 'all',
|
||||
enableModelRestriction: false,
|
||||
restrictedModels: [],
|
||||
modelInput: ''
|
||||
modelInput: '',
|
||||
enableClientRestriction: false,
|
||||
allowedClients: []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -2154,7 +2182,9 @@ const app = createApp({
|
||||
geminiAccountId: this.editApiKeyForm.geminiAccountId || null,
|
||||
permissions: this.editApiKeyForm.permissions || 'all',
|
||||
enableModelRestriction: this.editApiKeyForm.enableModelRestriction,
|
||||
restrictedModels: this.editApiKeyForm.restrictedModels
|
||||
restrictedModels: this.editApiKeyForm.restrictedModels,
|
||||
enableClientRestriction: this.editApiKeyForm.enableClientRestriction,
|
||||
allowedClients: this.editApiKeyForm.allowedClients
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -2205,6 +2205,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="apiKeyForm.enableClientRestriction"
|
||||
id="enableClientRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="enableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="apiKeyForm.enableClientRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
||||
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`client_${client.id}`"
|
||||
:value="client.id"
|
||||
v-model="apiKeyForm.allowedClients"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
>
|
||||
<label :for="`client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -2462,6 +2499,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户端限制 -->
|
||||
<div>
|
||||
<div class="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="editApiKeyForm.enableClientRestriction"
|
||||
id="editEnableClientRestriction"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="editEnableClientRestriction" class="ml-2 text-sm font-semibold text-gray-700 cursor-pointer">
|
||||
启用客户端限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="editApiKeyForm.enableClientRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-600 mb-2">允许的客户端</label>
|
||||
<p class="text-xs text-gray-500 mb-3">勾选允许使用此API Key的客户端</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`edit_client_${client.id}`"
|
||||
:value="client.id"
|
||||
v-model="editApiKeyForm.allowedClients"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
|
||||
>
|
||||
<label :for="`edit_client_${client.id}`" class="ml-2 flex-1 cursor-pointer">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="text-xs text-gray-500 block">{{ client.description }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user