From d6e5ae7ec2e6dabc15882bc9baad31d71f15b500 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Sat, 20 Jul 2024 14:21:27 +0530 Subject: [PATCH 01/25] feat: create member schema --- src/server/api/schema/team-member.ts | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/server/api/schema/team-member.ts diff --git a/src/server/api/schema/team-member.ts b/src/server/api/schema/team-member.ts new file mode 100644 index 000000000..00d0367fb --- /dev/null +++ b/src/server/api/schema/team-member.ts @@ -0,0 +1,71 @@ +import { MemberStatusEnum } from "@/prisma/enums"; +import { Roles } from "@/prisma/enums"; +import { z } from "@hono/zod-openapi"; + +const MemberStatusArr = Object.values(MemberStatusEnum) as [ + string, + ...string[], +]; +const RolesArr = Object.values(Roles) as [string, ...string[]]; + +export const TeamMemberSchema = z.object({ + id: z.string().cuid().optional().openapi({ + description: "Team member ID", + example: "cly13ipa40000i7ng42mv4x7b", + }), + + title: z.string().nullable().openapi({ + description: "Team member title", + example: "Co-Founder & CTO", + }), + + status: z.enum(MemberStatusArr).openapi({ + description: "Team member Status", + example: "ACTIVE", + }), + + isOnboarded: z.boolean().openapi({ + description: "Is team member onboarded", + example: false, + }), + + role: z.enum(RolesArr).nullable().openapi({ + description: "Role assigned to the member", + example: "ADMIN", + }), + + workEmail: z.string().nullable().openapi({ + description: "Work Email of the team member", + example: "ceo@westwood.com", + }), + + lastAccessed: z.string().datetime().optional().openapi({ + description: "Team member last accessed at", + example: "2022-01-01T00:00:00Z", + }), + + createdAt: z.string().datetime().optional().openapi({ + description: "Team member created at", + example: "2022-01-01T00:00:00Z", + }), + + updatedAt: z.string().datetime().optional().openapi({ + description: "Team member updated at", + example: "2022-01-01T00:00:00Z", + }), + + userId: z.string().cuid().openapi({ + description: "User ID of the team member", + example: "cly13ipa40000i7ng42mv4x7b", + }), + + companyId: z.string().cuid().openapi({ + description: "Company ID", + example: "cly13ipa40000i7ng42mv4x7b", + }), + + customRoleId: z.string().cuid().nullable().openapi({ + description: "Custom role ID of the team member", + example: "cly13ipa40000i7ng42mv4x7b", + }), +}); From 79dff664c378ee72e1e6f444a12832b08537670f Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Sat, 20 Jul 2024 14:23:33 +0530 Subject: [PATCH 02/25] feat: create getTeamMember route --- .../api/routes/company/team-member/getOne.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/server/api/routes/company/team-member/getOne.ts diff --git a/src/server/api/routes/company/team-member/getOne.ts b/src/server/api/routes/company/team-member/getOne.ts new file mode 100644 index 000000000..06b414c8f --- /dev/null +++ b/src/server/api/routes/company/team-member/getOne.ts @@ -0,0 +1,88 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ApiError, ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { TeamMemberSchema } from "@/server/api/schema/team-member"; +import { db } from "@/server/db"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const RequestParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + param: { + name: "id", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clytvs95t0002mhgjmkc8rtve", + }), + memberId: z + .string() + .cuid() + .openapi({ + param: { + name: "memberId", + in: "path", + }, + description: "Team Member ID", + type: "string", + example: "clytvs9gj0008mhgje2129y6g", + }), +}); + +const ResponseSchema = z + .object({ + data: TeamMemberSchema, + }) + .openapi({ + description: "Get a single Team Member by ID", + }); + +const route = createRoute({ + summary: "Get a Team Member", + description: "Get a single Team Member by ID", + tags: ["Member"], + method: "get", + path: "/v1/companies/{id}/teams/{memberId}", + request: { params: RequestParamsSchema }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Get a single Team Member by ID", + }, + + ...ErrorResponses, + }, +}); + +const getOne = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + await withCompanyAuth(c); + + const { memberId } = c.req.param(); + + const teamMember = await db.member.findUnique({ + where: { + id: memberId, + }, + }); + + if (!teamMember) { + throw new ApiError({ + code: "NOT_FOUND", + message: "Team member not found", + }); + } + + return c.json({ data: teamMember }, 200); + }); +}; + +export default getOne; From 7fb027263f9fd9024e05152b6e7be7eae97ff9ea Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Sat, 20 Jul 2024 14:24:38 +0530 Subject: [PATCH 03/25] feat: create register function and call getOne --- src/server/api/routes/company/team-member/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/server/api/routes/company/team-member/index.ts diff --git a/src/server/api/routes/company/team-member/index.ts b/src/server/api/routes/company/team-member/index.ts new file mode 100644 index 000000000..c7ca00946 --- /dev/null +++ b/src/server/api/routes/company/team-member/index.ts @@ -0,0 +1,6 @@ +import type { PublicAPI } from "@/server/api/hono"; +import getOne from "./getOne"; + +export const registerTeamMemberRoutes = (api: PublicAPI) => { + getOne(api); +}; From ab4b041c35a01078ca61f0a4d93b01162a279405 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 12:49:50 +0530 Subject: [PATCH 04/25] feat: create getMany Members service --- src/server/services/teamMember/get-members.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/server/services/teamMember/get-members.ts diff --git a/src/server/services/teamMember/get-members.ts b/src/server/services/teamMember/get-members.ts new file mode 100644 index 000000000..f89e0f779 --- /dev/null +++ b/src/server/services/teamMember/get-members.ts @@ -0,0 +1,44 @@ +import { ProxyPrismaModel } from "@/server/api/pagination/prisma-proxy"; +import { db } from "@/server/db"; + +type GetPaginatedMembers = { + companyId: string; + take: number; + cursor?: string; + total?: number; +}; + +export const getPaginatedMembers = async (payload: GetPaginatedMembers) => { + const queryCriteria = { + where: { + companyId: payload.companyId, + }, + orderBy: { + createdAt: "desc", + }, + }; + + console.log("Payload take is : ", payload.take); + + const paginationData = { + take: payload.take, + cursor: payload.cursor, + total: payload.total, + }; + + const prismaModel = ProxyPrismaModel(db.member); + + const { data, count, total, cursor } = await prismaModel.findManyPaginated( + queryCriteria, + paginationData, + ); + + return { + data, + meta: { + count, + total, + cursor, + }, + }; +}; From 7cbff8fd6f0d1b82c84c3ffbd84acf161b8488f6 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 12:50:45 +0530 Subject: [PATCH 05/25] feat: create getMany members route --- .../api/routes/company/team-member/getMany.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/server/api/routes/company/team-member/getMany.ts diff --git a/src/server/api/routes/company/team-member/getMany.ts b/src/server/api/routes/company/team-member/getMany.ts new file mode 100644 index 000000000..a8304ad23 --- /dev/null +++ b/src/server/api/routes/company/team-member/getMany.ts @@ -0,0 +1,86 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { + DEFAULT_PAGINATION_LIMIT, + PaginationQuerySchema, + PaginationResponseSchema, +} from "@/server/api/schema/pagination"; +import { TeamMemberSchema } from "@/server/api/schema/team-member"; +import { getPaginatedMembers } from "@/server/services/teamMember/get-members"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const ParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +const ResponseSchema = z + .object({ + data: z.array(TeamMemberSchema), + meta: PaginationResponseSchema, + }) + .openapi({ + description: "Get Team Members by Company ID", + }); + +const route = createRoute({ + method: "get", + path: "/v1/companies/{id}/teams", + summary: "Get list of Team Members", + description: "Get list of Team Members for a company", + tags: ["Member"], + request: { + params: ParamsSchema, + query: PaginationQuerySchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Retrieve Team Members for the company", + }, + ...ErrorResponses, + }, +}); + +const getMany = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company } = await withCompanyAuth(c); + + const { limit, cursor, total } = c.req.query(); + + const take = limit; + + const { data, meta } = await getPaginatedMembers({ + companyId: company.id, + take: Number(take || DEFAULT_PAGINATION_LIMIT), + cursor, + total: Number(total), + }); + + return c.json( + { + data, + meta, + }, + 200, + ); + }); +}; + +export default getMany; From 70c7fb912cf2255213587e73718f1734d4a7ea5b Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 12:51:21 +0530 Subject: [PATCH 06/25] feat: add getMany Members route in index.ts --- src/server/api/routes/company/team-member/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/routes/company/team-member/index.ts b/src/server/api/routes/company/team-member/index.ts index c7ca00946..94141acee 100644 --- a/src/server/api/routes/company/team-member/index.ts +++ b/src/server/api/routes/company/team-member/index.ts @@ -1,6 +1,8 @@ import type { PublicAPI } from "@/server/api/hono"; +import getMany from "./getMany"; import getOne from "./getOne"; export const registerTeamMemberRoutes = (api: PublicAPI) => { getOne(api); + getMany(api); }; From fa76497df730d33b5f533a2058fe6ff7ef3a26e3 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 13:44:56 +0530 Subject: [PATCH 07/25] feat: create create-team-member and check-user-membership --- .../team-members/check-user-membership.ts | 49 ++++++++++++ .../team-members/create-team-member.ts | 76 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/server/services/team-members/check-user-membership.ts create mode 100644 src/server/services/team-members/create-team-member.ts diff --git a/src/server/services/team-members/check-user-membership.ts b/src/server/services/team-members/check-user-membership.ts new file mode 100644 index 000000000..e869ec33f --- /dev/null +++ b/src/server/services/team-members/check-user-membership.ts @@ -0,0 +1,49 @@ +import type { PrismaClient } from "@prisma/client"; + +export type PrismaTransactionalClient = Parameters< + Parameters[0] +>[0]; + +type UserPayload = { + name: string; + email: string; + companyId: string; +}; + +export async function checkUserMembershipForInvitation( + tx: PrismaTransactionalClient, + user: UserPayload, +) { + const { name, email, companyId } = user; + + // create or find user + const invitedUser = await tx.user.upsert({ + where: { + email, + }, + update: {}, + create: { + name, + email, + }, + select: { + id: true, + }, + }); + + // check if user is already a member + const prevMember = await tx.member.findUnique({ + where: { + companyId_userId: { + companyId, + userId: invitedUser.id, + }, + }, + }); + + if (prevMember && prevMember.status === "ACTIVE") { + return false; + } + + return invitedUser; +} diff --git a/src/server/services/team-members/create-team-member.ts b/src/server/services/team-members/create-team-member.ts new file mode 100644 index 000000000..cbe1b57ad --- /dev/null +++ b/src/server/services/team-members/create-team-member.ts @@ -0,0 +1,76 @@ +import type { getRoleById } from "@/lib/rbac/access-control"; +import { generateInviteToken, generateMemberIdentifier } from "@/server/member"; +import type { PrismaClient } from "@prisma/client"; + +export type PrismaTransactionalClient = Parameters< + Parameters[0] +>[0]; + +type MemberPayload = { + userId: string; + name: string; + title: string; + email: string; + companyId: string; + role: Awaited>; +}; + +export async function createTeamMember( + tx: PrismaTransactionalClient, + memberPayload: MemberPayload, +) { + const { userId, companyId, email, title, role } = memberPayload; + // create member + const member = await tx.member.upsert({ + create: { + title, + isOnboarded: false, + lastAccessed: new Date(), + companyId, + userId, + status: "PENDING", + ...role, + }, + update: { + title, + isOnboarded: false, + lastAccessed: new Date(), + status: "PENDING", + ...role, + }, + where: { + companyId_userId: { + companyId, + userId, + }, + }, + select: { + id: true, + userId: true, + user: { + select: { + name: true, + }, + }, + }, + }); + + const { expires, memberInviteTokenHash } = await generateInviteToken(); + + // custom verification token for member invitation + const { token: verificationToken } = await tx.verificationToken.create({ + data: { + identifier: generateMemberIdentifier({ + email, + memberId: member.id, + }), + token: memberInviteTokenHash, + expires, + }, + }); + + return { + verificationToken, + member, + }; +} From bdba42277b4427579c2daea0e239f3644716285c Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 13:46:45 +0530 Subject: [PATCH 08/25] chore: rename file --- src/server/services/{teamMember => team-members}/get-members.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/server/services/{teamMember => team-members}/get-members.ts (100%) diff --git a/src/server/services/teamMember/get-members.ts b/src/server/services/team-members/get-members.ts similarity index 100% rename from src/server/services/teamMember/get-members.ts rename to src/server/services/team-members/get-members.ts From 739e1c668c941c7f8deff86cb8d7884cd90affb3 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 13:47:26 +0530 Subject: [PATCH 09/25] chore: update import --- src/server/api/routes/company/team-member/getMany.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routes/company/team-member/getMany.ts b/src/server/api/routes/company/team-member/getMany.ts index a8304ad23..23e8b4088 100644 --- a/src/server/api/routes/company/team-member/getMany.ts +++ b/src/server/api/routes/company/team-member/getMany.ts @@ -7,7 +7,7 @@ import { PaginationResponseSchema, } from "@/server/api/schema/pagination"; import { TeamMemberSchema } from "@/server/api/schema/team-member"; -import { getPaginatedMembers } from "@/server/services/teamMember/get-members"; +import { getPaginatedMembers } from "@/server/services/team-members/get-members"; import { createRoute, z } from "@hono/zod-openapi"; import type { Context } from "hono"; From d9672237cebc0ba294c07b88a22de01de6632892 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 13:50:06 +0530 Subject: [PATCH 10/25] chore: use checkUserMembership and createTeamMember functions --- .../member-router/procedures/invite-member.ts | 87 +++---------------- 1 file changed, 14 insertions(+), 73 deletions(-) diff --git a/src/trpc/routers/member-router/procedures/invite-member.ts b/src/trpc/routers/member-router/procedures/invite-member.ts index b1983474e..1cdbe2a44 100644 --- a/src/trpc/routers/member-router/procedures/invite-member.ts +++ b/src/trpc/routers/member-router/procedures/invite-member.ts @@ -2,7 +2,8 @@ import { SendMemberInviteEmailJob } from "@/jobs/member-inivite-email"; import { getRoleById } from "@/lib/rbac/access-control"; import { generatePasswordResetToken } from "@/lib/token"; import { Audit } from "@/server/audit"; -import { generateInviteToken, generateMemberIdentifier } from "@/server/member"; +import { checkUserMembershipForInvitation } from "@/server/services/team-members/check-user-membership"; +import { createTeamMember } from "@/server/services/team-members/create-team-member"; import { withAccessControl } from "@/trpc/api/trpc"; import { TRPCError } from "@trpc/server"; import { ZodInviteMemberMutationSchema } from "../schema"; @@ -23,8 +24,6 @@ export const inviteMemberProcedure = withAccessControl membership: { companyId }, } = ctx; - const { expires, memberInviteTokenHash } = await generateInviteToken(); - const { token: passwordResetToken } = await generatePasswordResetToken(email); @@ -40,33 +39,13 @@ export const inviteMemberProcedure = withAccessControl }, }); - // create or find user - const invitedUser = await tx.user.upsert({ - where: { - email, - }, - update: {}, - create: { - name, - email, - }, - select: { - id: true, - }, - }); - - // check if user is already a member - const prevMember = await tx.member.findUnique({ - where: { - companyId_userId: { - companyId, - userId: invitedUser.id, - }, - }, + const newUserOnTeam = await checkUserMembershipForInvitation(tx, { + name, + email, + companyId: company.id, }); - // if already a member, throw error - if (prevMember && prevMember.status === "ACTIVE") { + if (!newUserOnTeam) { throw new TRPCError({ code: "FORBIDDEN", message: "user already a member", @@ -75,51 +54,13 @@ export const inviteMemberProcedure = withAccessControl const role = await getRoleById({ id: roleId, tx }); - // create member - const member = await tx.member.upsert({ - create: { - title, - isOnboarded: false, - lastAccessed: new Date(), - companyId, - userId: invitedUser.id, - status: "PENDING", - ...role, - }, - update: { - title, - isOnboarded: false, - lastAccessed: new Date(), - status: "PENDING", - ...role, - }, - where: { - companyId_userId: { - companyId, - userId: invitedUser.id, - }, - }, - select: { - id: true, - userId: true, - user: { - select: { - name: true, - }, - }, - }, - }); - - // custom verification token for member invitation - const { token: verificationToken } = await tx.verificationToken.create({ - data: { - identifier: generateMemberIdentifier({ - email, - memberId: member.id, - }), - token: memberInviteTokenHash, - expires, - }, + const { member, verificationToken } = await createTeamMember(tx, { + userId: newUserOnTeam.id, + companyId: company.id, + name, + email, + title, + role, }); await Audit.create( From 728c9caecac3681fd94e4449bd8dd6e5f6f777e5 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Wed, 24 Jul 2024 13:52:10 +0530 Subject: [PATCH 11/25] chore: reuse types --- src/server/services/shares/get-shares.ts | 2 +- src/server/services/team-members/get-members.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/server/services/shares/get-shares.ts b/src/server/services/shares/get-shares.ts index 815ed07d9..5e13693b7 100644 --- a/src/server/services/shares/get-shares.ts +++ b/src/server/services/shares/get-shares.ts @@ -1,7 +1,7 @@ import { ProxyPrismaModel } from "@/server/api/pagination/prisma-proxy"; import { db } from "@/server/db"; -type GetPaginatedShares = { +export type GetPaginatedShares = { companyId: string; take: number; cursor?: string; diff --git a/src/server/services/team-members/get-members.ts b/src/server/services/team-members/get-members.ts index f89e0f779..37cf71136 100644 --- a/src/server/services/team-members/get-members.ts +++ b/src/server/services/team-members/get-members.ts @@ -1,12 +1,8 @@ import { ProxyPrismaModel } from "@/server/api/pagination/prisma-proxy"; import { db } from "@/server/db"; +import type { GetPaginatedShares } from "../shares/get-shares"; -type GetPaginatedMembers = { - companyId: string; - take: number; - cursor?: string; - total?: number; -}; +type GetPaginatedMembers = GetPaginatedShares; export const getPaginatedMembers = async (payload: GetPaginatedMembers) => { const queryCriteria = { From 657b0206d5c8122586c34781ba424a44daa2016f Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 10:02:06 +0530 Subject: [PATCH 12/25] chore: rename files --- .../team-members/{create-team-member.ts => create-member.ts} | 2 +- src/trpc/routers/member-router/procedures/invite-member.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/server/services/team-members/{create-team-member.ts => create-member.ts} (97%) diff --git a/src/server/services/team-members/create-team-member.ts b/src/server/services/team-members/create-member.ts similarity index 97% rename from src/server/services/team-members/create-team-member.ts rename to src/server/services/team-members/create-member.ts index cbe1b57ad..35d92fe88 100644 --- a/src/server/services/team-members/create-team-member.ts +++ b/src/server/services/team-members/create-member.ts @@ -15,7 +15,7 @@ type MemberPayload = { role: Awaited>; }; -export async function createTeamMember( +export async function createMember( tx: PrismaTransactionalClient, memberPayload: MemberPayload, ) { diff --git a/src/trpc/routers/member-router/procedures/invite-member.ts b/src/trpc/routers/member-router/procedures/invite-member.ts index 1cdbe2a44..1d26e2437 100644 --- a/src/trpc/routers/member-router/procedures/invite-member.ts +++ b/src/trpc/routers/member-router/procedures/invite-member.ts @@ -3,7 +3,7 @@ import { getRoleById } from "@/lib/rbac/access-control"; import { generatePasswordResetToken } from "@/lib/token"; import { Audit } from "@/server/audit"; import { checkUserMembershipForInvitation } from "@/server/services/team-members/check-user-membership"; -import { createTeamMember } from "@/server/services/team-members/create-team-member"; +import { createMember } from "@/server/services/team-members/create-member"; import { withAccessControl } from "@/trpc/api/trpc"; import { TRPCError } from "@trpc/server"; import { ZodInviteMemberMutationSchema } from "../schema"; @@ -54,7 +54,7 @@ export const inviteMemberProcedure = withAccessControl const role = await getRoleById({ id: roleId, tx }); - const { member, verificationToken } = await createTeamMember(tx, { + const { member, verificationToken } = await createMember(tx, { userId: newUserOnTeam.id, companyId: company.id, name, From 4be51ca6dd9f351dcd44f9a6eb87481bce149c41 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 11:59:55 +0530 Subject: [PATCH 13/25] feat: return additional member data --- src/server/services/team-members/create-member.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/services/team-members/create-member.ts b/src/server/services/team-members/create-member.ts index 35d92fe88..1059c97c4 100644 --- a/src/server/services/team-members/create-member.ts +++ b/src/server/services/team-members/create-member.ts @@ -52,6 +52,16 @@ export async function createMember( name: true, }, }, + title: true, + status: true, + isOnboarded: true, + role: true, + workEmail: true, + lastAccessed: true, + createdAt: true, + updatedAt: true, + companyId: true, + customRoleId: true, }, }); From f0d603c76f33b5b146211f7622c7a6cd18963e87 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 12:01:01 +0530 Subject: [PATCH 14/25] feat: add createTeamMember function --- .../team-members/create-team-member.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/server/services/team-members/create-team-member.ts diff --git a/src/server/services/team-members/create-team-member.ts b/src/server/services/team-members/create-team-member.ts new file mode 100644 index 000000000..b2aa78d55 --- /dev/null +++ b/src/server/services/team-members/create-team-member.ts @@ -0,0 +1,110 @@ +import { getRoleById } from "@/lib/rbac/access-control"; +import { ADMIN_ROLE_ID } from "@/lib/rbac/constants"; +import type { Roles } from "@/prisma/enums"; +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; +import { checkUserMembershipForInvitation } from "./check-user-membership"; +import { createMember } from "./create-member"; + +interface CreateTeamMember { + name: string; + email: string; + companyId: string; + requestIp: string; + userAgent: string; + userId: string; + role: Roles | null; + title: string | null; + customRoleId: string | null; + companyName: string; +} + +export const createTeamMember = async (payload: CreateTeamMember) => { + const { + title, + companyId, + customRoleId, + email, + name, + requestIp, + role, + userAgent, + userId, + companyName, + } = payload; + + const { verificationToken, member } = await db.$transaction(async (tx) => { + const newUserOnTeam = await checkUserMembershipForInvitation(tx, { + name, + email, + companyId, + }); + + if (!newUserOnTeam) { + return { + success: false, + message: "user already a member", + data: null, + }; + } + + let userRole: Awaited> = { + customRoleId: null, + role: null, + }; + + if (role === "ADMIN") { + userRole = await getRoleById({ id: ADMIN_ROLE_ID, tx }); + } else if (role === "CUSTOM") { + if (!customRoleId) { + return { + success: false, + message: "Enter the CustomRole ID when role set to CUSTOM", + data: null, + }; + } + + try { + userRole = await getRoleById({ id: customRoleId, tx }); + } catch (error) { + return { + success: false, + message: "Enter Valid CustomRole ID", + data: null, + }; + } + } + + const { member, verificationToken } = await createMember(tx, { + userId: newUserOnTeam.id, + companyId, + name, + email, + title: title || "", + role: userRole, + }); + + await Audit.create( + { + action: "member.invited", + companyId, + actor: { type: "user", id: userId }, + context: { + requestIp, + userAgent, + }, + target: [{ type: "user", id: member.userId }], + summary: `${name} invited ${member.user?.name} to join ${companyName}`, + }, + tx, + ); + + return { verificationToken, member }; + }); + + return { + data: { verificationToken, member }, + success: true, + message: "Team member created Successfully 🎉 !", + }; +}; From 617c4c84d30aa8ffd5763f47d3ec7116f34a9e76 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 12:02:53 +0530 Subject: [PATCH 15/25] feat: add CreateMemberSchema --- src/server/api/schema/team-member.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/server/api/schema/team-member.ts b/src/server/api/schema/team-member.ts index 00d0367fb..c4f5f7925 100644 --- a/src/server/api/schema/team-member.ts +++ b/src/server/api/schema/team-member.ts @@ -69,3 +69,18 @@ export const TeamMemberSchema = z.object({ example: "cly13ipa40000i7ng42mv4x7b", }), }); + +export type TeamMember = z.infer; + +export const CreateMemberSchema = TeamMemberSchema.pick({ + title: true, + role: true, + customRoleId: true, +}).extend({ + email: z.string().openapi({ + description: "Work Email of the team member", + example: "ceo@westwood.com", + }), +}); + +export type TCreateMember = z.infer; From f992d47df754227e6ee25d6182da59b8f3294ffa Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 12:04:23 +0530 Subject: [PATCH 16/25] feat: create post method for member route --- .../api/routes/company/team-member/create.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/server/api/routes/company/team-member/create.ts diff --git a/src/server/api/routes/company/team-member/create.ts b/src/server/api/routes/company/team-member/create.ts new file mode 100644 index 000000000..1966f54a2 --- /dev/null +++ b/src/server/api/routes/company/team-member/create.ts @@ -0,0 +1,139 @@ +import { SendMemberInviteEmailJob } from "@/jobs/member-inivite-email"; +import { generatePasswordResetToken } from "@/lib/token"; +import type { Roles } from "@/prisma/enums"; +import { withCompanyAuth } from "@/server/api/auth"; +import { ApiError, ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { + CreateMemberSchema, + type TeamMember, + TeamMemberSchema, +} from "@/server/api/schema/team-member"; +import { getIp } from "@/server/api/utils"; +import { createTeamMember } from "@/server/services/team-members/create-team-member"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const ParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clycjihpy0002c5fzcyf4gjjc", + }), +}); + +const ResponseSchema = z.object({ + message: z.string(), + data: TeamMemberSchema, +}); + +const route = createRoute({ + method: "post", + path: "/v1/companies/{id}/teams", + summary: "Create Team Members", + description: "Create Team Members in a company.", + tags: ["Member"], + request: { + params: ParamsSchema, + body: { + content: { + "application/json": { + schema: CreateMemberSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Create Team Members", + }, + ...ErrorResponses, + }, +}); + +const create = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, user } = await withCompanyAuth(c); + const body = await c.req.json(); + + const email: string = body.workEmail; + const title: string | null = body.title; + const role: Roles | null = body.role; + const customRoleId: string | null = body.customRoleId; + + const requestIp = getIp(c.req); + const userAgent = c.req.header("User-Agent") || ""; + + const { data, success, message } = await createTeamMember({ + companyId: company.id, + companyName: company.name, + email, + customRoleId, + name: user.name || "", + requestIp, + userAgent, + userId: user.id, + role, + title, + }); + + if (!success && !data) { + throw new ApiError({ + code: "BAD_REQUEST", + message, + }); + } + + const verificationToken = data.verificationToken as string; + const member = data.member; + + const currentTime = new Date().toISOString(); + + const responseData: TeamMember = { + companyId: member?.companyId || "", + customRoleId: member?.customRoleId || null, + isOnboarded: member?.isOnboarded || true, + role: member?.role || null, + status: member?.status || "", + title: member?.title || "", + userId: member?.userId || "", + workEmail: member?.workEmail || "", + id: member?.id || "", + createdAt: member?.createdAt.toString() || currentTime, + lastAccessed: member?.lastAccessed.toString() || currentTime, + updatedAt: member?.updatedAt.toString() || currentTime, + }; + + const { token: passwordResetToken } = + await generatePasswordResetToken(email); + + const payload = { + verificationToken, + passwordResetToken, + email, + company, + user: { + email: user.email, + name: user.name, + }, + }; + + await new SendMemberInviteEmailJob().emit(payload); + + return c.json({ message, data: responseData }); + }); +}; + +export default create; From 3b3c687b8dba051e33db3421f05f1339cf91ff76 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 12:05:27 +0530 Subject: [PATCH 17/25] feat: call create route in index.ts --- src/server/api/routes/company/team-member/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/routes/company/team-member/index.ts b/src/server/api/routes/company/team-member/index.ts index 94141acee..def295ef5 100644 --- a/src/server/api/routes/company/team-member/index.ts +++ b/src/server/api/routes/company/team-member/index.ts @@ -1,8 +1,10 @@ import type { PublicAPI } from "@/server/api/hono"; +import create from "./create"; import getMany from "./getMany"; import getOne from "./getOne"; export const registerTeamMemberRoutes = (api: PublicAPI) => { getOne(api); getMany(api); + create(api); }; From 5ef4688511e26a8bc9f34b5802b47ba9542ed953 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 12:31:28 +0530 Subject: [PATCH 18/25] fix: post member route and createTeamMember error responses --- .../api/routes/company/team-member/create.ts | 2 +- .../team-members/create-team-member.ts | 126 ++++++++++-------- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/server/api/routes/company/team-member/create.ts b/src/server/api/routes/company/team-member/create.ts index 1966f54a2..93b6639f8 100644 --- a/src/server/api/routes/company/team-member/create.ts +++ b/src/server/api/routes/company/team-member/create.ts @@ -68,7 +68,7 @@ const create = (app: PublicAPI) => { const { company, user } = await withCompanyAuth(c); const body = await c.req.json(); - const email: string = body.workEmail; + const email: string = body.email; const title: string | null = body.title; const role: Roles | null = body.role; const customRoleId: string | null = body.customRoleId; diff --git a/src/server/services/team-members/create-team-member.ts b/src/server/services/team-members/create-team-member.ts index b2aa78d55..c81ccae0d 100644 --- a/src/server/services/team-members/create-team-member.ts +++ b/src/server/services/team-members/create-team-member.ts @@ -33,78 +33,86 @@ export const createTeamMember = async (payload: CreateTeamMember) => { companyName, } = payload; - const { verificationToken, member } = await db.$transaction(async (tx) => { - const newUserOnTeam = await checkUserMembershipForInvitation(tx, { - name, - email, - companyId, - }); - - if (!newUserOnTeam) { - return { - success: false, - message: "user already a member", - data: null, - }; - } - - let userRole: Awaited> = { - customRoleId: null, - role: null, - }; + const { verificationToken, member, success, message } = await db.$transaction( + async (tx) => { + const newUserOnTeam = await checkUserMembershipForInvitation(tx, { + name, + email, + companyId, + }); - if (role === "ADMIN") { - userRole = await getRoleById({ id: ADMIN_ROLE_ID, tx }); - } else if (role === "CUSTOM") { - if (!customRoleId) { + if (!newUserOnTeam) { return { success: false, - message: "Enter the CustomRole ID when role set to CUSTOM", - data: null, + message: "user already a member", }; } - try { - userRole = await getRoleById({ id: customRoleId, tx }); - } catch (error) { - return { - success: false, - message: "Enter Valid CustomRole ID", - data: null, - }; - } - } + let userRole: Awaited> = { + customRoleId: null, + role: null, + }; - const { member, verificationToken } = await createMember(tx, { - userId: newUserOnTeam.id, - companyId, - name, - email, - title: title || "", - role: userRole, - }); + if (role === "ADMIN") { + userRole = await getRoleById({ id: ADMIN_ROLE_ID, tx }); + } else if (role === "CUSTOM") { + if (!customRoleId) { + return { + success: false, + message: "Enter the CustomRole ID when role set to CUSTOM", + }; + } + + try { + userRole = await getRoleById({ id: customRoleId, tx }); + } catch (error) { + return { + success: false, + message: "Enter Valid CustomRole ID", + }; + } + } - await Audit.create( - { - action: "member.invited", + const { member, verificationToken } = await createMember(tx, { + userId: newUserOnTeam.id, companyId, - actor: { type: "user", id: userId }, - context: { - requestIp, - userAgent, + name, + email, + title: title || "", + role: userRole, + }); + + await Audit.create( + { + action: "member.invited", + companyId, + actor: { type: "user", id: userId }, + context: { + requestIp, + userAgent, + }, + target: [{ type: "user", id: member.userId }], + summary: `${name} invited ${member.user?.name} to join ${companyName}`, }, - target: [{ type: "user", id: member.userId }], - summary: `${name} invited ${member.user?.name} to join ${companyName}`, - }, - tx, - ); + tx, + ); + + return { + verificationToken, + member, + success: true, + message: "Team member created Successfully 🎉 !", + }; + }, + ); - return { verificationToken, member }; - }); + if (!success) { + return { success, message }; + } return { data: { verificationToken, member }, - success: true, - message: "Team member created Successfully 🎉 !", + success, + message, }; }; From 20dc9b7e588f8a62b4cdac13f960bf3fa937dcf2 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 22:23:56 +0530 Subject: [PATCH 19/25] feat: export ErrorCodes --- src/server/api/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/error.ts b/src/server/api/error.ts index f1c41392b..f060857f1 100644 --- a/src/server/api/error.ts +++ b/src/server/api/error.ts @@ -8,7 +8,7 @@ import { z } from "@hono/zod-openapi"; const log = logger.child({ module: "api-error" }); -const ErrorCode = z.enum([ +export const ErrorCode = z.enum([ "BAD_REQUEST", "FORBIDDEN", "INTERNAL_SERVER_ERROR", From 135aa1f7b63f45c2d1e72619864773f4a34f416d Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 22:25:02 +0530 Subject: [PATCH 20/25] feat: add UpdateMemberSchema and update TeamMemberSchema --- src/server/api/schema/team-member.ts | 37 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/server/api/schema/team-member.ts b/src/server/api/schema/team-member.ts index c4f5f7925..471fb73da 100644 --- a/src/server/api/schema/team-member.ts +++ b/src/server/api/schema/team-member.ts @@ -9,12 +9,12 @@ const MemberStatusArr = Object.values(MemberStatusEnum) as [ const RolesArr = Object.values(Roles) as [string, ...string[]]; export const TeamMemberSchema = z.object({ - id: z.string().cuid().optional().openapi({ + id: z.string().cuid().nullish().openapi({ description: "Team member ID", example: "cly13ipa40000i7ng42mv4x7b", }), - title: z.string().nullable().openapi({ + title: z.string().nullish().openapi({ description: "Team member title", example: "Co-Founder & CTO", }), @@ -29,27 +29,27 @@ export const TeamMemberSchema = z.object({ example: false, }), - role: z.enum(RolesArr).nullable().openapi({ + role: z.enum(RolesArr).nullish().openapi({ description: "Role assigned to the member", example: "ADMIN", }), - workEmail: z.string().nullable().openapi({ + workEmail: z.string().nullish().openapi({ description: "Work Email of the team member", example: "ceo@westwood.com", }), - lastAccessed: z.string().datetime().optional().openapi({ + lastAccessed: z.string().datetime().nullish().openapi({ description: "Team member last accessed at", example: "2022-01-01T00:00:00Z", }), - createdAt: z.string().datetime().optional().openapi({ + createdAt: z.string().datetime().nullish().openapi({ description: "Team member created at", example: "2022-01-01T00:00:00Z", }), - updatedAt: z.string().datetime().optional().openapi({ + updatedAt: z.string().datetime().nullish().openapi({ description: "Team member updated at", example: "2022-01-01T00:00:00Z", }), @@ -64,7 +64,7 @@ export const TeamMemberSchema = z.object({ example: "cly13ipa40000i7ng42mv4x7b", }), - customRoleId: z.string().cuid().nullable().openapi({ + customRoleId: z.string().cuid().nullish().openapi({ description: "Custom role ID of the team member", example: "cly13ipa40000i7ng42mv4x7b", }), @@ -84,3 +84,24 @@ export const CreateMemberSchema = TeamMemberSchema.pick({ }); export type TCreateMember = z.infer; + +export const UpdateMemberSchema = TeamMemberSchema.omit({ + id: true, + createdAt: true, + updatedAt: true, + lastAccessed: true, +}) + .partial() + .refine( + (data) => { + return Object.values(data).some((val) => val !== undefined); + }, + { + message: "At least one field must be provided to update.", + }, + ) + .openapi({ + description: "Update a Team Member by ID", + }); + +export type UpdateMemberType = z.infer; From 96e203ba471f1ca70be27448d55114dbbed8cb98 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 22:25:52 +0530 Subject: [PATCH 21/25] feat: create update member route --- .../api/routes/company/team-member/update.ts | 124 ++++++++++++++++++ .../services/team-members/update-member.ts | 73 +++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/server/api/routes/company/team-member/update.ts create mode 100644 src/server/services/team-members/update-member.ts diff --git a/src/server/api/routes/company/team-member/update.ts b/src/server/api/routes/company/team-member/update.ts new file mode 100644 index 000000000..287d1cee2 --- /dev/null +++ b/src/server/api/routes/company/team-member/update.ts @@ -0,0 +1,124 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ApiError, type ErrorCode, ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { + UpdateMemberSchema, + type UpdateMemberType, +} from "@/server/api/schema/team-member"; +import { getHonoUserAgent, getIp } from "@/server/api/utils"; +import { + type UpdateMemberPayloadType, + updateMember, +} from "@/server/services/team-members/update-member"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +export const RequestParamsSchema = z + .object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clxwbok580000i7nge8nm1ry0", + }), + memberId: z + .string() + .cuid() + .openapi({ + description: "Team Member ID", + param: { + name: "memberId", + in: "path", + }, + + example: "clyd3i9sw000008ij619eabva", + }), + }) + .openapi({ + description: "Update a Team Member by ID", + }); + +const ResponseSchema = z + .object({ + message: z.string(), + data: UpdateMemberSchema, + }) + .openapi({ + description: "Update a Team Member by ID", + }); + +const route = createRoute({ + method: "put", + path: "/v1/companies/{id}/teams/{memberId}", + summary: "Update a Team Member by ID", + description: "Update a Team Member by ID", + tags: ["Member"], + request: { + params: RequestParamsSchema, + body: { + content: { + "application/json": { + schema: UpdateMemberSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Update a Team Member by ID", + }, + ...ErrorResponses, + }, +}); + +const update = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, user } = await withCompanyAuth(c); + const { memberId } = c.req.param(); + const body = await c.req.json(); + + const payload: UpdateMemberPayloadType = { + memberId: memberId as string, + companyId: company.id, + requestIp: getIp(c.req), + userAgent: getHonoUserAgent(c.req), + data: body as UpdateMemberType, + user: { + id: user.id, + name: user.name as string, + }, + }; + + const result = await updateMember(payload); + const { success, data } = result; + const code = result.code as z.infer; + + if (!success) { + throw new ApiError({ + code, + message: result.message, + }); + } + + return c.json( + { + message: result.message, + data: data as UpdateMemberType, + }, + 200, + ); + }); +}; + +export default update; diff --git a/src/server/services/team-members/update-member.ts b/src/server/services/team-members/update-member.ts new file mode 100644 index 000000000..7e2d6d3c3 --- /dev/null +++ b/src/server/services/team-members/update-member.ts @@ -0,0 +1,73 @@ +import type { UpdateMemberType } from "@/server/api/schema/team-member"; +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; +import type { UpdateSharePayloadType } from "../shares/update-share"; + +export interface UpdateMemberPayloadType + extends Omit { + data: UpdateMemberType; + memberId: string; +} + +export const updateMember = async (payload: UpdateMemberPayloadType) => { + const { companyId, data, memberId, requestIp, user, userAgent } = payload; + + try { + const existingMember = await db.member.findUnique({ + where: { + id: memberId, + }, + }); + + if (!existingMember) { + return { + code: "BAD_REQUEST", + success: false, + message: `Member with the ID ${memberId} not be found`, + data: null, + }; + } + + const memberData = { ...existingMember, ...data }; + + const { member, success, message } = await db.$transaction(async (tx) => { + const member = await tx.member.update({ + where: { + id: memberData.id, + }, + //@ts-ignore + data: memberData, + }); + + await Audit.create( + { + action: "member.updated", + companyId, + actor: { type: "user", id: user.id }, + context: { + requestIp, + userAgent, + }, + target: [{ type: "user", id: member.userId }], + summary: `${user.name} updated ${user.name} details`, + }, + tx, + ); + + return { + message: "Successfully updated Member details !", + success: true, + member, + }; + }); + + return { message, success, data: member, code: "SUCCESS" }; + } catch (error) { + return { + success: false, + code: "INTERNAL_SERVER_ERROR", + message: "Something went wrong, please try again or contact support", + data: null, + }; + } +}; From 19aa3a20f4a8f8211b0e86255a4ff1fc83919d96 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Thu, 25 Jul 2024 22:26:18 +0530 Subject: [PATCH 22/25] feat: call update member route --- src/server/api/routes/company/team-member/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/routes/company/team-member/index.ts b/src/server/api/routes/company/team-member/index.ts index def295ef5..4e267d345 100644 --- a/src/server/api/routes/company/team-member/index.ts +++ b/src/server/api/routes/company/team-member/index.ts @@ -2,9 +2,11 @@ import type { PublicAPI } from "@/server/api/hono"; import create from "./create"; import getMany from "./getMany"; import getOne from "./getOne"; +import update from "./update"; export const registerTeamMemberRoutes = (api: PublicAPI) => { getOne(api); getMany(api); create(api); + update(api); }; From 17278ba53bb78f9355e5d3798cfc02fb36ec2917 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Fri, 26 Jul 2024 00:00:54 +0530 Subject: [PATCH 23/25] feat: add delete member endpoints --- .../api/routes/company/team-member/delete.ts | 81 +++++++++++++++++++ .../services/team-members/delete-member.ts | 52 ++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/server/api/routes/company/team-member/delete.ts create mode 100644 src/server/services/team-members/delete-member.ts diff --git a/src/server/api/routes/company/team-member/delete.ts b/src/server/api/routes/company/team-member/delete.ts new file mode 100644 index 000000000..dc89c4003 --- /dev/null +++ b/src/server/api/routes/company/team-member/delete.ts @@ -0,0 +1,81 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ApiError, ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { getHonoUserAgent, getIp } from "@/server/api/utils"; +import { + type DeleteMemberPayload, + deleteMember, +} from "@/server/services/team-members/delete-member"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; +import { RequestParamsSchema } from "./update"; + +const ResponseSchema = z + .object({ + message: z.string(), + }) + .openapi({ + description: "Delete a Team Member by ID", + }); + +const DeleteParamsSchema = RequestParamsSchema.openapi({ + description: "Delete a Team Member by ID", +}); + +const route = createRoute({ + method: "delete", + path: "/v1/companies/{id}/teams/{memberId}", + summary: "Delete a Team Member by ID", + description: "Delete a Team Member by ID", + tags: ["Member"], + request: { + params: DeleteParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Delete a Share by ID", + }, + ...ErrorResponses, + }, +}); + +const deleteOne = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, user } = await withCompanyAuth(c); + const { memberId } = c.req.param(); + + const payload: DeleteMemberPayload = { + companyId: company.id, + memberId: memberId as string, + requestIp: getIp(c.req), + userAgent: getHonoUserAgent(c.req), + user: { + id: user.id, + name: user.name as string, + }, + }; + + const { success, message } = await deleteMember(payload); + + if (!success) { + throw new ApiError({ + code: "BAD_REQUEST", + message, + }); + } + + return c.json( + { + message: message, + }, + 200, + ); + }); +}; + +export default deleteOne; diff --git a/src/server/services/team-members/delete-member.ts b/src/server/services/team-members/delete-member.ts new file mode 100644 index 000000000..4e2ce0bd1 --- /dev/null +++ b/src/server/services/team-members/delete-member.ts @@ -0,0 +1,52 @@ +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; +import type { UpdateMemberPayloadType } from "./update-member"; + +export interface DeleteMemberPayload + extends Omit {} + +export const deleteMember = async (payload: DeleteMemberPayload) => { + const { companyId, memberId, requestIp, userAgent, user } = payload; + + const existingMember = await db.member.findUnique({ + where: { + id: memberId, + companyId, + }, + }); + + if (!existingMember) { + return { + success: false, + message: `Member with the ID ${memberId} not be found`, + data: null, + }; + } + + const member = await db.member.delete({ + where: { + id: existingMember.id, + }, + include: { + user: true, + company: true, + }, + }); + + await Audit.create( + { + action: "member.removed", + companyId, + actor: { type: "user", id: user.id }, + context: { + requestIp, + userAgent, + }, + target: [{ type: "user", id: member.userId }], + summary: `${user.name} removed ${member.user?.name} from ${member?.company?.name}`, + }, + db, + ); + + return { success: true, message: "Member removed successfully !" }; +}; From f96fa5ac299dda4b7eb5ffd4e9ebd9dd4a996282 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Fri, 26 Jul 2024 00:01:24 +0530 Subject: [PATCH 24/25] feat: call delete function in index.ts --- src/server/api/routes/company/team-member/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/routes/company/team-member/index.ts b/src/server/api/routes/company/team-member/index.ts index 4e267d345..7681c2e34 100644 --- a/src/server/api/routes/company/team-member/index.ts +++ b/src/server/api/routes/company/team-member/index.ts @@ -1,5 +1,6 @@ import type { PublicAPI } from "@/server/api/hono"; import create from "./create"; +import deleteOne from "./delete"; import getMany from "./getMany"; import getOne from "./getOne"; import update from "./update"; @@ -9,4 +10,5 @@ export const registerTeamMemberRoutes = (api: PublicAPI) => { getMany(api); create(api); update(api); + deleteOne(api); }; From 7fee741d112ac307c98c3f6a740f0ee4b6d8b5c0 Mon Sep 17 00:00:00 2001 From: Vishnu Kumar Date: Fri, 26 Jul 2024 00:01:53 +0530 Subject: [PATCH 25/25] feat: register teamMembers route --- src/server/api/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index d47a20038..cf9d3f7c1 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -3,6 +3,7 @@ import { initMiddleware } from "./middlewares/init"; import { registerCompanyRoutes } from "./routes/company"; import { registerShareRoutes } from "./routes/company/share"; import { registerStakeholderRoutes } from "./routes/company/stakeholder"; +import { registerTeamMemberRoutes } from "./routes/company/team-member"; const api = PublicAPI(); @@ -12,5 +13,6 @@ api.use("*", initMiddleware()); registerCompanyRoutes(api); registerShareRoutes(api); registerStakeholderRoutes(api); +registerTeamMemberRoutes(api); export default api;