import { BadRequestException, Body, Controller, Delete, ForbiddenException, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { firstValueFrom } from 'rxjs'; import { map } from 'rxjs'; import { CoreGrpcService } from '../core-grpc.service'; import { CurrentAdmin } from '../decorators/current-admin.decorator'; import { ListUsersQueryDto, ResetPasswordDto, SetSuperAdminDto, SetUserVerificationDto, UpdateUserDto, UserInsightsQueryDto } from '../dto/admin.dto'; import { AdminGuard, AdminRequestUser, assertAdminPermission, SuperAdminGuard } from '../guards/admin.guard'; @ApiTags('Администрирование') @ApiBearerAuth() @UseGuards(AdminGuard) @Controller('admin/users') export class AdminController { constructor(private readonly core: CoreGrpcService) {} @Get() @ApiOperation({ summary: 'Список пользователей', description: 'Возвращает пользователей для административной таблицы.' }) listUsers(@Query() query: ListUsersQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canViewUsers && !admin.canManageUsers) { throw new ForbiddenException('Недостаточно прав для просмотра пользователей'); } return this.core.admin.ListUsers(query).pipe( map((response) => { const payload = response as { users?: Array<{ roles?: string[] }> }; return { users: (payload.users ?? []).map((user) => ({ ...user, roles: user.roles ?? [] })) }; }) ); } @Get('verification-icons') @ApiOperation({ summary: 'Список значков верификации', description: 'Доступные значки для выбора при верификации пользователя.' }) listVerificationIcons(@CurrentAdmin() admin: AdminRequestUser) { if (!admin.canVerifyUsers) { throw new ForbiddenException('Недостаточно прав для верификации пользователей'); } return this.core.admin.ListVerificationIcons({}); } @Patch(':userId') @ApiOperation({ summary: 'Обновить профиль пользователя', description: 'Обновляет основные и резервные контакты пользователя.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: UpdateUserDto }) updateUser(@Param('userId') userId: string, @Body() dto: UpdateUserDto, @CurrentAdmin() admin: AdminRequestUser) { assertAdminPermission(admin, 'canManageUsers'); return this.core.admin.UpdateUserProfile({ userId, ...dto }); } @Post(':userId/reset-password') @ApiOperation({ summary: 'Сбросить пароль', description: 'Назначает пользователю новый пароль.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: ResetPasswordDto }) resetPassword(@Param('userId') userId: string, @Body() dto: ResetPasswordDto, @CurrentAdmin() admin: AdminRequestUser) { assertAdminPermission(admin, 'canManageUsers'); return this.core.admin.ResetPassword({ userId, newPassword: dto.newPassword }); } @Post(':userId/suspend') @ApiOperation({ summary: 'Заблокировать пользователя', description: 'Переводит пользователя в статус SUSPENDED и отзывает активные сессии.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) suspend(@Param('userId') userId: string, @CurrentAdmin() admin: AdminRequestUser) { assertAdminPermission(admin, 'canManageUsers'); return this.core.admin.SuspendUser({ userId }); } @Patch(':userId/super-admin') @UseGuards(SuperAdminGuard) @ApiOperation({ summary: 'Выдать или снять права супер-администратора', description: 'Доступно только супер-администратору.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: SetSuperAdminDto }) setSuperAdmin(@Param('userId') userId: string, @Body() dto: SetSuperAdminDto, @CurrentAdmin() admin: AdminRequestUser) { return this.core.admin.SetSuperAdmin({ actorUserId: admin.id, userId, isSuperAdmin: dto.isSuperAdmin }); } @Get('bot-accounts') @ApiOperation({ summary: 'Системные учётные записи ботов', description: 'Возвращает пользователей, связанных с Telegram-ботами (BotFather и боты пользователей).' }) listBotAccounts(@Query() query: ListUsersQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canViewUsers && !admin.canManageUsers && !admin.isSuperAdmin && !admin.permissions.includes('bots.manage.all')) { throw new ForbiddenException('Недостаточно прав для просмотра ботов'); } return this.core.admin.ListBotAccounts(query).pipe( map((response) => { const payload = response as { users?: Array<{ roles?: string[] }> }; return { users: (payload.users ?? []).map((user) => ({ ...user, roles: user.roles ?? [] })) }; }) ); } @Post(':userId/totp/admin-disable') @UseGuards(SuperAdminGuard) @ApiOperation({ summary: 'Отключить 2FA пользователя', description: 'Супер-администратор может принудительно отключить TOTP без кода пользователя.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) adminDisableTotp(@Param('userId') userId: string, @CurrentAdmin() admin: AdminRequestUser) { return firstValueFrom( this.core.security.AdminDisableTotp({ actorUserId: admin.id, userId }) ); } @Patch(':userId/verification') @ApiOperation({ summary: 'Верифицировать или снять верификацию', description: 'Требуется право users.verify.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: SetUserVerificationDto }) setUserVerification(@Param('userId') userId: string, @Body() dto: SetUserVerificationDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canVerifyUsers) { throw new ForbiddenException('Недостаточно прав для верификации пользователей'); } return this.core.admin.SetUserVerification({ actorUserId: admin.id, userId, isVerified: dto.isVerified, verificationIcon: dto.verificationIcon }); } @Get(':userId/sign-in-history') @ApiOperation({ summary: 'История входов пользователя', description: 'Журнал SignInEvent с поиском по IP, устройству и причине.' }) getUserSignInHistory(@Param('userId') userId: string, @Query() query: UserInsightsQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canViewUsers && !admin.canManageUsers) { throw new ForbiddenException('Недостаточно прав для просмотра журнала пользователя'); } return firstValueFrom( this.core.admin.GetUserSignInHistory({ userId, search: query.search, limit: query.limit, offset: query.offset, dateFrom: query.dateFrom, dateTo: query.dateTo }) ); } @Get(':userId/activity') @ApiOperation({ summary: 'Активность пользователя', description: 'Созданные документы, чаты, семьи, OAuth-согласия и журнал действий.' }) getUserActivity(@Param('userId') userId: string, @Query() query: UserInsightsQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canViewUsers && !admin.canManageUsers) { throw new ForbiddenException('Недостаточно прав для просмотра активности пользователя'); } return firstValueFrom( this.core.admin.GetUserActivity({ userId, search: query.search, limit: query.limit, offset: query.offset, dateFrom: query.dateFrom, dateTo: query.dateTo }) ); } @Get(':userId/chats') @ApiOperation({ summary: 'Чаты пользователя для модерации', description: 'Обычные чаты без E2E и ботов.' }) listUserChats(@Param('userId') userId: string, @Query() query: UserInsightsQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canModerateChats && !admin.isSuperAdmin) { throw new ForbiddenException('Недостаточно прав для модерации чатов'); } return firstValueFrom( this.core.admin.ListUserChatRooms({ userId, search: query.search, limit: query.limit, offset: query.offset, dateFrom: query.dateFrom, dateTo: query.dateTo }) ); } @Get(':userId/chats/search') @ApiOperation({ summary: 'Поиск по сообщениям пользователя', description: 'Поиск по обычным (не E2E) перепискам пользователя.' }) searchUserChats(@Param('userId') userId: string, @Query() query: UserInsightsQueryDto, @CurrentAdmin() admin: AdminRequestUser) { if (!admin.canModerateChats && !admin.isSuperAdmin) { throw new ForbiddenException('Недостаточно прав для модерации чатов'); } if (!query.search?.trim()) { throw new BadRequestException('Укажите параметр search'); } return firstValueFrom( this.core.admin.SearchUserChatMessages({ userId, search: query.search.trim(), limit: query.limit, offset: query.offset, dateFrom: query.dateFrom, dateTo: query.dateTo }) ); } @Get(':userId/chats/:roomId/messages') @ApiOperation({ summary: 'Сообщения чата пользователя', description: 'Просмотр переписки в обычном чате для модерации.' }) listUserChatMessages( @Param('userId') userId: string, @Param('roomId') roomId: string, @Query() query: UserInsightsQueryDto, @CurrentAdmin() admin: AdminRequestUser ) { if (!admin.canModerateChats && !admin.isSuperAdmin) { throw new ForbiddenException('Недостаточно прав для модерации чатов'); } return firstValueFrom( this.core.admin.ListUserChatMessages({ userId, roomId, search: query.search, limit: query.limit, beforeMessageId: query.beforeMessageId, dateFrom: query.dateFrom, dateTo: query.dateTo }) ); } @Delete(':userId/chat-messages/:messageId') @ApiOperation({ summary: 'Удалить сообщение (модерация)', description: 'Мягкое удаление сообщения в обычном чате.' }) deleteUserChatMessage( @Param('userId') userId: string, @Param('messageId') messageId: string, @CurrentAdmin() admin: AdminRequestUser ) { if (!admin.canModerateChats && !admin.isSuperAdmin) { throw new ForbiddenException('Недостаточно прав для модерации чатов'); } void userId; return firstValueFrom( this.core.admin.AdminDeleteChatMessage({ actorUserId: admin.id, messageId }) ); } }