import { ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { DatabaseService } from '../database/database.service'; import { AccessStore } from './access.store'; import { recordAudit } from './audit'; import type { Principal } from './identity.types'; import type { PageInput, RoleInput } from './identity.schemas'; function ensureGrantable(actor: Principal, permissions: string[]) { if ( permissions.some((permission) => !actor.permissions.includes(permission)) ) { throw new ForbiddenException('Cannot grant permissions you do not hold'); } } @Injectable() export class RoleStore { constructor( private readonly db: DatabaseService, private readonly access: AccessStore, ) {} list(actor: Principal, page: PageInput) { return this.db.role.findMany({ where: { organizationId: actor.organizationId }, orderBy: { id: 'asc' }, take: page.limit, skip: page.offset, }); } save(actor: Principal, input: RoleInput, id?: string) { return this.access.mutate(actor, 'roles.manage', async (tx, current) => { ensureGrantable(current, input.permissions); if (id) { const role = await tx.role.findFirst({ where: { id, organizationId: actor.organizationId }, }); if (!role) throw new NotFoundException(); if (role.isSystem) throw new ForbiddenException('System role is immutable'); ensureGrantable(current, role.permissions); } const role = id ? await tx.role.update({ where: { id }, data: input }) : await tx.role.create({ data: { ...input, organizationId: actor.organizationId }, }); await recordAudit( tx, actor.organizationId, actor.userId, id ? 'role.updated' : 'role.created', role.id, ); return role; }); } assign(actor: Principal, userId: string, roleIds: string[]) { return this.access.mutate( actor, 'users.roles.assign', async (tx, current) => { const user = await tx.user.findFirst({ where: { id: userId, organizationId: actor.organizationId }, include: { roles: { include: { role: true } } }, }); if (!user) throw new NotFoundException(); if (user.isOwner || user.id === current.userId) throw new ForbiddenException('Cannot change these role assignments'); ensureGrantable( current, user.roles.flatMap((assignment) => assignment.role.permissions), ); const roles = await tx.role.findMany({ where: { id: { in: roleIds }, organizationId: actor.organizationId }, }); if (roles.length !== roleIds.length) throw new NotFoundException(); if (roles.some((role) => role.isSystem)) throw new ForbiddenException('System role cannot be assigned'); ensureGrantable( current, roles.flatMap((role) => role.permissions), ); await tx.userRole.deleteMany({ where: { userId } }); await tx.userRole.createMany({ data: roleIds.map((roleId) => ({ userId, roleId, organizationId: actor.organizationId, })), }); await recordAudit( tx, actor.organizationId, actor.userId, 'user.roles_assigned', userId, ); }, ); } }