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

View File

@@ -18,10 +18,24 @@ export const create = async (
description: string; description: string;
createdBy: string; createdBy: string;
listId: number; listId: number;
index: number; position: "start" | "end";
}, },
) => { ) => {
return db.transaction(async (tx) => { 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 () => const getExistingCardAtIndex = async () =>
tx.query.cards.findFirst({ tx.query.cards.findFirst({
columns: { columns: {
@@ -29,7 +43,7 @@ export const create = async (
}, },
where: and( where: and(
eq(cards.listId, cardInput.listId), eq(cards.listId, cardInput.listId),
eq(cards.index, cardInput.index), eq(cards.index, index),
isNull(cards.deletedAt), isNull(cards.deletedAt),
), ),
}); });
@@ -40,12 +54,8 @@ export const create = async (
await tx.execute(sql` await tx.execute(sql`
UPDATE card UPDATE card
SET index = index + 1 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 const result = await tx
@@ -56,11 +66,11 @@ export const create = async (
description: cardInput.description, description: cardInput.description,
createdBy: cardInput.createdBy, createdBy: cardInput.createdBy,
listId: cardInput.listId, 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({ await tx.insert(cardActivities).values({
publicId: generateUID(), publicId: generateUID(),
@@ -69,6 +79,24 @@ export const create = async (
createdBy: cardInput.createdBy, 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]; return result[0];
}); });
}; };
@@ -405,6 +433,7 @@ export const getWithListAndMembersByPublicId = async (
comment: true, comment: true,
createdBy: true, createdBy: true,
updatedAt: true, updatedAt: true,
deletedAt: true,
}, },
// https://github.com/drizzle-team/drizzle-orm/issues/2903 // https://github.com/drizzle-team/drizzle-orm/issues/2903
// where: isNull(comments.deletedAt), // where: isNull(comments.deletedAt),
@@ -421,6 +450,9 @@ export const getWithListAndMembersByPublicId = async (
...card, ...card,
labels: card.labels.map((label) => label.label), labels: card.labels.map((label) => label.label),
members: card.members.map((member) => member.member), members: card.members.map((member) => member.member),
activities: card.activities.filter(
(activity) => !activity.comment?.deletedAt,
),
}; };
return formattedResult; return formattedResult;
@@ -679,3 +711,32 @@ export const hardDeleteAllCardLabelRelationships = async (
return result; 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;
};