/** * User Service * * Business logic for user profile management */ import { prisma } from '../../shared/database/prisma.client'; import { NotFoundError, ValidationError, ConflictError, AuthenticationError } from '../../shared/types'; import { logger } from '../../shared/utils/logger'; import type { User, UserRole } from '@prisma/client'; import bcrypt from 'bcrypt'; // ============================================ // SERVICE // ============================================ class UserService { /** * Get user profile by ID */ async getUserProfile(userId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, username: true, role: true, telegramChatId: true, isActive: true, createdAt: true, updatedAt: true, lastLoginAt: true, // Exclude passwordHash for security }, }); if (!user) { throw new NotFoundError('User'); } return user as User; } /** * Update user profile */ async updateProfile(userId: string, data: { username?: string; telegramChatId?: string | null; }): Promise { // Validate username uniqueness if changing if (data.username) { const existingUser = await prisma.user.findFirst({ where: { username: data.username, NOT: { id: userId }, }, }); if (existingUser) { throw new ConflictError('Username already taken'); } } // Validate telegramChatId uniqueness if setting if (data.telegramChatId) { const existingTelegram = await prisma.user.findFirst({ where: { telegramChatId: data.telegramChatId, NOT: { id: userId }, }, }); if (existingTelegram) { throw new ConflictError('Telegram chat ID already linked to another account'); } } const updatedUser = await prisma.user.update({ where: { id: userId }, data: { ...data, updatedAt: new Date(), }, select: { id: true, email: true, username: true, role: true, telegramChatId: true, isActive: true, createdAt: true, updatedAt: true, lastLoginAt: true, }, }); logger.info({ userId, updates: data }, 'User profile updated'); return updatedUser as User; } /** * Change user password */ async changePassword(userId: string, currentPassword: string, newPassword: string): Promise { // Validate input if (!currentPassword || !newPassword) { throw new ValidationError('Current and new password are required'); } if (newPassword.length < 8) { throw new ValidationError('New password must be at least 8 characters'); } // Find user with password hash const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, passwordHash: true, }, }); if (!user) { throw new NotFoundError('User'); } // Verify current password const isPasswordValid = await bcrypt.compare(currentPassword, user.passwordHash); if (!isPasswordValid) { throw new AuthenticationError('Current password is incorrect'); } // Hash the new password const saltRounds = 12; const newPasswordHash = await bcrypt.hash(newPassword, saltRounds); // Update user's password const updatedUser = await prisma.user.update({ where: { id: userId }, data: { passwordHash: newPasswordHash, updatedAt: new Date(), }, select: { id: true, email: true, username: true, role: true, telegramChatId: true, isActive: true, createdAt: true, updatedAt: true, lastLoginAt: true, }, }); logger.info({ userId }, 'Password changed successfully'); return updatedUser as User; } /** * Get user statistics */ async getUserStats(userId: string): Promise<{ totalExercisesCompleted: number; totalPoints: number; currentStreak: number; achievementsUnlocked: number; }> { // Get progress stats const progressStats = await prisma.progress.aggregate({ where: { userId }, _sum: { points: true, exercisesCompleted: true }, _count: { _all: true }, }); // Get ranking for streak info const globalRanking = await prisma.ranking.findFirst({ where: { userId, moduleId: null }, select: { streak: true }, }); // Get achievements count const achievementsCount = await prisma.userAchievement.count({ where: { userId, unlockedAt: { not: null } }, }); return { totalExercisesCompleted: progressStats._sum.exercisesCompleted || 0, totalPoints: progressStats._sum.points || 0, currentStreak: globalRanking?.streak || 0, achievementsUnlocked: achievementsCount, }; } /** * Get all users (admin only) */ async listUsers(options?: { role?: UserRole; isActive?: boolean; page?: number; limit?: number; }): Promise<{ users: User[]; total: number }> { const page = options?.page || 1; const limit = options?.limit || 50; const skip = (page - 1) * limit; const where: any = {}; if (options?.role) where.role = options.role; if (options?.isActive !== undefined) where.isActive = options.isActive; const [users, total] = await Promise.all([ prisma.user.findMany({ where, skip, take: limit, select: { id: true, email: true, username: true, role: true, isActive: true, createdAt: true, lastLoginAt: true, }, orderBy: { createdAt: 'desc' }, }), prisma.user.count({ where }), ]); return { users: users as User[], total }; } /** * Update user role (admin only) */ async updateUserRole(userId: string, role: UserRole): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user) { throw new NotFoundError('User'); } const updatedUser = await prisma.user.update({ where: { id: userId }, data: { role }, select: { id: true, email: true, username: true, role: true, isActive: true, }, }); logger.info({ userId, newRole: role }, 'User role updated'); return updatedUser as User; } /** * Deactivate user (admin only) */ async deactivateUser(userId: string): Promise { const user = await prisma.user.update({ where: { id: userId }, data: { isActive: false }, select: { id: true, email: true, username: true, role: true, isActive: true, }, }); logger.info({ userId }, 'User deactivated'); return user as User; } } // ============================================ // EXPORT // ============================================ export const userService = new UserService(); export default userService;