feat: add workspace member assertion to card router

This commit is contained in:
Henry
2025-05-26 21:30:09 +01:00
parent a6c6524749
commit 76b65acc0f
2 changed files with 161 additions and 40 deletions

View File

@@ -9,6 +9,7 @@ import * as listRepo from "@kan/db/repository/list.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { assertUserInWorkspace } from "../utils/auth";
export const cardRouter = createTRPCRouter({
create: protectedProcedure
@@ -42,31 +43,31 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const list = await listRepo.getWithCardsByPublicId(
const list = await listRepo.getWorkspaceAndListIdByListPublicId(
ctx.db,
input.listPublicId,
);
if (!list?.id)
if (!list)
throw new TRPCError({
message: `List with public ID ${input.listPublicId} not found`,
code: "NOT_FOUND",
});
const lastCard = list.cards.length && list.cards[0];
await assertUserInWorkspace(ctx.db, userId, list.workspaceId);
let index = 0;
if (list.cards.length && input.position === "end" && lastCard) {
index = lastCard.index + 1;
}
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const newCard = await cardRepo.create(ctx.db, {
title: input.title,
description: input.description,
createdBy: userId,
listId: list.id,
index,
position: input.position,
});
const newCardId = newCard.id;
@@ -183,7 +184,10 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!card)
throw new TRPCError({
@@ -191,6 +195,8 @@ export const cardRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const newComment = await cardCommentRepo.create(ctx.db, {
comment: input.comment,
createdBy: userId,
@@ -241,10 +247,9 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const existingComment = await cardCommentRepo.getByPublicId(
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.commentPublicId,
input.cardPublicId,
);
if (!card)
@@ -253,6 +258,13 @@ export const cardRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const existingComment = await cardCommentRepo.getByPublicId(
ctx.db,
input.commentPublicId,
);
if (!existingComment)
throw new TRPCError({
message: `Comment with public ID ${input.commentPublicId} not found`,
@@ -313,10 +325,9 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const existingComment = await cardCommentRepo.getByPublicId(
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.commentPublicId,
input.cardPublicId,
);
if (!card)
@@ -325,6 +336,13 @@ export const cardRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const existingComment = await cardCommentRepo.getByPublicId(
ctx.db,
input.commentPublicId,
);
if (!existingComment)
throw new TRPCError({
message: `Comment with public ID ${input.commentPublicId} not found`,
@@ -379,8 +397,10 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const label = await labelRepo.getByPublicId(ctx.db, input.labelPublicId);
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!card)
throw new TRPCError({
@@ -388,6 +408,10 @@ export const cardRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const label = await labelRepo.getByPublicId(ctx.db, input.labelPublicId);
if (!label)
throw new TRPCError({
message: `Label with public ID ${input.labelPublicId} not found`,
@@ -465,10 +489,9 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const member = await workspaceRepo.getMemberByPublicId(
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.workspaceMemberPublicId,
input.cardPublicId,
);
if (!card)
@@ -477,6 +500,13 @@ export const cardRouter = createTRPCRouter({
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const member = await workspaceRepo.getMemberByPublicId(
ctx.db,
input.workspaceMemberPublicId,
);
if (!member)
throw new TRPCError({
message: `Member with public ID ${input.workspaceMemberPublicId} not found`,
@@ -548,6 +578,27 @@ export const cardRouter = createTRPCRouter({
>(),
)
.query(async ({ ctx, input }) => {
const userId = ctx.user?.id;
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!card)
throw new TRPCError({
message: `Card with public ID ${input.cardPublicId} not found`,
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const result = await cardRepo.getWithListAndMembersByPublicId(
ctx.db,
input.cardPublicId,
@@ -591,6 +642,19 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!card)
throw new TRPCError({
message: `Card with public ID ${input.cardPublicId} not found`,
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const existingCard = await cardRepo.getByPublicId(
ctx.db,
input.cardPublicId,
@@ -708,31 +772,27 @@ export const cardRouter = createTRPCRouter({
code: "UNAUTHORIZED",
});
const card = await cardRepo.getCardWithListByPublicId(
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
);
if (!card?.list.id)
if (!card)
throw new TRPCError({
message: `Card with public ID ${input.cardPublicId} not found`,
code: "NOT_FOUND",
});
await assertUserInWorkspace(ctx.db, userId, card.workspaceId);
const deletedAt = new Date();
const deletedCard = await cardRepo.softDelete(ctx.db, {
await cardRepo.softDelete(ctx.db, {
cardId: card.id,
deletedAt,
deletedBy: userId,
});
if (!deletedCard)
throw new TRPCError({
message: `Failed to delete card`,
code: "INTERNAL_SERVER_ERROR",
});
await cardActivityRepo.create(ctx.db, {
type: "card.archived",
cardId: card.id,

View File

@@ -18,10 +18,24 @@ export const create = async (
description: string;
createdBy: string;
listId: number;
index: number;
position: "start" | "end";
},
) => {
return db.transaction(async (tx) => {
let index = 0;
if (cardInput.position === "end") {
const lastCard = await tx.query.cards.findFirst({
columns: {
index: true,
},
where: and(eq(cards.listId, cardInput.listId), isNull(cards.deletedAt)),
orderBy: desc(cards.index),
});
if (lastCard) index = lastCard.index + 1;
}
const getExistingCardAtIndex = async () =>
tx.query.cards.findFirst({
columns: {
@@ -29,7 +43,7 @@ export const create = async (
},
where: and(
eq(cards.listId, cardInput.listId),
eq(cards.index, cardInput.index),
eq(cards.index, index),
isNull(cards.deletedAt),
),
});
@@ -40,12 +54,8 @@ export const create = async (
await tx.execute(sql`
UPDATE card
SET index = index + 1
WHERE "listId" = ${cardInput.listId} AND index >= ${cardInput.index} AND "deletedAt" IS NULL;
WHERE "listId" = ${cardInput.listId} AND index >= ${index} AND "deletedAt" IS NULL;
`);
const refetchedExistingCardAtIndex = await getExistingCardAtIndex();
if (refetchedExistingCardAtIndex?.id) return tx.rollback();
}
const result = await tx
@@ -56,11 +66,11 @@ export const create = async (
description: cardInput.description,
createdBy: cardInput.createdBy,
listId: cardInput.listId,
index: cardInput.index,
index: index,
})
.returning({ id: cards.id });
.returning({ id: cards.id, listId: cards.listId });
if (!result[0]) return tx.rollback();
if (!result[0]) throw new Error("Unable to create card");
await tx.insert(cardActivities).values({
publicId: generateUID(),
@@ -69,6 +79,24 @@ export const create = async (
createdBy: cardInput.createdBy,
});
const countExpr = sql<number>`COUNT(*)`.mapWith(Number);
const duplicateIndices = await tx
.select({
index: cards.index,
count: countExpr,
})
.from(cards)
.where(and(eq(cards.listId, result[0].listId), isNull(cards.deletedAt)))
.groupBy(cards.listId, cards.index)
.having(gt(countExpr, 1));
if (duplicateIndices.length > 0) {
throw new Error(
`Duplicate indices found after creating card ${result[0].id}`,
);
}
return result[0];
});
};
@@ -405,6 +433,7 @@ export const getWithListAndMembersByPublicId = async (
comment: true,
createdBy: true,
updatedAt: true,
deletedAt: true,
},
// https://github.com/drizzle-team/drizzle-orm/issues/2903
// where: isNull(comments.deletedAt),
@@ -421,6 +450,9 @@ export const getWithListAndMembersByPublicId = async (
...card,
labels: card.labels.map((label) => label.label),
members: card.members.map((member) => member.member),
activities: card.activities.filter(
(activity) => !activity.comment?.deletedAt,
),
};
return formattedResult;
@@ -679,3 +711,32 @@ export const hardDeleteAllCardLabelRelationships = async (
return result;
};
export const getWorkspaceAndCardIdByCardPublicId = async (
db: dbClient,
cardPublicId: string,
) => {
const result = await db.query.cards.findFirst({
columns: { id: true },
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
with: {
list: {
columns: {},
with: {
board: {
columns: {
workspaceId: true,
},
},
},
},
},
});
return result
? {
id: result.id,
workspaceId: result.list.board.workspaceId,
}
: null;
};