Files
claude-relay-service/src/routes/userRoutes.js
2025-09-02 11:51:38 +08:00

738 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express')
const router = express.Router()
const ldapService = require('../services/ldapService')
const userService = require('../services/userService')
const apiKeyService = require('../services/apiKeyService')
const logger = require('../utils/logger')
const config = require('../../config/config')
const inputValidator = require('../utils/inputValidator')
const { RateLimiterRedis } = require('rate-limiter-flexible')
const redis = require('../models/redis')
const { authenticateUser, authenticateUserOrAdmin, requireAdmin } = require('../middleware/auth')
// 🚦 配置登录速率限制
// 只基于IP地址限制避免攻击者恶意锁定特定账户
// 延迟初始化速率限制器,确保 Redis 已连接
let ipRateLimiter = null
let strictIpRateLimiter = null
// 初始化速率限制器函数
function initRateLimiters() {
if (!ipRateLimiter) {
try {
const redisClient = redis.getClientSafe()
// IP地址速率限制 - 正常限制
ipRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_ip_limiter',
points: 30, // 每个IP允许30次尝试
duration: 900, // 15分钟窗口期
blockDuration: 900 // 超限后封禁15分钟
})
// IP地址速率限制 - 严格限制(用于检测暴力破解)
strictIpRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_ip_strict',
points: 100, // 每个IP允许100次尝试
duration: 3600, // 1小时窗口期
blockDuration: 3600 // 超限后封禁1小时
})
} catch (error) {
logger.error('❌ 初始化速率限制器失败:', error)
// 速率限制器初始化失败时继续运行,但记录错误
}
}
return { ipRateLimiter, strictIpRateLimiter }
}
// 🔐 用户登录端点
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body
const clientIp = req.ip || req.connection.remoteAddress || 'unknown'
// 初始化速率限制器(如果尚未初始化)
const limiters = initRateLimiters()
// 检查IP速率限制 - 基础限制
if (limiters.ipRateLimiter) {
try {
await limiters.ipRateLimiter.consume(clientIp)
} catch (rateLimiterRes) {
const retryAfter = Math.round(rateLimiterRes.msBeforeNext / 1000) || 900
logger.security(`🚫 Login rate limit exceeded for IP: ${clientIp}`)
res.set('Retry-After', String(retryAfter))
return res.status(429).json({
error: 'Too many requests',
message: `Too many login attempts from this IP. Please try again later.`
})
}
}
// 检查IP速率限制 - 严格限制(防止暴力破解)
if (limiters.strictIpRateLimiter) {
try {
await limiters.strictIpRateLimiter.consume(clientIp)
} catch (rateLimiterRes) {
const retryAfter = Math.round(rateLimiterRes.msBeforeNext / 1000) || 3600
logger.security(`🚫 Strict rate limit exceeded for IP: ${clientIp} - possible brute force`)
res.set('Retry-After', String(retryAfter))
return res.status(429).json({
error: 'Too many requests',
message: 'Too many login attempts detected. Access temporarily blocked.'
})
}
}
if (!username || !password) {
return res.status(400).json({
error: 'Missing credentials',
message: 'Username and password are required'
})
}
// 验证输入格式
let validatedUsername
try {
validatedUsername = inputValidator.validateUsername(username)
inputValidator.validatePassword(password)
} catch (validationError) {
return res.status(400).json({
error: 'Invalid input',
message: validationError.message
})
}
// 检查用户管理是否启用
if (!config.userManagement.enabled) {
return res.status(503).json({
error: 'Service unavailable',
message: 'User management is not enabled'
})
}
// 检查LDAP是否启用
if (!config.ldap || !config.ldap.enabled) {
return res.status(503).json({
error: 'Service unavailable',
message: 'LDAP authentication is not enabled'
})
}
// 尝试LDAP认证
const authResult = await ldapService.authenticateUserCredentials(validatedUsername, password)
if (!authResult.success) {
// 登录失败
logger.info(`🚫 Failed login attempt for user: ${validatedUsername} from IP: ${clientIp}`)
return res.status(401).json({
error: 'Authentication failed',
message: authResult.message
})
}
// 登录成功
logger.info(`✅ User login successful: ${validatedUsername} from IP: ${clientIp}`)
res.json({
success: true,
message: 'Login successful',
user: {
id: authResult.user.id,
username: authResult.user.username,
email: authResult.user.email,
displayName: authResult.user.displayName,
firstName: authResult.user.firstName,
lastName: authResult.user.lastName,
role: authResult.user.role
},
sessionToken: authResult.sessionToken
})
} catch (error) {
logger.error('❌ User login error:', error)
res.status(500).json({
error: 'Login error',
message: 'Internal server error during login'
})
}
})
// 🚪 用户登出端点
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({
error: 'Logout error',
message: 'Internal server error during logout'
})
}
})
// 👤 获取当前用户信息
router.get('/profile', authenticateUser, async (req, res) => {
try {
const user = await userService.getUserById(req.user.id)
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'User profile not found'
})
}
res.json({
success: true,
user: {
id: user.id,
username: user.username,
email: user.email,
displayName: user.displayName,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
isActive: user.isActive,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
apiKeyCount: user.apiKeyCount,
totalUsage: user.totalUsage
},
config: {
maxApiKeysPerUser: config.userManagement.maxApiKeysPerUser
}
})
} catch (error) {
logger.error('❌ Get user profile error:', error)
res.status(500).json({
error: 'Profile error',
message: 'Failed to retrieve user profile'
})
}
})
// 🔑 获取用户的API Keys
router.get('/api-keys', authenticateUser, async (req, res) => {
try {
const { includeDeleted = 'false' } = req.query
const apiKeys = await apiKeyService.getUserApiKeys(req.user.id, includeDeleted === 'true')
// 移除敏感信息并格式化usage数据
const safeApiKeys = apiKeys.map((key) => {
// Flatten usage structure for frontend compatibility
let flatUsage = {
requests: 0,
inputTokens: 0,
outputTokens: 0,
totalCost: 0
}
if (key.usage && key.usage.total) {
flatUsage = {
requests: key.usage.total.requests || 0,
inputTokens: key.usage.total.inputTokens || 0,
outputTokens: key.usage.total.outputTokens || 0,
totalCost: key.totalCost || 0
}
}
return {
id: key.id,
name: key.name,
description: key.description,
tokenLimit: key.tokenLimit,
isActive: key.isActive,
createdAt: key.createdAt,
lastUsedAt: key.lastUsedAt,
expiresAt: key.expiresAt,
usage: flatUsage,
dailyCost: key.dailyCost,
dailyCostLimit: key.dailyCostLimit,
// 不返回实际的key值只返回前缀和后几位
keyPreview: key.key
? `${key.key.substring(0, 8)}...${key.key.substring(key.key.length - 4)}`
: null,
// Include deletion fields for deleted keys
isDeleted: key.isDeleted,
deletedAt: key.deletedAt,
deletedBy: key.deletedBy,
deletedByType: key.deletedByType
}
})
res.json({
success: true,
apiKeys: safeApiKeys,
total: safeApiKeys.length
})
} catch (error) {
logger.error('❌ Get user API keys error:', error)
res.status(500).json({
error: 'API Keys error',
message: 'Failed to retrieve API keys'
})
}
})
// 🔑 创建新的API Key
router.post('/api-keys', authenticateUser, async (req, res) => {
try {
const { name, description, tokenLimit, expiresAt, dailyCostLimit } = req.body
if (!name || !name.trim()) {
return res.status(400).json({
error: 'Missing name',
message: 'API key name is required'
})
}
// 检查用户API Key数量限制
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
if (userApiKeys.length >= config.userManagement.maxApiKeysPerUser) {
return res.status(400).json({
error: 'API key limit exceeded',
message: `You can only have up to ${config.userManagement.maxApiKeysPerUser} API keys`
})
}
// 创建API Key数据
const apiKeyData = {
name: name.trim(),
description: description?.trim() || '',
userId: req.user.id,
userUsername: req.user.username,
tokenLimit: tokenLimit || null,
expiresAt: expiresAt || null,
dailyCostLimit: dailyCostLimit || null,
createdBy: 'user',
permissions: ['messages'] // 用户创建的API Key默认只有messages权限
}
const newApiKey = await apiKeyService.createApiKey(apiKeyData)
// 更新用户API Key数量
await userService.updateUserApiKeyCount(req.user.id, userApiKeys.length + 1)
logger.info(`🔑 User ${req.user.username} created API key: ${name}`)
res.status(201).json({
success: true,
message: 'API key created successfully',
apiKey: {
id: newApiKey.id,
name: newApiKey.name,
description: newApiKey.description,
key: newApiKey.apiKey, // 只在创建时返回完整key
tokenLimit: newApiKey.tokenLimit,
expiresAt: newApiKey.expiresAt,
dailyCostLimit: newApiKey.dailyCostLimit,
createdAt: newApiKey.createdAt
}
})
} catch (error) {
logger.error('❌ Create user API key error:', error)
res.status(500).json({
error: 'API Key creation error',
message: 'Failed to create API key'
})
}
})
// 🗑️ 删除API Key
router.delete('/api-keys/:keyId', authenticateUser, async (req, res) => {
try {
const { keyId } = req.params
// 检查API Key是否属于当前用户
const existingKey = await apiKeyService.getApiKeyById(keyId)
if (!existingKey || existingKey.userId !== req.user.id) {
return res.status(404).json({
error: 'API key not found',
message: 'API key not found or you do not have permission to access it'
})
}
await apiKeyService.deleteApiKey(keyId, req.user.username, 'user')
// 更新用户API Key数量
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id)
await userService.updateUserApiKeyCount(req.user.id, userApiKeys.length)
logger.info(`🗑️ User ${req.user.username} deleted API key: ${existingKey.name}`)
res.json({
success: true,
message: 'API key deleted successfully'
})
} catch (error) {
logger.error('❌ Delete user API key error:', error)
res.status(500).json({
error: 'API Key deletion error',
message: 'Failed to delete API key'
})
}
})
// 📊 获取用户使用统计
router.get('/usage-stats', authenticateUser, async (req, res) => {
try {
const { period = 'week', model } = req.query
// 获取用户的API Keys (including deleted ones for complete usage stats)
const userApiKeys = await apiKeyService.getUserApiKeys(req.user.id, true)
const apiKeyIds = userApiKeys.map((key) => key.id)
if (apiKeyIds.length === 0) {
return res.json({
success: true,
stats: {
totalRequests: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalCost: 0,
dailyStats: [],
modelStats: []
}
})
}
// 获取使用统计
const stats = await apiKeyService.getAggregatedUsageStats(apiKeyIds, { period, model })
res.json({
success: true,
stats
})
} catch (error) {
logger.error('❌ Get user usage stats error:', error)
res.status(500).json({
error: 'Usage stats error',
message: 'Failed to retrieve usage statistics'
})
}
})
// === 管理员用户管理端点 ===
// 📋 获取用户列表(管理员)
router.get('/', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { page = 1, limit = 20, role, isActive, search } = req.query
const options = {
page: parseInt(page),
limit: parseInt(limit),
role,
isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined
}
const result = await userService.getAllUsers(options)
// 如果有搜索条件,进行过滤
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)
)
}
res.json({
success: true,
users: filteredUsers,
pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages
}
})
} catch (error) {
logger.error('❌ Get users list error:', error)
res.status(500).json({
error: 'Users list error',
message: 'Failed to retrieve users list'
})
}
})
// 👤 获取特定用户信息(管理员)
router.get('/:userId', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { userId } = req.params
const user = await userService.getUserById(userId)
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'User not found'
})
}
// 获取用户的API Keys包括已删除的以保留统计数据
const apiKeys = await apiKeyService.getUserApiKeys(userId, true)
res.json({
success: true,
user: {
...user,
apiKeys: apiKeys.map((key) => {
// Flatten usage structure for frontend compatibility
let flatUsage = {
requests: 0,
inputTokens: 0,
outputTokens: 0,
totalCost: 0
}
if (key.usage && key.usage.total) {
flatUsage = {
requests: key.usage.total.requests || 0,
inputTokens: key.usage.total.inputTokens || 0,
outputTokens: key.usage.total.outputTokens || 0,
totalCost: key.totalCost || 0
}
}
return {
id: key.id,
name: key.name,
description: key.description,
isActive: key.isActive,
createdAt: key.createdAt,
lastUsedAt: key.lastUsedAt,
usage: flatUsage,
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({
error: 'User details error',
message: 'Failed to retrieve user details'
})
}
})
// 🔄 更新用户状态(管理员)
router.patch('/:userId/status', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { userId } = req.params
const { isActive } = req.body
if (typeof isActive !== 'boolean') {
return res.status(400).json({
error: 'Invalid status',
message: 'isActive must be a boolean value'
})
}
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}`
)
res.json({
success: true,
message: `User ${isActive ? 'enabled' : 'disabled'} successfully`,
user: {
id: updatedUser.id,
username: updatedUser.username,
isActive: updatedUser.isActive,
updatedAt: updatedUser.updatedAt
}
})
} catch (error) {
logger.error('❌ Update user status error:', error)
res.status(500).json({
error: 'Update status error',
message: error.message || 'Failed to update user status'
})
}
})
// 🔄 更新用户角色(管理员)
router.patch('/:userId/role', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { userId } = req.params
const { role } = req.body
const validRoles = ['user', 'admin']
if (!role || !validRoles.includes(role)) {
return res.status(400).json({
error: 'Invalid role',
message: `Role must be one of: ${validRoles.join(', ')}`
})
}
const updatedUser = await userService.updateUserRole(userId, role)
const adminUser = req.admin?.username || req.user?.username
logger.info(`🔄 Admin ${adminUser} changed user ${updatedUser.username} role to: ${role}`)
res.json({
success: true,
message: `User role updated to ${role} successfully`,
user: {
id: updatedUser.id,
username: updatedUser.username,
role: updatedUser.role,
updatedAt: updatedUser.updatedAt
}
})
} catch (error) {
logger.error('❌ Update user role error:', error)
res.status(500).json({
error: 'Update role error',
message: error.message || 'Failed to update user role'
})
}
})
// 🔑 禁用用户的所有API Keys管理员
router.post('/:userId/disable-keys', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { userId } = req.params
const user = await userService.getUserById(userId)
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'User not found'
})
}
const result = await apiKeyService.disableUserApiKeys(userId)
const adminUser = req.admin?.username || req.user?.username
logger.info(`🔑 Admin ${adminUser} disabled all API keys for user: ${user.username}`)
res.json({
success: true,
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({
error: 'Disable keys error',
message: 'Failed to disable user API keys'
})
}
})
// 📊 获取用户使用统计(管理员)
router.get('/:userId/usage-stats', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const { userId } = req.params
const { period = 'week', model } = req.query
const user = await userService.getUserById(userId)
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'User not found'
})
}
// 获取用户的API Keys包括已删除的以保留统计数据
const userApiKeys = await apiKeyService.getUserApiKeys(userId, true)
const apiKeyIds = userApiKeys.map((key) => key.id)
if (apiKeyIds.length === 0) {
return res.json({
success: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
},
stats: {
totalRequests: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
totalCost: 0,
dailyStats: [],
modelStats: []
}
})
}
// 获取使用统计
const stats = await apiKeyService.getAggregatedUsageStats(apiKeyIds, { period, model })
res.json({
success: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
},
stats
})
} catch (error) {
logger.error('❌ Get user usage stats (admin) error:', error)
res.status(500).json({
error: 'Usage stats error',
message: 'Failed to retrieve user usage statistics'
})
}
})
// 📊 获取用户管理统计(管理员)
router.get('/stats/overview', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const stats = await userService.getUserStats()
res.json({
success: true,
stats
})
} catch (error) {
logger.error('❌ Get user stats overview error:', error)
res.status(500).json({
error: 'Stats error',
message: 'Failed to retrieve user statistics'
})
}
})
// 🔧 测试LDAP连接管理员
router.get('/admin/ldap-test', authenticateUserOrAdmin, requireAdmin, async (req, res) => {
try {
const testResult = await ldapService.testConnection()
res.json({
success: true,
ldapTest: testResult,
config: ldapService.getConfigInfo()
})
} catch (error) {
logger.error('❌ LDAP test error:', error)
res.status(500).json({
error: 'LDAP test error',
message: 'Failed to test LDAP connection'
})
}
})
module.exports = router