import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import { UserStatus } from '../generated/prisma/client'; import { PrismaService } from '../infra/prisma.service'; import { AccessService } from './access.service'; import { DEFAULT_VERIFICATION_ICON, isAllowedVerificationIcon, VERIFICATION_ICONS } from './verification.constants'; import { isProtectedSystemAccount } from './system-account.util'; const HUMAN_USERS_WHERE = { isSystemAccount: false, linkedBotId: null }; function buildBotAccountsWhere(search?: string) { const botFilter = { OR: [{ linkedBotId: { not: null } }, { isSystemAccount: true }] }; if (!search) return botFilter; return { AND: [ botFilter, { OR: [ { displayName: { contains: search, mode: 'insensitive' as const } }, { username: { contains: search, mode: 'insensitive' as const } }, { linkedBot: { username: { contains: search, mode: 'insensitive' as const } } }, { linkedBot: { name: { contains: search, mode: 'insensitive' as const } } } ] } ] }; } @Injectable() export class AdminService { constructor( private readonly prisma: PrismaService, private readonly access: AccessService ) {} async listUsers(search?: string) { const users = await this.prisma.user.findMany({ where: { ...HUMAN_USERS_WHERE, ...(search ? { OR: [ { email: { contains: search, mode: 'insensitive' } }, { phone: { contains: search, mode: 'insensitive' } }, { displayName: { contains: search, mode: 'insensitive' } }, { username: { contains: search, mode: 'insensitive' } } ] } : {}) }, orderBy: { createdAt: 'desc' }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } } }); return users.map((user) => this.toAdminUser(user)); } async listBotAccounts(search?: string) { const users = await this.prisma.user.findMany({ where: buildBotAccountsWhere(search), orderBy: { createdAt: 'desc' }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return users.map((user) => this.toAdminUser(user, true)); } async updateUserProfile(userId: string, data: { displayName?: string; email?: string; phone?: string; backupEmail?: string; backupPhone?: string; status?: UserStatus }) { await this.ensureUserExists(userId); const user = await this.prisma.user.update({ where: { id: userId }, data, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } async resetPassword(userId: string, newPassword: string) { if (newPassword.length < 8) { throw new BadRequestException('Пароль должен содержать минимум 8 символов'); } await this.ensureUserExists(userId); const user = await this.prisma.user.update({ where: { id: userId }, data: { passwordHash: await bcrypt.hash(newPassword, 12) }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } async suspendUser(userId: string) { await this.ensureUserExists(userId); await this.prisma.session.updateMany({ where: { userId }, data: { status: 'REVOKED' } }); const user = await this.prisma.user.update({ where: { id: userId }, data: { status: UserStatus.SUSPENDED }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } async setSuperAdmin(actorUserId: string, userId: string, isSuperAdmin: boolean) { await this.access.assertSuperAdmin(actorUserId); await this.ensureUserExists(userId); const target = await this.prisma.user.findUnique({ where: { id: userId } }); if (!target) { throw new NotFoundException('Пользователь не найден'); } if (isProtectedSystemAccount(target)) { throw new ForbiddenException('Нельзя назначать системные учётные записи ботов супер-администраторами'); } if (target.isSuperAdmin && !isSuperAdmin) { const superAdmins = await this.prisma.user.count({ where: { isSuperAdmin: true, status: UserStatus.ACTIVE } }); if (superAdmins <= 1) { throw new BadRequestException('Нельзя снять права у единственного супер-администратора'); } } const user = await this.prisma.user.update({ where: { id: userId }, data: { isSuperAdmin }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } async setUserVerification(actorUserId: string, userId: string, data: { isVerified: boolean; verificationIcon?: string }) { await this.access.assertCanVerifyUsers(actorUserId); await this.ensureUserExists(userId); if (data.isVerified) { const icon = data.verificationIcon ?? DEFAULT_VERIFICATION_ICON; if (!isAllowedVerificationIcon(icon)) { throw new BadRequestException('Выберите значок верификации из списка'); } const user = await this.prisma.user.update({ where: { id: userId }, data: { isVerified: true, verificationIcon: icon, verifiedAt: new Date() }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } const user = await this.prisma.user.update({ where: { id: userId }, data: { isVerified: false, verificationIcon: null, verifiedAt: null }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } listVerificationIcons() { return { icons: VERIFICATION_ICONS.map((icon) => ({ slug: icon.slug, name: icon.name })) }; } async deleteUser(userId: string) { await this.ensureUserExists(userId); const user = await this.prisma.user.update({ where: { id: userId }, data: { status: UserStatus.DELETED, deletedAt: new Date() }, include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } } }); return this.toAdminUser(user, isProtectedSystemAccount(user)); } private toAdminUser( user: { id: string; email: string | null; phone: string | null; backupEmail: string | null; backupPhone: string | null; displayName: string; username: string | null; isSuperAdmin: boolean; isVerified: boolean; verificationIcon: string | null; status: UserStatus; createdAt: Date; isSystemAccount?: boolean; linkedBotId?: string | null; userRoles?: { role: { slug: string } }[]; userPermissions?: { permission: { slug: string } }[]; linkedBot?: { username: string; isSystemBot: boolean } | null; }, isBot = isProtectedSystemAccount(user) ) { return { id: user.id, email: user.email ?? undefined, phone: user.phone ?? undefined, backupEmail: user.backupEmail ?? undefined, backupPhone: user.backupPhone ?? undefined, displayName: user.displayName, username: user.username ?? undefined, isSuperAdmin: user.isSuperAdmin, isVerified: user.isVerified, verificationIcon: user.isVerified ? (user.verificationIcon ?? DEFAULT_VERIFICATION_ICON) : undefined, status: user.status, createdAt: user.createdAt.toISOString(), roles: user.userRoles?.map((link) => link.role.slug) ?? [], directPermissions: user.userPermissions?.map((link) => link.permission.slug) ?? [], isBot, linkedBotUsername: user.linkedBot?.username ?? undefined, isSystemBot: user.linkedBot?.isSystemBot ?? false }; } private async ensureUserExists(userId: string) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new NotFoundException('Пользователь не найден'); } } }