fix: use domain errors instead of trpc (#22341)

This commit is contained in:
sean-brydon authored and GitHub committed 2025-07-10 08:23:50 +01:00
1 parent 3285b252c2
commit 9892d521fb
4 files changed
+77 -17

No files matched your search

@@ -0,0 +1,12 @@
export class RoleManagementError extends Error {
constructor(message: string, public readonly code: RoleManagementErrorCode) {
super(message);
this.name = "RoleManagementError";
}
}
export enum RoleManagementErrorCode {
UNAUTHORIZED = "UNAUTHORIZED",
ROLE_NOT_FOUND = "ROLE_NOT_FOUND",
INVALID_ROLE = "INVALID_ROLE",
}
@@ -5,8 +5,7 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { RoleManagementError, RoleManagementErrorCode } from "../../domain/errors/role-management.error";
import { DEFAULT_ROLE_IDS } from "../../lib/constants";
import { PermissionCheckService } from "../permission-check.service";
import { RoleManagementFactory } from "../role-management.factory";
@@ -117,7 +116,12 @@ describe("RoleManagementFactory", () => {
it("should throw UNAUTHORIZED when user lacks permission", async () => {
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(TRPCError);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
new RoleManagementError(
"You do not have permission to change roles",
RoleManagementErrorCode.UNAUTHORIZED
)
);
});
});
@@ -144,14 +148,16 @@ describe("RoleManagementFactory", () => {
expect(mockRoleService.assignRoleToMember).toHaveBeenCalledWith(customRoleId, membershipId);
});
it("should throw UNAUTHORIZED for invalid custom role", async () => {
it("should throw INVALID_ROLE for invalid custom role", async () => {
const customRoleId = "invalid-role";
mockRoleService.roleBelongsToTeam.mockResolvedValue(false);
const manager = await factory.createRoleManager(organizationId);
await expect(
manager.assignRole(userId, organizationId, customRoleId as MembershipRole, membershipId)
).rejects.toThrow(TRPCError);
).rejects.toThrow(
new RoleManagementError("You do not have access to this role", RoleManagementErrorCode.INVALID_ROLE)
);
});
});
});
@@ -159,13 +165,33 @@ describe("RoleManagementFactory", () => {
describe("LegacyRoleManager", () => {
beforeEach(() => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(false);
vi.mocked(prisma.membership.update).mockResolvedValue({});
vi.mocked(prisma.membership.update).mockResolvedValue({
id: 1,
teamId: organizationId,
createdAt: new Date(),
updatedAt: new Date(),
role: MembershipRole.MEMBER,
userId: userId,
disableImpersonation: false,
accepted: true,
customRoleId: null,
});
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
});
describe("checkPermissionToChangeRole", () => {
it("should allow role change when user is owner", async () => {
vi.mocked(isOrganisationAdmin).mockResolvedValue(true);
vi.mocked(isOrganisationAdmin).mockResolvedValue({
id: 1,
teamId: organizationId,
userId: userId,
role: MembershipRole.OWNER,
accepted: true,
disableImpersonation: false,
createdAt: new Date(),
updatedAt: new Date(),
customRoleId: null,
});
const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
});
@@ -173,7 +199,12 @@ describe("RoleManagementFactory", () => {
it("should throw UNAUTHORIZED when user is not owner", async () => {
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(TRPCError);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
new RoleManagementError(
"Only owners or admin can update roles",
RoleManagementErrorCode.UNAUTHORIZED
)
);
});
});
@@ -3,8 +3,7 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { RoleManagementError, RoleManagementErrorCode } from "../domain/errors/role-management.error";
import { DEFAULT_ROLE_IDS } from "../lib/constants";
import { PermissionCheckService } from "./permission-check.service";
import { RoleService } from "./role.service";
@@ -38,7 +37,10 @@ class PBACRoleManager implements IRoleManager {
});
if (!hasPermission) {
throw new TRPCError({ code: "UNAUTHORIZED" });
throw new RoleManagementError(
"You do not have permission to change roles",
RoleManagementErrorCode.UNAUTHORIZED
);
}
}
@@ -55,7 +57,10 @@ class PBACRoleManager implements IRoleManager {
} else {
const roleExists = await this.roleService.roleBelongsToTeam(role, organizationId);
if (!roleExists) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "You do not have access to this role" });
throw new RoleManagementError(
"You do not have access to this role",
RoleManagementErrorCode.INVALID_ROLE
);
}
await this.roleService.assignRoleToMember(role, membershipId);
}
@@ -73,11 +78,14 @@ class PBACRoleManager implements IRoleManager {
class LegacyRoleManager implements IRoleManager {
public isPBACEnabled = false;
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
const isUpdaterAnOwner = await isOrganisationAdmin(userId, organizationId);
const membership = await isOrganisationAdmin(userId, organizationId);
// Only OWNER can update role to OWNER
if (!isUpdaterAnOwner) {
throw new TRPCError({ code: "UNAUTHORIZED" });
// Only OWNER/ADMIN can update role
if (!membership) {
throw new RoleManagementError(
"Only owners or admin can update roles",
RoleManagementErrorCode.UNAUTHORIZED
);
}
}
@@ -2,6 +2,7 @@ import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/c
import { ensureOrganizationIsReviewed } from "@calcom/ee/organizations/lib/ensureOrganizationIsReviewed";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { RoleManagementError } from "@calcom/features/pbac/domain/errors/role-management.error";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { uploadAvatar } from "@calcom/lib/server/avatar";
import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername";
@@ -44,7 +45,15 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED", message: "You must be a member of an organizaiton" });
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
await roleManager.checkPermissionToChangeRole(userId, organizationId);
try {
await roleManager.checkPermissionToChangeRole(userId, organizationId);
} catch (error) {
if (error instanceof RoleManagementError) {
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });
}
throw error;
}
await ensureOrganizationIsReviewed(organizationId);