From 556ae168dd670cbd7110d109067c64d2e19529c4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Sep 2024 19:25:48 +0400 Subject: [PATCH] feat: fetch specific user group memberships --- backend/src/server/routes/index.ts | 1 + backend/src/server/routes/v1/user-router.ts | 51 +++++++- .../group-project/group-project-dal.ts | 112 +++++++++++++++++- backend/src/services/user/user-service.ts | 14 +++ 4 files changed, 176 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f33456bd5..792bc8787 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -464,6 +464,7 @@ export const registerRoutes = async ( userAliasDAL, orgMembershipDAL, tokenService, + groupProjectDAL, smtpService, projectMembershipDAL }); diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index d3c0db242..944925a95 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { ProjectsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -134,4 +134,53 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { ); } }); + + server.route({ + method: "GET", + url: "/me/:username/groups", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + username: z.string().trim() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string(), + slug: z.string(), + orgId: z.string(), + projectMemberships: z.array( + z.object({ + id: z.string(), + project: ProjectsSchema.pick({ id: true, name: true, slug: true }), + roles: z.array( + z.object({ + id: z.string(), + role: z.string(), + customRoleId: z.string().nullable(), + customRoleName: z.string().nullable(), + customRoleSlug: z.string().nullable(), + temporaryRange: z.string().nullable(), + temporaryMode: z.string().nullable(), + temporaryAccessEndTime: z.string().nullable(), + temporaryAccessStartTime: z.string().nullable(), + isTemporary: z.boolean() + }) + ) + }) + ) + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const groupMemberships = await server.services.user.listUserGroups(req.params.username, req.permission.orgId); + + return groupMemberships; + } + }); }; diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index fedfcfbc3..b17aaee5b 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -95,6 +95,116 @@ export const groupProjectDALFactory = (db: TDbClient) => { } }; + const findByUserId = async (userId: string, orgId: string, tx?: Knex) => { + try { + const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership) + .where(`${TableName.UserGroupMembership}.userId`, userId) + .join(TableName.Groups, function () { + this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn( + `${TableName.Groups}.orgId`, + "=", + db.raw("?", [orgId]) + ); + }) + .leftJoin( + TableName.GroupProjectMembership, + `${TableName.GroupProjectMembership}.groupId`, + `${TableName.Groups}.id` + ) + .leftJoin( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + db.ref("id").withSchema(TableName.Groups).as("groupId"), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("orgId").withSchema(TableName.Groups), + db.ref("id").withSchema(TableName.GroupProjectMembership).as("projectMembershipId"), + db.ref("id").withSchema(TableName.Project).as("projectId"), + db.ref("name").withSchema(TableName.Project).as("projectName"), + db.ref("slug").withSchema(TableName.Project).as("projectSlug"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), + db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + 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) + ); + + const groupsWithProjects = sqlNestRelationships({ + data: docs, + parentMapper: ({ groupId, groupName, groupSlug, orgId: organizationId }) => ({ + id: groupId, + name: groupName, + slug: groupSlug, + orgId: organizationId, + projectMemberships: [] + }), + key: "groupId", + childrenMapper: [ + { + label: "projectMemberships" as const, + key: "projectMembershipId", + mapper: ({ projectId, projectName, projectSlug, projectMembershipId }) => ({ + id: projectMembershipId, + project: { + id: projectId, + name: projectName, + slug: projectSlug + }, + roles: [] + }), + childrenMapper: [ + { + label: "roles" as const, + key: "membershipRoleId", + mapper: ({ + role, + customRoleId, + customRoleName, + customRoleSlug, + membershipRoleId, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) => ({ + id: membershipRoleId, + role, + customRoleId, + customRoleName, + customRoleSlug, + temporaryRange, + temporaryMode, + temporaryAccessEndTime, + temporaryAccessStartTime, + isTemporary + }) + } + ] + } + ] + }); + + return groupsWithProjects; + } catch (error) { + throw new DatabaseError({ error, name: "FindByUserId" }); + } + }; + // The GroupProjectMembership table has a reference to the project (projectId) AND the group (groupId). // We need to join the GroupProjectMembership table with the Groups table to get the group name and slug. // We also need to join the GroupProjectMembershipRole table to get the role of the group in the project. @@ -197,5 +307,5 @@ export const groupProjectDALFactory = (db: TDbClient) => { return members; }; - return { ...groupProjectOrm, findByProjectId, findAllProjectGroupMembers }; + return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; }; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index f0b043279..97bbaecf0 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -8,6 +8,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { AuthMethod } from "../auth/auth-type"; +import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TUserDALFactory } from "./user-dal"; @@ -27,6 +28,7 @@ type TUserServiceFactoryDep = { | "delete" >; userAliasDAL: Pick; + groupProjectDAL: Pick; orgMembershipDAL: Pick; tokenService: Pick; projectMembershipDAL: Pick; @@ -40,6 +42,7 @@ export const userServiceFactory = ({ userAliasDAL, orgMembershipDAL, projectMembershipDAL, + groupProjectDAL, tokenService, smtpService }: TUserServiceFactoryDep) => { @@ -295,6 +298,16 @@ export const userServiceFactory = ({ return updatedOrgMembership.projectFavorites; }; + const listUserGroups = async (username: string, orgId: string) => { + const user = await userDAL.findOne({ + username + }); + + const memberships = await groupProjectDAL.findByUserId(user.id, orgId); + + return memberships; + }; + return { sendEmailVerificationCode, verifyEmailVerificationCode, @@ -304,6 +317,7 @@ export const userServiceFactory = ({ deleteUser, getMe, createUserAction, + listUserGroups, getUserAction, unlockUser, getUserPrivateKey,