From a6c6524749d55690e965b3f0d6e6bda0c6c32cf6 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 23 May 2025 13:43:51 +0100 Subject: [PATCH] feat: add workspace member assertion to list router --- packages/api/src/routers/board.ts | 12 ++-- packages/api/src/routers/import.ts | 9 +-- packages/api/src/routers/list.ts | 37 ++++++++-- packages/db/src/repository/board.repo.ts | 5 +- packages/db/src/repository/list.repo.ts | 90 +++++++++++++++++++----- 5 files changed, 116 insertions(+), 37 deletions(-) diff --git a/packages/api/src/routers/board.ts b/packages/api/src/routers/board.ts index 5ce7f788..2fc8dd66 100644 --- a/packages/api/src/routers/board.ts +++ b/packages/api/src/routers/board.ts @@ -81,18 +81,18 @@ export const boardRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const workspaceId = await boardRepo.getWorkspaceIdByBoardPublicId( + const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId( ctx.db, input.boardPublicId, ); - if (!workspaceId) + if (!board) throw new TRPCError({ message: `Board with public ID ${input.boardPublicId} not found`, code: "NOT_FOUND", }); - await assertUserInWorkspace(ctx.db, userId, workspaceId); + await assertUserInWorkspace(ctx.db, userId, board.workspaceId); const result = await boardRepo.getByPublicId( ctx.db, @@ -235,18 +235,18 @@ export const boardRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const workspaceId = await boardRepo.getWorkspaceIdByBoardPublicId( + const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId( ctx.db, input.boardPublicId, ); - if (!workspaceId) + if (!board) throw new TRPCError({ message: `Board with public ID ${input.boardPublicId} not found`, code: "NOT_FOUND", }); - await assertUserInWorkspace(ctx.db, userId, workspaceId); + await assertUserInWorkspace(ctx.db, userId, board.workspaceId); const result = await boardRepo.update(ctx.db, { name: input.name, diff --git a/packages/api/src/routers/import.ts b/packages/api/src/routers/import.ts index 7a2a618d..245be072 100644 --- a/packages/api/src/routers/import.ts +++ b/packages/api/src/routers/import.ts @@ -218,18 +218,15 @@ export const importRouter = createTRPCRouter({ .filter((label) => !!label.sourceId); } - let listIndex = 0; - for (const list of formattedData.lists) { const newList = await listRepo.create(ctx.db, { name: list.name, createdBy: userId, boardId: newBoardId, - index: listIndex, importId: newImportId, }); - const newListId = newList?.id; + const newListId = newList.id; if (list.cards.length && newListId) { const cardsInsert = list.cards.map((card, index) => ({ @@ -244,7 +241,7 @@ export const importRouter = createTRPCRouter({ const newCards = await cardRepo.bulkCreate(ctx.db, cardsInsert); - if (!newCards?.length) + if (!newCards.length) throw new TRPCError({ message: "Failed to create new cards", code: "INTERNAL_SERVER_ERROR", @@ -302,8 +299,6 @@ export const importRouter = createTRPCRouter({ } } } - - listIndex++; } boardsCreated++; diff --git a/packages/api/src/routers/list.ts b/packages/api/src/routers/list.ts index a5f51b08..8356238a 100644 --- a/packages/api/src/routers/list.ts +++ b/packages/api/src/routers/list.ts @@ -7,6 +7,7 @@ import * as activityRepo from "@kan/db/repository/cardActivity.repo"; import * as listRepo from "@kan/db/repository/list.repo"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { assertUserInWorkspace } from "../utils/auth"; export const listRouter = createTRPCRouter({ create: protectedProcedure @@ -36,7 +37,7 @@ export const listRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const board = await boardRepo.getWithLatestListIndexByPublicId( + const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId( ctx.db, input.boardPublicId, ); @@ -47,14 +48,12 @@ export const listRouter = createTRPCRouter({ code: "NOT_FOUND", }); - const latestListIndex = board.lists[0]?.index; + await assertUserInWorkspace(ctx.db, userId, board.workspaceId); const result = await listRepo.create(ctx.db, { name: input.name, createdBy: userId, boardId: board.id, - index: - (latestListIndex ?? latestListIndex === 0) ? latestListIndex + 1 : 0, }); if (!result) @@ -91,7 +90,10 @@ export const listRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const list = await listRepo.getByPublicId(ctx.db, input.listPublicId); + const list = await listRepo.getWorkspaceAndListIdByListPublicId( + ctx.db, + input.listPublicId, + ); if (!list) throw new TRPCError({ @@ -99,6 +101,8 @@ export const listRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, list.workspaceId); + const deletedAt = new Date(); const deletedList = await listRepo.softDeleteById(ctx.db, { @@ -131,7 +135,7 @@ export const listRouter = createTRPCRouter({ cardId: card.id, })); - await activityRepo.bulkCreate(ctx.db, activities); + if (activities.length) await activityRepo.bulkCreate(ctx.db, activities); return { success: true }; }), @@ -160,6 +164,27 @@ export const listRouter = createTRPCRouter({ >(), ) .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const list = await listRepo.getWorkspaceAndListIdByListPublicId( + ctx.db, + input.listPublicId, + ); + + if (!list) + throw new TRPCError({ + message: `List with public ID ${input.listPublicId} not found`, + code: "NOT_FOUND", + }); + + await assertUserInWorkspace(ctx.db, userId, list.workspaceId); + let result: { name: string; publicId: string } | undefined; if (input.name) { diff --git a/packages/db/src/repository/board.repo.ts b/packages/db/src/repository/board.repo.ts index 10266d18..6f13f24d 100644 --- a/packages/db/src/repository/board.repo.ts +++ b/packages/db/src/repository/board.repo.ts @@ -453,16 +453,17 @@ export const isSlugUnique = async ( return result === undefined; }; -export const getWorkspaceIdByBoardPublicId = async ( +export const getWorkspaceAndBoardIdByBoardPublicId = async ( db: dbClient, boardPublicId: string, ) => { const result = await db.query.boards.findFirst({ columns: { + id: true, workspaceId: true, }, where: eq(boards.publicId, boardPublicId), }); - return result?.workspaceId; + return result; }; diff --git a/packages/db/src/repository/list.repo.ts b/packages/db/src/repository/list.repo.ts index 94c37a09..73ad5516 100644 --- a/packages/db/src/repository/list.repo.ts +++ b/packages/db/src/repository/list.repo.ts @@ -10,27 +10,62 @@ export const create = async ( name: string; createdBy: string; boardId: number; - index: number; importId?: number; }, ) => { - const [result] = await db - .insert(lists) - .values({ - publicId: generateUID(), - name: listInput.name, - createdBy: listInput.createdBy, - boardId: listInput.boardId, - index: listInput.index, - importId: listInput.importId, - }) - .returning({ - id: lists.id, - publicId: lists.publicId, - name: lists.name, + return db.transaction(async (tx) => { + const list = await tx.query.lists.findFirst({ + columns: { + id: true, + boardId: true, + index: true, + }, + where: and(eq(lists.boardId, listInput.boardId), isNull(lists.deletedAt)), + orderBy: [desc(lists.index)], }); - return result; + const index = list?.index ? list.index + 1 : 0; + + const [result] = await tx + .insert(lists) + .values({ + publicId: generateUID(), + name: listInput.name, + createdBy: listInput.createdBy, + boardId: listInput.boardId, + index, + importId: listInput.importId, + }) + .returning({ + id: lists.id, + publicId: lists.publicId, + boardId: lists.boardId, + name: lists.name, + }); + + if (!result) + throw new Error(`Failed to create list for board ${listInput.boardId}`); + + const countExpr = sql`COUNT(*)`.mapWith(Number); + + const duplicateIndices = await tx + .select({ + index: lists.index, + count: countExpr, + }) + .from(lists) + .where(and(eq(lists.boardId, result.boardId), isNull(lists.deletedAt))) + .groupBy(lists.index) + .having(gt(countExpr, 1)); + + if (duplicateIndices.length > 0) { + throw new Error( + `Duplicate indices found after reordering in board ${result.boardId}`, + ); + } + + return result; + }); }; export const getByPublicId = async (db: dbClient, listPublicId: string) => { @@ -207,6 +242,8 @@ export const softDeleteById = async ( .groupBy(lists.index) .having(gt(countExpr, 1)); + console.log(duplicateIndices); + if (duplicateIndices.length > 0) { throw new Error( `Duplicate indices found after reordering in board ${result.boardId}`, @@ -216,3 +253,24 @@ export const softDeleteById = async ( return result; }); }; + +export const getWorkspaceAndListIdByListPublicId = async ( + db: dbClient, + listPublicId: string, +) => { + const result = await db.query.lists.findFirst({ + columns: { id: true }, + where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)), + with: { + board: { + columns: { + workspaceId: true, + }, + }, + }, + }); + + return result + ? { id: result.id, workspaceId: result.board.workspaceId } + : null; +};