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_USERNAME=cr_admin_custom
|
||||||
# ADMIN_PASSWORD=your-secure-password
|
# 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 配置
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
@@ -45,8 +62,10 @@ LOG_MAX_FILES=5
|
|||||||
CLEANUP_INTERVAL=3600000
|
CLEANUP_INTERVAL=3600000
|
||||||
TOKEN_USAGE_RETENTION=2592000000
|
TOKEN_USAGE_RETENTION=2592000000
|
||||||
HEALTH_CHECK_INTERVAL=60000
|
HEALTH_CHECK_INTERVAL=60000
|
||||||
TIMEZONE_OFFSET=8 # UTC偏移小时数,默认+8(中国时区)
|
SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
METRICS_WINDOW=5 # 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
TIMEZONE_OFFSET=8
|
||||||
|
# 实时指标统计窗口(分钟),可选1-60,默认5分钟
|
||||||
|
METRICS_WINDOW=5
|
||||||
|
|
||||||
# 🎨 Web 界面配置
|
# 🎨 Web 界面配置
|
||||||
WEB_TITLE=Claude Relay Service
|
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_TIMEOUT=10000
|
||||||
WEBHOOK_RETRIES=3
|
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_PORT=6379
|
||||||
REDIS_PASSWORD=
|
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通知配置(可选)
|
||||||
WEBHOOK_ENABLED=true
|
WEBHOOK_ENABLED=true
|
||||||
WEBHOOK_URLS=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
|
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) => {
|
router.get('/verify-ou', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ou = '微店' } = req.query
|
const defaultOU = process.env.LDAP_DEFAULT_OU || 'YourOU'
|
||||||
const testDN = `OU=${ou},DC=corp,DC=weidian-inc,DC=com`
|
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}`)
|
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) => {
|
router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.user
|
const { username } = req.user
|
||||||
const { limit } = req.body
|
// 用户创建的API Key不需要任何输入参数,都使用默认值
|
||||||
|
// const { limit } = req.body // 不再从请求体获取limit
|
||||||
|
|
||||||
// 检查用户是否已有API Key
|
// 检查用户是否已有API Key
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
@@ -492,8 +499,8 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
|||||||
const defaultName = displayName || username
|
const defaultName = displayName || username
|
||||||
|
|
||||||
const keyParams = {
|
const keyParams = {
|
||||||
name: defaultName, // 忽略用户输入的name,强制使用displayName
|
name: defaultName, // 使用displayName作为API Key名称
|
||||||
tokenLimit: limit || 0,
|
tokenLimit: 0, // 固定为无限制
|
||||||
description: `AD用户${username}创建的API Key`,
|
description: `AD用户${username}创建的API Key`,
|
||||||
// AD用户创建的Key添加owner信息以区分用户归属
|
// AD用户创建的Key添加owner信息以区分用户归属
|
||||||
owner: username,
|
owner: username,
|
||||||
@@ -521,7 +528,7 @@ router.post('/user/api-keys', authenticateUser, async (req, res) => {
|
|||||||
id: newKey.id,
|
id: newKey.id,
|
||||||
key: newKey.apiKey, // 返回完整的API Key
|
key: newKey.apiKey, // 返回完整的API Key
|
||||||
name: newKey.name,
|
name: newKey.name,
|
||||||
tokenLimit: newKey.tokenLimit || limit || 0,
|
tokenLimit: newKey.tokenLimit || 0,
|
||||||
used: 0,
|
used: 0,
|
||||||
createdAt: newKey.createdAt,
|
createdAt: newKey.createdAt,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -616,8 +623,8 @@ router.put('/user/api-keys/:keyId', authenticateUser, async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制用户只能修改特定字段
|
// 限制用户只能修改特定字段(不允许修改name)
|
||||||
const allowedFields = ['name', 'description', 'isActive']
|
const allowedFields = ['description', 'isActive']
|
||||||
const filteredUpdates = {}
|
const filteredUpdates = {}
|
||||||
for (const [key, value] of Object.entries(updates)) {
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
if (allowedFields.includes(key)) {
|
if (allowedFields.includes(key)) {
|
||||||
|
|||||||
@@ -4,11 +4,22 @@ const logger = require('../utils/logger')
|
|||||||
class LDAPService {
|
class LDAPService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.client = null
|
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 = {
|
this.config = {
|
||||||
url: process.env.LDAP_URL || 'ldap://172.25.3.100:389',
|
url: process.env.LDAP_URL || '',
|
||||||
bindDN: process.env.LDAP_BIND_DN || 'LDAP-Proxy-Read',
|
bindDN: process.env.LDAP_BIND_DN || '',
|
||||||
bindPassword: process.env.LDAP_BIND_PASSWORD || 'Y%77JsVK8W',
|
bindPassword: process.env.LDAP_BIND_PASSWORD || '',
|
||||||
baseDN: process.env.LDAP_BASE_DN || 'OU=微店,DC=corp,DC=weidian-inc,DC=com',
|
baseDN: process.env.LDAP_BASE_DN || '',
|
||||||
searchFilter: process.env.LDAP_SEARCH_FILTER || '(&(objectClass=user)(cn={username}))',
|
searchFilter: process.env.LDAP_SEARCH_FILTER || '(&(objectClass=user)(cn={username}))',
|
||||||
timeout: parseInt(process.env.LDAP_TIMEOUT) || 10000,
|
timeout: parseInt(process.env.LDAP_TIMEOUT) || 10000,
|
||||||
connectTimeout: parseInt(process.env.LDAP_CONNECT_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">
|
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
API Key 将用于访问 Claude Relay Service
|
API Key 将用于访问 Claude Relay Service
|
||||||
</p>
|
</p>
|
||||||
<form class="mx-auto max-w-md space-y-3" @submit.prevent="createApiKey">
|
<form class="mx-auto max-w-md space-y-4" @submit.prevent="createApiKey">
|
||||||
<p class="text-center text-sm text-gray-600 dark:text-gray-400">
|
<div class="text-center">
|
||||||
API Key 名称将自动设置为您的用户名
|
<p class="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
</p>
|
API Key 名称将自动设置为您的用户名
|
||||||
<input
|
</p>
|
||||||
v-model.number="newKeyForm.limit"
|
<p class="text-sm text-gray-500 dark:text-gray-500">使用额度:无限制</p>
|
||||||
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"
|
</div>
|
||||||
max="1000000"
|
|
||||||
min="0"
|
|
||||||
placeholder="使用额度(0表示无限制)"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<button
|
<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"
|
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"
|
:disabled="createLoading"
|
||||||
@@ -94,12 +89,6 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex items-center gap-2">
|
<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
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
apiKey.isActive
|
apiKey.isActive
|
||||||
@@ -136,7 +125,7 @@
|
|||||||
<i class="fas fa-info-circle" />
|
<i class="fas fa-info-circle" />
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
已关联的历史API
|
已关联的历史API
|
||||||
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请联系管理员或创建新Key。
|
Key无法显示原始内容,仅在创建时可见。如需查看完整Key,请删除原key创建新Key。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
<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 showNewApiKeyModal = ref(false)
|
||||||
const newApiKeyData = ref(null)
|
const newApiKeyData = ref(null)
|
||||||
|
|
||||||
const newKeyForm = ref({
|
const newKeyForm = ref({})
|
||||||
limit: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取用户的 API Keys
|
// 获取用户的 API Keys
|
||||||
const fetchApiKeys = async () => {
|
const fetchApiKeys = async () => {
|
||||||
@@ -349,8 +336,9 @@ const createApiKey = async () => {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
limit: newKeyForm.value.limit || 0
|
// name和limit字段都由后端自动生成/设置
|
||||||
// name字段由后端根据用户displayName自动生成
|
// name: 用户displayName
|
||||||
|
// limit: 0 (无限制)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -362,7 +350,7 @@ const createApiKey = async () => {
|
|||||||
|
|
||||||
// 更新API Keys列表
|
// 更新API Keys列表
|
||||||
apiKeys.value = [result.apiKey]
|
apiKeys.value = [result.apiKey]
|
||||||
newKeyForm.value = { limit: 0 }
|
newKeyForm.value = {}
|
||||||
|
|
||||||
// 清除错误信息
|
// 清除错误信息
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -389,14 +377,6 @@ const calculateCostUsagePercentage = (used, limit) => {
|
|||||||
return Math.round((used / limit) * 100)
|
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状态
|
// 切换API Key状态
|
||||||
const toggleApiKeyStatus = async (apiKey) => {
|
const toggleApiKeyStatus = async (apiKey) => {
|
||||||
const action = apiKey.isActive ? '禁用' : '激活'
|
const action = apiKey.isActive ? '禁用' : '激活'
|
||||||
|
|||||||
Reference in New Issue
Block a user