From dbb86171804d120a9d99b8c8c1922e06fda9a0e0 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 16 Sep 2024 02:12:24 +0800 Subject: [PATCH 01/14] misc: setup prerequisites for terraform project group --- .../server/routes/v2/group-project-router.ts | 95 +++++++++++++-- .../group-project/group-project-dal.ts | 105 +++++++++++++++- .../group-project/group-project-service.ts | 114 +++++++++++++++--- .../group-project/group-project-types.ts | 16 ++- 4 files changed, 299 insertions(+), 31 deletions(-) diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 6d438c1ff..2418cb0e9 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -28,14 +28,36 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug), groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug) }), - body: z.object({ - role: z - .string() - .trim() - .min(1) - .default(ProjectMembershipRole.NoAccess) - .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role) - }), + body: z + .object({ + role: z + .string() + .trim() + .min(1) + .default(ProjectMembershipRole.NoAccess) + .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role), + roles: z + .array( + z.union([ + z.object({ + role: z.string(), + isTemporary: z.literal(false).default(false) + }), + z.object({ + role: z.string(), + isTemporary: z.literal(true), + temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + ]) + ) + .optional() + }) + .refine((data) => data.role || data.roles, { + message: "Either role or roles must be present", + path: ["role", "roles"] + }), response: { 200: z.object({ groupMembership: GroupProjectMembershipsSchema @@ -50,8 +72,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, groupSlug: req.params.groupSlug, projectSlug: req.params.projectSlug, - role: req.body.role + roles: req.body.roles || [{ role: req.body.role }] }); + return { groupMembership }; } }); @@ -198,4 +221,58 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => return { groupMemberships }; } }); + + server.route({ + method: "GET", + url: "/:projectSlug/groups/:groupSlug", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Return project group", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectSlug: z.string().trim(), + groupSlug: z.string().trim() + }), + response: { + 200: z.object({ + groupMembership: z.object({ + id: z.string(), + groupId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + group: GroupsSchema.pick({ name: true, id: true, slug: true }) + }) + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.getGroupInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { groupMembership }; + } + }); }; diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index c74a6b1b2..8370ff881 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -10,6 +10,109 @@ export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); + const findByGroupSlugAndProject = async ( + { groupSlug, projectId }: { groupSlug: string; projectId: string }, + tx?: Knex + ) => { + try { + // this is a workaround so that the order of group roles is preserved as a necessary + // requirement for our terraform provider + + /* Approach: + 1. First we query the group project membership along with the attached roles + 2. Then we have a separate query to fetch the custom role details which we manually augment below + */ + const rawGroupResult = await (tx || db.replicaNode())(TableName.GroupProjectMembership) + .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .where(`${TableName.Groups}.slug`, "=", groupSlug) + .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) + .join( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .select( + db.ref("id").withSchema(TableName.GroupProjectMembership), + db.ref("createdAt").withSchema(TableName.GroupProjectMembership), + db.ref("updatedAt").withSchema(TableName.GroupProjectMembership), + db.ref("id").as("groupId").withSchema(TableName.Groups), + db.ref("name").as("groupName").withSchema(TableName.Groups), + db.ref("slug").as("groupSlug").withSchema(TableName.Groups), + db.ref("id").withSchema(TableName.GroupProjectMembership), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), + db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), + db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), + db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole) + ); + + // IMPORTANT NOTE: we do this separately because this breaks the order of the group project roles + const customRoleDetails = await (tx || db.replicaNode())(TableName.ProjectRoles) + .whereIn("id", rawGroupResult.map((entry) => entry.customRoleId) as string[]) + .select("id", "slug", "name"); + + const groupProjectMembershipRoleToCustomRole: Record = {}; + customRoleDetails.forEach( + // eslint-disable-next-line no-return-assign + (entry) => + (groupProjectMembershipRoleToCustomRole[entry.id] = { + name: entry.name, + slug: entry.slug + }) + ); + + const members = sqlNestRelationships({ + data: rawGroupResult, + parentMapper: ({ groupId, groupName, id, createdAt, updatedAt }) => ({ + id, + groupId, + createdAt, + updatedAt, + group: { + id: groupId, + name: groupName, + slug: groupSlug + } + }), + key: "id", + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.name, + customRoleSlug: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.slug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + }); + + return members[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindByGroupSlugAndProject" }); + } + }; + const findByProjectId = async (projectId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) @@ -221,5 +324,5 @@ export const groupProjectDALFactory = (db: TDbClient) => { return members; }; - return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; + return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers, findByGroupSlugAndProject }; }; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 17862dd6f..ebf8025ef 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -7,7 +7,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; @@ -22,12 +22,16 @@ import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membershi import { TCreateProjectGroupDTO, TDeleteProjectGroupDTO, + TGetGroupInProjectDTO, TListProjectGroupDTO, TUpdateProjectGroupDTO } from "./group-project-types"; type TGroupProjectServiceFactoryDep = { - groupProjectDAL: Pick; + groupProjectDAL: Pick< + TGroupProjectDALFactory, + "findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "findByGroupSlugAndProject" + >; groupProjectMembershipRoleDAL: Pick< TGroupProjectMembershipRoleDALFactory, "create" | "transaction" | "insertMany" | "delete" @@ -61,7 +65,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actorAuthMethod, projectSlug, - role + roles }: TCreateProjectGroupDTO) => { const project = await projectDAL.findOne({ slug: projectSlug @@ -88,16 +92,35 @@ export const groupProjectServiceFactory = ({ message: `Group with slug ${groupSlug} already exists in project with id ${project.id}` }); - const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole( - role, - project.id + for await (const { role: requestedRoleChange } of roles) { + const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( + requestedRoleChange, + project.id + ); + + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + + if (!hasRequiredPriviledges) { + throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); + } + } + + // validate custom roles input + const customInputRoles = roles.filter( + ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) ); - const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPrivilege) - throw new ForbiddenRequestError({ - message: "Failed to add group to project with more privileged role" - }); - const isCustomRole = Boolean(customRole); + const hasCustomRole = Boolean(customInputRoles.length); + const customRoles = hasCustomRole + ? await projectRoleDAL.find({ + projectId: project.id, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "Custom role not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const projectGroup = await groupProjectDAL.transaction(async (tx) => { const groupProjectMembership = await groupProjectDAL.create( @@ -108,14 +131,31 @@ export const groupProjectServiceFactory = ({ tx ); - await groupProjectMembershipRoleDAL.create( - { + const sanitizedProjectMembershipRoles = roles.map((inputRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); + if (!inputRole.isTemporary) { + return { + projectMembershipId: groupProjectMembership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null + }; + } + + // check cron or relative here later for now its just relative + const relativeTimeInMs = ms(inputRole.temporaryRange); + return { projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : role, - customRoleId: customRole?.id - }, - tx - ); + role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, + customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, + isTemporary: true, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: inputRole.temporaryRange, + temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) + }; + }); + + await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); // share project key with users in group that have not // individually been added to the project and that are not part of @@ -336,10 +376,44 @@ export const groupProjectServiceFactory = ({ return groupMemberships; }; + const getGroupInProject = async ({ + projectSlug, + actor, + actorId, + actorAuthMethod, + actorOrgId, + groupSlug + }: TGetGroupInProjectDTO) => { + const project = await projectDAL.findOne({ + slug: projectSlug + }); + + if (!project) { + throw new NotFoundError({ message: `Failed to find project with slug ${projectSlug}` }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + + const groupMembership = await groupProjectDAL.findByGroupSlugAndProject({ + groupSlug, + projectId: project.id + }); + + return groupMembership; + }; + return { addGroupToProject, updateGroupInProject, removeGroupFromProject, - listGroupsInProject + listGroupsInProject, + getGroupInProject }; }; diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts index c867b75c0..20827ce52 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -4,7 +4,19 @@ import { ProjectUserMembershipTemporaryMode } from "../project-membership/projec export type TCreateProjectGroupDTO = { groupSlug: string; - role: string; + roles: ( + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: ProjectUserMembershipTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } + )[]; } & TProjectSlugPermission; export type TUpdateProjectGroupDTO = { @@ -29,3 +41,5 @@ export type TDeleteProjectGroupDTO = { } & TProjectSlugPermission; export type TListProjectGroupDTO = TProjectSlugPermission; + +export type TGetGroupInProjectDTO = TProjectSlugPermission & { groupSlug: string }; From 335f3f7d372156c91e6f47a9c1418c88d9f0eede Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 17 Sep 2024 18:52:30 +0800 Subject: [PATCH 02/14] misc: removed hacky approach --- .../group-project/group-project-dal.ts | 112 ++---------------- .../group-project/group-project-service.ts | 10 +- 2 files changed, 10 insertions(+), 112 deletions(-) diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index 8370ff881..05724df74 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -10,113 +10,15 @@ export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); - const findByGroupSlugAndProject = async ( - { groupSlug, projectId }: { groupSlug: string; projectId: string }, - tx?: Knex - ) => { - try { - // this is a workaround so that the order of group roles is preserved as a necessary - // requirement for our terraform provider - - /* Approach: - 1. First we query the group project membership along with the attached roles - 2. Then we have a separate query to fetch the custom role details which we manually augment below - */ - const rawGroupResult = await (tx || db.replicaNode())(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - .where(`${TableName.Groups}.slug`, "=", groupSlug) - .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .select( - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership), - db.ref("id").as("groupId").withSchema(TableName.Groups), - db.ref("name").as("groupName").withSchema(TableName.Groups), - db.ref("slug").as("groupSlug").withSchema(TableName.Groups), - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole) - ); - - // IMPORTANT NOTE: we do this separately because this breaks the order of the group project roles - const customRoleDetails = await (tx || db.replicaNode())(TableName.ProjectRoles) - .whereIn("id", rawGroupResult.map((entry) => entry.customRoleId) as string[]) - .select("id", "slug", "name"); - - const groupProjectMembershipRoleToCustomRole: Record = {}; - customRoleDetails.forEach( - // eslint-disable-next-line no-return-assign - (entry) => - (groupProjectMembershipRoleToCustomRole[entry.id] = { - name: entry.name, - slug: entry.slug - }) - ); - - const members = sqlNestRelationships({ - data: rawGroupResult, - parentMapper: ({ groupId, groupName, id, createdAt, updatedAt }) => ({ - id, - groupId, - createdAt, - updatedAt, - group: { - id: groupId, - name: groupName, - slug: groupSlug - } - }), - key: "id", - childrenMapper: [ - { - label: "roles" as const, - key: "membershipRoleId", - mapper: ({ - role, - customRoleId, - membershipRoleId, - temporaryRange, - temporaryMode, - temporaryAccessEndTime, - temporaryAccessStartTime, - isTemporary - }) => ({ - id: membershipRoleId, - role, - customRoleId, - customRoleName: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.name, - customRoleSlug: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.slug, - temporaryRange, - temporaryMode, - temporaryAccessEndTime, - temporaryAccessStartTime, - isTemporary - }) - } - ] - }); - - return members[0]; - } catch (error) { - throw new DatabaseError({ error, name: "FindByGroupSlugAndProject" }); - } - }; - - const findByProjectId = async (projectId: string, tx?: Knex) => { + const findByProjectId = async (projectId: string, filter?: { groupSlug?: string }, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .where((qb) => { + if (filter?.groupSlug) { + void qb.where(`${TableName.Groups}.slug`, "=", filter.groupSlug); + } + }) .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) .join( TableName.GroupProjectMembershipRole, @@ -324,5 +226,5 @@ export const groupProjectDALFactory = (db: TDbClient) => { return members; }; - return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers, findByGroupSlugAndProject }; + return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; }; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index ebf8025ef..b8195cf53 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -28,10 +28,7 @@ import { } from "./group-project-types"; type TGroupProjectServiceFactoryDep = { - groupProjectDAL: Pick< - TGroupProjectDALFactory, - "findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "findByGroupSlugAndProject" - >; + groupProjectDAL: Pick; groupProjectMembershipRoleDAL: Pick< TGroupProjectMembershipRoleDALFactory, "create" | "transaction" | "insertMany" | "delete" @@ -401,9 +398,8 @@ export const groupProjectServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); - const groupMembership = await groupProjectDAL.findByGroupSlugAndProject({ - groupSlug, - projectId: project.id + const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { + groupSlug }); return groupMembership; From 613b97c93d1da3c8628636052da53001a1bd0dae Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 18 Sep 2024 00:29:50 +0800 Subject: [PATCH 03/14] misc: added handling of not found group membership --- backend/src/services/group-project/group-project-service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index b8195cf53..50cf6a1e1 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -402,6 +402,12 @@ export const groupProjectServiceFactory = ({ groupSlug }); + if (!groupMembership) { + throw new NotFoundError({ + message: "Cannot find group membership" + }); + } + return groupMembership; }; From 0daca059c7b3071b82a48db0331fe02a17177443 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 17 Sep 2024 20:53:23 -0400 Subject: [PATCH 04/14] fix small typo --- backend/src/services/group-project/group-project-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50cf6a1e1..77a336fc4 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -95,9 +95,9 @@ export const groupProjectServiceFactory = ({ project.id ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasRequiredPriviledges) { + if (!hasRequiredPrivileges) { throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); } } From 19e4a6de4dc5f48264b117c8ca61935f1b8d60ab Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 18 Sep 2024 14:43:25 +0800 Subject: [PATCH 05/14] misc: added helpful error message --- .../services/permission/permission-service.ts | 2 +- .../group-project/group-project-service.ts | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 72a35a326..399a2950d 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -334,7 +334,7 @@ export const permissionServiceFactory = ({ const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); if (isCustomRole) { const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); - if (!projectRole) throw new BadRequestError({ message: "Role not found" }); + if (!projectRole) throw new BadRequestError({ message: `Role not found: ${role}` }); return { permission: buildProjectPermission([ { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50cf6a1e1..c36ca75a9 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -113,10 +113,17 @@ export const groupProjectServiceFactory = ({ $in: { slug: customInputRoles.map(({ role }) => role) } }) : []; - if (customRoles.length !== customInputRoles.length) { - throw new NotFoundError({ message: "Custom role not found" }); - } + if (customRoles.length !== customInputRoles.length) { + const customRoleSlugs = customRoles.map((customRole) => customRole.slug); + const missingInputRoles = customInputRoles + .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) + .map((role) => role.role); + + throw new NotFoundError({ + message: `Custom role/s not found: ${missingInputRoles.join(", ")}` + }); + } const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const projectGroup = await groupProjectDAL.transaction(async (tx) => { @@ -260,7 +267,16 @@ export const groupProjectServiceFactory = ({ $in: { slug: customInputRoles.map(({ role }) => role) } }) : []; - if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" }); + if (customRoles.length !== customInputRoles.length) { + const customRoleSlugs = customRoles.map((customRole) => customRole.slug); + const missingInputRoles = customInputRoles + .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) + .map((role) => role.role); + + throw new NotFoundError({ + message: `Custom role/s not found: ${missingInputRoles.join(", ")}` + }); + } const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); From 97245c740e727ebfdb0bbf15075a9e25496ff46c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 19 Sep 2024 01:05:31 +0800 Subject: [PATCH 06/14] misc: added as least as privileged check to update --- .../services/group-project/group-project-service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index ba6528d28..fe9e68c55 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -256,6 +256,19 @@ export const groupProjectServiceFactory = ({ const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + for await (const { role: requestedRoleChange } of roles) { + const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( + requestedRoleChange, + project.id + ); + + const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); + + if (!hasRequiredPrivileges) { + throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); + } + } + // validate custom roles input const customInputRoles = roles.filter( ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) From fbebeaf38f1c978c758765acf1b0f9288a251c6b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 19 Sep 2024 01:08:11 +0800 Subject: [PATCH 07/14] misc: added rate limiter --- .../src/server/routes/v2/group-project-router.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 2418cb0e9..69f309e55 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -8,6 +8,7 @@ import { ProjectUserMembershipRolesSchema } from "@app/db/schemas"; import { PROJECTS } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; @@ -17,6 +18,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => method: "POST", url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: writeLimit + }, schema: { description: "Add group to project", security: [ @@ -138,6 +142,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => method: "DELETE", url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: writeLimit + }, schema: { description: "Remove group from project", security: [ @@ -172,6 +179,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => method: "GET", url: "/:projectSlug/groups", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, schema: { description: "Return list of groups in project", security: [ @@ -226,6 +236,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => method: "GET", url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, schema: { description: "Return project group", security: [ From 93b7f56337676e5feb80b1544c5237fc067be77f Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 20 Sep 2024 13:30:38 +0800 Subject: [PATCH 08/14] misc: migrated groups API to use ids instead of slug --- backend/src/ee/routes/v1/group-router.ts | 43 ++++++++++--------- .../src/ee/services/group/group-service.ts | 42 ++++++++---------- backend/src/ee/services/group/group-types.ts | 10 ++--- backend/src/lib/api-docs/constants.ts | 9 ++-- frontend/src/hooks/api/groups/mutations.tsx | 34 +++++++++++---- frontend/src/hooks/api/groups/queries.tsx | 16 ++++--- .../OrgGroupsSection/OrgGroupMembersModal.tsx | 4 ++ .../OrgGroupsSection/OrgGroupModal.tsx | 2 +- .../OrgGroupsSection/OrgGroupsSection.tsx | 6 +-- .../OrgGroupsSection/OrgGroupsTable.tsx | 11 ++--- 10 files changed, 99 insertions(+), 78 deletions(-) diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index d267564f2..11c5d6fba 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -10,7 +10,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { body: z.object({ name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name), @@ -43,12 +43,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:currentSlug", + url: "/:id", method: "PATCH", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ - currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug) + id: z.string().trim().describe(GROUPS.UPDATE.id) }), body: z .object({ @@ -70,7 +70,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const group = await server.services.group.updateGroup({ - currentSlug: req.params.currentSlug, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -83,12 +83,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:slug", + url: "/:id", method: "DELETE", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ - slug: z.string().trim().describe(GROUPS.DELETE.slug) + id: z.string().trim().describe(GROUPS.DELETE.id) }), response: { 200: GroupsSchema @@ -96,7 +96,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const group = await server.services.group.deleteGroup({ - groupSlug: req.params.slug, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -109,11 +109,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:slug/users", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/:id/users", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ - slug: z.string().trim().describe(GROUPS.LIST_USERS.slug) + id: z.string().trim().describe(GROUPS.LIST_USERS.id) }), querystring: z.object({ offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), @@ -141,24 +141,25 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { users, totalCount } = await server.services.group.listGroupUsers({ - groupSlug: req.params.slug, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.query }); + return { users, totalCount }; } }); server.route({ method: "POST", - url: "/:slug/users/:username", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/:id/users/:username", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ - slug: z.string().trim().describe(GROUPS.ADD_USER.slug), + id: z.string().trim().describe(GROUPS.ADD_USER.id), username: z.string().trim().describe(GROUPS.ADD_USER.username) }), response: { @@ -173,7 +174,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const user = await server.services.group.addUserToGroup({ - groupSlug: req.params.slug, + id: req.params.id, username: req.params.username, actor: req.permission.type, actorId: req.permission.id, @@ -187,11 +188,11 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:slug/users/:username", - onRequest: verifyAuth([AuthMode.JWT]), + url: "/:id/users/:username", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { params: z.object({ - slug: z.string().trim().describe(GROUPS.DELETE_USER.slug), + id: z.string().trim().describe(GROUPS.DELETE_USER.id), username: z.string().trim().describe(GROUPS.DELETE_USER.username) }), response: { @@ -206,7 +207,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const user = await server.services.group.removeUserFromGroup({ - groupSlug: req.params.slug, + id: req.params.id, username: req.params.username, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index e6a151bf7..05fafe180 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -95,7 +95,7 @@ export const groupServiceFactory = ({ }; const updateGroup = async ({ - currentSlug, + id, name, slug, role, @@ -121,8 +121,10 @@ export const groupServiceFactory = ({ message: "Failed to update group due to plan restrictio Upgrade plan to update group." }); - const group = await groupDAL.findOne({ orgId: actorOrgId, slug: currentSlug }); - if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` }); + const group = await groupDAL.findOne({ orgId: actorOrgId, id }); + if (!group) { + throw new BadRequestError({ message: `Failed to find group with ID ${id}` }); + } let customRole: TOrgRoles | undefined; if (role) { @@ -140,8 +142,7 @@ export const groupServiceFactory = ({ const [updatedGroup] = await groupDAL.update( { - orgId: actorOrgId, - slug: currentSlug + id: group.id }, { name, @@ -158,7 +159,7 @@ export const groupServiceFactory = ({ return updatedGroup; }; - const deleteGroup = async ({ groupSlug, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { + const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); const { permission } = await permissionService.getOrgPermission( @@ -178,15 +179,15 @@ export const groupServiceFactory = ({ }); const [group] = await groupDAL.delete({ - orgId: actorOrgId, - slug: groupSlug + id, + orgId: actorOrgId }); return group; }; const listGroupUsers = async ({ - groupSlug, + id, offset, limit, username, @@ -208,12 +209,12 @@ export const groupServiceFactory = ({ const group = await groupDAL.findOne({ orgId: actorOrgId, - slug: groupSlug + id }); if (!group) throw new BadRequestError({ - message: `Failed to find group with slug ${groupSlug}` + message: `Failed to find group with ID ${id}` }); const users = await groupDAL.findAllGroupMembers({ @@ -229,14 +230,7 @@ export const groupServiceFactory = ({ return { users, totalCount: count }; }; - const addUserToGroup = async ({ - groupSlug, - username, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TAddUserToGroupDTO) => { + const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new BadRequestError({ message: "Failed to create group without organization" }); const { permission } = await permissionService.getOrgPermission( @@ -251,12 +245,12 @@ export const groupServiceFactory = ({ // check if group with slug exists const group = await groupDAL.findOne({ orgId: actorOrgId, - slug: groupSlug + id }); if (!group) throw new BadRequestError({ - message: `Failed to find group with slug ${groupSlug}` + message: `Failed to find group with ID ${id}` }); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); @@ -285,7 +279,7 @@ export const groupServiceFactory = ({ }; const removeUserFromGroup = async ({ - groupSlug, + id, username, actor, actorId, @@ -306,12 +300,12 @@ export const groupServiceFactory = ({ // check if group with slug exists const group = await groupDAL.findOne({ orgId: actorOrgId, - slug: groupSlug + id }); if (!group) throw new BadRequestError({ - message: `Failed to find group with slug ${groupSlug}` + message: `Failed to find group with ID ${id}` }); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index ca9831ffb..125765a1c 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -17,7 +17,7 @@ export type TCreateGroupDTO = { } & TGenericPermission; export type TUpdateGroupDTO = { - currentSlug: string; + id: string; } & Partial<{ name: string; slug: string; @@ -26,23 +26,23 @@ export type TUpdateGroupDTO = { TGenericPermission; export type TDeleteGroupDTO = { - groupSlug: string; + id: string; } & TGenericPermission; export type TListGroupUsersDTO = { - groupSlug: string; + id: string; offset: number; limit: number; username?: string; } & TGenericPermission; export type TAddUserToGroupDTO = { - groupSlug: string; + id: string; username: string; } & TGenericPermission; export type TRemoveUserFromGroupDTO = { - groupSlug: string; + id: string; username: string; } & TGenericPermission; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5ed7ed8f2..9b64900a1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -5,26 +5,27 @@ export const GROUPS = { role: "The role of the group to create." }, UPDATE: { - currentSlug: "The current slug of the group to update.", + id: "The id of the group to update", name: "The new name of the group to update to.", slug: "The new slug of the group to update to.", role: "The new role of the group to update to." }, DELETE: { + id: "The id of the group to delete", slug: "The slug of the group to delete" }, LIST_USERS: { - slug: "The slug of the group to list users for", + id: "The id of the group to list users for", offset: "The offset to start from. If you enter 10, it will start from the 10th user.", limit: "The number of users to return.", username: "The username to search for." }, ADD_USER: { - slug: "The slug of the group to add the user to.", + id: "The id of the group to add the user to.", username: "The username of the user to add to the group." }, DELETE_USER: { - slug: "The slug of the group to remove the user from.", + id: "The id of the group to remove the user from.", username: "The username of the user to remove from the group." } } as const; diff --git a/frontend/src/hooks/api/groups/mutations.tsx b/frontend/src/hooks/api/groups/mutations.tsx index ad8d83597..445ae10bc 100644 --- a/frontend/src/hooks/api/groups/mutations.tsx +++ b/frontend/src/hooks/api/groups/mutations.tsx @@ -38,17 +38,17 @@ export const useUpdateGroup = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - currentSlug, + id, name, slug, role }: { - currentSlug: string; + id: string; name?: string; slug?: string; role?: string; }) => { - const { data: group } = await apiRequest.patch(`/api/v1/groups/${currentSlug}`, { + const { data: group } = await apiRequest.patch(`/api/v1/groups/${id}`, { name, slug, role @@ -65,8 +65,8 @@ export const useUpdateGroup = () => { export const useDeleteGroup = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ slug }: { slug: string }) => { - const { data: group } = await apiRequest.delete(`/api/v1/groups/${slug}`); + mutationFn: async ({ id }: { id: string }) => { + const { data: group } = await apiRequest.delete(`/api/v1/groups/${id}`); return group; }, @@ -79,8 +79,15 @@ export const useDeleteGroup = () => { export const useAddUserToGroup = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ slug, username }: { slug: string; username: string }) => { - const { data } = await apiRequest.post(`/api/v1/groups/${slug}/users/${username}`); + mutationFn: async ({ + groupId, + username + }: { + groupId: string; + username: string; + slug: string; + }) => { + const { data } = await apiRequest.post(`/api/v1/groups/${groupId}/users/${username}`); return data; }, @@ -93,8 +100,17 @@ export const useAddUserToGroup = () => { export const useRemoveUserFromGroup = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ slug, username }: { slug: string; username: string }) => { - const { data } = await apiRequest.delete(`/api/v1/groups/${slug}/users/${username}`); + mutationFn: async ({ + username, + groupId + }: { + slug: string; + username: string; + groupId: string; + }) => { + const { data } = await apiRequest.delete( + `/api/v1/groups/${groupId}/users/${username}` + ); return data; }, diff --git a/frontend/src/hooks/api/groups/queries.tsx b/frontend/src/hooks/api/groups/queries.tsx index ba0543135..9012d2dcf 100644 --- a/frontend/src/hooks/api/groups/queries.tsx +++ b/frontend/src/hooks/api/groups/queries.tsx @@ -4,7 +4,8 @@ import { apiRequest } from "@app/config/request"; export const groupKeys = { allGroupUserMemberships: () => ["group-user-memberships"] as const, - forGroupUserMemberships: (slug: string) => [...groupKeys.allGroupUserMemberships(), slug] as const, + forGroupUserMemberships: (slug: string) => + [...groupKeys.allGroupUserMemberships(), slug] as const, specificGroupUserMemberships: ({ slug, offset, @@ -28,11 +29,13 @@ type TUser = { }; export const useListGroupUsers = ({ + id, groupSlug, offset = 0, limit = 10, username }: { + id: string; groupSlug: string; offset: number; limit: number; @@ -52,14 +55,15 @@ export const useListGroupUsers = ({ limit: String(limit), username }); - - const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number; }>( - `/api/v1/groups/${groupSlug}/users`, { + + const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number }>( + `/api/v1/groups/${id}/users`, + { params } ); - + return data; - }, + } }); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx index d6b1c515f..78b805df9 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx @@ -35,10 +35,12 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => { const [searchMemberFilter, setSearchMemberFilter] = useState(""); const popUpData = popUp?.groupMembers?.data as { + groupId: string; slug: string; }; const { data, isLoading } = useListGroupUsers({ + id: popUpData?.groupId, groupSlug: popUpData?.slug, offset: (page - 1) * perPage, limit: perPage, @@ -54,11 +56,13 @@ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => { if (assign) { await assignMutateAsync({ + groupId: popUpData.groupId, username, slug: popUpData.slug }); } else { await unassignMutateAsync({ + groupId: popUpData.groupId, username, slug: popUpData.slug }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx index 85a5c0c23..4ea4516de 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupModal.tsx @@ -85,7 +85,7 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr if (group) { await updateMutateAsync({ - currentSlug: group.slug, + id: group.groupId, name, slug, role: role || undefined diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 2df78fe5f..7738d1353 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -34,10 +34,10 @@ export const OrgGroupsSection = () => { } }; - const onDeleteGroupSubmit = async ({ name, slug }: { name: string; slug: string }) => { + const onDeleteGroupSubmit = async ({ name, groupId }: { name: string; groupId: string }) => { try { await deleteMutateAsync({ - slug + id: groupId }); createNotification({ text: `Successfully deleted the group named ${name}`, @@ -87,7 +87,7 @@ export const OrgGroupsSection = () => { onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; slug: string }) + onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; groupId: string }) } /> { const { data: roles } = useGetOrgRoles(orgId); - const handleChangeRole = async ({ currentSlug, role }: { currentSlug: string; role: string }) => { + const handleChangeRole = async ({ id, role }: { id: string; role: string }) => { try { await updateMutateAsync({ - currentSlug, + id, role }); @@ -112,7 +112,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" onValueChange={(selectedRole) => handleChangeRole({ - currentSlug: slug, + id, role: selectedRole }) } @@ -147,6 +147,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("groupMembers", { + groupId: id, slug }); }} @@ -195,7 +196,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("deleteGroup", { - slug, + groupId: id, name }); }} From 4eeea0b27c55e34af9a6f6468657b5b0a3189d06 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 20 Sep 2024 14:05:22 +0800 Subject: [PATCH 09/14] misc: added endpoint for fetching group details by ID --- backend/src/ee/routes/v1/group-router.ts | 25 +++++++++++++++ .../src/ee/services/group/group-service.ts | 32 +++++++++++++++++-- backend/src/ee/services/group/group-types.ts | 4 +++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index 11c5d6fba..d606aedd0 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -42,6 +42,31 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + url: "/:id", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: GroupsSchema + } + }, + handler: async (req) => { + const group = await server.services.group.getGroupById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + + return group; + } + }); + server.route({ url: "/:id", method: "PATCH", diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 05fafe180..48942c52a 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; -import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -21,6 +21,7 @@ import { TAddUserToGroupDTO, TCreateGroupDTO, TDeleteGroupDTO, + TGetGroupByIdDTO, TListGroupUsersDTO, TRemoveUserFromGroupDTO, TUpdateGroupDTO @@ -29,7 +30,7 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; type TGroupServiceFactoryDep = { userDAL: Pick; - groupDAL: Pick; + groupDAL: Pick; groupProjectDAL: Pick; orgDAL: Pick; userGroupMembershipDAL: Pick< @@ -186,6 +187,30 @@ export const groupServiceFactory = ({ return group; }; + const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { + if (!actorOrgId) { + throw new BadRequestError({ message: "Failed to read group without organization" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + + const group = await groupDAL.findById(id); + if (!group) { + throw new NotFoundError({ + message: `Cannot find group with ID ${id}` + }); + } + + return group; + }; + const listGroupUsers = async ({ id, offset, @@ -336,6 +361,7 @@ export const groupServiceFactory = ({ deleteGroup, listGroupUsers, addUserToGroup, - removeUserFromGroup + removeUserFromGroup, + getGroupById }; }; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 125765a1c..a6c80ef43 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -29,6 +29,10 @@ export type TDeleteGroupDTO = { id: string; } & TGenericPermission; +export type TGetGroupByIdDTO = { + id: string; +} & TGenericPermission; + export type TListGroupUsersDTO = { id: string; offset: number; From 02085ce9022081568afd486b94a42aff25dd2c29 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 20 Sep 2024 14:45:43 +0800 Subject: [PATCH 10/14] fix: addressed overlooked update --- .../components/UserProjectsSection/UserGroupsRow.tsx | 1 + .../components/UserProjectsSection/UserGroupsSection.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx index 620fe5b7e..3287cabae 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx @@ -30,6 +30,7 @@ export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("removeUserFromGroup", { + groupId: group.id, groupSlug: group.slug }); }} diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx index 2967348a2..3947c6ddc 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsSection.tsx @@ -19,9 +19,10 @@ export const UserGroupsSection = ({ orgMembership }: Props) => { const { mutateAsync: removeUserFromGroup } = useRemoveUserFromGroup(); - const handleRemoveUserFromGroup = useCallback(async (groupSlug: string) => { + const handleRemoveUserFromGroup = useCallback(async (groupId: string, groupSlug: string) => { try { await removeUserFromGroup({ + groupId, slug: groupSlug, username: orgMembership.user.username }); @@ -57,10 +58,11 @@ export const UserGroupsSection = ({ orgMembership }: Props) => { deleteKey="confirm" onDeleteApproved={() => { const popupData = popUp?.removeUserFromGroup?.data as { + groupId: string; groupSlug: string; }; - return handleRemoveUserFromGroup(popupData.groupSlug); + return handleRemoveUserFromGroup(popupData.groupId, popupData.groupSlug); }} /> From f72cedae10c6c7398f39d05a85f31689853f6983 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 20 Sep 2024 16:24:22 +0800 Subject: [PATCH 11/14] misc: added groups endpoint --- backend/src/ee/routes/v1/group-router.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index d606aedd0..fcfc6051c 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -67,6 +67,28 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + url: "/", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + response: { + 200: GroupsSchema.array() + } + }, + handler: async (req) => { + const groups = await server.services.org.getOrgGroups({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return groups; + } + }); + server.route({ url: "/:id", method: "PATCH", From 8d71b295eabfaeafb576f58a63aa165fbd78505e Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 20 Sep 2024 17:24:46 +0800 Subject: [PATCH 12/14] misc: add copy group ID to clipboard --- .../components/OrgGroupsSection/OrgGroupsTable.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 0ed41ba6e..e7d53d1ca 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -135,6 +135,18 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { + { + e.stopPropagation(); + createNotification({ + text: "Copied group ID to clipboard", + type: "info" + }); + navigator.clipboard.writeText(id); + }} + > + Copy Group ID + Date: Sun, 22 Sep 2024 19:46:36 +0800 Subject: [PATCH 13/14] misc: moved to group project v3 for get with ID based --- .../server/routes/v2/group-project-router.ts | 57 ---------------- .../server/routes/v3/group-project-router.ts | 65 +++++++++++++++++++ backend/src/server/routes/v3/index.ts | 9 ++- .../group-project/group-project-dal.ts | 6 +- .../group-project/group-project-service.ts | 14 ++-- .../group-project/group-project-types.ts | 4 +- 6 files changed, 84 insertions(+), 71 deletions(-) create mode 100644 backend/src/server/routes/v3/group-project-router.ts diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 69f309e55..77649049d 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -231,61 +231,4 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => return { groupMemberships }; } }); - - server.route({ - method: "GET", - url: "/:projectSlug/groups/:groupSlug", - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - config: { - rateLimit: readLimit - }, - schema: { - description: "Return project group", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - projectSlug: z.string().trim(), - groupSlug: z.string().trim() - }), - response: { - 200: z.object({ - groupMembership: z.object({ - id: z.string(), - groupId: z.string(), - createdAt: z.date(), - updatedAt: z.date(), - roles: z.array( - z.object({ - id: z.string(), - role: z.string(), - customRoleId: z.string().optional().nullable(), - customRoleName: z.string().optional().nullable(), - customRoleSlug: z.string().optional().nullable(), - isTemporary: z.boolean(), - temporaryMode: z.string().optional().nullable(), - temporaryRange: z.string().nullable().optional(), - temporaryAccessStartTime: z.date().nullable().optional(), - temporaryAccessEndTime: z.date().nullable().optional() - }) - ), - group: GroupsSchema.pick({ name: true, id: true, slug: true }) - }) - }) - } - }, - handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params - }); - - return { groupMembership }; - } - }); }; diff --git a/backend/src/server/routes/v3/group-project-router.ts b/backend/src/server/routes/v3/group-project-router.ts new file mode 100644 index 000000000..8e4e7dffe --- /dev/null +++ b/backend/src/server/routes/v3/group-project-router.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +import { GroupsSchema } from "@app/db/schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + description: "Return project group", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim(), + groupId: z.string().trim() + }), + response: { + 200: z.object({ + groupMembership: z.object({ + id: z.string(), + groupId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + group: GroupsSchema.pick({ name: true, id: true, slug: true }) + }) + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.getGroupInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { groupMembership }; + } + }); +}; diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 6a061a635..10f3c9330 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,3 +1,4 @@ +import { registerGroupProjectRouter } from "./group-project-router"; import { registerLoginRouter } from "./login-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretRouter } from "./secret-router"; @@ -9,5 +10,11 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerLoginRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); - await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); + await server.register( + async (projectServer) => { + await projectServer.register(registerSecretBlindIndexRouter); + await projectServer.register(registerGroupProjectRouter); + }, + { prefix: "/workspaces" } + ); }; diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index 05724df74..bbcadb96e 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -10,13 +10,13 @@ export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); - const findByProjectId = async (projectId: string, filter?: { groupSlug?: string }, tx?: Knex) => { + const findByProjectId = async (projectId: string, filter?: { groupId?: string }, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) .where(`${TableName.GroupProjectMembership}.projectId`, projectId) .where((qb) => { - if (filter?.groupSlug) { - void qb.where(`${TableName.Groups}.slug`, "=", filter.groupSlug); + if (filter?.groupId) { + void qb.where(`${TableName.Groups}.id`, "=", filter.groupId); } }) .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index fe9e68c55..90fd5a785 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -34,7 +34,7 @@ type TGroupProjectServiceFactoryDep = { "create" | "transaction" | "insertMany" | "delete" >; userGroupMembershipDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; @@ -403,19 +403,17 @@ export const groupProjectServiceFactory = ({ }; const getGroupInProject = async ({ - projectSlug, actor, actorId, actorAuthMethod, actorOrgId, - groupSlug + groupId, + projectId }: TGetGroupInProjectDTO) => { - const project = await projectDAL.findOne({ - slug: projectSlug - }); + const project = await projectDAL.findById(projectId); if (!project) { - throw new NotFoundError({ message: `Failed to find project with slug ${projectSlug}` }); + throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); } const { permission } = await permissionService.getProjectPermission( @@ -428,7 +426,7 @@ export const groupProjectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { - groupSlug + groupId }); if (!groupMembership) { diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts index 20827ce52..2b596365c 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -1,4 +1,4 @@ -import { TProjectSlugPermission } from "@app/lib/types"; +import { TProjectPermission, TProjectSlugPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; @@ -42,4 +42,4 @@ export type TDeleteProjectGroupDTO = { export type TListProjectGroupDTO = TProjectSlugPermission; -export type TGetGroupInProjectDTO = TProjectSlugPermission & { groupSlug: string }; +export type TGetGroupInProjectDTO = TProjectPermission & { groupId: string }; From 839f0c7e1c0a7e0698da7a9d5d8b802898e282c2 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 23 Sep 2024 17:59:10 +0800 Subject: [PATCH 14/14] misc: moved the rest of project group methods to IDs --- backend/src/lib/api-docs/constants.ts | 14 +-- .../server/routes/v2/group-project-router.ts | 98 +++++++++++++++---- .../server/routes/v3/group-project-router.ts | 65 ------------ backend/src/server/routes/v3/index.ts | 9 +- .../group-project/group-project-service.ts | 60 +++++------- .../group-project/group-project-types.ts | 16 +-- .../src/hooks/api/workspace/mutations.tsx | 37 +++---- frontend/src/hooks/api/workspace/queries.tsx | 6 +- frontend/src/hooks/api/workspace/types.ts | 4 +- .../components/GroupsSection/GroupModal.tsx | 32 +++--- .../components/GroupsSection/GroupRoles.tsx | 8 +- .../GroupsSection/GroupsSection.tsx | 8 +- .../components/GroupsSection/GroupsTable.tsx | 10 +- 13 files changed, 175 insertions(+), 192 deletions(-) delete mode 100644 backend/src/server/routes/v3/group-project-router.ts diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a8f984df4..c3261a631 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -410,21 +410,21 @@ export const PROJECTS = { secretSnapshotId: "The ID of the snapshot to rollback to." }, ADD_GROUP_TO_PROJECT: { - projectSlug: "The slug of the project to add the group to.", - groupSlug: "The slug of the group to add to the project.", + projectId: "The ID of the project to add the group to.", + groupId: "The ID of the group to add to the project.", role: "The role for the group to assume in the project." }, UPDATE_GROUP_IN_PROJECT: { - projectSlug: "The slug of the project to update the group in.", - groupSlug: "The slug of the group to update in the project.", + projectId: "The ID of the project to update the group in.", + groupId: "The ID of the group to update in the project.", roles: "A list of roles to update the group to." }, REMOVE_GROUP_FROM_PROJECT: { - projectSlug: "The slug of the project to delete the group from.", - groupSlug: "The slug of the group to delete from the project." + projectId: "The ID of the project to delete the group from.", + groupId: "The ID of the group to delete from the project." }, LIST_GROUPS_IN_PROJECT: { - projectSlug: "The slug of the project to list groups for." + projectId: "The ID of the project to list groups for." }, LIST_INTEGRATION: { workspaceId: "The ID of the project to list integrations for." diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 77649049d..cbc54f5ac 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -16,7 +16,7 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:projectSlug/groups/:groupSlug", + url: "/:projectId/groups/:groupId", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), config: { rateLimit: writeLimit @@ -29,8 +29,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug), - groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug) + projectId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectId), + groupId: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupId) }), body: z .object({ @@ -74,9 +74,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - groupSlug: req.params.groupSlug, - projectSlug: req.params.projectSlug, - roles: req.body.roles || [{ role: req.body.role }] + roles: req.body.roles || [{ role: req.body.role }], + projectId: req.params.projectId, + groupId: req.params.groupId }); return { groupMembership }; @@ -85,7 +85,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => server.route({ method: "PATCH", - url: "/:projectSlug/groups/:groupSlug", + url: "/:projectId/groups/:groupId", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { description: "Update group in project", @@ -95,8 +95,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug), - groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug) + projectId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectId), + groupId: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupId) }), body: z.object({ roles: z @@ -130,17 +130,18 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - groupSlug: req.params.groupSlug, - projectSlug: req.params.projectSlug, + projectId: req.params.projectId, + groupId: req.params.groupId, roles: req.body.roles }); + return { roles }; } }); server.route({ method: "DELETE", - url: "/:projectSlug/groups/:groupSlug", + url: "/:projectId/groups/:groupId", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), config: { rateLimit: writeLimit @@ -153,8 +154,8 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug), - groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug) + projectId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectId), + groupId: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupId) }), response: { 200: z.object({ @@ -168,16 +169,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - groupSlug: req.params.groupSlug, - projectSlug: req.params.projectSlug + groupId: req.params.groupId, + projectId: req.params.projectId }); + return { groupMembership }; } }); server.route({ method: "GET", - url: "/:projectSlug/groups", + url: "/:projectId/groups", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), config: { rateLimit: readLimit @@ -190,7 +192,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } ], params: z.object({ - projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug) + projectId: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectId) }), response: { 200: z.object({ @@ -226,9 +228,67 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectSlug: req.params.projectSlug + projectId: req.params.projectId }); + return { groupMemberships }; } }); + + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, + schema: { + description: "Return project group", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim(), + groupId: z.string().trim() + }), + response: { + 200: z.object({ + groupMembership: z.object({ + id: z.string(), + groupId: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().optional().nullable(), + customRoleName: z.string().optional().nullable(), + customRoleSlug: z.string().optional().nullable(), + isTemporary: z.boolean(), + temporaryMode: z.string().optional().nullable(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional() + }) + ), + group: GroupsSchema.pick({ name: true, id: true, slug: true }) + }) + }) + } + }, + handler: async (req) => { + const groupMembership = await server.services.groupProject.getGroupInProject({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.params + }); + + return { groupMembership }; + } + }); }; diff --git a/backend/src/server/routes/v3/group-project-router.ts b/backend/src/server/routes/v3/group-project-router.ts deleted file mode 100644 index 8e4e7dffe..000000000 --- a/backend/src/server/routes/v3/group-project-router.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { z } from "zod"; - -import { GroupsSchema } from "@app/db/schemas"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "GET", - url: "/:projectId/groups/:groupId", - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - config: { - rateLimit: readLimit - }, - schema: { - description: "Return project group", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - projectId: z.string().trim(), - groupId: z.string().trim() - }), - response: { - 200: z.object({ - groupMembership: z.object({ - id: z.string(), - groupId: z.string(), - createdAt: z.date(), - updatedAt: z.date(), - roles: z.array( - z.object({ - id: z.string(), - role: z.string(), - customRoleId: z.string().optional().nullable(), - customRoleName: z.string().optional().nullable(), - customRoleSlug: z.string().optional().nullable(), - isTemporary: z.boolean(), - temporaryMode: z.string().optional().nullable(), - temporaryRange: z.string().nullable().optional(), - temporaryAccessStartTime: z.date().nullable().optional(), - temporaryAccessEndTime: z.date().nullable().optional() - }) - ), - group: GroupsSchema.pick({ name: true, id: true, slug: true }) - }) - }) - } - }, - handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params - }); - - return { groupMembership }; - } - }); -}; diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 6a9bbf664..f77c25c73 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,5 +1,4 @@ import { registerDashboardRouter } from "./dashboard-router"; -import { registerGroupProjectRouter } from "./group-project-router"; import { registerLoginRouter } from "./login-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretRouter } from "./secret-router"; @@ -11,12 +10,6 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerLoginRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); - await server.register( - async (projectServer) => { - await projectServer.register(registerSecretBlindIndexRouter); - await projectServer.register(registerGroupProjectRouter); - }, - { prefix: "/workspaces" } - ); + await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" }); }; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 90fd5a785..b204d3b48 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -56,19 +56,17 @@ export const groupProjectServiceFactory = ({ permissionService }: TGroupProjectServiceFactoryDep) => { const addGroupToProject = async ({ - groupSlug, actor, actorId, actorOrgId, actorAuthMethod, - projectSlug, - roles + roles, + projectId, + groupId }: TCreateProjectGroupDTO) => { - const project = await projectDAL.findOne({ - slug: projectSlug - }); + const project = await projectDAL.findById(projectId); - if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` }); if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); const { permission } = await permissionService.getProjectPermission( @@ -80,13 +78,13 @@ export const groupProjectServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups); - const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); - if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); + if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` }); const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (existingGroup) throw new BadRequestError({ - message: `Group with slug ${groupSlug} already exists in project with id ${project.id}` + message: `Group with ID ${groupId} already exists in project with id ${project.id}` }); for await (const { role: requestedRoleChange } of roles) { @@ -227,19 +225,17 @@ export const groupProjectServiceFactory = ({ }; const updateGroupInProject = async ({ - projectSlug, - groupSlug, + projectId, + groupId, roles, actor, actorId, actorAuthMethod, actorOrgId }: TUpdateProjectGroupDTO) => { - const project = await projectDAL.findOne({ - slug: projectSlug - }); + const project = await projectDAL.findById(projectId); - if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` }); const { permission } = await permissionService.getProjectPermission( actor, @@ -250,11 +246,11 @@ export const groupProjectServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups); - const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); - if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); + if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` }); const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` }); for await (const { role: requestedRoleChange } of roles) { const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( @@ -326,24 +322,22 @@ export const groupProjectServiceFactory = ({ }; const removeGroupFromProject = async ({ - projectSlug, - groupSlug, + projectId, + groupId, actorId, actor, actorOrgId, actorAuthMethod }: TDeleteProjectGroupDTO) => { - const project = await projectDAL.findOne({ - slug: projectSlug - }); + const project = await projectDAL.findById(projectId); - if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + if (!project) throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` }); - const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); - if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); + if (!group) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` }); const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); + if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with ID ${groupId}` }); const { permission } = await permissionService.getProjectPermission( actor, @@ -377,17 +371,17 @@ export const groupProjectServiceFactory = ({ }; const listGroupsInProject = async ({ - projectSlug, + projectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectGroupDTO) => { - const project = await projectDAL.findOne({ - slug: projectSlug - }); + const project = await projectDAL.findById(projectId); - if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + if (!project) { + throw new BadRequestError({ message: `Failed to find project with ID ${projectId}` }); + } const { permission } = await permissionService.getProjectPermission( actor, diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts index 2b596365c..1e1794963 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -1,9 +1,9 @@ -import { TProjectPermission, TProjectSlugPermission } from "@app/lib/types"; +import { TProjectPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; export type TCreateProjectGroupDTO = { - groupSlug: string; + groupId: string; roles: ( | { role: string; @@ -17,7 +17,7 @@ export type TCreateProjectGroupDTO = { temporaryAccessStartTime: string; } )[]; -} & TProjectSlugPermission; +} & TProjectPermission; export type TUpdateProjectGroupDTO = { roles: ( @@ -33,13 +33,13 @@ export type TUpdateProjectGroupDTO = { temporaryAccessStartTime: string; } )[]; - groupSlug: string; -} & TProjectSlugPermission; + groupId: string; +} & TProjectPermission; export type TDeleteProjectGroupDTO = { - groupSlug: string; -} & TProjectSlugPermission; + groupId: string; +} & TProjectPermission; -export type TListProjectGroupDTO = TProjectSlugPermission; +export type TListProjectGroupDTO = TProjectPermission; export type TGetGroupInProjectDTO = TProjectPermission & { groupId: string }; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index a09d47e15..ae8829591 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -10,23 +10,24 @@ export const useAddGroupToWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - groupSlug, - projectSlug, + groupId, + projectId, role }: { - groupSlug: string; - projectSlug: string; + groupId: string; + projectId: string; role?: string; }) => { const { data: { groupMembership } - } = await apiRequest.post(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, { + } = await apiRequest.post(`/api/v2/workspace/${projectId}/groups/${groupId}`, { role }); + return groupMembership; }, - onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectId)); } }); }; @@ -34,17 +35,17 @@ export const useAddGroupToWorkspace = () => { export const useUpdateGroupWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ groupSlug, projectSlug, roles }: TUpdateWorkspaceGroupRoleDTO) => { + mutationFn: async ({ groupId, projectId, roles }: TUpdateWorkspaceGroupRoleDTO) => { const { data: { groupMembership } - } = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, { + } = await apiRequest.patch(`/api/v2/workspace/${projectId}/groups/${groupId}`, { roles }); return groupMembership; }, - onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectId)); } }); }; @@ -53,20 +54,20 @@ export const useDeleteGroupFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - groupSlug, - projectSlug + groupId, + projectId }: { - groupSlug: string; - projectSlug: string; + groupId: string; + projectId: string; username?: string; }) => { const { data: { groupMembership } - } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/groups/${groupSlug}`); + } = await apiRequest.delete(`/api/v2/workspace/${projectId}/groups/${groupId}`); return groupMembership; }, - onSuccess: (_, { projectSlug, username }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); + onSuccess: (_, { projectId, username }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectId)); if (username) { queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username)); diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index c5d13b415..ecefe7513 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -535,14 +535,14 @@ export const useGetWorkspaceIdentityMemberships = ( }); }; -export const useListWorkspaceGroups = (projectSlug: string) => { +export const useListWorkspaceGroups = (projectId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectSlug), + queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), queryFn: async () => { const { data: { groupMemberships } } = await apiRequest.get<{ groupMemberships: TGroupMembership[] }>( - `/api/v2/workspace/${projectSlug}/groups` + `/api/v2/workspace/${projectId}/groups` ); return groupMemberships; }, diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 3af4d1e16..aa57a6fd4 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -127,8 +127,8 @@ export type TUpdateWorkspaceIdentityRoleDTO = { }; export type TUpdateWorkspaceGroupRoleDTO = { - groupSlug: string; - projectSlug: string; + groupId: string; + projectId: string; roles: ( | { role: string; diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index 3fe9bc8e7..b66424509 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -16,7 +16,7 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - slug: z.string(), + id: z.string(), role: z.string() }); @@ -35,7 +35,7 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { const projectSlug = currentWorkspace?.slug || ""; const { data: groups } = useGetOrganizationGroups(orgId); - const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || ""); + const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.id || ""); const { data: roles } = useGetProjectRoles(projectSlug); @@ -60,11 +60,11 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { resolver: zodResolver(schema) }); - const onFormSubmit = async ({ slug, role }: FormData) => { + const onFormSubmit = async ({ id, role }: FormData) => { try { await addGroupToWorkspaceMutateAsync({ - projectSlug: currentWorkspace?.slug || "", - groupSlug: slug, + projectId: currentWorkspace?.id || "", + groupId: id, role: role || undefined }); @@ -96,7 +96,7 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
( @@ -107,8 +107,8 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { className="w-full border border-mineshaft-600" placeholder="Select group..." > - {filteredGroupMembershipOrgs.map(({ name, slug, id }) => ( - + {filteredGroupMembershipOrgs.map(({ name, id }) => ( + {name} ))} @@ -143,7 +143,7 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> -
+
- +
) : ( diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 449c0c95f..5563624ce 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -195,13 +195,13 @@ type TForm = z.infer; export type TMemberRolesProp = { disableEdit?: boolean; - groupSlug: string; + groupId: string; roles: TGroupMembership["roles"]; }; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; -export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => { +export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMemberRolesProp) => { const { currentWorkspace } = useWorkspace(); const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); const [searchRoles, setSearchRoles] = useState(""); @@ -248,8 +248,8 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemb try { await updateGroupWorkspaceRole.mutateAsync({ - projectSlug: currentWorkspace?.slug || "", - groupSlug, + projectId: currentWorkspace?.id || "", + groupId, roles: selectedRoles }); createNotification({ text: "Successfully updated group role", type: "success" }); diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx index 612a83b71..0ab7816fe 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx @@ -39,11 +39,11 @@ export const GroupsSection = () => { } }; - const onRemoveGroupSubmit = async (groupSlug: string) => { + const onRemoveGroupSubmit = async (groupId: string) => { try { await deleteMutateAsync({ - groupSlug, - projectSlug: currentWorkspace?.slug || "" + groupId, + projectId: currentWorkspace?.id || "" }); createNotification({ @@ -92,7 +92,7 @@ export const GroupsSection = () => { onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onRemoveGroupSubmit((popUp?.deleteGroup?.data as { slug: string })?.slug) + onRemoveGroupSubmit((popUp?.deleteGroup?.data as { id: string })?.id) } /> , data?: { - slug?: string; + id?: string; name?: string; } ) => void; @@ -34,7 +34,7 @@ type Props = { export const GroupTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.slug || ""); + const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.id || ""); return ( @@ -51,7 +51,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { {!isLoading && data && data.length > 0 && - data.map(({ group: { id, name, slug }, roles, createdAt }) => { + data.map(({ group: { id, name }, roles, createdAt }) => { return ( @@ -61,7 +61,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { a={ProjectPermissionSub.Groups} > {(isAllowed) => ( - + )} @@ -77,7 +77,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { { handlePopUpOpen("deleteGroup", { - slug, + id, name }); }}
{name}