fix sso core folder

This commit is contained in:
lendry
2026-06-24 14:45:45 +03:00
parent 995adeedd4
commit d2bbf35d40
45 changed files with 6592 additions and 1 deletions

View File

@@ -0,0 +1,203 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'node:crypto';
import { OAuthClientType } from '../generated/prisma/client';
import { PrismaService } from '../infra/prisma.service';
import { AccessService } from './access.service';
@Injectable()
export class RbacService {
constructor(
private readonly prisma: PrismaService,
private readonly access: AccessService
) {}
async createRole(actorUserId: string, data: { slug: string; name: string; description?: string; permissionSlugs?: string[] }) {
await this.access.assertSuperAdmin(actorUserId);
return this.prisma.role.create({
data: {
slug: data.slug,
name: data.name,
description: data.description,
permissions: data.permissionSlugs?.length
? {
create: data.permissionSlugs.map((slug) => ({
permission: { connect: { slug } }
}))
}
: undefined
},
include: { permissions: { include: { permission: true } } }
});
}
async upsertPermission(data: { slug: string; name: string; description?: string }) {
return this.prisma.permission.upsert({
where: { slug: data.slug },
create: data,
update: { name: data.name, description: data.description }
});
}
async assignRole(actorUserId: string, userId: string, roleSlug: string) {
await this.access.assertSuperAdmin(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: {}
});
return this.getUserRoleSlugs(userId);
}
async removeRole(actorUserId: string, userId: string, roleSlug: string) {
await this.access.assertSuperAdmin(actorUserId);
const role = await this.prisma.role.findUnique({ where: { slug: roleSlug } });
if (!role) {
throw new NotFoundException('Роль не найдена');
}
await this.prisma.userRole.deleteMany({ where: { userId, roleId: role.id } });
return this.getUserRoleSlugs(userId);
}
async listRoles() {
return this.prisma.role.findMany({
orderBy: { name: 'asc' },
include: { permissions: { include: { permission: true } } }
});
}
async listPermissions() {
return this.prisma.permission.findMany({ orderBy: { slug: 'asc' } });
}
async listOAuthScopes() {
return this.prisma.oAuthScope.findMany({ orderBy: { slug: 'asc' } });
}
async createOAuthClient(actorUserId: string, data: { name: string; redirectUris: string[]; scopes: string[]; type?: OAuthClientType }) {
await this.assertOAuthPermission(actorUserId);
if (!data.redirectUris.length) {
throw new BadRequestException('Укажите хотя бы один redirect URI');
}
if (!data.scopes.length) {
throw new BadRequestException('Выберите хотя бы один scope');
}
const clientSecret = data.type === OAuthClientType.PUBLIC ? null : randomBytes(32).toString('base64url');
const client = await this.prisma.oAuthClient.create({
data: {
name: data.name,
clientId: `lendry_${randomBytes(16).toString('hex')}`,
clientSecret,
redirectUris: data.redirectUris,
type: data.type ?? OAuthClientType.CONFIDENTIAL,
scopes: {
create: data.scopes.map((slug) => ({
scope: { connect: { slug } }
}))
}
},
include: { scopes: { include: { scope: true } } }
});
return { client, clientSecret };
}
async updateOAuthClient(
actorUserId: string,
clientId: string,
data: { name?: string; redirectUris?: string[]; scopes?: string[]; isActive?: boolean }
) {
await this.assertOAuthPermission(actorUserId);
const existing = await this.prisma.oAuthClient.findUnique({
where: { clientId },
include: { scopes: true }
});
if (!existing) {
throw new NotFoundException('OAuth-приложение не найдено');
}
if (data.redirectUris && !data.redirectUris.length) {
throw new BadRequestException('Укажите хотя бы один redirect URI');
}
if (data.scopes) {
await this.prisma.oAuthClientScope.deleteMany({ where: { clientId: existing.id } });
for (const slug of data.scopes) {
const scope = await this.prisma.oAuthScope.findUnique({ where: { slug } });
if (!scope) continue;
await this.prisma.oAuthClientScope.create({
data: { clientId: existing.id, scopeId: scope.id }
});
}
}
const updated = await this.prisma.oAuthClient.update({
where: { id: existing.id },
data: {
name: data.name,
redirectUris: data.redirectUris,
isActive: data.isActive
},
include: { scopes: { include: { scope: true } } }
});
return { client: updated, clientSecret: undefined as string | undefined };
}
async rotateOAuthSecret(actorUserId: string, clientId: string) {
await this.assertOAuthPermission(actorUserId);
const existing = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
if (!existing) {
throw new NotFoundException('OAuth-приложение не найдено');
}
if (existing.type === OAuthClientType.PUBLIC) {
throw new BadRequestException('У публичного клиента нет client secret');
}
const clientSecret = randomBytes(32).toString('base64url');
await this.prisma.oAuthClient.update({
where: { id: existing.id },
data: { clientSecret }
});
return { clientId, clientSecret };
}
async listOAuthClients() {
return this.prisma.oAuthClient.findMany({
orderBy: { createdAt: 'desc' },
include: { scopes: { include: { scope: true } } }
});
}
private async assertOAuthPermission(actorUserId: string) {
const actor = await this.prisma.user.findUnique({ where: { id: actorUserId } });
if (!actor) {
throw new NotFoundException('Пользователь не найден');
}
const access = await this.access.getUserAccess(actor.id, actor.isSuperAdmin);
if (!access.canManageOAuth) {
throw new ForbiddenException('Недостаточно прав для управления OAuth-приложениями');
}
}
private async getUserRoleSlugs(userId: string) {
const links = await this.prisma.userRole.findMany({
where: { userId },
include: { role: true }
});
return links.map((link) => link.role.slug);
}
private async ensureUserExists(userId: string) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new NotFoundException('Пользователь не найден');
}
}
}