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