mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 完善AD域控用户系统,增加配置说明
- 完善用户API Key创建流程,移除名称编辑权限 - 清理硬编码敏感信息,改用环境变量配置 - 在README.md和.env.example中添加AD域控配置说明 - 修复ESLint no-shadow错误 - 删除测试文件test-fixed-auto-link.js 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
31
.env.example
31
.env.example
@@ -15,6 +15,23 @@ ENCRYPTION_KEY=your-encryption-key-here
|
||||
# ADMIN_USERNAME=cr_admin_custom
|
||||
# ADMIN_PASSWORD=your-secure-password
|
||||
|
||||
|
||||
# 🏢 LDAP/Windows AD 域控认证配置(可选,用于企业内部用户登录)
|
||||
# 启用LDAP认证功能
|
||||
# LDAP_ENABLED=true
|
||||
# AD域控服务器地址
|
||||
# LDAP_URL=ldap://your-domain-controller-ip:389
|
||||
# 绑定用户
|
||||
# LDAP_BIND_DN=your-bind-user
|
||||
# 绑定用户密码
|
||||
# LDAP_BIND_PASSWORD=your-bind-password
|
||||
# 搜索基础DN
|
||||
# LDAP_BASE_DN=OU=YourOU,DC=your,DC=domain,DC=com
|
||||
# 用户搜索过滤器
|
||||
# LDAP_SEARCH_FILTER=(&(objectClass=user)(|(cn={username})(sAMAccountName={username})))
|
||||
# 连接超时设置
|
||||
# LDAP_TIMEOUT=10000
|
||||
|
||||
# 📊 Redis 配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
@@ -45,8 +62,10 @@ LOG_MAX_FILES=5
|
||||
CLEANUP_INTERVAL=3600000
|
||||
TOKEN_USAGE_RETENTION=2592000000
|
||||
HEALTH_CHECK_INTERVAL=60000
|
||||
TIMEZONE_OFFSET=8 # UTC偏移小时数,默认+8(中国时区)
|
||||
METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||
SYSTEM_TIMEZONE=Asia/Shanghai
|
||||
TIMEZONE_OFFSET=8
|
||||
# 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||
METRICS_WINDOW=5
|
||||
|
||||
# 🎨 Web 界面配置
|
||||
WEB_TITLE=Claude Relay Service
|
||||
@@ -67,11 +86,3 @@ WEBHOOK_URLS=https://your-webhook-url.com/notify,https://backup-webhook.com/noti
|
||||
WEBHOOK_TIMEOUT=10000
|
||||
WEBHOOK_RETRIES=3
|
||||
|
||||
# 🏢 LDAP/AD 域控配置
|
||||
LDAP_URL=ldap://172.25.3.100:389
|
||||
LDAP_BIND_DN=LDAP-Proxy-Read
|
||||
LDAP_BIND_PASSWORD=Y%77JsVK8W
|
||||
LDAP_BASE_DN=OU=微店,DC=corp,DC=weidian-inc,DC=com
|
||||
LDAP_SEARCH_FILTER=(&(objectClass=user)(cn={username}))
|
||||
LDAP_TIMEOUT=10000
|
||||
LDAP_CONNECT_TIMEOUT=10000
|
||||
@@ -250,6 +250,15 @@ REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# AD域控配置(可选,用于企业内部用户登录)
|
||||
LDAP_ENABLED=true
|
||||
LDAP_URL=ldap://your-domain-controller-ip:389
|
||||
LDAP_BIND_DN=your-bind-user
|
||||
LDAP_BIND_PASSWORD=your-bind-password
|
||||
LDAP_BASE_DN=DC=your-domain,DC=com
|
||||
LDAP_SEARCH_FILTER=(&(objectClass=user)(|(cn={username})(sAMAccountName={username})))
|
||||
LDAP_TIMEOUT=10000
|
||||
|
||||
# Webhook通知配置(可选)
|
||||
WEBHOOK_ENABLED=true
|
||||
WEBHOOK_URLS=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
|
||||
|
||||
@@ -196,8 +196,14 @@ router.get('/list-ous', async (req, res) => {
|
||||
*/
|
||||
router.get('/verify-ou', async (req, res) => {
|
||||
try {
|
||||
const { ou = '微店' } = req.query
|
||||
const testDN = `OU=${ou},DC=corp,DC=weidian-inc,DC=com`
|
||||
const defaultOU = process.env.LDAP_DEFAULT_OU || 'YourOU'
|
||||
const { ou = defaultOU } = req.query
|
||||
// 使用配置的baseDN来构建测试DN,而不是硬编码域名
|
||||
const config = ldapService.getConfig()
|
||||
// 从baseDN中提取域部分,替换OU部分
|
||||
const baseDNParts = config.baseDN.split(',')
|
||||
const domainParts = baseDNParts.filter((part) => part.trim().startsWith('DC='))
|
||||
const testDN = `OU=${ou},${domainParts.join(',')}`
|
||||
|
||||
logger.info(`Verifying OU exists: ${testDN}`)
|
||||
|
||||
@@ -461,7 +467,8 @@ router.get('/user/api-keys', authenticateUser, async (req, res) => {
|
||||
router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
const { username } = req.user
|
||||
const { limit } = req.body
|
||||
// 用户创建的API Key不需要任何输入参数,都使用默认值
|
||||
// const { limit } = req.body // 不再从请求体获取limit
|
||||
|
||||
// 检查用户是否已有API Key
|
||||
const redis = require('../models/redis')
|
||||
@@ -492,8 +499,8 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
||||
const defaultName = displayName || username
|
||||
|
||||
const keyParams = {
|
||||
name: defaultName, // 忽略用户输入的name,强制使用displayName
|
||||
tokenLimit: limit || 0,
|
||||
name: defaultName, // 使用displayName作为API Key名称
|
||||
tokenLimit: 0, // 固定为无限制
|
||||
description: `AD用户${username}创建的API Key`,
|
||||
// AD用户创建的Key添加owner信息以区分用户归属
|
||||
owner: username,
|
||||
@@ -521,7 +528,7 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
||||
id: newKey.id,
|
||||
key: newKey.apiKey, // 返回完整的API Key
|
||||
name: newKey.name,
|
||||
tokenLimit: newKey.tokenLimit || limit || 0,
|
||||
tokenLimit: newKey.tokenLimit || 0,
|
||||
used: 0,
|
||||
createdAt: newKey.createdAt,
|
||||
isActive: true,
|
||||
@@ -616,8 +623,8 @@ router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 限制用户只能修改特定字段
|
||||
const allowedFields = ['name', 'description', 'isActive']
|
||||
// 限制用户只能修改特定字段(不允许修改name)
|
||||
const allowedFields = ['description', 'isActive']
|
||||
const filteredUpdates = {}
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (allowedFields.includes(key)) {
|
||||
|
||||
@@ -4,11 +4,22 @@ const logger = require('../utils/logger')
|
||||
class LDAPService {
|
||||
constructor() {
|
||||
this.client = null
|
||||
|
||||
// 检查必需的LDAP配置
|
||||
if (
|
||||
!process.env.LDAP_URL ||
|
||||
!process.env.LDAP_BIND_DN ||
|
||||
!process.env.LDAP_BIND_PASSWORD ||
|
||||
!process.env.LDAP_BASE_DN
|
||||
) {
|
||||
logger.warn('⚠️ LDAP配置不完整,请检查.env文件中的LDAP配置项')
|
||||
}
|
||||
|
||||
this.config = {
|
||||
url: process.env.LDAP_URL || 'ldap://172.25.3.100:389',
|
||||
bindDN: process.env.LDAP_BIND_DN || 'LDAP-Proxy-Read',
|
||||
bindPassword: process.env.LDAP_BIND_PASSWORD || 'Y%77JsVK8W',
|
||||
baseDN: process.env.LDAP_BASE_DN || 'OU=微店,DC=corp,DC=weidian-inc,DC=com',
|
||||
url: process.env.LDAP_URL || '',
|
||||
bindDN: process.env.LDAP_BIND_DN || '',
|
||||
bindPassword: process.env.LDAP_BIND_PASSWORD || '',
|
||||
baseDN: process.env.LDAP_BASE_DN || '',
|
||||
searchFilter: process.env.LDAP_SEARCH_FILTER || '(&(objectClass=user)(cn={username}))',
|
||||
timeout: parseInt(process.env.LDAP_TIMEOUT) || 10000,
|
||||
connectTimeout: parseInt(process.env.LDAP_CONNECT_TIMEOUT) || 10000
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const config = require('./config/config');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
// 模拟创建一个包含displayName的JWT token
|
||||
const userInfo = {
|
||||
type: 'ad_user',
|
||||
username: 'zhangji',
|
||||
displayName: '张佶',
|
||||
email: 'zhangji@weidian.com',
|
||||
groups: ['CN=Weidian-IT组,OU=Weidian Groups,OU=微店,DC=corp,DC=weidian-inc,DC=com'],
|
||||
loginTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
const token = jwt.sign(userInfo, config.security.jwtSecret, {
|
||||
expiresIn: '8h'
|
||||
});
|
||||
|
||||
console.log('测试修正后的自动关联功能');
|
||||
console.log('用户displayName: 张佶');
|
||||
|
||||
async function testFixedAutoLink() {
|
||||
try {
|
||||
console.log('\n=== 测试获取用户API Keys ===');
|
||||
|
||||
const response = await fetch('http://localhost:3000/admin/ldap/user/api-keys', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
console.log('\n结果:', JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.success && result.apiKeys && result.apiKeys.length > 0) {
|
||||
console.log('\n✅ 成功!找到了关联的API Key:');
|
||||
result.apiKeys.forEach(key => {
|
||||
console.log(`- ID: ${key.id}`);
|
||||
console.log(`- Name: ${key.name}`);
|
||||
console.log(`- Key: ${key.key.substring(0, 10)}...${key.key.substring(key.key.length-10)}`);
|
||||
console.log(`- Limit: ${key.limit}`);
|
||||
console.log(`- Status: ${key.status}`);
|
||||
});
|
||||
} else {
|
||||
console.log('\n❌ 没有找到关联的API Key');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('测试错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testFixedAutoLink();
|
||||
@@ -25,18 +25,13 @@
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
API Key 将用于访问 Claude Relay Service
|
||||
</p>
|
||||
<form class="mx-auto max-w-md space-y-3" @submit.prevent="createApiKey">
|
||||
<p class="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
API Key 名称将自动设置为您的用户名
|
||||
</p>
|
||||
<input
|
||||
v-model.number="newKeyForm.limit"
|
||||
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-800 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
max="1000000"
|
||||
min="0"
|
||||
placeholder="使用额度(0表示无限制)"
|
||||
type="number"
|
||||
/>
|
||||
<form class="mx-auto max-w-md space-y-4" @submit.prevent="createApiKey">
|
||||
<div class="text-center">
|
||||
<p class="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
API Key 名称将自动设置为您的用户名
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500">使用额度:无限制</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-full rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 px-6 py-3 font-medium text-white transition-all hover:from-blue-600 hover:to-purple-700 disabled:opacity-50"
|
||||
:disabled="createLoading"
|
||||
@@ -94,12 +89,6 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
@click="editApiKey(apiKey)"
|
||||
>
|
||||
<i class="fas fa-edit mr-1" />编辑
|
||||
</button>
|
||||
<button
|
||||
:class="[
|
||||
apiKey.isActive
|
||||
@@ -136,7 +125,7 @@
|
||||
<i class="fas fa-info-circle" />
|
||||
<span class="text-sm">
|
||||
已关联的历史API
|
||||
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请联系管理员或创建新Key。
|
||||
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请删除原key创建新Key。
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
@@ -303,9 +292,7 @@ const selectedApiKeyForDetail = ref(null)
|
||||
const showNewApiKeyModal = ref(false)
|
||||
const newApiKeyData = ref(null)
|
||||
|
||||
const newKeyForm = ref({
|
||||
limit: 0
|
||||
})
|
||||
const newKeyForm = ref({})
|
||||
|
||||
// 获取用户的 API Keys
|
||||
const fetchApiKeys = async () => {
|
||||
@@ -349,8 +336,9 @@ const createApiKey = async () => {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
limit: newKeyForm.value.limit || 0
|
||||
// name字段由后端根据用户displayName自动生成
|
||||
// name和limit字段都由后端自动生成/设置
|
||||
// name: 用户displayName
|
||||
// limit: 0 (无限制)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -362,7 +350,7 @@ const createApiKey = async () => {
|
||||
|
||||
// 更新API Keys列表
|
||||
apiKeys.value = [result.apiKey]
|
||||
newKeyForm.value = { limit: 0 }
|
||||
newKeyForm.value = {}
|
||||
|
||||
// 清除错误信息
|
||||
error.value = ''
|
||||
@@ -389,14 +377,6 @@ const calculateCostUsagePercentage = (used, limit) => {
|
||||
return Math.round((used / limit) * 100)
|
||||
}
|
||||
|
||||
// 编辑API Key(简化版,只允许修改名称和描述)
|
||||
const editApiKey = (apiKey) => {
|
||||
const newName = prompt('请输入新的API Key名称:', apiKey.name)
|
||||
if (newName !== null && newName.trim() !== '') {
|
||||
updateApiKey(apiKey.id, { name: newName.trim() })
|
||||
}
|
||||
}
|
||||
|
||||
// 切换API Key状态
|
||||
const toggleApiKeyStatus = async (apiKey) => {
|
||||
const action = apiKey.isActive ? '禁用' : '激活'
|
||||
|
||||
Reference in New Issue
Block a user