import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import * as argon2 from 'argon2'; import { generateStrongPassword } from '@b2bcall/shared'; import { PrismaService } from '../prisma/prisma.service'; import { AuditService } from '../audit/audit.service'; import { MailerService } from '../auth/mailer.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import type { RequestContext } from '../auth/auth.service'; function toSafeUser(user: { id: string; name: string; email: string; isActive: boolean; mustChangePassword: boolean; lastLoginAt: Date | null; createdAt: Date; roles?: { role: { id: string; name: string } }[]; }) { return { id: user.id, name: user.name, email: user.email, isActive: user.isActive, mustChangePassword: user.mustChangePassword, lastLoginAt: user.lastLoginAt, createdAt: user.createdAt, roles: user.roles?.map((r) => r.role) ?? [], }; } @Injectable() export class UsersService { constructor( private readonly prisma: PrismaService, private readonly audit: AuditService, private readonly mailer: MailerService, ) {} async list() { const users = await this.prisma.user.findMany({ include: { roles: { include: { role: true } } }, orderBy: { createdAt: 'asc' }, }); return users.map(toSafeUser); } async findByIdOrThrow(id: string) { const user = await this.prisma.user.findUnique({ where: { id }, include: { roles: { include: { role: true } } }, }); if (!user) throw new NotFoundException('Usuário não encontrado.'); return toSafeUser(user); } async create(dto: CreateUserDto, actor: { id: string }, ctx: RequestContext) { const existing = await this.prisma.user.findUnique({ where: { email: dto.email }, }); if (existing) throw new BadRequestException('Já existe um usuário com este e-mail.'); const roles = await this.prisma.role.findMany({ where: { id: { in: dto.roleIds } }, }); if (roles.length !== dto.roleIds.length) { throw new BadRequestException( 'Um ou mais perfis informados não existem.', ); } const password = generateStrongPassword(); const passwordHash = await argon2.hash(password, { type: argon2.argon2id }); const user = await this.prisma.user.create({ data: { name: dto.name, email: dto.email, passwordHash, mustChangePassword: true, roles: { create: dto.roleIds.map((roleId) => ({ roleId })) }, }, include: { roles: { include: { role: true } } }, }); await this.audit.log({ userId: actor.id, action: 'user_created', entityType: 'user', entityId: user.id, after: { name: user.name, email: user.email, roleIds: dto.roleIds }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }); // Stub de log até SMTP ser configurado — ver MailerService. this.mailer.sendNewUserCredentials(user.email, password); return toSafeUser(user); } async update( id: string, dto: UpdateUserDto, actor: { id: string }, ctx: RequestContext, ) { // Nunca permitir que alguém altere os próprios perfis (agente.md seção // 92: "agent não consegue elevar a própria permissão"), independente da // permissão que já possua. if (id === actor.id && dto.roleIds !== undefined) { throw new ForbiddenException( 'Você não pode alterar seus próprios perfis de acesso.', ); } const before = await this.prisma.user.findUnique({ where: { id }, include: { roles: true }, }); if (!before) throw new NotFoundException('Usuário não encontrado.'); if (dto.roleIds) { const roles = await this.prisma.role.findMany({ where: { id: { in: dto.roleIds } }, }); if (roles.length !== dto.roleIds.length) { throw new BadRequestException( 'Um ou mais perfis informados não existem.', ); } } const user = await this.prisma.$transaction(async (tx) => { if (dto.roleIds) { await tx.userRole.deleteMany({ where: { userId: id } }); await tx.userRole.createMany({ data: dto.roleIds.map((roleId) => ({ userId: id, roleId })), }); } return tx.user.update({ where: { id }, data: { name: dto.name, isActive: dto.isActive, }, include: { roles: { include: { role: true } } }, }); }); await this.audit.log({ userId: actor.id, action: 'user_updated', entityType: 'user', entityId: id, before: { name: before.name, isActive: before.isActive, roleIds: before.roles.map((r) => r.roleId), }, after: { name: user.name, isActive: user.isActive, roleIds: dto.roleIds }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }); return toSafeUser(user); } }