feat: add workspace member assertion to card router
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user