105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
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,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|