import bcrypt from 'bcryptjs'; import prisma from '../../config/database'; import { AppError } from '../../shared/middleware/errorHandler'; import { AuditLogger } from '../../shared/utils/auditLogger'; import { config } from '../../config'; import { Prisma } from '@prisma/client'; export interface UserFilters { search?: string; isActive?: boolean; positionId?: string; page?: number; pageSize?: number; } export interface CreateUserData { email: string; username: string; password: string; employeeId: string; isActive?: boolean; } export interface UpdateUserData { email?: string; username?: string; password?: string; employeeId?: string; isActive?: boolean; } export interface AuditLogFilters { entityType?: string; action?: string; userId?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number; } class AdminService { // ========== USERS ========== async findAllUsers(filters: UserFilters) { const page = filters.page || 1; const pageSize = Math.min(filters.pageSize || 20, 100); const skip = (page - 1) * pageSize; const where: Prisma.UserWhereInput = {}; if (filters.isActive !== undefined) { where.isActive = filters.isActive; } if (filters.positionId) { where.employee = { positionId: filters.positionId, }; } if (filters.search?.trim()) { const search = filters.search.trim().toLowerCase(); where.OR = [ { username: { contains: search, mode: 'insensitive' } }, { email: { contains: search, mode: 'insensitive' } }, { employee: { OR: [ { firstName: { contains: search, mode: 'insensitive' } }, { lastName: { contains: search, mode: 'insensitive' } }, ], }, }, ]; } const [users, total] = await Promise.all([ prisma.user.findMany({ where, skip, take: pageSize, include: { employee: { include: { position: { select: { id: true, title: true, titleAr: true } }, department: { select: { name: true, nameAr: true } }, }, }, }, orderBy: { createdAt: 'desc' }, }), prisma.user.count({ where }), ]); const sanitized = users.map((u) => { const { password: _, ...rest } = u; return rest; }); return { users: sanitized, total, page, pageSize, totalPages: Math.ceil(total / pageSize), }; } async findUserById(id: string) { const user = await prisma.user.findUnique({ where: { id }, include: { employee: { include: { position: { include: { permissions: true } }, department: true, }, }, }, }); if (!user) { throw new AppError(404, 'المستخدم غير موجود - User not found'); } const { password: _, ...rest } = user; return rest; } async createUser(data: CreateUserData, createdById: string) { const employee = await prisma.employee.findUnique({ where: { id: data.employeeId }, }); if (!employee) { throw new AppError(400, 'الموظف غير موجود - Employee not found'); } if (employee.status !== 'ACTIVE') { throw new AppError(400, 'الموظف غير نشط - Employee must be ACTIVE'); } const existingUser = await prisma.user.findFirst({ where: { employeeId: data.employeeId }, }); if (existingUser) { throw new AppError(400, 'هذا الموظف مرتبط بحساب مستخدم بالفعل - Employee already has a user account'); } const emailExists = await prisma.user.findFirst({ where: { email: data.email }, }); if (emailExists) { throw new AppError(400, 'البريد الإلكتروني مستخدم بالفعل - Email already in use'); } const usernameExists = await prisma.user.findUnique({ where: { username: data.username }, }); if (usernameExists) { throw new AppError(400, 'اسم المستخدم مستخدم بالفعل - Username already in use'); } const hashedPassword = await bcrypt.hash(data.password, config.security?.bcryptRounds || 10); const user = await prisma.user.create({ data: { email: data.email, username: data.username, password: hashedPassword, employeeId: data.employeeId, isActive: data.isActive ?? true, }, select: { id: true, email: true, username: true, employeeId: true, isActive: true, lastLogin: true, createdAt: true, employee: { include: { position: { select: { title: true, titleAr: true } }, }, }, }, }); await AuditLogger.log({ entityType: 'USER', entityId: user.id, action: 'CREATE', userId: createdById, changes: { created: { email: user.email, username: user.username } }, }); return user; } async updateUser(id: string, data: UpdateUserData, updatedById: string) { const existing = await prisma.user.findUnique({ where: { id } }); if (!existing) { throw new AppError(404, 'المستخدم غير موجود - User not found'); } if (data.email && data.email !== existing.email) { const emailExists = await prisma.user.findFirst({ where: { email: data.email } }); if (emailExists) { throw new AppError(400, 'البريد الإلكتروني مستخدم بالفعل - Email already in use'); } } if (data.username && data.username !== existing.username) { const usernameExists = await prisma.user.findUnique({ where: { username: data.username } }); if (usernameExists) { throw new AppError(400, 'اسم المستخدم مستخدم بالفعل - Username already in use'); } } const updateData: Prisma.UserUpdateInput = { ...(data.email && { email: data.email }), ...(data.username && { username: data.username }), ...(data.isActive !== undefined && { isActive: data.isActive }), ...(data.employeeId !== undefined && { employeeId: data.employeeId || null }), }; if (data.password && data.password.length >= 8) { updateData.password = await bcrypt.hash(data.password, config.security?.bcryptRounds || 10); } const user = await prisma.user.update({ where: { id }, data: updateData, include: { employee: { include: { position: { select: { title: true, titleAr: true } }, }, }, }, }); const { password: _, ...sanitized } = user; await AuditLogger.log({ entityType: 'USER', entityId: id, action: 'UPDATE', userId: updatedById, changes: { before: existing, after: sanitized }, }); return sanitized; } async toggleUserActive(id: string, userId: string) { const user = await prisma.user.findUnique({ where: { id } }); if (!user) { throw new AppError(404, 'المستخدم غير موجود - User not found'); } const updated = await prisma.user.update({ where: { id }, data: { isActive: !user.isActive }, include: { employee: { include: { position: { select: { title: true, titleAr: true } }, }, }, }, }); const { password: _, ...sanitized } = updated; await AuditLogger.log({ entityType: 'USER', entityId: id, action: user.isActive ? 'DEACTIVATE' : 'ACTIVATE', userId, changes: { isActive: updated.isActive }, }); return sanitized; } async deleteUser(id: string, deletedById: string) { const user = await prisma.user.findUnique({ where: { id } }); if (!user) { throw new AppError(404, 'المستخدم غير موجود - User not found'); } await prisma.user.update({ where: { id }, data: { isActive: false }, }); await AuditLogger.log({ entityType: 'USER', entityId: id, action: 'DELETE', userId: deletedById, changes: { softDeleted: true, email: user.email }, }); return { success: true, message: 'User deactivated successfully' }; } // ========== AUDIT LOGS ========== async getAuditLogs(filters: AuditLogFilters) { const page = filters.page || 1; const pageSize = Math.min(filters.pageSize || 20, 100); const skip = (page - 1) * pageSize; const where: Prisma.AuditLogWhereInput = {}; if (filters.entityType) where.entityType = filters.entityType; if (filters.action) where.action = filters.action; if (filters.userId) where.userId = filters.userId; if (filters.startDate || filters.endDate) { where.createdAt = {}; if (filters.startDate) where.createdAt.gte = new Date(filters.startDate); if (filters.endDate) where.createdAt.lte = new Date(filters.endDate); } const [logs, total] = await Promise.all([ prisma.auditLog.findMany({ where, skip, take: pageSize, include: { user: { select: { id: true, username: true, email: true }, }, }, orderBy: { createdAt: 'desc' }, }), prisma.auditLog.count({ where }), ]); return { logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize), }; } // ========== STATS ========== async getStats() { const today = new Date(); today.setHours(0, 0, 0, 0); const [totalUsers, activeUsers, inactiveUsers, loginsToday] = await Promise.all([ prisma.user.count(), prisma.user.count({ where: { isActive: true } }), prisma.user.count({ where: { isActive: false } }), prisma.user.count({ where: { lastLogin: { gte: today } }, }), ]); return { totalUsers, activeUsers, inactiveUsers, loginsToday, }; } // ========== POSITIONS (ROLES) ========== async getPositions() { const positions = await prisma.position.findMany({ where: { isActive: true }, include: { department: { select: { name: true, nameAr: true } }, permissions: true, _count: { select: { employees: true, }, }, }, orderBy: { level: 'asc' }, }); const withUserCount = await Promise.all( positions.map(async (p) => { const usersCount = await prisma.user.count({ where: { employee: { positionId: p.id }, isActive: true, }, }); return { ...p, usersCount }; }) ); return withUserCount; } async createPosition(data: { title: string; titleAr?: string; code: string; departmentId: string; level?: number; description?: string; isActive?: boolean; }) { const existing = await prisma.position.findUnique({ where: { code: data.code }, }); if (existing) { throw new AppError(400, 'كود الدور مستخدم - Position code already exists'); } const dept = await prisma.department.findUnique({ where: { id: data.departmentId }, }); if (!dept) { throw new AppError(400, 'القسم غير موجود - Department not found'); } return prisma.position.create({ data: { title: data.title, titleAr: data.titleAr, code: data.code.trim().toUpperCase().replace(/\s+/g, '_'), departmentId: data.departmentId, level: data.level ?? 5, description: data.description, isActive: data.isActive ?? true, }, include: { department: { select: { name: true, nameAr: true } }, permissions: true, }, }); } async updatePosition( positionId: string, data: { title?: string; titleAr?: string; code?: string; departmentId?: string; level?: number; description?: string; isActive?: boolean; } ) { const position = await prisma.position.findUnique({ where: { id: positionId }, }); if (!position) { throw new AppError(404, 'الدور غير موجود - Position not found'); } if (data.code && data.code !== position.code) { const existing = await prisma.position.findUnique({ where: { code: data.code }, }); if (existing) { throw new AppError(400, 'كود الدور مستخدم - Position code already exists'); } } if (data.departmentId && data.departmentId !== position.departmentId) { const dept = await prisma.department.findUnique({ where: { id: data.departmentId }, }); if (!dept) { throw new AppError(400, 'القسم غير موجود - Department not found'); } } const updateData: Record = {}; if (data.title !== undefined) updateData.title = data.title; if (data.titleAr !== undefined) updateData.titleAr = data.titleAr; if (data.code !== undefined) updateData.code = data.code.trim().toUpperCase().replace(/\s+/g, '_'); if (data.departmentId !== undefined) updateData.departmentId = data.departmentId; if (data.level !== undefined) updateData.level = data.level; if (data.description !== undefined) updateData.description = data.description; if (data.isActive !== undefined) updateData.isActive = data.isActive; return prisma.position.update({ where: { id: positionId }, data: updateData, include: { department: { select: { name: true, nameAr: true } }, permissions: true, }, }); } async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) { const position = await prisma.position.findUnique({ where: { id: positionId } }); if (!position) { throw new AppError(404, 'الدور غير موجود - Position not found'); } await prisma.positionPermission.deleteMany({ where: { positionId }, }); if (permissions.length > 0) { await prisma.positionPermission.createMany({ data: permissions.map((p) => ({ positionId, module: p.module, resource: p.resource, actions: p.actions, })), }); } return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position); } // ========== PERMISSION GROUPS (Phase 3 - optional roles for multi-group) ========== async getPermissionGroups() { return prisma.role.findMany({ where: { isActive: true }, include: { permissions: true, _count: { select: { userRoles: true } }, }, orderBy: { name: 'asc' }, }); } async createPermissionGroup(data: { name: string; nameAr?: string; description?: string }) { const existing = await prisma.role.findUnique({ where: { name: data.name } }); if (existing) { throw new AppError(400, 'اسم المجموعة مستخدم - Group name already exists'); } return prisma.role.create({ data: { name: data.name, nameAr: data.nameAr, description: data.description, }, include: { permissions: true }, }); } async updatePermissionGroup( id: string, data: { name?: string; nameAr?: string; description?: string; isActive?: boolean } ) { const role = await prisma.role.findUnique({ where: { id } }); if (!role) { throw new AppError(404, 'المجموعة غير موجودة - Group not found'); } if (data.name && data.name !== role.name) { const existing = await prisma.role.findUnique({ where: { name: data.name } }); if (existing) { throw new AppError(400, 'اسم المجموعة مستخدم - Group name already exists'); } } return prisma.role.update({ where: { id }, data, include: { permissions: true }, }); } async updatePermissionGroupPermissions( roleId: string, permissions: Array<{ module: string; resource: string; actions: string[] }> ) { await prisma.rolePermission.deleteMany({ where: { roleId } }); if (permissions.length > 0) { await prisma.rolePermission.createMany({ data: permissions.map((p) => ({ roleId, module: p.module, resource: p.resource, actions: p.actions, })), }); } return prisma.role.findUnique({ where: { id: roleId }, include: { permissions: true }, }); } async getUserRoles(userId: string) { return prisma.userRole.findMany({ where: { userId }, include: { role: { include: { permissions: true } }, }, }); } async assignUserRole(userId: string, roleId: string) { const [user, role] = await Promise.all([ prisma.user.findUnique({ where: { id: userId } }), prisma.role.findFirst({ where: { id: roleId, isActive: true } }), ]); if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found'); if (!role) throw new AppError(404, 'المجموعة غير موجودة - Group not found'); const existing = await prisma.userRole.findUnique({ where: { userId_roleId: { userId, roleId } }, }); if (existing) { throw new AppError(400, 'المستخدم منتمي بالفعل لهذه المجموعة - User already in group'); } return prisma.userRole.create({ data: { userId, roleId }, include: { role: true }, }); } async removeUserRole(userId: string, roleId: string) { const deleted = await prisma.userRole.deleteMany({ where: { userId, roleId }, }); if (deleted.count === 0) { throw new AppError(404, 'لم يتم العثور على الانتماء - User not in group'); } return { success: true }; } } export const adminService = new AdminService();