| const redis = require('../models/redis') |
| const crypto = require('crypto') |
| const logger = require('../utils/logger') |
| const config = require('../../config/config') |
|
|
| class UserService { |
| constructor() { |
| this.userPrefix = 'user:' |
| this.usernamePrefix = 'username:' |
| this.userSessionPrefix = 'user_session:' |
| } |
|
|
| |
| generateUserId() { |
| return crypto.randomBytes(16).toString('hex') |
| } |
|
|
| |
| generateSessionToken() { |
| return crypto.randomBytes(32).toString('hex') |
| } |
|
|
| |
| async createOrUpdateUser(userData) { |
| try { |
| const { |
| username, |
| email, |
| displayName, |
| firstName, |
| lastName, |
| role = config.userManagement.defaultUserRole, |
| isActive = true |
| } = userData |
|
|
| |
| let user = await this.getUserByUsername(username) |
| const isNewUser = !user |
|
|
| if (isNewUser) { |
| const userId = this.generateUserId() |
| user = { |
| id: userId, |
| username, |
| email, |
| displayName, |
| firstName, |
| lastName, |
| role, |
| isActive, |
| createdAt: new Date().toISOString(), |
| updatedAt: new Date().toISOString(), |
| lastLoginAt: null, |
| apiKeyCount: 0, |
| totalUsage: { |
| requests: 0, |
| inputTokens: 0, |
| outputTokens: 0, |
| totalCost: 0 |
| } |
| } |
| } else { |
| |
| user = { |
| ...user, |
| email, |
| displayName, |
| firstName, |
| lastName, |
| updatedAt: new Date().toISOString() |
| } |
| } |
|
|
| |
| await redis.set(`${this.userPrefix}${user.id}`, JSON.stringify(user)) |
| await redis.set(`${this.usernamePrefix}${username}`, user.id) |
|
|
| |
| if (isNewUser) { |
| await this.transferMatchingApiKeys(user) |
| } |
|
|
| logger.info(`📝 ${isNewUser ? 'Created' : 'Updated'} user: ${username} (${user.id})`) |
| return user |
| } catch (error) { |
| logger.error('❌ Error creating/updating user:', error) |
| throw error |
| } |
| } |
|
|
| |
| async getUserByUsername(username) { |
| try { |
| const userId = await redis.get(`${this.usernamePrefix}${username}`) |
| if (!userId) { |
| return null |
| } |
|
|
| const userData = await redis.get(`${this.userPrefix}${userId}`) |
| return userData ? JSON.parse(userData) : null |
| } catch (error) { |
| logger.error('❌ Error getting user by username:', error) |
| throw error |
| } |
| } |
|
|
| |
| async getUserById(userId, calculateUsage = true) { |
| try { |
| const userData = await redis.get(`${this.userPrefix}${userId}`) |
| if (!userData) { |
| return null |
| } |
|
|
| const user = JSON.parse(userData) |
|
|
| |
| 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) |
| |
| 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 |
| } |
| } |
|
|
| |
| async calculateUserUsageStats(userId) { |
| try { |
| |
| const apiKeyService = require('./apiKeyService') |
| const userApiKeys = await apiKeyService.getUserApiKeys(userId, true) |
|
|
| const totalUsage = { |
| requests: 0, |
| inputTokens: 0, |
| outputTokens: 0, |
| totalCost: 0 |
| } |
|
|
| for (const apiKey of userApiKeys) { |
| if (apiKey.usage && apiKey.usage.total) { |
| totalUsage.requests += apiKey.usage.total.requests || 0 |
| totalUsage.inputTokens += apiKey.usage.total.inputTokens || 0 |
| totalUsage.outputTokens += apiKey.usage.total.outputTokens || 0 |
| totalUsage.totalCost += apiKey.totalCost || 0 |
| } |
| } |
|
|
| logger.debug( |
| `📊 Calculated user ${userId} usage: ${totalUsage.requests} requests, ${totalUsage.inputTokens} input tokens, $${totalUsage.totalCost.toFixed(4)} total cost from ${userApiKeys.length} API keys` |
| ) |
|
|
| |
| const activeApiKeyCount = userApiKeys.filter((key) => key.isDeleted !== 'true').length |
|
|
| return { |
| totalUsage, |
| apiKeyCount: activeApiKeyCount |
| } |
| } 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 { |
| const client = redis.getClientSafe() |
| 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 |
| } |
|
|
| |
| try { |
| const usageStats = await this.calculateUserUsageStats(user.id) |
| user.totalUsage = usageStats.totalUsage |
| user.apiKeyCount = usageStats.apiKeyCount |
| } catch (error) { |
| logger.error(`❌ Error calculating usage for user ${user.id}:`, error) |
| |
| user.totalUsage = user.totalUsage || { |
| requests: 0, |
| inputTokens: 0, |
| outputTokens: 0, |
| totalCost: 0 |
| } |
| user.apiKeyCount = user.apiKeyCount || 0 |
| } |
|
|
| users.push(user) |
| } |
| } |
|
|
| |
| users.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) |
| const startIndex = (page - 1) * limit |
| const endIndex = startIndex + limit |
| const paginatedUsers = users.slice(startIndex, endIndex) |
|
|
| return { |
| users: paginatedUsers, |
| total: users.length, |
| page, |
| limit, |
| totalPages: Math.ceil(users.length / limit) |
| } |
| } catch (error) { |
| logger.error('❌ Error getting all users:', error) |
| throw error |
| } |
| } |
|
|
| |
| async updateUserStatus(userId, isActive) { |
| try { |
| const user = await this.getUserById(userId, false) |
| if (!user) { |
| throw new Error('User not found') |
| } |
|
|
| user.isActive = isActive |
| user.updatedAt = new Date().toISOString() |
|
|
| await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) |
| logger.info(`🔄 Updated user status: ${user.username} -> ${isActive ? 'active' : 'disabled'}`) |
|
|
| |
| if (!isActive) { |
| await this.invalidateUserSessions(userId) |
|
|
| |
| try { |
| const apiKeyService = require('./apiKeyService') |
| const result = await apiKeyService.disableUserApiKeys(userId) |
| logger.info(`🔑 Disabled ${result.count} API keys for disabled user: ${user.username}`) |
| } catch (error) { |
| logger.error('❌ Error disabling user API keys during user disable:', error) |
| } |
| } |
|
|
| return user |
| } catch (error) { |
| logger.error('❌ Error updating user status:', error) |
| throw error |
| } |
| } |
|
|
| |
| async updateUserRole(userId, role) { |
| try { |
| const user = await this.getUserById(userId, false) |
| if (!user) { |
| throw new Error('User not found') |
| } |
|
|
| user.role = role |
| user.updatedAt = new Date().toISOString() |
|
|
| await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) |
| logger.info(`🔄 Updated user role: ${user.username} -> ${role}`) |
|
|
| return user |
| } catch (error) { |
| logger.error('❌ Error updating user role:', error) |
| throw error |
| } |
| } |
|
|
| |
| async updateUserApiKeyCount(userId, _count) { |
| |
| |
| logger.debug( |
| `📊 updateUserApiKeyCount called for ${userId} but is now deprecated (count auto-calculated)` |
| ) |
| } |
|
|
| |
| async recordUserLogin(userId) { |
| try { |
| const user = await this.getUserById(userId, false) |
| if (!user) { |
| return |
| } |
|
|
| user.lastLoginAt = new Date().toISOString() |
| await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) |
| } catch (error) { |
| logger.error('❌ Error recording user login:', error) |
| } |
| } |
|
|
| |
| async createUserSession(userId, sessionData = {}) { |
| try { |
| const sessionToken = this.generateSessionToken() |
| const session = { |
| token: sessionToken, |
| userId, |
| createdAt: new Date().toISOString(), |
| expiresAt: new Date(Date.now() + config.userManagement.userSessionTimeout).toISOString(), |
| ...sessionData |
| } |
|
|
| const ttl = Math.floor(config.userManagement.userSessionTimeout / 1000) |
| await redis.setex(`${this.userSessionPrefix}${sessionToken}`, ttl, JSON.stringify(session)) |
|
|
| logger.info(`🎫 Created session for user: ${userId}`) |
| return sessionToken |
| } catch (error) { |
| logger.error('❌ Error creating user session:', error) |
| throw error |
| } |
| } |
|
|
| |
| async validateUserSession(sessionToken) { |
| try { |
| const sessionData = await redis.get(`${this.userSessionPrefix}${sessionToken}`) |
| if (!sessionData) { |
| return null |
| } |
|
|
| const session = JSON.parse(sessionData) |
|
|
| |
| if (new Date() > new Date(session.expiresAt)) { |
| await this.invalidateUserSession(sessionToken) |
| return null |
| } |
|
|
| |
| const user = await this.getUserById(session.userId, false) |
| if (!user || !user.isActive) { |
| await this.invalidateUserSession(sessionToken) |
| return null |
| } |
|
|
| return { session, user } |
| } catch (error) { |
| logger.error('❌ Error validating user session:', error) |
| return null |
| } |
| } |
|
|
| |
| async invalidateUserSession(sessionToken) { |
| try { |
| await redis.del(`${this.userSessionPrefix}${sessionToken}`) |
| logger.info(`🚫 Invalidated session: ${sessionToken}`) |
| } catch (error) { |
| logger.error('❌ Error invalidating user session:', error) |
| } |
| } |
|
|
| |
| async invalidateUserSessions(userId) { |
| try { |
| 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) { |
| const session = JSON.parse(sessionData) |
| if (session.userId === userId) { |
| await client.del(key) |
| } |
| } |
| } |
|
|
| logger.info(`🚫 Invalidated all sessions for user: ${userId}`) |
| } catch (error) { |
| logger.error('❌ Error invalidating user sessions:', error) |
| } |
| } |
|
|
| |
| async deleteUser(userId) { |
| try { |
| const user = await this.getUserById(userId, false) |
| if (!user) { |
| throw new Error('User not found') |
| } |
|
|
| |
| user.isActive = false |
| user.deletedAt = new Date().toISOString() |
| user.updatedAt = new Date().toISOString() |
|
|
| await redis.set(`${this.userPrefix}${userId}`, JSON.stringify(user)) |
|
|
| |
| await this.invalidateUserSessions(userId) |
|
|
| |
| try { |
| const apiKeyService = require('./apiKeyService') |
| const result = await apiKeyService.disableUserApiKeys(userId) |
| logger.info(`🔑 Disabled ${result.count} API keys for deleted user: ${user.username}`) |
| } catch (error) { |
| logger.error('❌ Error disabling user API keys during user deletion:', error) |
| } |
|
|
| logger.info(`🗑️ Soft deleted user: ${user.username} (${userId})`) |
| return user |
| } catch (error) { |
| logger.error('❌ Error deleting user:', error) |
| throw error |
| } |
| } |
|
|
| |
| async getUserStats() { |
| try { |
| const client = redis.getClientSafe() |
| const pattern = `${this.userPrefix}*` |
| const keys = await client.keys(pattern) |
|
|
| const stats = { |
| totalUsers: 0, |
| activeUsers: 0, |
| adminUsers: 0, |
| regularUsers: 0, |
| totalApiKeys: 0, |
| totalUsage: { |
| requests: 0, |
| inputTokens: 0, |
| outputTokens: 0, |
| totalCost: 0 |
| } |
| } |
|
|
| for (const key of keys) { |
| const userData = await client.get(key) |
| if (userData) { |
| const user = JSON.parse(userData) |
| stats.totalUsers++ |
|
|
| if (user.isActive) { |
| stats.activeUsers++ |
| } |
|
|
| if (user.role === 'admin') { |
| stats.adminUsers++ |
| } else { |
| stats.regularUsers++ |
| } |
|
|
| |
| try { |
| const usageStats = await this.calculateUserUsageStats(user.id) |
| stats.totalApiKeys += usageStats.apiKeyCount |
| stats.totalUsage.requests += usageStats.totalUsage.requests |
| stats.totalUsage.inputTokens += usageStats.totalUsage.inputTokens |
| stats.totalUsage.outputTokens += usageStats.totalUsage.outputTokens |
| stats.totalUsage.totalCost += usageStats.totalUsage.totalCost |
| } catch (error) { |
| logger.error(`❌ Error calculating usage for user ${user.id} in stats:`, error) |
| |
| stats.totalApiKeys += user.apiKeyCount || 0 |
| stats.totalUsage.requests += user.totalUsage?.requests || 0 |
| stats.totalUsage.inputTokens += user.totalUsage?.inputTokens || 0 |
| stats.totalUsage.outputTokens += user.totalUsage?.outputTokens || 0 |
| stats.totalUsage.totalCost += user.totalUsage?.totalCost || 0 |
| } |
| } |
| } |
|
|
| return stats |
| } catch (error) { |
| logger.error('❌ Error getting user stats:', error) |
| throw error |
| } |
| } |
|
|
| |
| async transferMatchingApiKeys(user) { |
| try { |
| const apiKeyService = require('./apiKeyService') |
| const { displayName, username, email } = user |
|
|
| |
| const allApiKeys = await apiKeyService.getAllApiKeys() |
|
|
| |
| const unownedApiKeys = allApiKeys.filter((key) => !key.userId || key.userId === '') |
|
|
| if (unownedApiKeys.length === 0) { |
| logger.debug(`📝 No unowned API keys found for potential transfer to user: ${username}`) |
| return |
| } |
|
|
| |
| const matchStrings = new Set() |
| if (displayName) { |
| matchStrings.add(displayName.toLowerCase().trim()) |
| } |
| if (username) { |
| matchStrings.add(username.toLowerCase().trim()) |
| } |
| if (email) { |
| matchStrings.add(email.toLowerCase().trim()) |
| } |
|
|
| const matchingKeys = [] |
|
|
| |
| for (const apiKey of unownedApiKeys) { |
| const keyName = apiKey.name ? apiKey.name.toLowerCase().trim() : '' |
|
|
| |
| for (const matchString of matchStrings) { |
| if (keyName === matchString) { |
| matchingKeys.push(apiKey) |
| break |
| } |
| } |
| } |
|
|
| |
| let transferredCount = 0 |
| for (const apiKey of matchingKeys) { |
| try { |
| await apiKeyService.updateApiKey(apiKey.id, { |
| userId: user.id, |
| userUsername: user.username, |
| createdBy: user.username |
| }) |
|
|
| transferredCount++ |
| logger.info(`🔄 Transferred API key "${apiKey.name}" (${apiKey.id}) to user: ${username}`) |
| } catch (error) { |
| logger.error(`❌ Failed to transfer API key ${apiKey.id} to user ${username}:`, error) |
| } |
| } |
|
|
| if (transferredCount > 0) { |
| logger.success( |
| `🎉 Successfully transferred ${transferredCount} API key(s) to new user: ${username} (${displayName})` |
| ) |
| } else if (matchingKeys.length === 0) { |
| logger.debug(`📝 No matching API keys found for user: ${username} (${displayName})`) |
| } |
| } catch (error) { |
| logger.error('❌ Error transferring matching API keys:', error) |
| |
| } |
| } |
| } |
|
|
| module.exports = new UserService() |
|
|