roles update

This commit is contained in:
lendry
2026-06-25 15:14:50 +03:00
parent 9671fe458b
commit 1c55c871fc
21 changed files with 721 additions and 93 deletions

View File

@@ -3,6 +3,8 @@ import { randomBytes } from 'node:crypto';
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';
export interface OAuthClientListItem {
id: string;
@@ -21,11 +23,12 @@ export interface OAuthClientListItem {
export class RbacService {
constructor(
private readonly prisma: PrismaService,
private readonly access: AccessService
private readonly access: AccessService,
private readonly notifications: NotificationsService
) {}
async createRole(actorUserId: string, data: { slug: string; name: string; description?: string; permissionSlugs?: string[] }) {
await this.access.assertSuperAdmin(actorUserId);
await this.access.assertRbacManage(actorUserId);
return this.prisma.role.create({
data: {
slug: data.slug,
@@ -51,28 +54,105 @@ export class RbacService {
});
}
async updateRole(
actorUserId: string,
roleSlug: string,
data: { name?: string; description?: string; permissionSlugs?: string[] }
) {
await this.access.assertRbacManage(actorUserId);
const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
if (!role) {
throw new NotFoundException('Роль не найдена');
}
if (data.permissionSlugs !== undefined) {
await this.prisma.rolePermission.deleteMany({ where: { roleId: role.id } });
for (const slug of data.permissionSlugs) {
const permission = await this.prisma.permission.findUnique({ where: { slug } });
if (!permission) continue;
await this.prisma.rolePermission.create({
data: { roleId: role.id, permissionId: permission.id }
});
}
}
return this.prisma.role.update({
where: { id: role.id },
data: {
name: data.name,
description: data.description
},
include: { permissions: { include: { permission: true } } }
});
}
async deleteRole(actorUserId: string, roleSlug: string) {
await this.access.assertRbacManage(actorUserId);
const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
if (!role) {
throw new NotFoundException('Роль не найдена');
}
if (role.isSystem) {
throw new BadRequestException('Системную роль нельзя удалить');
}
await this.prisma.userRole.deleteMany({ where: { roleId: role.id } });
await this.prisma.rolePermission.deleteMany({ where: { roleId: role.id } });
await this.prisma.role.delete({ where: { id: role.id } });
return { slug: roleSlug };
}
async assignRole(actorUserId: string, userId: string, roleSlug: string) {
await this.access.assertSuperAdmin(actorUserId);
await this.access.assertRbacManage(actorUserId);
const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
if (!role) {
throw new NotFoundException('Роль не найдена');
}
await this.ensureUserExists(userId);
await this.prisma.userRole.upsert({
where: { userId_roleId: { userId, roleId: role.id } },
create: { userId, roleId: role.id },
update: {}
const existingLink = await this.prisma.userRole.findUnique({
where: { userId_roleId: { userId, roleId: role.id } }
});
if (existingLink) {
return this.getUserRoleSlugs(userId);
}
await this.prisma.userRole.create({
data: { userId, roleId: role.id }
});
await this.notifications.create(userId, 'role_assigned', 'Роли', `Вам назначена роль: ${role.name}`, {
roleSlug: role.slug,
roleName: role.name
});
return this.getUserRoleSlugs(userId);
}
async removeRole(actorUserId: string, userId: string, roleSlug: string) {
await this.access.assertSuperAdmin(actorUserId);
await this.access.assertRbacManage(actorUserId);
const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
if (!role) {
throw new NotFoundException('Роль не найдена');
}
if (role.isSystem || role.isDefault || role.slug === DEFAULT_USER_ROLE_SLUG) {
throw new BadRequestException('Стандартную роль «Пользователь» нельзя снять');
}
const existingLink = await this.prisma.userRole.findUnique({
where: { userId_roleId: { userId, roleId: role.id } }
});
if (!existingLink) {
return this.getUserRoleSlugs(userId);
}
await this.prisma.userRole.deleteMany({ where: { userId, roleId: role.id } });
await this.notifications.create(userId, 'role_removed', 'Роли', `С вас снята роль: ${role.name}`, {
roleSlug: role.slug,
roleName: role.name
});
return this.getUserRoleSlugs(userId);
}
@@ -163,6 +243,54 @@ export class RbacService {
return { client: updated, clientSecret: undefined as string | undefined };
}
async deleteOAuthClient(actorUserId: string, clientId: string) {
const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
if (!existing) {
throw new NotFoundException('OAuth-приложение не найдено');
}
await this.assertCanManageOAuthClient(actorUserId, existing);
await this.prisma.oAuthClient.delete({ where: { id: existing.id } });
return { clientId };
}
async ensureDefaultUserRole(userId: string) {
const role = await this.prisma.role.findUnique({ where: { slug: DEFAULT_USER_ROLE_SLUG } });
if (!role) return;
await this.prisma.userRole.upsert({
where: { userId_roleId: { userId, roleId: role.id } },
create: { userId, roleId: role.id },
update: {}
});
}
async assignUserPermission(actorUserId: string, userId: string, permissionSlug: string) {
await this.access.assertRbacManage(actorUserId);
await this.ensureUserExists(userId);
const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug } });
if (!permission) {
throw new NotFoundException('Право не найдено');
}
await this.prisma.userPermission.upsert({
where: { userId_permissionId: { userId, permissionId: permission.id } },
create: { userId, permissionId: permission.id },
update: {}
});
return this.getUserPermissionSlugs(userId);
}
async removeUserPermission(actorUserId: string, userId: string, permissionSlug: string) {
await this.access.assertRbacManage(actorUserId);
const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug } });
if (!permission) {
throw new NotFoundException('Право не найдено');
}
await this.prisma.userPermission.deleteMany({ where: { userId, permissionId: permission.id } });
return this.getUserPermissionSlugs(userId);
}
async rotateOAuthSecret(actorUserId: string, clientId: string) {
const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
if (!existing) {
@@ -256,6 +384,14 @@ export class RbacService {
return links.map((link) => link.role.slug);
}
private async getUserPermissionSlugs(userId: string) {
const links = await this.prisma.userPermission.findMany({
where: { userId },
include: { permission: true }
});
return links.map((link) => link.permission.slug);
}
private async ensureUserExists(userId: string) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {