fix document 500 error and fix users tabel for bot
This commit is contained in:
@@ -30,6 +30,14 @@ const ROLES = [
|
||||
isSystem: true,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
slug: 'bot',
|
||||
name: 'Бот',
|
||||
description: 'Системная учётная запись Telegram-бота',
|
||||
permissions: [] as string[],
|
||||
isSystem: true,
|
||||
isDefault: false
|
||||
},
|
||||
{
|
||||
slug: 'admin',
|
||||
name: 'Администратор',
|
||||
@@ -120,9 +128,13 @@ export class AdminSeedService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
const users = await this.prisma.user.findMany({ where: { deletedAt: null }, select: { id: true } });
|
||||
const users = await this.prisma.user.findMany({ where: { deletedAt: null }, select: { id: true, isSystemAccount: true, linkedBotId: true } });
|
||||
for (const user of users) {
|
||||
await this.rbac.ensureDefaultUserRole(user.id);
|
||||
if (user.linkedBotId || user.isSystemAccount) {
|
||||
await this.rbac.ensureBotUserRole(user.id);
|
||||
} else {
|
||||
await this.rbac.ensureDefaultUserRole(user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,32 @@ 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 {
|
||||
@@ -14,16 +40,19 @@ export class AdminService {
|
||||
|
||||
async listUsers(search?: string) {
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: search
|
||||
? {
|
||||
OR: [
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ phone: { contains: search, mode: 'insensitive' } },
|
||||
{ displayName: { contains: search, mode: 'insensitive' } },
|
||||
{ username: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
: undefined,
|
||||
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 } },
|
||||
@@ -34,14 +63,28 @@ export class AdminService {
|
||||
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 } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
async resetPassword(userId: string, newPassword: string) {
|
||||
@@ -53,9 +96,9 @@ export class AdminService {
|
||||
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 } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
async suspendUser(userId: string) {
|
||||
@@ -64,9 +107,9 @@ export class AdminService {
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { status: UserStatus.SUSPENDED },
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
async setSuperAdmin(actorUserId: string, userId: string, isSuperAdmin: boolean) {
|
||||
@@ -77,6 +120,9 @@ export class AdminService {
|
||||
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 } });
|
||||
@@ -88,9 +134,9 @@ export class AdminService {
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { isSuperAdmin },
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
async setUserVerification(actorUserId: string, userId: string, data: { isVerified: boolean; verificationIcon?: string }) {
|
||||
@@ -109,9 +155,9 @@ export class AdminService {
|
||||
verificationIcon: icon,
|
||||
verifiedAt: new Date()
|
||||
},
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.update({
|
||||
@@ -121,9 +167,9 @@ export class AdminService {
|
||||
verificationIcon: null,
|
||||
verifiedAt: null
|
||||
},
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
return this.toAdminUser(user, isProtectedSystemAccount(user));
|
||||
}
|
||||
|
||||
listVerificationIcons() {
|
||||
@@ -135,27 +181,33 @@ export class AdminService {
|
||||
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 } } }
|
||||
include: { userRoles: { include: { role: true } }, userPermissions: { include: { permission: true } }, linkedBot: { select: { username: true, isSystemBot: true } } }
|
||||
});
|
||||
return this.toAdminUser(user);
|
||||
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;
|
||||
userRoles?: { role: { slug: string } }[];
|
||||
userPermissions?: { permission: { slug: string } }[];
|
||||
}) {
|
||||
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,
|
||||
@@ -170,7 +222,10 @@ export class AdminService {
|
||||
status: user.status,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
roles: user.userRoles?.map((link) => link.role.slug) ?? [],
|
||||
directPermissions: user.userPermissions?.map((link) => link.permission.slug) ?? []
|
||||
directPermissions: user.userPermissions?.map((link) => link.permission.slug) ?? [],
|
||||
isBot,
|
||||
linkedBotUsername: user.linkedBot?.username ?? undefined,
|
||||
isSystemBot: user.linkedBot?.isSystemBot ?? false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,12 @@ export class AuthGrpcController {
|
||||
return { users };
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'ListBotAccounts')
|
||||
async listBotAccounts(command: { search?: string }) {
|
||||
const users = await this.admin.listBotAccounts(command.search);
|
||||
return { users };
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'SuspendUser')
|
||||
suspendUser(command: { userId: string }) {
|
||||
return this.admin.suspendUser(command.userId);
|
||||
@@ -461,6 +467,11 @@ export class AuthGrpcController {
|
||||
return this.totp.disable(command.userId, command.code);
|
||||
}
|
||||
|
||||
@GrpcMethod('SecurityService', 'AdminDisableTotp')
|
||||
adminDisableTotp(command: { actorUserId: string; userId: string }) {
|
||||
return this.totp.adminDisable(command.actorUserId, command.userId);
|
||||
}
|
||||
|
||||
@GrpcMethod('SecurityService', 'SetupPin')
|
||||
setupPin(command: { userId: string; pin: string }) {
|
||||
return this.pin.setupPin(command.userId, command.pin);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaService } from '../../infra/prisma.service';
|
||||
import { RbacService } from '../rbac.service';
|
||||
import {
|
||||
BOTFATHER_BOT_USERNAME,
|
||||
BOTFATHER_DISPLAY_NAME,
|
||||
@@ -16,7 +17,10 @@ export class BotFatherSeedService implements OnModuleInit {
|
||||
private cachedUserId: string | null = null;
|
||||
private cachedBotId: string | null = null;
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly rbac: RbacService
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.ensureBotFather();
|
||||
@@ -101,6 +105,7 @@ export class BotFatherSeedService implements OnModuleInit {
|
||||
|
||||
await this.upsertSetting(BOTFATHER_SETTING_USER_ID, botFatherUser.id, 'UUID системного пользователя BotFather');
|
||||
await this.upsertSetting(BOTFATHER_SETTING_BOT_ID, bot.id, 'UUID системного бота BotFather');
|
||||
await this.rbac.ensureBotUserRole(botFatherUser.id);
|
||||
|
||||
this.cachedUserId = botFatherUser.id;
|
||||
this.cachedBotId = bot.id;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PrismaService } from '../../infra/prisma.service';
|
||||
import { AccessService } from '../access.service';
|
||||
import { NotificationsService } from '../notifications.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { RbacService } from '../rbac.service';
|
||||
import { BotRateLimitService } from './bot-rate-limit.service';
|
||||
import { assertValidBotUsername, generateTelegramStyleBotToken, hashBotToken, normalizeBotUsername } from './bot-token.util';
|
||||
import { menuButtonToJson, parseMenuButtonInput, resolveComposerMenuButton } from './bot-menu-button.util';
|
||||
@@ -45,7 +46,8 @@ export class BotFatherService implements OnModuleInit {
|
||||
private readonly access: AccessService,
|
||||
private readonly settings: SettingsService,
|
||||
private readonly rateLimit: BotRateLimitService,
|
||||
private readonly notifications: NotificationsService
|
||||
private readonly notifications: NotificationsService,
|
||||
private readonly rbac: RbacService
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
@@ -84,6 +86,8 @@ export class BotFatherService implements OnModuleInit {
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
await this.rbac.ensureBotUserRole(systemUser.id);
|
||||
|
||||
this.logger.log(`Создан системный пользователь для бота @${bot.username}`);
|
||||
return { userId: systemUser.id, botId: bot.id };
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
||||
import { Prisma } from '../generated/prisma/client';
|
||||
import { EncryptionService } from '../infra/encryption.service';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { isProtectedSystemAccount } from './system-account.util';
|
||||
|
||||
export const DOCUMENT_TYPES = [
|
||||
'PASSPORT_RF',
|
||||
@@ -70,11 +71,29 @@ export class DocumentsService {
|
||||
}
|
||||
|
||||
async list(userId: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isSystemAccount: true, linkedBotId: true }
|
||||
});
|
||||
if (user && isProtectedSystemAccount(user)) {
|
||||
return { documents: [] };
|
||||
}
|
||||
|
||||
const documents = await this.prisma.userDocument.findMany({
|
||||
where: { userId },
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
});
|
||||
return { documents: documents.map((document) => this.toResponse(document)) };
|
||||
|
||||
const results = [];
|
||||
for (const document of documents) {
|
||||
try {
|
||||
results.push(this.toResponse(document));
|
||||
} catch {
|
||||
results.push(this.toFallbackResponse(document));
|
||||
}
|
||||
}
|
||||
|
||||
return { documents: results };
|
||||
}
|
||||
|
||||
async get(documentId: string, userId: string) {
|
||||
@@ -124,14 +143,33 @@ export class DocumentsService {
|
||||
}
|
||||
|
||||
private toResponse(document: Awaited<ReturnType<PrismaService['userDocument']['create']>>) {
|
||||
const response: Record<string, string> = {
|
||||
id: document.id,
|
||||
userId: document.userId,
|
||||
type: document.type,
|
||||
number: this.encryption.decryptIfNeeded(document.number ?? '—'),
|
||||
createdAt: this.formatRequiredDate(document.createdAt),
|
||||
updatedAt: this.formatRequiredDate(document.updatedAt)
|
||||
};
|
||||
|
||||
const issuedAt = this.formatOptionalDate(document.issuedAt);
|
||||
if (issuedAt) response.issuedAt = issuedAt;
|
||||
|
||||
const expiresAt = this.formatOptionalDate(document.expiresAt);
|
||||
if (expiresAt) response.expiresAt = expiresAt;
|
||||
|
||||
const metadataJson = this.resolveMetadataJson(document.metadata);
|
||||
if (metadataJson) response.metadataJson = metadataJson;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private toFallbackResponse(document: Awaited<ReturnType<PrismaService['userDocument']['create']>>) {
|
||||
return {
|
||||
id: document.id,
|
||||
userId: document.userId,
|
||||
type: document.type,
|
||||
number: this.encryption.decryptIfNeeded(document.number),
|
||||
issuedAt: this.formatOptionalDate(document.issuedAt),
|
||||
expiresAt: this.formatOptionalDate(document.expiresAt),
|
||||
metadataJson: this.resolveMetadataJson(document.metadata),
|
||||
number: '—',
|
||||
createdAt: this.formatRequiredDate(document.createdAt),
|
||||
updatedAt: this.formatRequiredDate(document.updatedAt)
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const DEFAULT_USER_ROLE_SLUG = 'user';
|
||||
export const BOT_ROLE_SLUG = 'bot';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OAuthClientType } from '../generated/prisma/client';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { AccessService, UserAccessContext } from './access.service';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { DEFAULT_USER_ROLE_SLUG } from './rbac.constants';
|
||||
import { DEFAULT_USER_ROLE_SLUG, BOT_ROLE_SLUG } from './rbac.constants';
|
||||
|
||||
export interface OAuthClientListItem {
|
||||
id: string;
|
||||
@@ -254,6 +254,15 @@ export class RbacService {
|
||||
}
|
||||
|
||||
async ensureDefaultUserRole(userId: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { linkedBotId: true, isSystemAccount: true }
|
||||
});
|
||||
if (user?.linkedBotId || user?.isSystemAccount) {
|
||||
await this.ensureBotUserRole(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
const role = await this.prisma.role.findUnique({ where: { slug: DEFAULT_USER_ROLE_SLUG } });
|
||||
if (!role) return;
|
||||
await this.prisma.userRole.upsert({
|
||||
@@ -263,6 +272,26 @@ export class RbacService {
|
||||
});
|
||||
}
|
||||
|
||||
async ensureBotUserRole(userId: string) {
|
||||
const [botRole, userRole] = await Promise.all([
|
||||
this.prisma.role.findUnique({ where: { slug: BOT_ROLE_SLUG } }),
|
||||
this.prisma.role.findUnique({ where: { slug: DEFAULT_USER_ROLE_SLUG } })
|
||||
]);
|
||||
if (!botRole) return;
|
||||
|
||||
if (userRole) {
|
||||
await this.prisma.userRole.deleteMany({
|
||||
where: { userId, roleId: userRole.id }
|
||||
});
|
||||
}
|
||||
|
||||
await this.prisma.userRole.upsert({
|
||||
where: { userId_roleId: { userId, roleId: botRole.id } },
|
||||
create: { userId, roleId: botRole.id },
|
||||
update: {}
|
||||
});
|
||||
}
|
||||
|
||||
async assignUserPermission(actorUserId: string, userId: string, permissionSlug: string) {
|
||||
await this.access.assertRbacManage(actorUserId);
|
||||
await this.ensureUserExists(userId);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authenticator } from 'otplib';
|
||||
import { EncryptionService } from '../infra/encryption.service';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { AccessService } from './access.service';
|
||||
|
||||
@Injectable()
|
||||
export class TotpService {
|
||||
@@ -12,7 +13,8 @@ export class TotpService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly encryption: EncryptionService,
|
||||
private readonly settings: SettingsService
|
||||
private readonly settings: SettingsService,
|
||||
private readonly access: AccessService
|
||||
) {}
|
||||
|
||||
async getStatus(userId: string) {
|
||||
@@ -91,6 +93,19 @@ export class TotpService {
|
||||
return { isEnabled: false };
|
||||
}
|
||||
|
||||
async adminDisable(actorUserId: string, targetUserId: string) {
|
||||
await this.access.assertSuperAdmin(actorUserId);
|
||||
await this.ensureUserExists(targetUserId);
|
||||
|
||||
const record = await this.prisma.userTotpSecret.findUnique({ where: { userId: targetUserId } });
|
||||
if (!record?.isEnabled) {
|
||||
throw new BadRequestException('Двухфакторная аутентификация не включена');
|
||||
}
|
||||
|
||||
await this.prisma.userTotpSecret.delete({ where: { userId: targetUserId } });
|
||||
return { isEnabled: false };
|
||||
}
|
||||
|
||||
async verifyEnabledCode(userId: string, code: string) {
|
||||
const record = await this.prisma.userTotpSecret.findUnique({ where: { userId } });
|
||||
if (!record?.isEnabled) {
|
||||
|
||||
Reference in New Issue
Block a user