feat: add workspace member assertion to list router

This commit is contained in:
Henry
2025-05-23 13:43:51 +01:00
parent 2e97e2be53
commit a6c6524749
5 changed files with 116 additions and 37 deletions

View File

@@ -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,

View File

@@ -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++;

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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<number>`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;
};