import { ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { Prisma } from '../../generated/prisma'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; const BCRYPT_SALT_ROUNDS = 12; const USER_WITH_ROLE = { include: { role: true }, } satisfies { include: Prisma.UserInclude }; type UserWithRole = Prisma.UserGetPayload; export interface UserSummary { id: string; email: string; name: string; roleId: string; roleKey: string; roleName: string; isActive: boolean; createdAt: Date; deactivatedAt: Date | null; } @Injectable() export class UsersService { constructor( private readonly prisma: PrismaService, private readonly auditLog: AuditLogService, ) {} private toSummary(user: UserWithRole): UserSummary { return { id: user.id, email: user.email, name: user.name, roleId: user.roleId, roleKey: user.role.key, roleName: user.role.name, isActive: user.isActive, createdAt: user.createdAt, deactivatedAt: user.deactivatedAt, }; } private async assertActiveRole(roleId: string): Promise { const role = await this.prisma.role.findUnique({ where: { id: roleId } }); if (!role) { throw new NotFoundException('Role not found'); } if (role.status !== 'ACTIVE') { throw new ConflictException('Role is not active'); } } async findAll(): Promise { const users = await this.prisma.user.findMany({ ...USER_WITH_ROLE, orderBy: { createdAt: 'asc' }, }); return users.map((user) => this.toSummary(user)); } async findById(id: string): Promise { const user = await this.prisma.user.findUnique({ where: { id }, ...USER_WITH_ROLE, }); if (!user) { throw new NotFoundException('User not found'); } return user; } async findSummaryById(id: string): Promise { return this.toSummary(await this.findById(id)); } async findByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { email }, ...USER_WITH_ROLE }); } async create( dto: CreateUserDto, actingUserId?: string, ): Promise { const existing = await this.findByEmail(dto.email); if (existing) { throw new ConflictException('Email already in use'); } await this.assertActiveRole(dto.roleId); const passwordHash = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS); const user = await this.prisma.user.create({ data: { email: dto.email, passwordHash, name: dto.name, roleId: dto.roleId, }, ...USER_WITH_ROLE, }); await this.auditLog.record({ entityType: 'User', entityId: user.id, action: 'CREATE', userId: actingUserId, afterData: this.toSummary(user), }); return this.toSummary(user); } async update( id: string, dto: UpdateUserDto, actingUserId?: string, ): Promise { const before = await this.findById(id); if (dto.roleId) { await this.assertActiveRole(dto.roleId); } const user = await this.prisma.user.update({ where: { id }, data: { name: dto.name, roleId: dto.roleId, }, ...USER_WITH_ROLE, }); await this.auditLog.record({ entityType: 'User', entityId: id, action: 'UPDATE', userId: actingUserId, beforeData: this.toSummary(before), afterData: this.toSummary(user), }); return this.toSummary(user); } async deactivate(id: string, actingUserId?: string): Promise { const before = await this.findById(id); const user = await this.prisma.user.update({ where: { id }, data: { isActive: false, deactivatedAt: new Date() }, ...USER_WITH_ROLE, }); await this.auditLog.record({ entityType: 'User', entityId: id, action: 'DEACTIVATE', userId: actingUserId, beforeData: this.toSummary(before), afterData: this.toSummary(user), }); return this.toSummary(user); } async activate(id: string, actingUserId?: string): Promise { const before = await this.findById(id); if (before.isActive) { throw new ConflictException('User is already active'); } const user = await this.prisma.user.update({ where: { id }, data: { isActive: true, deactivatedAt: null }, ...USER_WITH_ROLE, }); await this.auditLog.record({ entityType: 'User', entityId: id, action: 'UPDATE', userId: actingUserId, beforeData: this.toSummary(before), afterData: this.toSummary(user), }); return this.toSummary(user); } }