From 84724e5f65d65d099e5d4686b75568440543418f Mon Sep 17 00:00:00 2001 From: = Date: Fri, 17 May 2024 20:45:31 +0530 Subject: [PATCH] feat: added endpoints to fetch a particule project user membership and identity --- backend/src/lib/api-docs/constants.ts | 8 +++ .../routes/v1/project-membership-router.ts | 60 +++++++++++++++++++ .../routes/v2/identity-project-router.ts | 57 ++++++++++++++++++ .../identity-project/identity-project-dal.ts | 7 ++- .../identity-project-service.ts | 26 +++++++- .../identity-project-types.ts | 4 ++ .../project-membership-dal.ts | 10 +++- .../project-membership-service.ts | 24 ++++++++ .../project-membership-types.ts | 4 ++ 9 files changed, 197 insertions(+), 3 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0638034de..51f7a822e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -197,6 +197,10 @@ export const PROJECT_USERS = { GET_USER_MEMBERSHIPS: { workspaceId: "The ID of the project to get memberships from." }, + GET_USER_MEMBERSHIP: { + workspaceId: "The ID of the project to get memberships from.", + username: "The username to get project membership of. Email is the default username." + }, UPDATE_USER_MEMBERSHIP: { workspaceId: "The ID of the project to update the membership for.", membershipId: "The ID of the membership to update.", @@ -208,6 +212,10 @@ export const PROJECT_IDENTITIES = { LIST_IDENTITY_MEMBERSHIPS: { projectId: "The ID of the project to get identity memberships from." }, + GET_IDENTITY_MEMBERSHIP_BY_ID: { + identityId: "The ID of the identity to get the membership for.", + projectId: "The ID of the project to get the identity membership for." + }, UPDATE_IDENTITY_MEMBERSHIP: { projectId: "The ID of the project to update the identity membership for.", identityId: "The ID of the identity to update the membership for.", diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 001775d96..6bbb8d7ef 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -74,6 +74,66 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider } }); + server.route({ + method: "POST", + url: "/:workspaceId/memberships/details", + config: { + rateLimit: readLimit + }, + schema: { + description: "Return project user memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId) + }), + body: z.object({ + username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username) + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema.extend({ + user: UsersSchema.pick({ + email: true, + firstName: true, + lastName: true, + id: true + }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), + 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() + }) + ) + }).omit({ createdAt: true, updatedAt: true }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const membership = await server.services.projectMembership.getProjectMembershipByUsername({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.workspaceId, + username: req.body.username + }); + return { membership }; + } + }); + server.route({ method: "POST", url: "/:workspaceId/memberships", diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index 00bb6a6cc..d259a46fd 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -251,4 +251,61 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) return { identityMemberships }; } }); + + server.route({ + method: "GET", + url: "/:projectId/identity-memberships/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Return project identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + projectId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId), + identityId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId) + }), + response: { + 200: z.object({ + identityMembership: z.object({ + id: z.string(), + identityId: 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() + }) + ), + identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) + }) + }) + } + }, + handler: async (req) => { + const identityMembership = await server.services.identityProject.getProjectIdentityByIdentityId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.params.projectId, + identityId: req.params.identityId + }); + return { identityMembership }; + } + }); }; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index e932d2068..c1cfe79cc 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -10,11 +10,16 @@ export type TIdentityProjectDALFactory = ReturnType { const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership); - const findByProjectId = async (projectId: string, tx?: Knex) => { + const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => { try { const docs = await (tx || db)(TableName.IdentityProjectMembership) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + .where((qb) => { + if (filter.identityId) { + void qb.where("identityId", filter.identityId); + } + }) .join( TableName.IdentityProjectMembershipRole, `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 2494bfb0e..fb5dc6fb0 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -18,6 +18,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-mem import { TCreateProjectIdentityDTO, TDeleteProjectIdentityDTO, + TGetProjectIdentityByIdentityIdDTO, TListProjectIdentityDTO, TUpdateProjectIdentityDTO } from "./identity-project-types"; @@ -282,10 +283,33 @@ export const identityProjectServiceFactory = ({ return identityMemberships; }; + const getProjectIdentityByIdentityId = async ({ + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + identityId + }: TGetProjectIdentityByIdentityIdDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + + const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); + if (!identityMembership) throw new BadRequestError({ message: `Membership not found for identity ${identityId}` }); + return identityMembership; + }; + return { createProjectIdentity, updateProjectIdentity, deleteProjectIdentity, - listProjectIdentities + listProjectIdentities, + getProjectIdentityByIdentityId }; }; diff --git a/backend/src/services/identity-project/identity-project-types.ts b/backend/src/services/identity-project/identity-project-types.ts index adead31e3..43c671e50 100644 --- a/backend/src/services/identity-project/identity-project-types.ts +++ b/backend/src/services/identity-project/identity-project-types.ts @@ -41,3 +41,7 @@ export type TDeleteProjectIdentityDTO = { } & TProjectPermission; export type TListProjectIdentityDTO = TProjectPermission; + +export type TGetProjectIdentityByIdentityIdDTO = { + identityId: string; +} & TProjectPermission; diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index 8faab487e..f8acc10d8 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -9,11 +9,19 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const projectMemberOrm = ormify(db, TableName.ProjectMembership); // special query - const findAllProjectMembers = async (projectId: string) => { + const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => { try { const docs = await db(TableName.ProjectMembership) .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .where((qb) => { + if (filter.usernames) { + void qb.whereIn("username", filter.usernames); + } + if (filter.username) { + void qb.where("username", filter.username); + } + }) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index e12114fef..a6682465f 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -34,6 +34,7 @@ import { TAddUsersToWorkspaceNonE2EEDTO, TDeleteProjectMembershipOldDTO, TDeleteProjectMembershipsDTO, + TGetProjectMembershipByUsernameDTO, TGetProjectMembershipDTO, TUpdateProjectMembershipDTO } from "./project-membership-types"; @@ -89,6 +90,28 @@ export const projectMembershipServiceFactory = ({ return projectMembershipDAL.findAllProjectMembers(projectId); }; + const getProjectMembershipByUsername = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId, + username + }: TGetProjectMembershipByUsernameDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + + const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username }); + if (!membership) throw new BadRequestError({ message: `Project membership not found for user ${username}` }); + return membership; + }; + const addUsersToProject = async ({ projectId, actorId, @@ -510,6 +533,7 @@ export const projectMembershipServiceFactory = ({ return { getProjectMemberships, + getProjectMembershipByUsername, updateProjectMembership, addUsersToProjectNonE2EE, deleteProjectMemberships, diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 2ba245c8c..1eab75265 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -9,6 +9,10 @@ export type TInviteUserToProjectDTO = { emails: string[]; } & TProjectPermission; +export type TGetProjectMembershipByUsernameDTO = { + username: string; +} & TProjectPermission; + export type TUpdateProjectMembershipDTO = { membershipId: string; roles: (