diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index d267564f2c..fcfc6051c6 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,59 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:currentSlug", + 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: "/", + 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", - 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 +117,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 +130,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 +143,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 +156,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 +188,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 +221,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 +235,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 +254,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 e6a151bf75..48942c52a0 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< @@ -95,7 +96,7 @@ export const groupServiceFactory = ({ }; const updateGroup = async ({ - currentSlug, + id, name, slug, role, @@ -121,8 +122,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 +143,7 @@ export const groupServiceFactory = ({ const [updatedGroup] = await groupDAL.update( { - orgId: actorOrgId, - slug: currentSlug + id: group.id }, { name, @@ -158,7 +160,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 +180,39 @@ export const groupServiceFactory = ({ }); const [group] = await groupDAL.delete({ - orgId: actorOrgId, - slug: groupSlug + id, + orgId: actorOrgId }); 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 ({ - groupSlug, + id, offset, limit, username, @@ -208,12 +234,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 +255,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 +270,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 +304,7 @@ export const groupServiceFactory = ({ }; const removeUserFromGroup = async ({ - groupSlug, + id, username, actor, actorId, @@ -306,12 +325,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); @@ -342,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 ca9831ffbb..a6c80ef438 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,27 @@ export type TUpdateGroupDTO = { TGenericPermission; export type TDeleteGroupDTO = { - groupSlug: string; + id: string; +} & TGenericPermission; + +export type TGetGroupByIdDTO = { + 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/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index d96b8a9781..cb1e84677e 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -346,7 +346,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/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index bf1f7fd819..c3261a6317 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; @@ -409,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 6d438c1ff0..cbc54f5ace 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"; @@ -15,8 +16,11 @@ 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 + }, schema: { description: "Add group to project", security: [ @@ -25,17 +29,39 @@ 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) - }), - body: z.object({ - role: z - .string() - .trim() - .min(1) - .default(ProjectMembershipRole.NoAccess) - .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role) + 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({ + 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 @@ -48,17 +74,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, - role: req.body.role + roles: req.body.roles || [{ role: req.body.role }], + projectId: req.params.projectId, + groupId: req.params.groupId }); + return { groupMembership }; } }); 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", @@ -68,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 @@ -103,18 +130,22 @@ 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 + }, schema: { description: "Remove group from project", security: [ @@ -123,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({ @@ -138,17 +169,21 @@ 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 + }, schema: { description: "Return list of groups in project", security: [ @@ -157,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({ @@ -193,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/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index ca22d3f78c..dbe43c30a4 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -10,10 +10,15 @@ export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); - const findByProjectId = async (projectId: 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?.groupId) { + void qb.where(`${TableName.Groups}.id`, "=", filter.groupId); + } + }) .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) .join( TableName.GroupProjectMembershipRole, diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 17862dd6f6..b204d3b480 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,6 +22,7 @@ import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membershi import { TCreateProjectGroupDTO, TDeleteProjectGroupDTO, + TGetGroupInProjectDTO, TListProjectGroupDTO, TUpdateProjectGroupDTO } from "./group-project-types"; @@ -33,7 +34,7 @@ type TGroupProjectServiceFactoryDep = { "create" | "transaction" | "insertMany" | "delete" >; userGroupMembershipDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; @@ -55,19 +56,17 @@ export const groupProjectServiceFactory = ({ permissionService }: TGroupProjectServiceFactoryDep) => { const addGroupToProject = async ({ - groupSlug, actor, actorId, actorOrgId, actorAuthMethod, - projectSlug, - role + 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( @@ -79,25 +78,51 @@ 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}` }); - 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 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) ); - const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPrivilege) - throw new ForbiddenRequestError({ - message: "Failed to add group to project with more privileged role" + 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) { + 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 isCustomRole = Boolean(customRole); + } + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const projectGroup = await groupProjectDAL.transaction(async (tx) => { const groupProjectMembership = await groupProjectDAL.create( @@ -108,14 +133,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 @@ -183,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, @@ -206,11 +246,24 @@ 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( + 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( @@ -223,7 +276,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); @@ -260,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, @@ -311,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, @@ -336,10 +396,47 @@ export const groupProjectServiceFactory = ({ return groupMemberships; }; + const getGroupInProject = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + groupId, + projectId + }: TGetGroupInProjectDTO) => { + const project = await projectDAL.findById(projectId); + + if (!project) { + throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + + const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { + groupId + }); + + if (!groupMembership) { + throw new NotFoundError({ + message: "Cannot find group membership" + }); + } + + 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 c867b75c04..1e17949637 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -1,11 +1,23 @@ -import { TProjectSlugPermission } from "@app/lib/types"; +import { TProjectPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; export type TCreateProjectGroupDTO = { - groupSlug: string; - role: string; -} & TProjectSlugPermission; + groupId: string; + roles: ( + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: ProjectUserMembershipTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } + )[]; +} & TProjectPermission; export type TUpdateProjectGroupDTO = { roles: ( @@ -21,11 +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 = TProjectPermission; -export type TListProjectGroupDTO = TProjectSlugPermission; +export type TGetGroupInProjectDTO = TProjectPermission & { groupId: string }; diff --git a/frontend/src/hooks/api/groups/mutations.tsx b/frontend/src/hooks/api/groups/mutations.tsx index ad8d835977..445ae10bc6 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 ba05431359..9012d2dcf5 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/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index a09d47e152..ae88295910 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 c5d13b415c..ecefe7513c 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 3af4d1e160..aa57a6fd48 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/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupMembersModal.tsx index d6b1c515f7..78b805df96 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 85a5c0c239..4ea4516ded 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 2df78fe5f5..7738d1353d 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 }) } @@ -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 + { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("groupMembers", { + groupId: id, slug }); }} @@ -195,7 +208,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("deleteGroup", { - slug, + groupId: id, name }); }} diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserGroupsRow.tsx index 620fe5b7ef..3287cabae4 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 2967348a24..3947c6ddcb 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); }} /> 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 3fe9bc8e75..b66424509d 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 449c0c95fd..5563624ce1 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 612a83b71e..0ab7816fed 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}