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:
iRubbish
2025-08-26 15:55:13 +08:00
parent 82f545c3b0
commit 8a5d4b5d8f
6 changed files with 73 additions and 109 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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

View File

@@ -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();

View File

@@ -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 ? '禁用' : '激活'