feat: implement board access control (#349)
* feat: implement board access control * fix: deepsource issues * wip: address pull request feedback * chore: address pull request feedback * fix: format issue * test: improve tests * fix: type and lint issue * chore: address pull request feedback * refactor: rename board procedures
This commit is contained in:
@@ -11,6 +11,7 @@ import { useConfirmModal } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
|
||||
const iconProps = {
|
||||
size: 16,
|
||||
@@ -18,7 +19,10 @@ const iconProps = {
|
||||
};
|
||||
|
||||
interface BoardCardMenuDropdownProps {
|
||||
board: Pick<RouterOutputs["board"]["getAll"][number], "id" | "name">;
|
||||
board: Pick<
|
||||
RouterOutputs["board"]["getAllBoards"][number],
|
||||
"id" | "name" | "creator" | "permissions" | "isPublic"
|
||||
>;
|
||||
}
|
||||
|
||||
export const BoardCardMenuDropdown = ({
|
||||
@@ -27,9 +31,11 @@ export const BoardCardMenuDropdown = ({
|
||||
const t = useScopedI18n("management.page.board.action");
|
||||
const tCommon = useScopedI18n("common");
|
||||
|
||||
const { hasFullAccess, hasChangeAccess } = useBoardPermissions(board);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.delete.useMutation({
|
||||
const { mutateAsync, isPending } = clientApi.board.deleteBoard.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathAction("/manage/boards");
|
||||
},
|
||||
@@ -51,26 +57,31 @@ export const BoardCardMenuDropdown = ({
|
||||
|
||||
return (
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/boards/${board.name}/settings`}
|
||||
leftSection={<IconSettings {...iconProps} />}
|
||||
>
|
||||
{t("settings.label")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Label c="red.7">
|
||||
{tCommon("menu.section.dangerZone.title")}
|
||||
</Menu.Label>
|
||||
<Menu.Item
|
||||
c="red.7"
|
||||
leftSection={<IconTrash {...iconProps} />}
|
||||
onClick={handleDeletion}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t("delete.label")}
|
||||
</Menu.Item>
|
||||
{hasChangeAccess && (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/boards/${board.name}/settings`}
|
||||
leftSection={<IconSettings {...iconProps} />}
|
||||
>
|
||||
{t("settings.label")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{hasFullAccess && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Label c="red.7">
|
||||
{tCommon("menu.section.dangerZone.title")}
|
||||
</Menu.Label>
|
||||
<Menu.Item
|
||||
c="red.7"
|
||||
leftSection={<IconTrash {...iconProps} />}
|
||||
onClick={handleDeletion}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t("delete.label")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
||||
const t = useI18n();
|
||||
const { openModal } = useModalAction(AddBoardModal);
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.board.create.useMutation({
|
||||
const { mutateAsync, isPending } = clientApi.board.createBoard.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathAction("/manage/boards");
|
||||
},
|
||||
|
||||
@@ -19,13 +19,14 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { getBoardPermissions } from "~/components/board/permissions/server";
|
||||
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
||||
import { CreateBoardButton } from "./_components/create-board-button";
|
||||
|
||||
export default async function ManageBoardsPage() {
|
||||
const t = await getScopedI18n("management.page.board");
|
||||
|
||||
const boards = await api.board.getAll();
|
||||
const boards = await api.board.getAllBoards();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,11 +47,12 @@ export default async function ManageBoardsPage() {
|
||||
}
|
||||
|
||||
interface BoardCardProps {
|
||||
board: RouterOutputs["board"]["getAll"][number];
|
||||
board: RouterOutputs["board"]["getAllBoards"][number];
|
||||
}
|
||||
|
||||
const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
const t = await getScopedI18n("management.page.board");
|
||||
const { hasChangeAccess: isMenuVisible } = await getBoardPermissions(board);
|
||||
const visibility = board.isPublic ? "public" : "private";
|
||||
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
||||
|
||||
@@ -79,14 +81,16 @@ const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
>
|
||||
{t("action.open.label")}
|
||||
</Button>
|
||||
<Menu position="bottom-end">
|
||||
<MenuTarget>
|
||||
<ActionIcon variant="default" size="lg">
|
||||
<IconDotsVertical size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</MenuTarget>
|
||||
<BoardCardMenuDropdown board={board} />
|
||||
</Menu>
|
||||
{isMenuVisible && (
|
||||
<Menu position="bottom-end">
|
||||
<MenuTarget>
|
||||
<ActionIcon variant="default" size="lg">
|
||||
<IconDotsVertical size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</MenuTarget>
|
||||
<BoardCardMenuDropdown board={board} />
|
||||
</Menu>
|
||||
)}
|
||||
</Group>
|
||||
</CardSection>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user