mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: user stats
This commit is contained in:
@@ -490,7 +490,9 @@ const authenticateUser = async (req, res, next) => {
|
||||
|
||||
// 检查用户是否被禁用
|
||||
if (!user.isActive) {
|
||||
logger.security(`🔒 Disabled user login attempt: ${user.username} from ${req.ip || 'unknown'}`)
|
||||
logger.security(
|
||||
`🔒 Disabled user login attempt: ${user.username} from ${req.ip || 'unknown'}`
|
||||
)
|
||||
return res.status(403).json({
|
||||
error: 'Account disabled',
|
||||
message: 'Your account has been disabled. Please contact administrator.'
|
||||
@@ -506,7 +508,7 @@ const authenticateUser = async (req, res, next) => {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.role,
|
||||
sessionToken: sessionToken,
|
||||
sessionToken,
|
||||
sessionCreatedAt: session.createdAt
|
||||
}
|
||||
|
||||
@@ -559,7 +561,7 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
loginTime: adminSession.loginTime
|
||||
}
|
||||
req.userType = 'admin'
|
||||
|
||||
|
||||
const authDuration = Date.now() - startTime
|
||||
logger.security(`🔐 Admin authenticated: ${adminSession.username} in ${authDuration}ms`)
|
||||
return next()
|
||||
@@ -575,7 +577,7 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
const sessionValidation = await userService.validateUserSession(userToken)
|
||||
if (sessionValidation) {
|
||||
const { session, user } = sessionValidation
|
||||
|
||||
|
||||
if (user.isActive) {
|
||||
req.user = {
|
||||
id: user.id,
|
||||
@@ -589,7 +591,7 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
sessionCreatedAt: session.createdAt
|
||||
}
|
||||
req.userType = 'user'
|
||||
|
||||
|
||||
const authDuration = Date.now() - startTime
|
||||
logger.info(`👤 User authenticated: ${user.username} (${user.id}) in ${authDuration}ms`)
|
||||
return next()
|
||||
@@ -606,7 +608,6 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
error: 'Authentication required',
|
||||
message: 'Please login as user or admin to access this resource'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
const authDuration = Date.now() - startTime
|
||||
logger.error(`❌ User/Admin authentication error (${authDuration}ms):`, {
|
||||
@@ -624,34 +625,34 @@ const authenticateUserOrAdmin = async (req, res, next) => {
|
||||
}
|
||||
|
||||
// 🛡️ 权限检查中间件
|
||||
const requireRole = (allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
// 管理员始终有权限
|
||||
if (req.admin) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// 检查用户角色
|
||||
if (req.user) {
|
||||
const userRole = req.user.role
|
||||
const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles]
|
||||
|
||||
if (allowed.includes(userRole)) {
|
||||
return next()
|
||||
} else {
|
||||
logger.security(`🚫 Access denied for user ${req.user.username} (role: ${userRole}) to ${req.originalUrl}`)
|
||||
return res.status(403).json({
|
||||
error: 'Insufficient permissions',
|
||||
message: `This resource requires one of the following roles: ${allowed.join(', ')}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required',
|
||||
message: 'Please login to access this resource'
|
||||
})
|
||||
const requireRole = (allowedRoles) => (req, res, next) => {
|
||||
// 管理员始终有权限
|
||||
if (req.admin) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// 检查用户角色
|
||||
if (req.user) {
|
||||
const userRole = req.user.role
|
||||
const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles]
|
||||
|
||||
if (allowed.includes(userRole)) {
|
||||
return next()
|
||||
} else {
|
||||
logger.security(
|
||||
`🚫 Access denied for user ${req.user.username} (role: ${userRole}) to ${req.originalUrl}`
|
||||
)
|
||||
return res.status(403).json({
|
||||
error: 'Insufficient permissions',
|
||||
message: `This resource requires one of the following roles: ${allowed.join(', ')}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required',
|
||||
message: 'Please login to access this resource'
|
||||
})
|
||||
}
|
||||
|
||||
// 🔒 管理员权限检查中间件
|
||||
@@ -665,7 +666,9 @@ const requireAdmin = (req, res, next) => {
|
||||
return next()
|
||||
}
|
||||
|
||||
logger.security(`🚫 Admin access denied for ${req.user?.username || 'unknown'} from ${req.ip || 'unknown'}`)
|
||||
logger.security(
|
||||
`🚫 Admin access denied for ${req.user?.username || 'unknown'} from ${req.ip || 'unknown'}`
|
||||
)
|
||||
return res.status(403).json({
|
||||
error: 'Admin access required',
|
||||
message: 'This resource requires administrator privileges'
|
||||
|
||||
@@ -66,7 +66,6 @@ router.post('/login', async (req, res) => {
|
||||
},
|
||||
sessionToken: authResult.sessionToken
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ User login error:', error)
|
||||
res.status(500).json({
|
||||
@@ -80,14 +79,13 @@ router.post('/login', async (req, res) => {
|
||||
router.post('/logout', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
await userService.invalidateUserSession(req.user.sessionToken)
|
||||
|
||||
|
||||
logger.info(`👋 User logout: ${req.user.username}`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logout successful'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ User logout error:', error)
|
||||
res.status(500).json({
|
||||
@@ -125,7 +123,6 @@ router.get('/profile', authenticateUser, async (req, res) => {
|
||||
totalUsage: user.totalUsage
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user profile error:', error)
|
||||
res.status(500).json({
|
||||
@@ -139,9 +136,9 @@ router.get('/profile', authenticateUser, async (req, res) => {
|
||||
router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||
try {
|
||||
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
||||
|
||||
|
||||
// 移除敏感信息
|
||||
const safeApiKeys = apiKeys.map(key => ({
|
||||
const safeApiKeys = apiKeys.map((key) => ({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
description: key.description,
|
||||
@@ -154,7 +151,9 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||
dailyCost: key.dailyCost,
|
||||
dailyCostLimit: key.dailyCostLimit,
|
||||
// 不返回实际的key值,只返回前缀和后几位
|
||||
keyPreview: key.key ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` : null
|
||||
keyPreview: key.key
|
||||
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
|
||||
: null
|
||||
}))
|
||||
|
||||
res.json({
|
||||
@@ -162,7 +161,6 @@ router.get('/api-keys', authenticateUser, async (req, res) => {
|
||||
apiKeys: safeApiKeys,
|
||||
total: safeApiKeys.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user API keys error:', error)
|
||||
res.status(500).json({
|
||||
@@ -207,7 +205,7 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
}
|
||||
|
||||
const newApiKey = await apiKeyService.createApiKey(apiKeyData)
|
||||
|
||||
|
||||
// 更新用户API Key数量
|
||||
await userService.updateUserApiKeyCount(req.user.id, userApiKeys.length + 1)
|
||||
|
||||
@@ -227,7 +225,6 @@ router.post('/api-keys', authenticateUser, async (req, res) => {
|
||||
createdAt: newApiKey.createdAt
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Create user API key error:', error)
|
||||
res.status(500).json({
|
||||
@@ -265,7 +262,6 @@ router.post('/api-keys/:keyId/regenerate', authenticateUser, async (req, res) =>
|
||||
updatedAt: newKey.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Regenerate user API key error:', error)
|
||||
res.status(500).json({
|
||||
@@ -301,7 +297,6 @@ router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => {
|
||||
success: true,
|
||||
message: 'API key deleted successfully'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Delete user API key error:', error)
|
||||
res.status(500).json({
|
||||
@@ -318,7 +313,7 @@ router.get('/usage-stats', authenticateUser, async (req, res) => {
|
||||
|
||||
// 获取用户的API Keys
|
||||
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
|
||||
const apiKeyIds = userApiKeys.map(key => key.id)
|
||||
const apiKeyIds = userApiKeys.map((key) => key.id)
|
||||
|
||||
if (apiKeyIds.length === 0) {
|
||||
return res.json({
|
||||
@@ -341,7 +336,6 @@ router.get('/usage-stats', authenticateUser, async (req, res) => {
|
||||
success: true,
|
||||
stats
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user usage stats error:', error)
|
||||
res.status(500).json({
|
||||
@@ -371,10 +365,11 @@ router.get('/', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
|
||||
let filteredUsers = result.users
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase()
|
||||
filteredUsers = result.users.filter(user =>
|
||||
user.username.toLowerCase().includes(searchLower) ||
|
||||
user.displayName.toLowerCase().includes(searchLower) ||
|
||||
user.email.toLowerCase().includes(searchLower)
|
||||
filteredUsers = result.users.filter(
|
||||
(user) =>
|
||||
user.username.toLowerCase().includes(searchLower) ||
|
||||
user.displayName.toLowerCase().includes(searchLower) ||
|
||||
user.email.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -388,7 +383,6 @@ router.get('/', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
|
||||
totalPages: result.totalPages
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get users list error:', error)
|
||||
res.status(500).json({
|
||||
@@ -418,7 +412,7 @@ router.get('/:userId', authenticateUserOrAdmin, requireAdmin, async (req, res) =
|
||||
success: true,
|
||||
user: {
|
||||
...user,
|
||||
apiKeys: apiKeys.map(key => ({
|
||||
apiKeys: apiKeys.map((key) => ({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
description: key.description,
|
||||
@@ -426,11 +420,12 @@ router.get('/:userId', authenticateUserOrAdmin, requireAdmin, async (req, res) =
|
||||
createdAt: key.createdAt,
|
||||
lastUsedAt: key.lastUsedAt,
|
||||
usage: key.usage,
|
||||
keyPreview: key.key ? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}` : null
|
||||
keyPreview: key.key
|
||||
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
|
||||
: null
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user details error:', error)
|
||||
res.status(500).json({
|
||||
@@ -456,7 +451,9 @@ router.patch('/:userId/status', authenticateUserOrAdmin, requireAdmin, async (re
|
||||
const updatedUser = await userService.updateUserStatus(userId, isActive)
|
||||
|
||||
const adminUser = req.admin?.username || req.user?.username
|
||||
logger.info(`🔄 Admin ${adminUser} ${isActive ? 'enabled' : 'disabled'} user: ${updatedUser.username}`)
|
||||
logger.info(
|
||||
`🔄 Admin ${adminUser} ${isActive ? 'enabled' : 'disabled'} user: ${updatedUser.username}`
|
||||
)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -468,7 +465,6 @@ router.patch('/:userId/status', authenticateUserOrAdmin, requireAdmin, async (re
|
||||
updatedAt: updatedUser.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Update user status error:', error)
|
||||
res.status(500).json({
|
||||
@@ -507,7 +503,6 @@ router.patch('/:userId/role', authenticateUserOrAdmin, requireAdmin, async (req,
|
||||
updatedAt: updatedUser.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Update user role error:', error)
|
||||
res.status(500).json({
|
||||
@@ -540,7 +535,6 @@ router.post('/:userId/disable-keys', authenticateUserOrAdmin, requireAdmin, asyn
|
||||
message: `Disabled ${result.count} API keys for user ${user.username}`,
|
||||
disabledCount: result.count
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Disable user API keys error:', error)
|
||||
res.status(500).json({
|
||||
@@ -566,7 +560,7 @@ router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async
|
||||
|
||||
// 获取用户的API Keys
|
||||
const userApiKeys = await apiKeyService.getUserApiKeys(userId)
|
||||
const apiKeyIds = userApiKeys.map(key => key.id)
|
||||
const apiKeyIds = userApiKeys.map((key) => key.id)
|
||||
|
||||
if (apiKeyIds.length === 0) {
|
||||
return res.json({
|
||||
@@ -599,7 +593,6 @@ router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async
|
||||
},
|
||||
stats
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user usage stats (admin) error:', error)
|
||||
res.status(500).json({
|
||||
@@ -618,7 +611,6 @@ router.get('/stats/overview', authenticateUserOrAdmin, requireAdmin, async (req,
|
||||
success: true,
|
||||
stats
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Get user stats overview error:', error)
|
||||
res.status(500).json({
|
||||
@@ -638,7 +630,6 @@ router.get('/admin/ldap-test', authenticateUserOrAdmin, requireAdmin, async (req
|
||||
ldapTest: testResult,
|
||||
config: ldapService.getConfigInfo()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ LDAP test error:', error)
|
||||
res.status(500).json({
|
||||
@@ -648,4 +639,4 @@ router.get('/admin/ldap-test', authenticateUserOrAdmin, requireAdmin, async (req
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
||||
@@ -492,8 +492,8 @@ class ApiKeyService {
|
||||
try {
|
||||
const allKeys = await redis.getAllApiKeys()
|
||||
return allKeys
|
||||
.filter(key => key.userId === userId)
|
||||
.map(key => ({
|
||||
.filter((key) => key.userId === userId)
|
||||
.map((key) => ({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
description: key.description,
|
||||
@@ -520,7 +520,9 @@ class ApiKeyService {
|
||||
async getApiKeyById(keyId, userId = null) {
|
||||
try {
|
||||
const keyData = await redis.getApiKey(keyId)
|
||||
if (!keyData) return null
|
||||
if (!keyData) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果指定了用户ID,检查权限
|
||||
if (userId && keyData.userId !== userId) {
|
||||
|
||||
@@ -7,7 +7,7 @@ class LdapService {
|
||||
constructor() {
|
||||
this.config = config.ldap
|
||||
this.client = null
|
||||
|
||||
|
||||
// 验证配置
|
||||
if (this.config.enabled) {
|
||||
this.validateConfiguration()
|
||||
@@ -17,31 +17,34 @@ class LdapService {
|
||||
// 🔍 验证LDAP配置
|
||||
validateConfiguration() {
|
||||
const errors = []
|
||||
|
||||
|
||||
if (!this.config.server) {
|
||||
errors.push('LDAP server configuration is missing')
|
||||
} else {
|
||||
if (!this.config.server.url || typeof this.config.server.url !== 'string') {
|
||||
errors.push('LDAP server URL is not configured or invalid')
|
||||
}
|
||||
|
||||
|
||||
if (!this.config.server.bindDN || typeof this.config.server.bindDN !== 'string') {
|
||||
errors.push('LDAP bind DN is not configured or invalid')
|
||||
}
|
||||
|
||||
if (!this.config.server.bindCredentials || typeof this.config.server.bindCredentials !== 'string') {
|
||||
|
||||
if (
|
||||
!this.config.server.bindCredentials ||
|
||||
typeof this.config.server.bindCredentials !== 'string'
|
||||
) {
|
||||
errors.push('LDAP bind credentials are not configured or invalid')
|
||||
}
|
||||
|
||||
|
||||
if (!this.config.server.searchBase || typeof this.config.server.searchBase !== 'string') {
|
||||
errors.push('LDAP search base is not configured or invalid')
|
||||
}
|
||||
|
||||
|
||||
if (!this.config.server.searchFilter || typeof this.config.server.searchFilter !== 'string') {
|
||||
errors.push('LDAP search filter is not configured or invalid')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (errors.length > 0) {
|
||||
logger.error('❌ LDAP configuration validation failed:', errors)
|
||||
// Don't throw error during initialization, just log warnings
|
||||
@@ -59,7 +62,7 @@ class LdapService {
|
||||
|
||||
// Try different ways to get the DN
|
||||
let dn = null
|
||||
|
||||
|
||||
// Method 1: Direct dn property
|
||||
if (ldapEntry.dn) {
|
||||
dn = ldapEntry.dn
|
||||
@@ -107,35 +110,35 @@ class LdapService {
|
||||
// 如果使用 LDAPS (SSL/TLS),添加 TLS 选项
|
||||
if (this.config.server.url.toLowerCase().startsWith('ldaps://')) {
|
||||
const tlsOptions = {}
|
||||
|
||||
|
||||
// 证书验证设置
|
||||
if (this.config.server.tls) {
|
||||
if (typeof this.config.server.tls.rejectUnauthorized === 'boolean') {
|
||||
tlsOptions.rejectUnauthorized = this.config.server.tls.rejectUnauthorized
|
||||
}
|
||||
|
||||
|
||||
// CA 证书
|
||||
if (this.config.server.tls.ca) {
|
||||
tlsOptions.ca = this.config.server.tls.ca
|
||||
}
|
||||
|
||||
|
||||
// 客户端证书和私钥 (双向认证)
|
||||
if (this.config.server.tls.cert) {
|
||||
tlsOptions.cert = this.config.server.tls.cert
|
||||
}
|
||||
|
||||
|
||||
if (this.config.server.tls.key) {
|
||||
tlsOptions.key = this.config.server.tls.key
|
||||
}
|
||||
|
||||
|
||||
// 服务器名称 (SNI)
|
||||
if (this.config.server.tls.servername) {
|
||||
tlsOptions.servername = this.config.server.tls.servername
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clientOptions.tlsOptions = tlsOptions
|
||||
|
||||
|
||||
logger.debug('🔒 Creating LDAPS client with TLS options:', {
|
||||
url: this.config.server.url,
|
||||
rejectUnauthorized: tlsOptions.rejectUnauthorized,
|
||||
@@ -184,23 +187,23 @@ class LdapService {
|
||||
async bindClient(client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 验证绑定凭据
|
||||
const bindDN = this.config.server.bindDN
|
||||
const bindCredentials = this.config.server.bindCredentials
|
||||
|
||||
const { bindDN } = this.config.server
|
||||
const { bindCredentials } = this.config.server
|
||||
|
||||
if (!bindDN || typeof bindDN !== 'string') {
|
||||
const error = new Error('LDAP bind DN is not configured or invalid')
|
||||
logger.error('❌ LDAP configuration error:', error.message)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!bindCredentials || typeof bindCredentials !== 'string') {
|
||||
const error = new Error('LDAP bind credentials are not configured or invalid')
|
||||
logger.error('❌ LDAP configuration error:', error.message)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
client.bind(bindDN, bindCredentials, (err) => {
|
||||
if (err) {
|
||||
logger.error('❌ LDAP bind failed:', err)
|
||||
@@ -226,7 +229,7 @@ class LdapService {
|
||||
logger.debug(`🔍 Searching for user: ${username} with filter: ${searchFilter}`)
|
||||
|
||||
const entries = []
|
||||
|
||||
|
||||
client.search(this.config.server.searchBase, searchOptions, (err, res) => {
|
||||
if (err) {
|
||||
logger.error('❌ LDAP search error:', err)
|
||||
@@ -256,8 +259,10 @@ class LdapService {
|
||||
})
|
||||
|
||||
res.on('end', (result) => {
|
||||
logger.debug(`✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries`)
|
||||
|
||||
logger.debug(
|
||||
`✅ LDAP search completed. Status: ${result.status}, Found ${entries.length} entries`
|
||||
)
|
||||
|
||||
if (entries.length === 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
@@ -270,7 +275,7 @@ class LdapService {
|
||||
entryStringified: JSON.stringify(entries[0], null, 2).substring(0, 500)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (entries.length === 1) {
|
||||
resolve(entries[0])
|
||||
} else {
|
||||
@@ -293,18 +298,18 @@ class LdapService {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!password || typeof password !== 'string') {
|
||||
logger.debug(`🚫 Invalid or empty password for DN: ${userDN}`)
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const authClient = this.createClient()
|
||||
|
||||
|
||||
authClient.bind(userDN, password, (err) => {
|
||||
authClient.unbind() // 立即关闭认证客户端
|
||||
|
||||
|
||||
if (err) {
|
||||
if (err.name === 'InvalidCredentialsError') {
|
||||
logger.debug(`🚫 Invalid credentials for DN: ${userDN}`)
|
||||
@@ -329,7 +334,7 @@ class LdapService {
|
||||
|
||||
// 创建属性映射
|
||||
const attrMap = {}
|
||||
attributes.forEach(attr => {
|
||||
attributes.forEach((attr) => {
|
||||
const name = attr.type || attr.name
|
||||
const values = Array.isArray(attr.values) ? attr.values : [attr.values]
|
||||
attrMap[name] = values.length === 1 ? values[0] : values
|
||||
@@ -337,7 +342,7 @@ class LdapService {
|
||||
|
||||
// 根据配置映射用户属性
|
||||
const mapping = this.config.userMapping
|
||||
|
||||
|
||||
userInfo.displayName = attrMap[mapping.displayName] || username
|
||||
userInfo.email = attrMap[mapping.email] || ''
|
||||
userInfo.firstName = attrMap[mapping.firstName] || ''
|
||||
@@ -386,7 +391,10 @@ class LdapService {
|
||||
throw new Error('LDAP bind DN is not configured')
|
||||
}
|
||||
|
||||
if (!this.config.server.bindCredentials || typeof this.config.server.bindCredentials !== 'string') {
|
||||
if (
|
||||
!this.config.server.bindCredentials ||
|
||||
typeof this.config.server.bindCredentials !== 'string'
|
||||
) {
|
||||
throw new Error('LDAP bind credentials are not configured')
|
||||
}
|
||||
|
||||
@@ -450,9 +458,9 @@ class LdapService {
|
||||
// 7. 检查用户是否被禁用
|
||||
if (!user.isActive) {
|
||||
logger.security(`🔒 Disabled user LDAP login attempt: ${username} from LDAP authentication`)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Your account has been disabled. Please contact administrator.'
|
||||
return {
|
||||
success: false,
|
||||
message: 'Your account has been disabled. Please contact administrator.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +478,6 @@ class LdapService {
|
||||
sessionToken,
|
||||
message: 'Authentication successful'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ LDAP authentication error:', error)
|
||||
return {
|
||||
@@ -499,7 +506,7 @@ class LdapService {
|
||||
|
||||
try {
|
||||
await this.bindClient(client)
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'LDAP connection successful',
|
||||
@@ -553,4 +560,4 @@ class LdapService {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LdapService()
|
||||
module.exports = new LdapService()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const redis = require('../models/redis')
|
||||
const crypto = require('crypto')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const logger = require('../utils/logger')
|
||||
const config = require('../../config/config')
|
||||
|
||||
@@ -88,7 +87,9 @@ class UserService {
|
||||
async getUserByUsername(username) {
|
||||
try {
|
||||
const userId = await redis.get(`${this.usernamePrefix}${username}`)
|
||||
if (!userId) return null
|
||||
if (!userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const userData = await redis.get(`${this.userPrefix}${userId}`)
|
||||
return userData ? JSON.parse(userData) : null
|
||||
@@ -99,16 +100,100 @@ class UserService {
|
||||
}
|
||||
|
||||
// 👤 通过ID获取用户
|
||||
async getUserById(userId) {
|
||||
async getUserById(userId, calculateUsage = true) {
|
||||
try {
|
||||
const userData = await redis.get(`${this.userPrefix}${userId}`)
|
||||
return userData ? JSON.parse(userData) : null
|
||||
if (!userData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = JSON.parse(userData)
|
||||
|
||||
// Calculate totalUsage by aggregating user's API keys usage (if requested)
|
||||
if (calculateUsage) {
|
||||
try {
|
||||
const usageStats = await this.calculateUserUsageStats(userId)
|
||||
user.totalUsage = usageStats.totalUsage
|
||||
user.apiKeyCount = usageStats.apiKeyCount
|
||||
} catch (error) {
|
||||
logger.error('❌ Error calculating user usage stats:', error)
|
||||
// Fallback to stored values if calculation fails
|
||||
user.totalUsage = user.totalUsage || {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0
|
||||
}
|
||||
user.apiKeyCount = user.apiKeyCount || 0
|
||||
}
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
logger.error('❌ Error getting user by ID:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 计算用户使用统计(通过聚合API Keys)
|
||||
async calculateUserUsageStats(userId) {
|
||||
try {
|
||||
// Use redis directly to avoid circular dependency
|
||||
const client = redis.getClientSafe()
|
||||
const pattern = 'api_key:*'
|
||||
const keys = await client.keys(pattern)
|
||||
|
||||
const userApiKeys = []
|
||||
for (const key of keys) {
|
||||
const keyData = await client.get(key)
|
||||
if (keyData) {
|
||||
const apiKey = JSON.parse(keyData)
|
||||
if (apiKey.userId === userId) {
|
||||
// Get usage stats for this API key
|
||||
const usage = await redis.getUsageStats(apiKey.id)
|
||||
userApiKeys.push({
|
||||
id: apiKey.id,
|
||||
name: apiKey.name,
|
||||
usage
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalUsage = {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0
|
||||
}
|
||||
|
||||
for (const apiKey of userApiKeys) {
|
||||
if (apiKey.usage) {
|
||||
totalUsage.requests += apiKey.usage.requests || 0
|
||||
totalUsage.inputTokens += apiKey.usage.inputTokens || 0
|
||||
totalUsage.outputTokens += apiKey.usage.outputTokens || 0
|
||||
totalUsage.totalCost += apiKey.usage.totalCost || 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalUsage,
|
||||
apiKeyCount: userApiKeys.length
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Error calculating user usage stats:', error)
|
||||
return {
|
||||
totalUsage: {
|
||||
requests: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0
|
||||
},
|
||||
apiKeyCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 📋 获取所有用户列表(管理员功能)
|
||||
async getAllUsers(options = {}) {
|
||||
try {
|
||||
@@ -116,17 +201,21 @@ class UserService {
|
||||
const { page = 1, limit = 20, role, isActive } = options
|
||||
const pattern = `${this.userPrefix}*`
|
||||
const keys = await client.keys(pattern)
|
||||
|
||||
|
||||
const users = []
|
||||
for (const key of keys) {
|
||||
const userData = await client.get(key)
|
||||
if (userData) {
|
||||
const user = JSON.parse(userData)
|
||||
|
||||
|
||||
// 应用过滤条件
|
||||
if (role && user.role !== role) continue
|
||||
if (typeof isActive === 'boolean' && user.isActive !== isActive) continue
|
||||
|
||||
if (role && user.role !== role) {
|
||||
continue
|
||||
}
|
||||
if (typeof isActive === 'boolean' && user.isActive !== isActive) {
|
||||
continue
|
||||
}
|
||||
|
||||
users.push(user)
|
||||
}
|
||||
}
|
||||
@@ -153,7 +242,7 @@ class UserService {
|
||||
// 🔄 更新用户状态
|
||||
async updateUserStatus(userId, isActive) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
const user = await this.getUserById(userId, false) // Skip usage calculation
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
@@ -179,7 +268,7 @@ class UserService {
|
||||
// 🔄 更新用户角色
|
||||
async updateUserRole(userId, role) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
const user = await this.getUserById(userId, false) // Skip usage calculation
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
@@ -197,46 +286,22 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 更新用户使用统计
|
||||
async updateUserUsage(userId, usage) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
if (!user) return
|
||||
|
||||
const { requests = 0, inputTokens = 0, outputTokens = 0, cost = 0 } = usage
|
||||
|
||||
user.totalUsage.requests += requests
|
||||
user.totalUsage.inputTokens += inputTokens
|
||||
user.totalUsage.outputTokens += outputTokens
|
||||
user.totalUsage.totalCost += cost
|
||||
user.updatedAt = new Date().toISOString()
|
||||
|
||||
await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user))
|
||||
} catch (error) {
|
||||
logger.error('❌ Error updating user usage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 更新用户API Key数量
|
||||
async updateUserApiKeyCount(userId, count) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
if (!user) return
|
||||
|
||||
user.apiKeyCount = count
|
||||
user.updatedAt = new Date().toISOString()
|
||||
|
||||
await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user))
|
||||
} catch (error) {
|
||||
logger.error('❌ Error updating user API key count:', error)
|
||||
}
|
||||
// 📊 更新用户API Key数量 (已废弃,现在通过聚合计算)
|
||||
async updateUserApiKeyCount(userId, _count) {
|
||||
// This method is deprecated since apiKeyCount is now calculated dynamically
|
||||
// in getUserById by aggregating the user's API keys
|
||||
logger.debug(
|
||||
`📊 updateUserApiKeyCount called for ${userId} but is now deprecated (count auto-calculated)`
|
||||
)
|
||||
}
|
||||
|
||||
// 📝 记录用户登录
|
||||
async recordUserLogin(userId) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
if (!user) return
|
||||
const user = await this.getUserById(userId, false) // Skip usage calculation
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
user.lastLoginAt = new Date().toISOString()
|
||||
await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user))
|
||||
@@ -272,10 +337,12 @@ class UserService {
|
||||
async validateUserSession(sessionToken) {
|
||||
try {
|
||||
const sessionData = await redis.get(`${this.userSessionPrefix}${sessionToken}`)
|
||||
if (!sessionData) return null
|
||||
if (!sessionData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const session = JSON.parse(sessionData)
|
||||
|
||||
|
||||
// 检查会话是否过期
|
||||
if (new Date() > new Date(session.expiresAt)) {
|
||||
await this.invalidateUserSession(sessionToken)
|
||||
@@ -283,7 +350,7 @@ class UserService {
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await this.getUserById(session.userId)
|
||||
const user = await this.getUserById(session.userId, false) // Skip usage calculation for validation
|
||||
if (!user || !user.isActive) {
|
||||
await this.invalidateUserSession(sessionToken)
|
||||
return null
|
||||
@@ -312,7 +379,7 @@ class UserService {
|
||||
const client = redis.getClientSafe()
|
||||
const pattern = `${this.userSessionPrefix}*`
|
||||
const keys = await client.keys(pattern)
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await client.get(key)
|
||||
if (sessionData) {
|
||||
@@ -322,7 +389,7 @@ class UserService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.info(`🚫 Invalidated all sessions for user: ${userId}`)
|
||||
} catch (error) {
|
||||
logger.error('❌ Error invalidating user sessions:', error)
|
||||
@@ -332,7 +399,7 @@ class UserService {
|
||||
// 🗑️ 删除用户(软删除,标记为不活跃)
|
||||
async deleteUser(userId) {
|
||||
try {
|
||||
const user = await this.getUserById(userId)
|
||||
const user = await this.getUserById(userId, false) // Skip usage calculation
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
@@ -343,10 +410,10 @@ class UserService {
|
||||
user.updatedAt = new Date().toISOString()
|
||||
|
||||
await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user))
|
||||
|
||||
|
||||
// 删除所有会话
|
||||
await this.invalidateUserSessions(userId)
|
||||
|
||||
|
||||
logger.info(`🗑️ Soft deleted user: ${user.username} (${userId})`)
|
||||
return user
|
||||
} catch (error) {
|
||||
@@ -361,7 +428,7 @@ class UserService {
|
||||
const client = redis.getClientSafe()
|
||||
const pattern = `${this.userPrefix}*`
|
||||
const keys = await client.keys(pattern)
|
||||
|
||||
|
||||
const stats = {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
@@ -381,17 +448,17 @@ class UserService {
|
||||
if (userData) {
|
||||
const user = JSON.parse(userData)
|
||||
stats.totalUsers++
|
||||
|
||||
|
||||
if (user.isActive) {
|
||||
stats.activeUsers++
|
||||
}
|
||||
|
||||
|
||||
if (user.role === 'admin') {
|
||||
stats.adminUsers++
|
||||
} else {
|
||||
stats.regularUsers++
|
||||
}
|
||||
|
||||
|
||||
stats.totalApiKeys += user.apiKeyCount || 0
|
||||
stats.totalUsage.requests += user.totalUsage?.requests || 0
|
||||
stats.totalUsage.inputTokens += user.totalUsage?.inputTokens || 0
|
||||
@@ -408,4 +475,4 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UserService()
|
||||
module.exports = new UserService()
|
||||
|
||||
Reference in New Issue
Block a user