manicanldes-backend/src/identity/role.store.ts

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,
);
},
);
}
}