import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import * as workspaceRepo from "~/server/db/repository/workspace.repo"; import * as memberRepo from "~/server/db/repository/member.repo"; import * as userRepo from "~/server/db/repository/user.repo"; import { sendEmail } from "~/email/sendEmail"; export const memberRouter = createTRPCRouter({ invite: protectedProcedure .input( z.object({ email: z.string().email(), workspacePublicId: z.string().min(12), }), ) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; if (!userId) throw new TRPCError({ message: `User not authenticated`, code: "UNAUTHORIZED", }); const workspace = await workspaceRepo.getByPublicIdWithMembers( ctx.db, input.workspacePublicId, ); if (!workspace) throw new TRPCError({ message: `Workspace with public ID ${input.workspacePublicId} not found`, code: "NOT_FOUND", }); const isInvitedEmailAlreadyMember = workspace?.members.some( (member) => member.user?.email === input.email, ); if (isInvitedEmailAlreadyMember) { throw new TRPCError({ message: `User with email ${input.email} is already a member of this workspace`, code: "BAD_REQUEST", }); } let invitedUserId: string | undefined; let hashedToken: string | undefined; let verificationType: string | undefined; const existingUser = await userRepo.getByEmail(ctx.adminDb, input.email); if (existingUser) { invitedUserId = existingUser.id; const magicLink = await ctx.adminDb.auth.admin.generateLink({ type: "magiclink", email: input.email, options: { redirectTo: process.env.WEBSITE_URL, }, }); hashedToken = magicLink.data.properties?.hashed_token; verificationType = magicLink.data.properties?.verification_type; } else { const invite = await ctx.adminDb.auth.admin.generateLink({ type: "invite", email: input.email, options: { redirectTo: process.env.WEBSITE_URL, }, }); hashedToken = invite.data.properties?.hashed_token; verificationType = invite.data.properties?.verification_type; const invitedUserAuthId = invite.data.user?.id; const invitedUserEmail = invite.data.user?.email; if (invitedUserAuthId && invitedUserEmail) { const newUser = await userRepo.create(ctx.adminDb, { email: invitedUserEmail, id: invitedUserAuthId, }); invitedUserId = newUser?.id; } } if (!invitedUserId) throw new TRPCError({ message: `Unable to invite user with email ${input.email}`, code: "INTERNAL_SERVER_ERROR", }); if (!hashedToken || !verificationType) throw new TRPCError({ message: `Unable to generate magic link for user with email ${input.email}`, code: "INTERNAL_SERVER_ERROR", }); const invite = await memberRepo.create(ctx.db, { workspaceId: workspace.id, userId: invitedUserId, createdBy: userId, role: "member", status: "invited", }); if (!invite) throw new TRPCError({ message: `Unable to invite user with email ${input.email}`, code: "INTERNAL_SERVER_ERROR", }); const magicLoginUrl = `${process.env.WEBSITE_URL}/api/auth/confirm?token_hash=${hashedToken}&type=${verificationType}&memberPublicId=${invite.publicId}`; await sendEmail( input.email, "Invitation to join workspace", "JOIN_WORKSPACE", { magicLoginUrl, }, ); return invite; }), delete: protectedProcedure .input( z.object({ memberPublicId: z.string().min(12), }), ) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; if (!userId) throw new TRPCError({ message: `User not authenticated`, code: "UNAUTHORIZED", }); const member = await memberRepo.getByPublicId( ctx.db, input.memberPublicId, ); if (!member) throw new TRPCError({ message: `Member with public ID ${input.memberPublicId} not found`, code: "NOT_FOUND", }); const deletedMember = await memberRepo.softDelete(ctx.db, { memberId: member.id, deletedAt: new Date().toISOString(), deletedBy: userId, }); return deletedMember; }), });