diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 393b10c1a..02aa6999b 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -1,3 +1,4 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { @@ -79,7 +80,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .enum(["true", "false"]) .default("false") .transform((value) => value === "true"), - roles: z.string().trim().transform(decodeURIComponent).optional() + roles: z + .string() + .trim() + .transform(decodeURIComponent) + .refine((value) => { + if (!value) return true; + const slugs = value.split(","); + return slugs.every((slug) => slugify(slug.trim(), { lowercase: true }) === slug.trim()); + }) + .optional() }), params: z.object({ workspaceId: z.string().trim() @@ -118,7 +128,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const roles = req.query.roles?.split(",") ?? []; + const roles = (req.query.roles?.split(",") || []).filter(Boolean); const users = await server.services.projectMembership.getProjectMemberships({ actorId: req.permission.id, actor: req.permission.type, diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 8a9d41cba..1692020fb 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -31,6 +31,26 @@ export const projectMembershipDALFactory = (db: TDbClient) => { if (filter.id) { void qb.where(`${TableName.ProjectMembership}.id`, filter.id); } + if (filter.roles && filter.roles.length > 0) { + void qb.whereExists((subQuery) => { + void subQuery + .select("role") + .from(TableName.ProjectUserMembershipRole) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectRoles}.id`, + `${TableName.ProjectUserMembershipRole}.customRoleId` + ) + .whereRaw( + `"${TableName.ProjectUserMembershipRole}"."projectMembershipId" = "${TableName.ProjectMembership}"."id"` + ) + .where((subQb) => { + void subQb + .whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[]) + .orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]); + }); + }); + } }) .join( TableName.UserEncryptionKey, @@ -126,11 +146,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => { } ] }); - if (filter.roles && filter.roles.length > 0) { - return members.filter((member) => - member.roles.some((role) => filter.roles?.includes(role.role) || filter.roles?.includes(role.customRoleSlug)) - ); - } return members; } catch (error) { throw new DatabaseError({ error, name: "Find all project members" }); diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index a3f45626e..2527f7bcf 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -445,7 +445,10 @@ export const useGetWorkspaceUsers = ( { params: { includeGroupMembers, - roles: roles && roles.length > 0 ? encodeURIComponent(roles.join(",")) : undefined + roles: + roles && roles.length > 0 + ? roles.map((role) => encodeURIComponent(role)).join(",") + : undefined } } ); diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx index 433d1941e..fd303f653 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx @@ -173,7 +173,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { setOrderDirection(OrderByDirection.ASC); }; - const isTableFiltered = Boolean(Object.values(filter.roles).length); + const isTableFiltered = Boolean(filter.roles.length); const handleRoleToggle = useCallback( (roleSlug: string) =>