Files
calendar/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts
T

199 lines
6.2 KiB
TypeScript

import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
import {
Resource,
CustomAction,
PermissionString,
} from "@calcom/features/pbac/domain/types/permission-registry";
import { getSpecificPermissions } from "@calcom/features/pbac/lib/resource-permissions";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TListMembersInputSchema } from "./listMembers.schema";
type ListMembersHandlerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TListMembersInputSchema;
};
const userSelect = {
username: true,
email: true,
name: true,
avatarUrl: true,
id: true,
bio: true,
disableImpersonation: true,
lastActiveAt: true,
} satisfies Prisma.UserSelect;
export const listMembersHandler = async ({ ctx, input }: ListMembersHandlerOptions) => {
const { cursor, limit, teamId, searchTerm } = input;
const canAccessMembers = await checkCanAccessMembers(ctx, teamId);
if (!canAccessMembers) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to see members of any teams",
});
}
const whereCondition: Prisma.MembershipWhereInput = {
teamId,
};
if (searchTerm) {
whereCondition.user = {
OR: [
{ email: { contains: searchTerm, mode: "insensitive" } },
{ username: { contains: searchTerm, mode: "insensitive" } },
{ name: { contains: searchTerm, mode: "insensitive" } },
],
};
}
const totalMembers = await prisma.membership.count({ where: whereCondition });
const teamMembers = await prisma.membership.findMany({
where: whereCondition,
select: {
id: true,
role: true,
accepted: true,
teamId: true,
customRoleId: true,
user: { select: userSelect },
},
cursor: cursor ? { id: cursor } : undefined,
take: limit + 1,
orderBy: { id: "asc" },
});
let nextCursor: typeof cursor | undefined = undefined;
if (teamMembers.length > limit) {
const nextItem = teamMembers.pop();
nextCursor = nextItem?.id;
}
const teamRepo = new TeamRepository(prisma);
const team = await teamRepo.findById({ id: input.teamId });
const organizationId = team?.parentId || teamId;
// Get custom roles if PBAC is enabled
let customRoles: { [key: string]: { id: string; name: string } } = {};
try {
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
if (roleManager.isPBACEnabled) {
const roles = await roleManager.getTeamRoles(teamId);
customRoles = roles.reduce(
(acc, role) => {
acc[role.id] = role;
return acc;
},
{} as { [key: string]: { id: string; name: string } }
);
}
} catch (error) {
// PBAC not enabled or error occurred, continue with traditional roles
}
const users = teamMembers.map((member) => member.user);
const enrichedUsers = await new UserRepository(prisma).enrichUsersWithTheirProfileExcludingOrgMetadata(
users
);
const enrichedUserMap = new Map<number, (typeof enrichedUsers)[0]>();
enrichedUsers.forEach((enrichedUser) => {
enrichedUserMap.set(enrichedUser.id, enrichedUser);
});
const membersWithApps = teamMembers
.map((member) => {
const user = enrichedUserMap.get(member.user.id);
if (!user) {
return null;
}
const { profile, ...restUser } = user;
// Determine the role to display
let customRole = null;
if (member.customRoleId && customRoles[member.customRoleId]) {
customRole = customRoles[member.customRoleId];
}
return {
...restUser,
username: profile?.username ?? restUser.username,
role: member.role,
customRoleId: member.customRoleId,
customRole,
profile: profile,
organizationId: profile?.organizationId ?? null,
organization: profile?.organization,
accepted: member.accepted,
disableImpersonation: user.disableImpersonation,
bookerUrl: getBookerBaseUrlSync(profile?.organization?.slug || ""),
teamId: member.teamId,
lastActiveAt: member.user.lastActiveAt
? new Intl.DateTimeFormat(ctx.user.locale, {
timeZone: ctx.user.timeZone,
})
.format(member.user.lastActiveAt)
.toLowerCase()
: null,
};
})
.filter((member): member is NonNullable<typeof member> => member !== null);
return {
members: membersWithApps,
nextCursor,
meta: {
totalRowCount: totalMembers,
},
};
};
const checkCanAccessMembers = async (ctx: ListMembersHandlerOptions["ctx"], teamId: number) => {
const isTargetingOrg = teamId === ctx.user.organizationId;
// Get team info to check if it's private
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
if (!team) return false;
// Determine the resource type based on whether this is an org or team
const resource = isTargetingOrg ? Resource.Organization : Resource.Team;
const targetAction = team.isPrivate ? CustomAction.ListMembersPrivate : CustomAction.ListMembers;
const permissionString = `${resource}.${targetAction}` as PermissionString;
// Check PBAC permissions for listing members
const permissionCheckService = new PermissionCheckService();
return permissionCheckService.checkPermission({
userId: ctx.user.id,
teamId: teamId,
permission: permissionString,
fallbackRoles: team.isPrivate
? [MembershipRole.ADMIN, MembershipRole.OWNER]
: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER],
});
};
export default listMembersHandler;