import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requirePermission } from "../../middleware/auth"; import { logAudit } from "../../services/audit"; import { success, error, parseId } from "../../utils/response"; import { parseBody } from "../../schemas/common"; import { CreateRoleSchema, UpdateRoleSchema } from "../../schemas/roles.schema"; export default async function rolesRoutes( fastify: FastifyInstance, ): Promise { // GET /api/admin/roles fastify.get( "/", { preHandler: requirePermission("settings.roles") }, async (request, reply) => { const [roles, userCounts] = await Promise.all([ prisma.roles.findMany({ include: { role_permissions: { include: { permissions: true }, }, }, orderBy: { id: "asc" }, }), prisma.users.groupBy({ by: ["role_id"], _count: { id: true }, }), ]); const countMap = new Map( userCounts.map((u) => [u.role_id, Number(u._count.id)]), ); const data = roles.map((r) => ({ ...r, permissions: r.role_permissions.map((rp) => rp.permissions), user_count: countMap.get(r.id) || 0, })); return success(reply, data); }, ); // GET /api/admin/roles/permissions fastify.get( "/permissions", { preHandler: requirePermission("settings.roles") }, async (_request, reply) => { const permissions = await prisma.permissions.findMany({ orderBy: [{ module: "asc" }, { id: "asc" }], }); return success(reply, permissions); }, ); // POST /api/admin/roles fastify.post( "/", { preHandler: requirePermission("settings.roles") }, async (request, reply) => { // Privilege-escalation guard: only an admin may create roles (and thus // attach arbitrary permission_ids). A non-admin `settings.roles` holder // must not be able to mint a new high-privilege role. if (request.authData!.roleName !== "admin") { return error(reply, "Pouze administrátor může vytvářet role", 403); } const parsed = parseBody(CreateRoleSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const name = String(body.name); // Never allow minting another "admin" role (would shadow / escalate). if (name.toLowerCase() === "admin") { return error(reply, "Nelze vytvořit roli s názvem admin", 400); } // Enforce role-name uniqueness explicitly (clear 409 instead of a raw // Prisma constraint 500). const duplicate = await prisma.roles.findFirst({ where: { name } }); if (duplicate) { return error(reply, "Role s tímto názvem již existuje", 409); } // Role insert + permission assignment in a SINGLE transaction so a // partial failure can never leave an orphan role with no permissions. const role = await prisma.$transaction(async (tx) => { const created = await tx.roles.create({ data: { name, display_name: String(body.display_name), description: body.description ? String(body.description) : null, }, }); if (Array.isArray(body.permission_ids) && body.permission_ids.length) { await tx.role_permissions.createMany({ data: (body.permission_ids as number[]).map((pid) => ({ role_id: created.id, permission_id: pid, })), }); } return created; }); await logAudit({ request, authData: request.authData, action: "create", entityType: "role", entityId: role.id, description: `Vytvořena role ${role.name}`, }); return success(reply, { id: role.id }, 201, "Role byla vytvořena"); }, ); // PUT /api/admin/roles/:id fastify.put<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("settings.roles") }, async (request, reply) => { // Privilege-escalation guard: only an admin may modify roles or their // permission sets. if (request.authData!.roleName !== "admin") { return error(reply, "Pouze administrátor může upravovat role", 403); } const id = parseId(request.params.id, reply); if (id === null) return; const parsed = parseBody(UpdateRoleSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const existing = await prisma.roles.findUnique({ where: { id } }); if (!existing) return error(reply, "Role nenalezena", 404); // Mirror the DELETE guard: the built-in admin role's permission set is // immutable (it is resolved as "all permissions" anyway). Block any // attempt to rewrite it via permission_ids → no lockout / escalation. if (existing.name === "admin" && Array.isArray(body.permission_ids)) { return error(reply, "Oprávnění role admin nelze měnit", 400); } await prisma.roles.update({ where: { id }, data: { display_name: body.display_name ? String(body.display_name) : undefined, description: body.description !== undefined ? String(body.description) : undefined, }, }); if (Array.isArray(body.permission_ids)) { await prisma.$transaction([ prisma.role_permissions.deleteMany({ where: { role_id: id } }), prisma.role_permissions.createMany({ data: (body.permission_ids as number[]).map((pid) => ({ role_id: id, permission_id: pid, })), }), ]); } await logAudit({ request, authData: request.authData, action: "update", entityType: "role", entityId: id, description: `Upravena role ${existing.name}`, }); return success(reply, { id }, 200, "Role byla aktualizována"); }, ); // DELETE /api/admin/roles/:id fastify.delete<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("settings.roles") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const existing = await prisma.roles.findUnique({ where: { id } }); if (!existing) return error(reply, "Role nenalezena", 404); if (existing.name === "admin") { return error(reply, "Nelze smazat roli admin", 400); } await prisma.roles.delete({ where: { id } }); await logAudit({ request, authData: request.authData, action: "delete", entityType: "role", entityId: id, description: `Smazána role ${existing.name}`, }); return success(reply, { id }, 200, "Role byla smazána"); }, ); }