From 936e554e934bc36a561c3ba4972cd68b299cc4ac Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 17 Oct 2024 21:37:51 +0100 Subject: [PATCH] feat: invite members --- bun.lockb | Bin 353096 -> 353565 bytes package.json | 2 +- src/email/sendEmail.tsx | 42 + src/email/templates/join-workspace.tsx | 100 ++ .../{emails => templates}/magic-link.tsx | 4 +- src/server/api/root.ts | 2 + src/server/api/routers/member.ts | 128 +++ src/server/api/trpc.ts | 7 +- .../migrations/0003_naive_secret_warriors.sql | 22 + .../db/migrations/meta/0003_snapshot.json | 1018 +++++++++++++++++ src/server/db/migrations/meta/_journal.json | 9 +- src/server/db/repository/member.repo.ts | 30 + src/server/db/repository/user.repo.ts | 28 + src/server/db/repository/workspace.repo.ts | 6 +- src/server/db/schema.ts | 6 + src/types/database.types.ts | 5 + src/types/router.types.ts | 1 + src/utils/helpers.ts | 22 + src/utils/supabase/api.ts | 10 + .../members/components/InviteMemberForm.tsx | 69 ++ src/views/members/index.tsx | 79 +- supabase/seed.sql | 46 +- 22 files changed, 1588 insertions(+), 48 deletions(-) create mode 100644 src/email/sendEmail.tsx create mode 100644 src/email/templates/join-workspace.tsx rename src/email/{emails => templates}/magic-link.tsx (95%) create mode 100644 src/server/api/routers/member.ts create mode 100644 src/server/db/migrations/0003_naive_secret_warriors.sql create mode 100644 src/server/db/migrations/meta/0003_snapshot.json create mode 100644 src/server/db/repository/member.repo.ts create mode 100644 src/views/members/components/InviteMemberForm.tsx diff --git a/bun.lockb b/bun.lockb index ebf79ef3a798f0c9865bb3839c2b0a45cc790246..807725d787664f0fc575e81c37df7ced1dead24d 100755 GIT binary patch delta 62944 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 NOwF@i1)F{-{tu0|ud4t6 delta 63513 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 diff --git a/package.json b/package.json index fa5799a1..524f6547 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@edge-runtime/cookies": "^4.1.1", "@headlessui/react": "^1.7.17", "@hookform/resolvers": "^3.3.4", - "@react-email/render": "^0.0.10", + "@react-email/render": "^1.0.1", "@supabase/ssr": "^0.3.0", "@supabase/supabase-js": "^2.42.0", "@t3-oss/env-nextjs": "^0.9.2", diff --git a/src/email/sendEmail.tsx b/src/email/sendEmail.tsx new file mode 100644 index 00000000..55aa3419 --- /dev/null +++ b/src/email/sendEmail.tsx @@ -0,0 +1,42 @@ +import MagicLinkTemplate from "~/email/templates/magic-link"; +import JoinWorkspaceTemplate from "~/email/templates/join-workspace"; + +import { render } from "@react-email/render"; + +type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE"; + +const emailTemplates: Record = { + MAGIC_LINK: MagicLinkTemplate, + JOIN_WORKSPACE: JoinWorkspaceTemplate, +}; + +export const sendEmail = async ( + to: string, + subject: string, + template: Templates, + data: Record, +) => { + const EmailTemplate = emailTemplates[template]; + + const html = await render(, { pretty: true }); + + const response = await fetch(process.env.EMAIL_URL!, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.EMAIL_TOKEN}`, + }, + body: JSON.stringify({ + from: process.env.EMAIL_FROM, + to, + subject, + html, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to send email: ${response.statusText}`); + } + + return response; +}; diff --git a/src/email/templates/join-workspace.tsx b/src/email/templates/join-workspace.tsx new file mode 100644 index 00000000..b2d56f02 --- /dev/null +++ b/src/email/templates/join-workspace.tsx @@ -0,0 +1,100 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Html, + Hr, + Link, + Preview, + Tailwind, + Text, +} from "@react-email/components"; + +import * as React from "react"; + +export const JoinWorkspaceTemplate = ({ + magicLoginUrl, +}: { + magicLoginUrl?: string; +}) => ( + + + + Log in with this magic link + + + + kan.bn + + + Login to your Kan account + + + Click the button below to instantly login to your account. + + + + If you didn't try to login, you can safely ignore this email. + +


+ + + Kan + + , the open source Trello alternative. + + + + + +); + +export default JoinWorkspaceTemplate; diff --git a/src/email/emails/magic-link.tsx b/src/email/templates/magic-link.tsx similarity index 95% rename from src/email/emails/magic-link.tsx rename to src/email/templates/magic-link.tsx index ed0304b8..99cf01f3 100644 --- a/src/email/emails/magic-link.tsx +++ b/src/email/templates/magic-link.tsx @@ -18,7 +18,7 @@ interface MagicLinkEmailProps { loginUrl?: string; } -export const MagicLinkEmail = ({ loginUrl }: MagicLinkEmailProps) => ( +export const MagicLinkTemplate = ({ loginUrl }: MagicLinkEmailProps) => ( ( ); -export default MagicLinkEmail; +export default MagicLinkTemplate; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index f5282ca5..de59247e 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -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, }); diff --git a/src/server/api/routers/member.ts b/src/server/api/routers/member.ts new file mode 100644 index 00000000..6130d8a4 --- /dev/null +++ b/src/server/api/routers/member.ts @@ -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; + }), +}); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index ad1f8701..b5ef9d07 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -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; + adminDb: SupabaseClient; } /** @@ -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 }); }; /** diff --git a/src/server/db/migrations/0003_naive_secret_warriors.sql b/src/server/db/migrations/0003_naive_secret_warriors.sql new file mode 100644 index 00000000..e510124b --- /dev/null +++ b/src/server/db/migrations/0003_naive_secret_warriors.sql @@ -0,0 +1,22 @@ +DO $$ BEGIN + CREATE TYPE "member_status" AS ENUM('invited', 'active', 'removed'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "_card_workspace_members" DROP CONSTRAINT "_card_workspace_members_cardId_card_id_fk"; +--> statement-breakpoint +ALTER TABLE "_card_labels" DROP CONSTRAINT "_card_labels_cardId_card_id_fk"; +--> statement-breakpoint +ALTER TABLE "workspace_members" ADD COLUMN "status" "member_status" DEFAULT 'invited' NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_card_workspace_members" ADD CONSTRAINT "_card_workspace_members_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "_card_labels" ADD CONSTRAINT "_card_labels_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "card"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/meta/0003_snapshot.json b/src/server/db/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..ad09b83b --- /dev/null +++ b/src/server/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,1018 @@ +{ + "version": "5", + "dialect": "pg", + "id": "4e3f672e-52ef-4b8c-8628-19f6db49d7c4", + "prevId": "3cce2217-3ea4-4132-bf7c-1c028901693c", + "tables": { + "board": { + "name": "board", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "board_createdBy_user_id_fk": { + "name": "board_createdBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_deletedBy_user_id_fk": { + "name": "board_deletedBy_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_importId_import_id_fk": { + "name": "board_importId_import_id_fk", + "tableFrom": "board", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "board_workspaceId_workspace_id_fk": { + "name": "board_workspaceId_workspace_id_fk", + "tableFrom": "board", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "board_publicId_unique": { + "name": "board_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_workspace_members": { + "name": "_card_workspace_members", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "workspaceMemberId": { + "name": "workspaceMemberId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_workspace_members_cardId_card_id_fk": { + "name": "_card_workspace_members_cardId_card_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "_card_workspace_members_workspaceMemberId_workspace_members_id_fk": { + "name": "_card_workspace_members_workspaceMemberId_workspace_members_id_fk", + "tableFrom": "_card_workspace_members", + "tableTo": "workspace_members", + "columnsFrom": [ + "workspaceMemberId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_workspace_members_cardId_workspaceMemberId": { + "name": "_card_workspace_members_cardId_workspaceMemberId", + "columns": [ + "cardId", + "workspaceMemberId" + ] + } + }, + "uniqueConstraints": {} + }, + "card": { + "name": "card", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "listId": { + "name": "listId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "card_createdBy_user_id_fk": { + "name": "card_createdBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_deletedBy_user_id_fk": { + "name": "card_deletedBy_user_id_fk", + "tableFrom": "card", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_listId_list_id_fk": { + "name": "card_listId_list_id_fk", + "tableFrom": "card", + "tableTo": "list", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "card_importId_import_id_fk": { + "name": "card_importId_import_id_fk", + "tableFrom": "card", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "card_publicId_unique": { + "name": "card_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "_card_labels": { + "name": "_card_labels", + "schema": "", + "columns": { + "cardId": { + "name": "cardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "labelId": { + "name": "labelId", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "_card_labels_cardId_card_id_fk": { + "name": "_card_labels_cardId_card_id_fk", + "tableFrom": "_card_labels", + "tableTo": "card", + "columnsFrom": [ + "cardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "_card_labels_labelId_label_id_fk": { + "name": "_card_labels_labelId_label_id_fk", + "tableFrom": "_card_labels", + "tableTo": "label", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "_card_labels_cardId_labelId": { + "name": "_card_labels_cardId_labelId", + "columns": [ + "cardId", + "labelId" + ] + } + }, + "uniqueConstraints": {} + }, + "import": { + "name": "import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "source", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "import_createdBy_user_id_fk": { + "name": "import_createdBy_user_id_fk", + "tableFrom": "import", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "import_publicId_unique": { + "name": "import_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "label": { + "name": "label", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "colourCode": { + "name": "colourCode", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "label_createdBy_user_id_fk": { + "name": "label_createdBy_user_id_fk", + "tableFrom": "label", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "label_boardId_board_id_fk": { + "name": "label_boardId_board_id_fk", + "tableFrom": "label", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "label_importId_import_id_fk": { + "name": "label_importId_import_id_fk", + "tableFrom": "label", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_publicId_unique": { + "name": "label_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "list": { + "name": "list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boardId": { + "name": "boardId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "importId": { + "name": "importId", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "list_createdBy_user_id_fk": { + "name": "list_createdBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_deletedBy_user_id_fk": { + "name": "list_deletedBy_user_id_fk", + "tableFrom": "list", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "list_boardId_board_id_fk": { + "name": "list_boardId_board_id_fk", + "tableFrom": "list", + "tableTo": "board", + "columnsFrom": [ + "boardId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "list_importId_import_id_fk": { + "name": "list_importId_import_id_fk", + "tableFrom": "list", + "tableTo": "import", + "columnsFrom": [ + "importId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "list_publicId_unique": { + "name": "list_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspaceId": { + "name": "workspaceId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "role", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "member_status", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_members_userId_user_id_fk": { + "name": "workspace_members_userId_user_id_fk", + "tableFrom": "workspace_members", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_members_workspaceId_workspace_id_fk": { + "name": "workspace_members_workspaceId_workspace_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspace", + "columnsFrom": [ + "workspaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_members_publicId_unique": { + "name": "workspace_members_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + } + }, + "workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deletedBy": { + "name": "deletedBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_createdBy_user_id_fk": { + "name": "workspace_createdBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_deletedBy_user_id_fk": { + "name": "workspace_deletedBy_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "deletedBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_publicId_unique": { + "name": "workspace_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + }, + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + } + } + }, + "enums": { + "source": { + "name": "source", + "values": { + "trello": "trello" + } + }, + "status": { + "name": "status", + "values": { + "started": "started", + "success": "success", + "failed": "failed" + } + }, + "role": { + "name": "role", + "values": { + "admin": "admin", + "member": "member", + "guest": "guest" + } + }, + "member_status": { + "name": "member_status", + "values": { + "invited": "invited", + "active": "active", + "removed": "removed" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index e1ddc2ef..5bcd3a2e 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1724967733894, "tag": "0002_clever_robin_chapel", "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1728246215706, + "tag": "0003_naive_secret_warriors", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/server/db/repository/member.repo.ts b/src/server/db/repository/member.repo.ts new file mode 100644 index 00000000..a87c8684 --- /dev/null +++ b/src/server/db/repository/member.repo.ts @@ -0,0 +1,30 @@ +import { generateUID } from "~/utils/generateUID"; +import { type Database } from "~/types/database.types"; +import { type SupabaseClient } from "@supabase/supabase-js"; + +export const create = async ( + db: SupabaseClient, + memberInput: { + userId: string; + workspaceId: number; + createdBy: string; + role: "admin" | "member" | "guest"; + status: "invited" | "active" | "removed"; + }, +) => { + const { data } = await db + .from("workspace_members") + .insert({ + publicId: generateUID(), + userId: memberInput.userId, + workspaceId: memberInput.workspaceId, + createdBy: memberInput.createdBy, + role: memberInput.role, + status: memberInput.status, + }) + .select(`id, publicId`) + .limit(1) + .single(); + + return data; +}; diff --git a/src/server/db/repository/user.repo.ts b/src/server/db/repository/user.repo.ts index ccaa4b19..360e0371 100644 --- a/src/server/db/repository/user.repo.ts +++ b/src/server/db/repository/user.repo.ts @@ -11,3 +11,31 @@ export const getById = async (db: SupabaseClient, userId: string) => { return data; }; + +export const getByEmail = async ( + db: SupabaseClient, + email: string, +) => { + const { data } = await db + .from("user") + .select(`id, name, email`) + .eq("email", email) + .limit(1) + .single(); + + return data; +}; + +export const create = async ( + db: SupabaseClient, + user: { id: string; email: string }, +) => { + const { data } = await db + .from("user") + .insert({ id: user.id, email: user.email }) + .select() + .limit(1) + .single(); + + return data; +}; diff --git a/src/server/db/repository/workspace.repo.ts b/src/server/db/repository/workspace.repo.ts index f99dff7f..924f9b14 100644 --- a/src/server/db/repository/workspace.repo.ts +++ b/src/server/db/repository/workspace.repo.ts @@ -75,6 +75,7 @@ export const getByPublicIdWithMembers = async ( .from("workspace") .select( ` + id, publicId, members: workspace_members ( publicId, @@ -89,6 +90,7 @@ export const getByPublicIdWithMembers = async ( ) .eq("publicId", workspacePublicId) .is("deletedAt", null) + .is("members.deletedAt", null) .limit(1) .single(); @@ -99,7 +101,7 @@ export const getAllByUserId = async ( db: SupabaseClient, userId: string, ) => { - const { data } = await db + const { data, error } = await db .from("workspace_members") .select( ` @@ -113,6 +115,8 @@ export const getAllByUserId = async ( .eq("userId", userId) .is("deletedAt", null); + console.log({ error }); + return data; }; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index be2396a0..6331cec6 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -19,6 +19,11 @@ export const importStatusEnum = pgEnum("status", [ "failed", ]); export const memberRoleEnum = pgEnum("role", ["admin", "member", "guest"]); +export const memberStatusEnum = pgEnum("member_status", [ + "invited", + "active", + "removed", +]); export const boards = pgTable("board", { id: bigserial("id", { mode: "number" }).primaryKey(), @@ -294,6 +299,7 @@ export const workspaceMembers = pgTable("workspace_members", { updatedAt: timestamp("updatedAt"), deletedAt: timestamp("deletedAt"), role: memberRoleEnum("role").notNull(), + status: memberStatusEnum("status").default("invited").notNull(), }); export const usersToWorkspacesRelations = relations( diff --git a/src/types/database.types.ts b/src/types/database.types.ts index c1614345..8e3532a3 100644 --- a/src/types/database.types.ts +++ b/src/types/database.types.ts @@ -458,6 +458,7 @@ export type Database = { id: number publicId: string role: Database["public"]["Enums"]["role"] + status: Database["public"]["Enums"]["member_status"] updatedAt: string | null userId: string workspaceId: number @@ -469,6 +470,7 @@ export type Database = { id?: number publicId: string role: Database["public"]["Enums"]["role"] + status?: Database["public"]["Enums"]["member_status"] updatedAt?: string | null userId: string workspaceId: number @@ -480,6 +482,7 @@ export type Database = { id?: number publicId?: string role?: Database["public"]["Enums"]["role"] + status?: Database["public"]["Enums"]["member_status"] updatedAt?: string | null userId?: string workspaceId?: number @@ -548,9 +551,11 @@ export type Database = { } } Enums: { + member_status: "invited" | "active" | "removed" role: "admin" | "member" | "guest" source: "trello" status: "started" | "success" | "failed" + workspace_invite_status: "pending" | "accepted" | "cancelled" } CompositeTypes: { [_ in never]: never diff --git a/src/types/router.types.ts b/src/types/router.types.ts index 3366de8f..9762d694 100644 --- a/src/types/router.types.ts +++ b/src/types/router.types.ts @@ -8,3 +8,4 @@ export type NewLabelInput = RouterInputs["label"]["create"]; export type NewListInput = RouterInputs["list"]["create"]; export type NewCardInput = RouterInputs["card"]["create"]; export type NewBoardInput = RouterInputs["board"]["create"]; +export type InviteMemberInput = RouterInputs["member"]["invite"]; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index f849c4cc..19d11c15 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -6,3 +6,25 @@ export const formatToArray = ( } return value ? [value] : []; }; + +export const inferInitialsFromEmail = (email: string) => { + const localPart = email.split("@")[0]; + if (!localPart) return ""; + const separators = /[._-]/; + const parts = localPart.split(separators); + + if (parts.length > 1) { + return ( + (parts[0]?.[0] ?? "") + (parts[parts.length - 1]?.[0] ?? "") + ).toUpperCase(); + } else { + return localPart.slice(0, 2).toUpperCase(); + } +}; + +export const getInitialsFromName = (name: string) => { + return name + .split(" ") + .map((namePart) => namePart.charAt(0).toUpperCase()) + .join(""); +}; diff --git a/src/utils/supabase/api.ts b/src/utils/supabase/api.ts index 89f60fc3..8c45a0d4 100644 --- a/src/utils/supabase/api.ts +++ b/src/utils/supabase/api.ts @@ -56,3 +56,13 @@ export function createTRPCClient(req: Request, resHeaders: Headers) { return supabase; } + +export function createTRPCAdminClient() { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_API_KEY!, + { cookies: {} }, + ); + + return supabase; +} diff --git a/src/views/members/components/InviteMemberForm.tsx b/src/views/members/components/InviteMemberForm.tsx new file mode 100644 index 00000000..7cac3c5f --- /dev/null +++ b/src/views/members/components/InviteMemberForm.tsx @@ -0,0 +1,69 @@ +import { api } from "~/utils/api"; +import { HiXMark } from "react-icons/hi2"; +import { useModal } from "~/providers/modal"; +import { useWorkspace } from "~/providers/workspace"; +import { useForm } from "react-hook-form"; +import { type InviteMemberInput } from "~/types/router.types"; + +export function InviteMemberForm() { + const utils = api.useUtils(); + const { closeModal } = useModal(); + const { workspace } = useWorkspace(); + + const { register, handleSubmit } = useForm({ + defaultValues: { + email: "", + workspacePublicId: workspace?.publicId || "", + }, + }); + + const refetchBoards = () => utils.board.all.refetch(); + + const createBoard = api.member.invite.useMutation({ + onSuccess: async () => { + closeModal(); + await utils.workspace.byId.refetch(); + await refetchBoards(); + }, + }); + + const onSubmit = (data: InviteMemberInput) => { + createBoard.mutate(data); + }; + + return ( +
+
+
+

Add member

+ +
+ +
+ +
+
+ +
+
+
+ ); +} diff --git a/src/views/members/index.tsx b/src/views/members/index.tsx index 5e5dc896..0f92b0c7 100644 --- a/src/views/members/index.tsx +++ b/src/views/members/index.tsx @@ -4,11 +4,12 @@ import { useWorkspace } from "~/providers/workspace"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; - +import { InviteMemberForm } from "./components/InviteMemberForm"; import { api } from "~/utils/api"; +import { getInitialsFromName, inferInitialsFromEmail } from "~/utils/helpers"; export default function MembersPage() { - const { modalContentType } = useModal(); + const { modalContentType, openModal } = useModal(); const { workspace } = useWorkspace(); const { data } = api.workspace.byId.useQuery( @@ -26,7 +27,7 @@ export default function MembersPage() {