feat: assert board permissions

This commit is contained in:
Henry
2026-01-26 17:23:03 +00:00
parent 3f81b9daa3
commit 7239f958c0
3 changed files with 36 additions and 24 deletions

View File

@@ -7,6 +7,7 @@ import {
} from "react-icons/hi2"; } from "react-icons/hi2";
import Dropdown from "~/components/Dropdown"; import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup"; import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@@ -23,6 +24,7 @@ export default function BoardDropdown({
workspacePublicId: string; workspacePublicId: string;
}) { }) {
const { openModal } = useModal(); const { openModal } = useModal();
const { canEditBoard, canDeleteBoard } = usePermissions();
return ( return (
<Dropdown <Dropdown
disabled={isLoading} disabled={isLoading}
@@ -30,25 +32,33 @@ export default function BoardDropdown({
...(isTemplate ...(isTemplate
? [] ? []
: [ : [
{ ...(canEditBoard
label: t`Make template`, ? [
action: () => openModal("CREATE_TEMPLATE"), {
icon: ( label: t`Make template`,
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" /> action: () => openModal("CREATE_TEMPLATE"),
), icon: (
}, <HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
{ ),
label: t`Edit board URL`, },
action: () => openModal("UPDATE_BOARD_SLUG"), {
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />, label: t`Edit board URL`,
}, action: () => openModal("UPDATE_BOARD_SLUG"),
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
]
: []),
]), ]),
{ ...(canDeleteBoard
label: isTemplate ? t`Delete template` : t`Delete board`, ? [
action: () => openModal("DELETE_BOARD"), {
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />, label: isTemplate ? t`Delete template` : t`Delete board`,
}, action: () => openModal("DELETE_BOARD"),
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
},
]
: []),
]} ]}
> >
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" /> <HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />

View File

@@ -7,6 +7,7 @@ import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { Tooltip } from "~/components/Tooltip"; import { Tooltip } from "~/components/Tooltip";
import { usePermissions } from "~/hooks/usePermissions";
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts"; import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace"; import { useWorkspace } from "~/providers/workspace";
@@ -17,12 +18,13 @@ import { NewBoardForm } from "./components/NewBoardForm";
export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) { export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
const { openModal, modalContentType, isOpen } = useModal(); const { openModal, modalContentType, isOpen } = useModal();
const { workspace } = useWorkspace(); const { workspace } = useWorkspace();
const { canCreateBoard } = usePermissions();
const { tooltipContent: createModalShortcutTooltipContent } = const { tooltipContent: createModalShortcutTooltipContent } =
useKeyboardShortcut({ useKeyboardShortcut({
type: "PRESS", type: "PRESS",
stroke: { key: "C" }, stroke: { key: "C" },
action: () => openModal("NEW_BOARD"), action: () => canCreateBoard && openModal("NEW_BOARD"),
description: t`Create new ${isTemplate ? "template" : "board"}`, description: t`Create new ${isTemplate ? "template" : "board"}`,
group: "ACTIONS", group: "ACTIONS",
}); });

View File

@@ -15,7 +15,7 @@ import {
} from "@kan/shared/utils"; } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { assertUserInWorkspace } from "../utils/auth"; import { assertPermission } from "../utils/permissions";
export const boardRouter = createTRPCRouter({ export const boardRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@@ -58,7 +58,7 @@ export const boardRouter = createTRPCRouter({
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
await assertUserInWorkspace(ctx.db, userId, workspace.id); await assertPermission(ctx.db, userId, workspace.id, "board:view");
const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, { const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, {
type: input.type, type: input.type,
@@ -119,7 +119,7 @@ export const boardRouter = createTRPCRouter({
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
await assertUserInWorkspace(ctx.db, userId, board.workspaceId); await assertPermission(ctx.db, userId, board.workspaceId, "board:view");
// Convert semantic string filters to date ranges expected by the repo // Convert semantic string filters to date ranges expected by the repo
const dueDateFilters = input.dueDateFilters const dueDateFilters = input.dueDateFilters
@@ -255,7 +255,7 @@ export const boardRouter = createTRPCRouter({
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
await assertUserInWorkspace(ctx.db, userId, workspace.id); await assertPermission(ctx.db, userId, workspace.id, "board:create");
// If sourceBoardPublicId is provided, clone the source board // If sourceBoardPublicId is provided, clone the source board
if (input.sourceBoardPublicId) { if (input.sourceBoardPublicId) {
@@ -422,7 +422,7 @@ export const boardRouter = createTRPCRouter({
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
await assertUserInWorkspace(ctx.db, userId, board.workspaceId); await assertPermission(ctx.db, userId, board.workspaceId, "board:edit");
if (input.slug) { if (input.slug) {
const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable( const isBoardSlugAvailable = await boardRepo.isBoardSlugAvailable(
@@ -491,7 +491,7 @@ export const boardRouter = createTRPCRouter({
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
await assertUserInWorkspace(ctx.db, userId, board.workspaceId); await assertPermission(ctx.db, userId, board.workspaceId, "board:delete");
const listIds = board.lists.map((list) => list.id); const listIds = board.lists.map((list) => list.id);