diff --git a/.vscode/settings.json b/.vscode/settings.json index 22073e71..ab243a37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "spellright.addToSystemDictionary": true + "spellright.addToSystemDictionary": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, } \ No newline at end of file diff --git a/src/server/api/root.ts b/src/server/api/root.ts index b7c4b71e..f5282ca5 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -33,4 +33,4 @@ export type AppRouter = typeof appRouter; * ^? Post[] */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -// export const createCaller = createCallerFactory(appRouter); \ No newline at end of file +// export const createCaller = createCallerFactory(appRouter); diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 04c932fa..91eea7c2 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -1,9 +1,6 @@ import { z } from "zod"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; export const authRouter = createTRPCRouter({ login: publicProcedure @@ -12,10 +9,10 @@ export const authRouter = createTRPCRouter({ const { data } = await ctx.db.auth.signInWithOtp({ email: input.email, options: { - emailRedirectTo: '/boards', + emailRedirectTo: "/boards", }, - }) + }); return data; - }) -}); \ No newline at end of file + }), +}); diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 52e13974..e1faa642 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -1,98 +1,94 @@ import { z } from "zod"; import { generateUID } from "~/utils/generateUID"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const boardRouter = createTRPCRouter({ - all: publicProcedure + all: protectedProcedure .input(z.object({ workspacePublicId: z.string().min(12) })) .query(async ({ ctx, input }) => { const workspace = await ctx.db - .from('workspace') + .from("workspace") .select(`id`) - .eq('publicId', input.workspacePublicId) + .eq("publicId", input.workspacePublicId) .limit(1) .single(); if (!workspace.data) return; const { data } = await ctx.db - .from('board') - .select(` - publicId, - name - `) - .is('deletedAt', null) - .eq('workspaceId', workspace.data.id); + .from("board") + .select(`publicId, name`) + .is("deletedAt", null) + .eq("workspaceId", workspace.data.id); return data; }), - byId: publicProcedure + byId: protectedProcedure .input(z.object({ id: z.string().min(12) })) .query(async ({ ctx, input }) => { const { data } = await ctx.db - .from('board') - .select(` - publicId, - name, - workspace ( - publicId, - members:workspace_members ( - publicId, - user ( - name - ) - ) - ), - labels:label ( + .from("board") + .select( + ` publicId, name, - colourCode - ), - lists:list ( - publicId, - name, - boardId, - index, - cards:card ( + workspace ( publicId, - title, - description, - listId, - index, - labels:label ( - publicId, - name, - colourCode - ), members:workspace_members ( publicId, user ( name ) ) + ), + labels:label ( + publicId, + name, + colourCode + ), + lists:list ( + publicId, + name, + boardId, + index, + cards:card ( + publicId, + title, + description, + listId, + index, + labels:label ( + publicId, + name, + colourCode + ), + members:workspace_members ( + publicId, + user ( + name + ) + ) + ) ) - ) - `) - .eq('publicId', input.id) - .is('deletedAt', null) - .is('lists.deletedAt', null) - .is('lists.cards.deletedAt', null) - .order('index', { foreignTable: 'list', ascending: true }) - .order('index', { foreignTable: 'list.card', ascending: true }) + `, + ) + .eq("publicId", input.id) + .is("deletedAt", null) + .is("lists.deletedAt", null) + .is("lists.cards.deletedAt", null) + .order("index", { foreignTable: "list", ascending: true }) + .order("index", { foreignTable: "list.card", ascending: true }) .limit(1) - .single() + .single(); return data; }), - create: publicProcedure + create: protectedProcedure .input( z.object({ name: z.string().min(1), - workspacePublicId: z.string().min(12) + workspacePublicId: z.string().min(12), }), ) .mutation(async ({ ctx, input }) => { @@ -101,91 +97,81 @@ export const boardRouter = createTRPCRouter({ if (!userId) return; const workspace = await ctx.db - .from('workspace') + .from("workspace") .select(`id`) - .eq('publicId', input.workspacePublicId) + .eq("publicId", input.workspacePublicId) .limit(1) .single(); if (!workspace.data) return; const { data } = await ctx.db - .from('board') + .from("board") .insert({ publicId: generateUID(), name: input.name, createdBy: userId, - workspaceId: workspace.data.id + workspaceId: workspace.data.id, }) - .select(` - publicId, - name - `); + .select(`publicId, name`); return data; }), - update: publicProcedure - .input( - z.object({ - boardId: z.string().min(12), - name: z.string().min(1), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; - - const { data } = await ctx.db - .from('board') - .update({ name: input.name }) - .eq('publicId', input.boardId); - - return data; + update: protectedProcedure + .input( + z.object({ + boardId: z.string().min(12), + name: z.string().min(1), }), - delete: publicProcedure - .input( - z.object({ - boardPublicId: z.string().min(12), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; + ) + .mutation(async ({ ctx, input }) => { + const { data } = await ctx.db + .from("board") + .update({ name: input.name }) + .eq("publicId", input.boardId); - const board = await ctx.db - .from('board') - .select(` - id, - lists:list (id) - `) - .eq('publicId', input.boardPublicId) - .limit(1) - .single(); - - if (!board.data) return; + return data; + }), + delete: protectedProcedure + .input( + z.object({ + boardPublicId: z.string().min(12), + }), + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; - const listIds = board.data.lists.map((list) => list.id); + const board = await ctx.db + .from("board") + .select(`id, lists:list (id)`) + .eq("publicId", input.boardPublicId) + .limit(1) + .single(); - const deletedAt = new Date().toISOString(); + if (!board.data) return; + + const listIds = board.data.lists.map((list) => list.id); + + const deletedAt = new Date().toISOString(); + + await ctx.db + .from("board") + .update({ deletedAt, deletedBy: userId }) + .eq("id", board.data.id) + .is("deletedAt", null); + + if (listIds.length) { + await ctx.db + .from("list") + .update({ deletedAt, deletedBy: userId }) + .eq("boardId", board.data.id) + .is("deletedAt", null); await ctx.db - .from('board') + .from("card") .update({ deletedAt, deletedBy: userId }) - .eq('id', board.data.id) - .is('deletedAt', null); - - if (listIds.length) { - await ctx.db - .from('list') - .update({ deletedAt, deletedBy: userId }) - .eq('boardId', board.data.id) - .is('deletedAt', null); - - await ctx.db - .from('card') - .update({ deletedAt, deletedBy: userId }) - .in('listId', listIds) - .is('deletedAt', null); - } - }) + .in("listId", listIds) + .is("deletedAt", null); + } + }), }); diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index ba70c735..c01e557e 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -1,19 +1,16 @@ import { z } from "zod"; import { generateUID } from "~/utils/generateUID"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const cardRouter = createTRPCRouter({ - create: publicProcedure + create: protectedProcedure .input( z.object({ title: z.string().min(1), listPublicId: z.string().min(12), labelsPublicIds: z.array(z.string().min(12)), - memberPublicIds: z.array(z.string().min(12)) + memberPublicIds: z.array(z.string().min(12)), }), ) .mutation(async ({ ctx, input }) => { @@ -22,25 +19,25 @@ export const cardRouter = createTRPCRouter({ if (!userId) return; const list = await ctx.db - .from('list') + .from("list") .select(`id, cards:card (index)`) - .eq('publicId', input.listPublicId) - .order('index', { foreignTable: 'card', ascending: false }) + .eq("publicId", input.listPublicId) + .order("index", { foreignTable: "card", ascending: false }) .limit(1) .single(); if (!list.data?.id) return; - const latestCard = list.data.cards.length && list.data.cards[0] + const latestCard = list.data.cards.length && list.data.cards[0]; const newCard = await ctx.db - .from('card') + .from("card") .insert({ publicId: generateUID(), title: input.title, createdBy: userId, listId: list.data.id, - index: latestCard ? latestCard.index + 1 : 0 + index: latestCard ? latestCard.index + 1 : 0, }) .select(`id`) .limit(1) @@ -49,153 +46,152 @@ export const cardRouter = createTRPCRouter({ const newCardId = newCard.data?.id; if (newCardId && input.labelsPublicIds.length) { - const labels = await ctx.db - .from('label') - .select(`id`) - .eq('publicId', input.labelsPublicIds); + const labels = await ctx.db + .from("label") + .select(`id`) + .eq("publicId", input.labelsPublicIds); if (!labels.data?.length) return; - const labelsInsert = labels.data.map((label) => ({ cardId: newCardId, labelId: label.id })); + const labelsInsert = labels.data.map((label) => ({ + cardId: newCardId, + labelId: label.id, + })); - await ctx.db - .from('_card_labels') - .insert(labelsInsert); + await ctx.db.from("_card_labels").insert(labelsInsert); } if (newCardId && input.memberPublicIds.length) { const members = await ctx.db - .from('workspace_members') + .from("workspace_members") .select(`id`) - .eq('publicId', input.memberPublicIds); + .eq("publicId", input.memberPublicIds); if (!members.data?.length) return; - const membersInsert = members.data.map((member) => ({ cardId: newCardId, workspaceMemberId: member.id })); + const membersInsert = members.data.map((member) => ({ + cardId: newCardId, + workspaceMemberId: member.id, + })); - await ctx.db - .from('_card_workspace_members') - .insert(membersInsert); + await ctx.db.from("_card_workspace_members").insert(membersInsert); } return newCard; }), - addOrRemoveLabel: publicProcedure - .input( - z.object({ - cardPublicId: z.string().min(12), - labelPublicId: z.string().min(12), - }), - ) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; - - const card = await ctx.db - .from('card') - .select(`id`) - .eq('publicId', input.cardPublicId) - .limit(1) - .single(); - - const label = await ctx.db - .from('label') - .select(`id`) - .eq('publicId', input.labelPublicId) - .limit(1) - .single(); - - if (!card.data || !label.data) return; - - const existingLabel = await ctx.db - .from('_card_labels') - .select() - .eq('cardId', card.data.id) - .eq('labelId', label.data.id) - .limit(1) - .single(); - - if (existingLabel.data) { - await ctx.db - .from('_card_labels') - .delete() - .eq('cardId', card.data.id) - .eq('labelId', label.data.id); - - return { newLabel: false }; - } - - await ctx.db - .from('_card_labels') - .insert({ - cardId: card.data.id, - labelId: label.data.id, - }); - - return { newLabel: true }; + addOrRemoveLabel: protectedProcedure + .input( + z.object({ + cardPublicId: z.string().min(12), + labelPublicId: z.string().min(12), }), - addOrRemoveMember: publicProcedure - .input( - z.object({ - cardPublicId: z.string().min(12), - workspaceMemberPublicId: z.string().min(12), - }), - ) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; - if (!userId) return; + if (!userId) return; - const card = await ctx.db - .from('card') - .select(`id`) - .eq('publicId', input.cardPublicId) - .limit(1) - .single(); + const card = await ctx.db + .from("card") + .select(`id`) + .eq("publicId", input.cardPublicId) + .limit(1) + .single(); - const member = await ctx.db - .from('workspace_members') - .select(`id`) - .eq('publicId', input.workspaceMemberPublicId) - .limit(1) - .single(); - - if (!card.data || !member.data) return; + const label = await ctx.db + .from("label") + .select(`id`) + .eq("publicId", input.labelPublicId) + .limit(1) + .single(); - const existingMember = await ctx.db - .from('_card_workspace_members') - .select() - .eq('cardId', card.data.id) - .eq('workspaceMemberId', member.data.id) - .limit(1) - .single(); + if (!card.data || !label.data) return; - if (existingMember.data) { - await ctx.db - .from('_card_workspace_members') - .delete() - .eq('cardId', card.data.id) - .eq('workspaceMemberId', member.data.id); - - return { newMember: false }; - } + const existingLabel = await ctx.db + .from("_card_labels") + .select() + .eq("cardId", card.data.id) + .eq("labelId", label.data.id) + .limit(1) + .single(); + if (existingLabel.data) { await ctx.db - .from('_card_workspace_members') - .insert({ - cardId: card.data.id, - workspaceMemberId: member.data.id, - }); + .from("_card_labels") + .delete() + .eq("cardId", card.data.id) + .eq("labelId", label.data.id); - return { newMember: true }; + return { newLabel: false }; + } + + await ctx.db.from("_card_labels").insert({ + cardId: card.data.id, + labelId: label.data.id, + }); + + return { newLabel: true }; + }), + addOrRemoveMember: protectedProcedure + .input( + z.object({ + cardPublicId: z.string().min(12), + workspaceMemberPublicId: z.string().min(12), }), - byId: publicProcedure - .input(z.object({ id: z.string().min(12) })) - .query(async ({ ctx, input }) => { - const { data } = await ctx.db - .from('card') - .select(` + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) return; + + const card = await ctx.db + .from("card") + .select(`id`) + .eq("publicId", input.cardPublicId) + .limit(1) + .single(); + + const member = await ctx.db + .from("workspace_members") + .select(`id`) + .eq("publicId", input.workspaceMemberPublicId) + .limit(1) + .single(); + + if (!card.data || !member.data) return; + + const existingMember = await ctx.db + .from("_card_workspace_members") + .select() + .eq("cardId", card.data.id) + .eq("workspaceMemberId", member.data.id) + .limit(1) + .single(); + + if (existingMember.data) { + await ctx.db + .from("_card_workspace_members") + .delete() + .eq("cardId", card.data.id) + .eq("workspaceMemberId", member.data.id); + + return { newMember: false }; + } + + await ctx.db.from("_card_workspace_members").insert({ + cardId: card.data.id, + workspaceMemberId: member.data.id, + }); + + return { newMember: true }; + }), + byId: protectedProcedure + .input(z.object({ id: z.string().min(12) })) + .query(async ({ ctx, input }) => { + const { data } = await ctx.db + .from("card") + .select( + ` publicId, title, description, @@ -238,127 +234,131 @@ export const cardRouter = createTRPCRouter({ name ) ) - `) - .eq('publicId', input.id) - .is('deletedAt', null) - .is('list.board.lists.deletedAt', null) - .limit(1) - .single(); + `, + ) + .eq("publicId", input.id) + .is("deletedAt", null) + .is("list.board.lists.deletedAt", null) + .limit(1) + .single(); - return data; + return data; + }), + update: protectedProcedure + .input( + z.object({ + cardId: z.string().min(12), + title: z.string().min(1), + description: z.string(), }), - update: publicProcedure - .input( - z.object({ - cardId: z.string().min(12), - title: z.string().min(1), - description: z.string(), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; - if (!userId) return; + if (!userId) return; - const { data } = await ctx.db - .from('card') - .update({ title: input.title, description: input.description }) - .eq('publicId', input.cardId) - .is('deletedAt', null); + const { data } = await ctx.db + .from("card") + .update({ title: input.title, description: input.description }) + .eq("publicId", input.cardId) + .is("deletedAt", null); - return data; + return data; + }), + delete: protectedProcedure + .input( + z.object({ + cardPublicId: z.string().min(12), }), - delete: publicProcedure - .input( - z.object({ - cardPublicId: z.string().min(12), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; - if (!userId) return; + if (!userId) return; - const card = await ctx.db - .from('card') - .select(`id, index, listId`) - .eq('publicId', input.cardPublicId) - .limit(1) - .single(); + const card = await ctx.db + .from("card") + .select(`id, index, listId`) + .eq("publicId", input.cardPublicId) + .limit(1) + .single(); - if (!card.data) return; + if (!card.data) return; - const deletedAt = new Date().toISOString(); + const deletedAt = new Date().toISOString(); - await ctx.db - .from('card') - .update({ deletedAt, deletedBy: userId}) - .eq('publicId', input.cardPublicId); + await ctx.db + .from("card") + .update({ deletedAt, deletedBy: userId }) + .eq("publicId", input.cardPublicId); - await ctx.db - .from('card') - .update({ deletedAt, deletedBy: userId}) - .eq('publicId', input.cardPublicId); + await ctx.db + .from("card") + .update({ deletedAt, deletedBy: userId }) + .eq("publicId", input.cardPublicId); - await ctx.db - .rpc('shift_card_index', { - list_id: card.data.listId, - card_index: card.data.index, - }); + await ctx.db.rpc("shift_card_index", { + list_id: card.data.listId, + card_index: card.data.index, + }); + }), + reorder: protectedProcedure + .input( + z.object({ + cardId: z.string().min(12), + newListId: z.string().min(12), + newIndex: z.number().optional(), }), - reorder: publicProcedure - .input( - z.object({ - cardId: z.string().min(12), - newListId: z.string().min(12), - newIndex: z.number().optional(), - })) - .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; - if (!userId) return; + if (!userId) return; - const card = await ctx.db - .from('card') - .select(`id, index, list (id)`) - .eq('publicId', input.cardId) - .is('deletedAt', null) - .limit(1) - .single(); + const card = await ctx.db + .from("card") + .select(`id, index, list (id)`) + .eq("publicId", input.cardId) + .is("deletedAt", null) + .limit(1) + .single(); - if (!card.data) return; + if (!card.data) return; - const currentList = card.data.list; - const currentIndex = card.data.index; + const currentList = card.data.list; + const currentIndex = card.data.index; - let newIndex = input.newIndex; + let newIndex = input.newIndex; - const newList = await ctx.db - .from('list') - .select(`id, cards:card (index)`) - .eq('publicId', input.newListId) - .is('deletedAt', null) - .order('index', { foreignTable: 'card', ascending: false }) - .limit(1) - .single(); + const newList = await ctx.db + .from("list") + .select(`id, cards:card (index)`) + .eq("publicId", input.newListId) + .is("deletedAt", null) + .order("index", { foreignTable: "card", ascending: false }) + .limit(1) + .single(); - if (!newList.data) return; + if (!newList.data) return; - if (newIndex === undefined) { - const lastCardIndex = newList.data.cards.length ? newList.data.cards[0]?.index : undefined; + if (newIndex === undefined) { + const lastCardIndex = newList.data.cards.length + ? newList.data.cards[0]?.index + : undefined; - newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0; - } + newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0; + } - if (!currentList?.id || !newList?.data.id) return; + if (!currentList?.id || !newList?.data.id) return; - const { error } = await ctx.db - .rpc('reorder_cards', { - current_list_id: currentList.id, - new_list_id: newList.data.id, - current_index: currentIndex, - new_index: newIndex, - card_id: card.data.id, - }); + const { error } = await ctx.db.rpc("reorder_cards", { + current_list_id: currentList.id, + new_list_id: newList.data.id, + current_index: currentIndex, + new_index: newIndex, + card_id: card.data.id, + }); - return { success: !!error } - }) -}); \ No newline at end of file + return { success: !!error }; + }), +}); diff --git a/src/server/api/routers/import.ts b/src/server/api/routers/import.ts index 53207bc3..b708b220 100644 --- a/src/server/api/routers/import.ts +++ b/src/server/api/routers/import.ts @@ -1,13 +1,10 @@ import { z } from "zod"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { generateUID } from "~/utils/generateUID"; -const TRELLO_API_URL = 'https://api.trello.com/1'; +const TRELLO_API_URL = "https://api.trello.com/1"; interface TrelloBoard { id: string; @@ -34,29 +31,29 @@ interface MemberData { export const importRouter = createTRPCRouter({ trello: createTRPCRouter({ - getBoards: publicProcedure + getBoards: protectedProcedure .input( z.object({ apiKey: z.string().length(32), token: z.string().length(76), }), ) - .query(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; + .query(async ({ input }) => { + const fetchMemberRes = await fetch( + `${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`, + ); - const fetchMemberRes = await fetch(`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`) - - const member = await fetchMemberRes.json() as MemberData; + const member = (await fetchMemberRes.json()) as MemberData; const boardIds = member.idBoards; const fetchBoard = async (boardId: string) => { try { - const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`); - const data = await response.json() as TrelloBoard; - + const response = await fetch( + `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`, + ); + const data = (await response.json()) as TrelloBoard; + return data; } catch (error) { throw error; @@ -70,30 +67,33 @@ export const importRouter = createTRPCRouter({ } const boardDataArray = await Promise.all(boards); - - return boardDataArray.map((board) => ({ id: board.id, name: board.name })); + + return boardDataArray.map((board) => ({ + id: board.id, + name: board.name, + })); }), - importBoards: publicProcedure + importBoards: protectedProcedure .input( z.object({ boardIds: z.array(z.string()), apiKey: z.string().length(32), token: z.string().length(76), - workspacePublicId: z.string().min(12) + workspacePublicId: z.string().min(12), }), ) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; - + if (!userId) return; const newImport = await ctx.db - .from('import') + .from("import") .insert({ publicId: generateUID(), - source: 'trello', + source: "trello", createdBy: userId, - status: 'started' + status: "started", }) .select(`id`) .limit(1) @@ -104,18 +104,20 @@ export const importRouter = createTRPCRouter({ let boardsCreated = 0; const workspace = await ctx.db - .from('workspace') + .from("workspace") .select(`id`) - .eq('publicId', input.workspacePublicId) - .is('deletedAt', null) + .eq("publicId", input.workspacePublicId) + .is("deletedAt", null) .limit(1) .single(); if (!workspace.data) return; for (const boardId of input.boardIds) { - const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`); - const data = await response.json() as TrelloBoard; + const response = await fetch( + `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`, + ); + const data = (await response.json()) as TrelloBoard; const formattedData = { name: data.name, @@ -125,25 +127,25 @@ export const importRouter = createTRPCRouter({ .filter((card) => card.idList === list.id) .map((_card) => ({ name: _card.name, - description: _card.desc - })) - })) - } + description: _card.desc, + })), + })), + }; const newBoard = await ctx.db - .from('board') + .from("board") .insert({ publicId: generateUID(), name: data.name, createdBy: userId, importId: newImportId, - workspaceId: workspace.data.id + workspaceId: workspace.data.id, }) .select(`id`) .limit(1) .single(); - const newBoardId = newBoard.data?.id + const newBoardId = newBoard.data?.id; if (!newBoardId) return; @@ -151,50 +153,48 @@ export const importRouter = createTRPCRouter({ for (const list of formattedData.lists) { const newList = await ctx.db - .from('list') + .from("list") .insert({ publicId: generateUID(), name: list.name, createdBy: userId, boardId: newBoardId, index: listIndex, - importId: newImportId + importId: newImportId, }) .select(`id`) .limit(1) .single(); - const newListId = newList.data?.id + const newListId = newList.data?.id; if (list.cards.length && newListId) { - const cardsInsert = list.cards.map((card, index) => ({ + const cardsInsert = list.cards.map((card, index) => ({ publicId: generateUID(), title: card.name, description: card.description, createdBy: userId, listId: newListId, index, - importId: newImportId - })) + importId: newImportId, + })); - await ctx.db - .from('card') - .insert(cardsInsert); + await ctx.db.from("card").insert(cardsInsert); } - listIndex ++ + listIndex++; } - boardsCreated ++ + boardsCreated++; } if (boardsCreated > 0 && newImportId) { await ctx.db - .from('import') - .update({ status: 'success' }) - .eq('importId', newImportId); + .from("import") + .update({ status: "success" }) + .eq("importId", newImportId); } - return { boardsCreated } + return { boardsCreated }; }), - }) -}); \ No newline at end of file + }), +}); diff --git a/src/server/api/routers/label.ts b/src/server/api/routers/label.ts index dde4f3f6..a790d897 100644 --- a/src/server/api/routers/label.ts +++ b/src/server/api/routers/label.ts @@ -1,13 +1,10 @@ import { z } from "zod"; import { generateUID } from "~/utils/generateUID"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const labelRouter = createTRPCRouter({ - create: publicProcedure + create: protectedProcedure .input( z.object({ name: z.string().min(1).max(36), @@ -21,17 +18,17 @@ export const labelRouter = createTRPCRouter({ if (!userId) return; const card = await ctx.db - .from('card') + .from("card") .select(`id, list (boardId)`) - .eq('publicId', input.cardPublicId) - .is('deletedAt', null) + .eq("publicId", input.cardPublicId) + .is("deletedAt", null) .limit(1) .single(); if (!card.data?.list) return; const newLabel = await ctx.db - .from('label') + .from("label") .insert({ publicId: generateUID(), name: input.name, @@ -45,13 +42,11 @@ export const labelRouter = createTRPCRouter({ if (!newLabel.data) return; - await ctx.db - .from('_card_labels') - .insert({ - cardId: card.data.id, - labelId: newLabel.data.id, - }) + await ctx.db.from("_card_labels").insert({ + cardId: card.data.id, + labelId: newLabel.data.id, + }); return newLabel.data; }), -}); \ No newline at end of file +}); diff --git a/src/server/api/routers/list.ts b/src/server/api/routers/list.ts index e9c67a63..8093d8cf 100644 --- a/src/server/api/routers/list.ts +++ b/src/server/api/routers/list.ts @@ -1,17 +1,14 @@ import { z } from "zod"; import { generateUID } from "~/utils/generateUID"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const listRouter = createTRPCRouter({ - create: publicProcedure + create: protectedProcedure .input( z.object({ name: z.string().min(1), - boardPublicId: z.string().min(12) + boardPublicId: z.string().min(12), }), ) .mutation(async ({ ctx, input }) => { @@ -20,80 +17,72 @@ export const listRouter = createTRPCRouter({ if (!userId) return; const board = await ctx.db - .from('board') + .from("board") .select(`id, lists:list (index)`) - .eq('publicId', input.boardPublicId) - .order('index', { foreignTable: 'list', ascending: false }) - .is('list.deletedAt', null) + .eq("publicId", input.boardPublicId) + .order("index", { foreignTable: "list", ascending: false }) + .is("list.deletedAt", null) .limit(1) .single(); if (!board.data) return; - const latestListIndex = board.data.lists[0]?.index + const latestListIndex = board.data.lists[0]?.index; - const { data } = await ctx.db - .from('list') - .insert({ - publicId: generateUID(), - name: input.name, - createdBy: userId, - boardId: board.data.id, - index: latestListIndex ? latestListIndex + 1 : 0 - }) - .select(` + const { data } = await ctx.db.from("list").insert({ + publicId: generateUID(), + name: input.name, + createdBy: userId, + boardId: board.data.id, + index: latestListIndex ? latestListIndex + 1 : 0, + }).select(` publicId, name `); - return data; + return data; }), - reorder: publicProcedure + reorder: protectedProcedure .input( - z.object({ + z.object({ boardId: z.string().min(12), listId: z.string().min(12), currentIndex: z.number(), newIndex: z.number(), - })) + }), + ) .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; - const list = await ctx.db - .from('list') + .from("list") .select(`id, boardId`) - .eq('publicId', input.listId) + .eq("publicId", input.listId) .limit(1) .single(); if (!list?.data) return; - const { data } = await ctx.db - .rpc('reorder_list', { - board_id: list.data.boardId, - list_id: list.data.id, - current_index: input.currentIndex, - new_index: input.newIndex, - }) + const { data } = await ctx.db.rpc("reorder_lists", { + board_id: list.data.boardId, + list_id: list.data.id, + current_index: input.currentIndex, + new_index: input.newIndex, + }); return data; }), - delete: publicProcedure + delete: protectedProcedure .input( - z.object({ + z.object({ listPublicId: z.string().min(12), - })) + }), + ) .mutation(async ({ ctx, input }) => { const userId = ctx.user?.id; - if (!userId) return; - const list = await ctx.db - .from('list') + .from("list") .select(`id, boardId, index`) - .eq('publicId', input.listPublicId) + .eq("publicId", input.listPublicId) .limit(1) .single(); @@ -102,43 +91,39 @@ export const listRouter = createTRPCRouter({ const deletedAt = new Date().toISOString(); await ctx.db - .from('list') + .from("list") .update({ deletedAt, deletedBy: userId }) - .eq('id', list.data.id) - .is('deletedAt', null); + .eq("id", list.data.id) + .is("deletedAt", null); await ctx.db - .from('card') + .from("card") .update({ deletedAt, deletedBy: userId }) - .eq('listId', list.data.id) - .is('deletedAt', null); - - const { data } = await ctx.db - .rpc('shift_list_index', { - board_id: list.data.boardId, - list_index: list.data.index - }) + .eq("listId", list.data.id) + .is("deletedAt", null); + + const { data } = await ctx.db.rpc("shift_list_index", { + board_id: list.data.boardId, + list_index: list.data.index, + }); return data; }), - update: publicProcedure + update: protectedProcedure .input( - z.object({ + z.object({ listPublicId: z.string().min(12), name: z.string().min(1), - })) + }), + ) .mutation(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; - const { data } = await ctx.db - .from('list') + .from("list") .update({ name: input.name }) - .eq('publicId', input.listPublicId) - .is('deletedAt', null) + .eq("publicId", input.listPublicId) + .is("deletedAt", null) .select(`publicId, name`); - return data; + return data; }), -}); \ No newline at end of file +}); diff --git a/src/server/api/routers/workspace.ts b/src/server/api/routers/workspace.ts index 40b8330b..8d45d420 100644 --- a/src/server/api/routers/workspace.ts +++ b/src/server/api/routers/workspace.ts @@ -1,61 +1,57 @@ import { z } from "zod"; import { generateUID } from "~/utils/generateUID"; -import { - createTRPCRouter, - publicProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const workspaceRouter = createTRPCRouter({ - all: publicProcedure - .query(async ({ ctx }) => { - const userId = ctx.user?.id; + all: protectedProcedure.query(async ({ ctx }) => { + const userId = ctx.user?.id; - if (!userId) return; + if (!userId) return; - const { data } = await ctx.db - .from('workspace_members') - .select(` + const { data } = await ctx.db + .from("workspace_members") + .select( + ` role, workspace ( publicId, name ) - `) - .eq('userId', userId) - .is('deletedAt', null); + `, + ) + .eq("userId", userId) + .is("deletedAt", null); - return data; - }), - byId: publicProcedure + return data; + }), + byId: protectedProcedure .input(z.object({ publicId: z.string().min(12) })) .query(async ({ ctx, input }) => { - const userId = ctx.user?.id; - - if (!userId) return; - const { data } = await ctx.db - .from('workspace') - .select(` - publicId, - members: workspace_members ( + .from("workspace") + .select( + ` publicId, - role, - user ( - id, - name, - email + members: workspace_members ( + publicId, + role, + user ( + id, + name, + email + ) ) - ) - `) - .eq('publicId', input.publicId) - .is('deletedAt', null) + `, + ) + .eq("publicId", input.publicId) + .is("deletedAt", null) .limit(1) .single(); return data; }), - create: publicProcedure + create: protectedProcedure .input( z.object({ name: z.string().min(1), @@ -67,34 +63,28 @@ export const workspaceRouter = createTRPCRouter({ if (!userId) return; const workspace = await ctx.db - .from('workspace') + .from("workspace") .insert({ publicId: generateUID(), name: input.name, slug: input.name.toLowerCase(), createdBy: userId, }) - .select(` - id, - publicId, - name - `) + .select(`id, publicId, name`) .limit(1) - .single() + .single(); - const newWorkspaceId = workspace.data?.id + const newWorkspaceId = workspace.data?.id; if (!newWorkspaceId) return; - await ctx.db - .from('workspace_members') - .insert({ - publicId: generateUID(), - userId, - workspaceId: newWorkspaceId, - createdBy: userId, - role: 'admin' - }) + await ctx.db.from("workspace_members").insert({ + publicId: generateUID(), + userId, + workspaceId: newWorkspaceId, + createdBy: userId, + role: "admin", + }); const newWorkspace = { ...workspace.data }; @@ -102,4 +92,4 @@ export const workspaceRouter = createTRPCRouter({ return newWorkspace; }), -}); \ No newline at end of file +}); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index b15a09ed..633ba5bb 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -6,14 +6,14 @@ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will * need to use are documented accordingly near the end. */ -import { initTRPC } from "@trpc/server"; +import { initTRPC, TRPCError } from "@trpc/server"; import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; import superjson from "superjson"; import { ZodError } from "zod"; import createClient from "~/utils/supabase/api"; import { type Database } from "~/types/database.types"; -import { type SupabaseClient } from '@supabase/supabase-js'; +import { type SupabaseClient } from "@supabase/supabase-js"; /** * 1. CONTEXT @@ -29,7 +29,7 @@ type User = { interface CreateContextOptions { user: User | null; - db: SupabaseClient + db: SupabaseClient; } /** @@ -59,9 +59,11 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createTRPCContext = async (_opts: CreateNextContextOptions) => { const db = createClient(_opts.req, _opts.res); - const { data: { user } } = await db.auth.getUser() + const { + data: { user }, + } = await db.auth.getUser(); - return createInnerTRPCContext({ user, db }); + return createInnerTRPCContext({ db, user }); }; /** @@ -115,3 +117,24 @@ export const createTRPCRouter = t.router; * are logged in. */ export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { + if (!ctx.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next({ + ctx, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);