diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 87d4824cd..f312e6438 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -1,53 +1,54 @@ -import { Link, Navigate, Outlet, useParams } from "react-router"; +import { useTranslate } from "@probo/i18n"; import { + Avatar, + Badge, + Button, + Dropdown, + DropdownItem, DropdownSeparator, IconArrowBoxLeft, IconBank, IconBook, + IconBox, + IconCalendar1, IconCheckmark1, + IconChevronGrabberVertical, + IconCircleProgress, IconCircleQuestionmark, IconClock, IconCrossLargeX, IconFire3, IconGroup1, IconInboxEmpty, + IconKey, + IconListStack, + IconLock, + IconMedal, IconPageTextLine, + IconPeopleAdd, + IconPlusLarge, + IconRotateCw, IconSettingsGear2, + IconShield, IconStore, IconTodo, - IconListStack, - IconBox, - IconShield, - IconRotateCw, - IconCircleProgress, - IconMedal, - IconCalendar1, Layout, SidebarItem, - UserDropdown as UserDropdownRoot, - UserDropdownItem, Skeleton, - Dropdown, - Button, - DropdownItem, - IconChevronGrabberVertical, - IconPlusLarge, - Avatar, - IconPeopleAdd, - Badge, - IconKey, - IconLock, + UserDropdownItem, + UserDropdown as UserDropdownRoot, + useToast, } from "@probo/ui"; -import { useTranslate } from "@probo/i18n"; -import { graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; -import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; -import { Suspense, useState, useEffect } from "react"; -import { useToast } from "@probo/ui"; +import { Suspense, useEffect, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { useLazyLoadQuery } from "react-relay"; +import { Link, Navigate, Outlet, useParams } from "react-router"; +import { graphql } from "relay-runtime"; +import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; import { PageError } from "/components/PageError"; -import { buildEndpoint } from "/providers/RelayProviders"; import { Authorized } from "/permissions"; +import { PermissionsProvider } from "/providers/PermissionsProvider"; +import { buildEndpoint } from "/providers/RelayProviders"; const MainLayoutQuery = graphql` query MainLayoutQuery($organizationId: ID!) { @@ -82,7 +83,9 @@ export function MainLayout() { return ( }> - + + + ); } diff --git a/apps/console/src/pages/organizations/assets/AssetsPage.tsx b/apps/console/src/pages/organizations/assets/AssetsPage.tsx index e5bac48eb..8d4ff247d 100644 --- a/apps/console/src/pages/organizations/assets/AssetsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetsPage.tsx @@ -1,28 +1,29 @@ +import { usePageTitle } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; import { Button, IconPlusLarge, PageHeader, } from "@probo/ui"; -import { useTranslate } from "@probo/i18n"; -import { usePageTitle } from "@probo/hooks"; +import { use } from "react"; import { graphql, - type PreloadedQuery, usePaginationFragment, usePreloadedQuery, + type PreloadedQuery, } from "react-relay"; -import { useOrganizationId } from "/hooks/useOrganizationId"; import { useParams } from "react-router"; +import { AssetsTable } from "../../../components/assets/AssetsTable"; +import { ReadOnlyAssetsTable } from "../../../components/assets/ReadOnlyAssetsTable"; +import type { AssetsPageFragment$key } from "./__generated__/AssetsPageFragment.graphql"; +import { CreateAssetDialog } from "./dialogs/CreateAssetDialog"; +import { SnapshotBanner } from "/components/SnapshotBanner"; import { assetsQuery, } from "/hooks/graph/AssetGraph"; import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql"; -import type { AssetsPageFragment$key } from "./__generated__/AssetsPageFragment.graphql"; -import { SnapshotBanner } from "/components/SnapshotBanner"; -import { CreateAssetDialog } from "./dialogs/CreateAssetDialog"; -import { Authorized, isAuthorized } from "/permissions"; -import { AssetsTable } from "../../../components/assets/AssetsTable"; -import { ReadOnlyAssetsTable } from "../../../components/assets/ReadOnlyAssetsTable"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { PermissionsContext } from "/providers/PermissionsProvider"; const paginatedAssetsFragment = graphql` fragment AssetsPageFragment on Organization @@ -90,9 +91,10 @@ export default function AssetsPage(props: Props) { const assets = pagination.data.assets?.edges.map((edge) => edge.node); const connectionId = pagination.data.assets.__id; + const { isAuthorized } = use(PermissionsContext); const canWrite = ( - isAuthorized(organizationId, "Asset", "updateAsset") || - isAuthorized(organizationId, "Asset", "deleteAsset") + isAuthorized("Asset", "updateAsset") || + isAuthorized("Asset", "deleteAsset") ); usePageTitle(__("Assets")); @@ -105,15 +107,13 @@ export default function AssetsPage(props: Props) { "Manage your organization's assets and their classifications.", )} > - {!isSnapshotMode && ( - - - - - + {!isSnapshotMode && isAuthorized("Organization", "createAsset") && ( + + + )} {isSnapshotMode || !canWrite ? diff --git a/apps/console/src/providers/PermissionsProvider.tsx b/apps/console/src/providers/PermissionsProvider.tsx new file mode 100644 index 000000000..13efb7247 --- /dev/null +++ b/apps/console/src/providers/PermissionsProvider.tsx @@ -0,0 +1,51 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { createContext, type PropsWithChildren } from "react"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +enum Role { + OWNER = "OWNER", + ADMIN = "ADMIN", + VIEWER = "VIEWER", +} + +type PermissionsResponse = { + permissions: Record>; + role: Role; +}; + +type PermissionsContextType = { + isAuthorized: (entity: string, action: string) => boolean; +} & PermissionsResponse; + +export const PermissionsContext = createContext({ + permissions: {}, + role: Role.VIEWER, + isAuthorized: () => false, +}); + +export function PermissionsProvider(props: PropsWithChildren) { + const { children } = props; + + const organizationId = useOrganizationId(); + + const { data } = useSuspenseQuery({ + queryKey: ["permissions", organizationId], + queryFn: async () => { + const response = await fetch(`/authz/${organizationId}/permissions`, { credentials: "include" }); + if (!response.ok) { + throw new Error("Failed to fetch permissions"); + } + return response.json() as Promise; + }, + }); + + const isAuthorized = (entity: string, action: string) => { + return data.permissions[entity]?.[action] ?? false; + } + + return ( + + {children} + + ); +}