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:
Meier Lukas
2024-04-30 21:32:55 +02:00
committed by GitHub
parent 56388eb8ef
commit 7ab9dc2501
50 changed files with 1020 additions and 324 deletions

View File

@@ -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>
);
};

View File

@@ -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");
},

View File

@@ -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>