feat: invite members
This commit is contained in:
@@ -3,6 +3,7 @@ import { boardRouter } from "~/server/api/routers/board";
|
||||
import { cardRouter } from "~/server/api/routers/card";
|
||||
import { labelRouter } from "~/server/api/routers/label";
|
||||
import { listRouter } from "~/server/api/routers/list";
|
||||
import { memberRouter } from "~/server/api/routers/member";
|
||||
import { importRouter } from "~/server/api/routers/import";
|
||||
import { workspaceRouter } from "~/server/api/routers/workspace";
|
||||
import { createTRPCRouter } from "~/server/api/trpc";
|
||||
@@ -18,6 +19,7 @@ export const appRouter = createTRPCRouter({
|
||||
card: cardRouter,
|
||||
label: labelRouter,
|
||||
list: listRouter,
|
||||
member: memberRouter,
|
||||
import: importRouter,
|
||||
workspace: workspaceRouter,
|
||||
});
|
||||
|
||||
128
src/server/api/routers/member.ts
Normal file
128
src/server/api/routers/member.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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 | null = null;
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const magicLinkUrl = magicLink.data.properties?.action_link;
|
||||
|
||||
if (!magicLinkUrl)
|
||||
throw new TRPCError({
|
||||
message: `Unable to generate magic link for user with email ${input.email}`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
await sendEmail(
|
||||
input.email,
|
||||
"Invitation to join workspace",
|
||||
"JOIN_WORKSPACE",
|
||||
{
|
||||
magicLinkUrl,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const invite = await ctx.adminDb.auth.admin.generateLink({
|
||||
type: "invite",
|
||||
email: input.email,
|
||||
});
|
||||
|
||||
const magicLinkUrl = invite.data.properties?.action_link;
|
||||
|
||||
if (!magicLinkUrl)
|
||||
throw new TRPCError({
|
||||
message: `Unable to generate invite link for user with email ${input.email}`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
await sendEmail(
|
||||
input.email,
|
||||
"Invitation to join workspace",
|
||||
"JOIN_WORKSPACE",
|
||||
{
|
||||
magicLinkUrl,
|
||||
},
|
||||
);
|
||||
|
||||
invitedUserId = invite.data.user?.id ?? null;
|
||||
const invitedUserEmail = invite.data.user?.email;
|
||||
|
||||
if (invitedUserId && invitedUserEmail)
|
||||
userRepo.create(ctx.adminDb, {
|
||||
email: invitedUserEmail,
|
||||
id: invitedUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!invitedUserId)
|
||||
throw new TRPCError({
|
||||
message: `Unable to invite 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",
|
||||
});
|
||||
|
||||
return invite;
|
||||
}),
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { createTRPCClient } from "~/utils/supabase/api";
|
||||
import { createTRPCClient, createTRPCAdminClient } from "~/utils/supabase/api";
|
||||
import { type Database } from "~/types/database.types";
|
||||
import { type SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
@@ -30,6 +30,7 @@ type User = {
|
||||
interface CreateContextOptions {
|
||||
user: User | null;
|
||||
db: SupabaseClient<Database>;
|
||||
adminDb: SupabaseClient<Database>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,7 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
return {
|
||||
user: opts.user,
|
||||
db: opts.db,
|
||||
adminDb: opts.adminDb,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -61,12 +63,13 @@ export const createTRPCContext = async ({
|
||||
resHeaders,
|
||||
}: FetchCreateContextFnOptions) => {
|
||||
const db = createTRPCClient(req, resHeaders);
|
||||
const adminDb = createTRPCAdminClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await db.auth.getUser();
|
||||
|
||||
return createInnerTRPCContext({ db, user });
|
||||
return createInnerTRPCContext({ db, adminDb, user });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user