From bf83e162d9fa116dba0384225456291cacdfdb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 30 Dec 2025 12:52:56 +0100 Subject: [PATCH] Add new permissions handling for custom domain page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- apps/console/package.json | 4 +- apps/console/relay.config.json | 15 +- ...inSettingsPage_permissionsQuery.graphql.ts | 136 ++++++++++++++++++ .../settings/DomainSettingsPage.tsx | 13 +- .../settings/DomainSettingsPageLoader.tsx | 20 ++- .../settings/_components/DomainCard.tsx | 13 +- .../settings/domainSettingsPage.iam.ts | 16 +++ .../src/providers/NewPermissionsContext.tsx | 5 + .../src/providers/NewPermissionsProvider.tsx | 72 ++++++++++ apps/console/vite.config.ts | 4 +- 10 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 apps/console/src/__generated__/iam/domainSettingsPage_permissionsQuery.graphql.ts create mode 100644 apps/console/src/pages/organizations/settings/domainSettingsPage.iam.ts create mode 100644 apps/console/src/providers/NewPermissionsContext.tsx create mode 100644 apps/console/src/providers/NewPermissionsProvider.tsx diff --git a/apps/console/package.json b/apps/console/package.json index c2b86e58e..392034d32 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler", + "relay-clean": "find src -type d -name \"__generated__\" -exec rm -rf {} +", + "relay-compile": "npx relay-compiler", + "relay": "npm run relay-clean && npm run relay-compile", "build": "tsc -b && vite build", "lint": "eslint .", "check": "tsc --noEmit -p tsconfig.app.json", diff --git a/apps/console/relay.config.json b/apps/console/relay.config.json index c973efb97..2ea3bc32b 100644 --- a/apps/console/relay.config.json +++ b/apps/console/relay.config.json @@ -15,11 +15,22 @@ "eagerEsModules": true, "noFutureProofEnums": true, "output": "apps/console/src/__generated__/core", - "relativizeJsModulePaths": false + "relativizeJsModulePaths": false, + "excludesExtensions": ["*.iam.ts"] + }, + "inline-iam": { + "base": "core", + "schema": "pkg/server/api/connect/v1/schema.graphql", + "language": "typescript", + "eagerEsModules": true, + "noFutureProofEnums": true, + "output": "apps/console/src/__generated__/iam", + "relativizeJsModulePaths": false, + "excludesExtensions": ["*.tsx", "**/hooks/**.ts"] } }, "sources": { "apps/console/src/pages/iam": "iam", - "apps/console/src": "core" + "apps/console/src": ["core", "inline-iam"] } } diff --git a/apps/console/src/__generated__/iam/domainSettingsPage_permissionsQuery.graphql.ts b/apps/console/src/__generated__/iam/domainSettingsPage_permissionsQuery.graphql.ts new file mode 100644 index 000000000..b623d6399 --- /dev/null +++ b/apps/console/src/__generated__/iam/domainSettingsPage_permissionsQuery.graphql.ts @@ -0,0 +1,136 @@ +/** + * @generated SignedSource<<2c18e7bff6b76551479def8a10cce183>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type domainSettingsPage_permissionsQuery$variables = { + organizationId: string; +}; +export type domainSettingsPage_permissionsQuery$data = { + readonly viewer: { + readonly canCreateCustomDomain: boolean; + readonly canDeleteCustomDomain: boolean; + }; +}; +export type domainSettingsPage_permissionsQuery = { + response: domainSettingsPage_permissionsQuery$data; + variables: domainSettingsPage_permissionsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" +}, +v2 = { + "alias": "canCreateCustomDomain", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:custom-domain:create" + }, + (v1/*: any*/) + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": null +}, +v3 = { + "alias": "canDeleteCustomDomain", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:custom-domain:delete" + }, + (v1/*: any*/) + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "domainSettingsPage_permissionsQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "domainSettingsPage_permissionsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "bb016bc1f53b609efc4ac93a93653d08", + "id": null, + "metadata": {}, + "name": "domainSettingsPage_permissionsQuery", + "operationKind": "query", + "text": "query domainSettingsPage_permissionsQuery(\n $organizationId: ID!\n) {\n viewer {\n canCreateCustomDomain: permission(action: \"core:custom-domain:create\", id: $organizationId)\n canDeleteCustomDomain: permission(action: \"core:custom-domain:delete\", id: $organizationId)\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f09bdc9336bf1ad1339bb3b60987f53f"; + +export default node; diff --git a/apps/console/src/pages/organizations/settings/DomainSettingsPage.tsx b/apps/console/src/pages/organizations/settings/DomainSettingsPage.tsx index 83dd24260..1487ac2cf 100644 --- a/apps/console/src/pages/organizations/settings/DomainSettingsPage.tsx +++ b/apps/console/src/pages/organizations/settings/DomainSettingsPage.tsx @@ -5,6 +5,8 @@ import { useTranslate } from "@probo/i18n"; import { DomainCard } from "./_components/DomainCard"; import { NewDomainDialog } from "./_components/NewDomainDialog"; import { Button, Card, IconPlusLarge } from "@probo/ui"; +import { PermissionsContext } from "/providers/NewPermissionsContext"; +import { use } from "react"; export const domainSettingsPageQuery = graphql` query DomainSettingsPageQuery($organizationId: ID!) { @@ -27,6 +29,7 @@ export function DomainSettingsPage(props: { const { queryRef } = props; const { __ } = useTranslate(); + const { canCreateCustomDomain } = use(PermissionsContext); const { organization } = usePreloadedQuery( domainSettingsPageQuery, @@ -53,11 +56,11 @@ export function DomainSettingsPage(props: { )}

- {/* {isAuthorized("Organization", "createCustomDomain") && ( */} - - - - {/* )} */} + {canCreateCustomDomain && ( + + + + )}
diff --git a/apps/console/src/pages/organizations/settings/DomainSettingsPageLoader.tsx b/apps/console/src/pages/organizations/settings/DomainSettingsPageLoader.tsx index 2237ff4ef..e422d4c9a 100644 --- a/apps/console/src/pages/organizations/settings/DomainSettingsPageLoader.tsx +++ b/apps/console/src/pages/organizations/settings/DomainSettingsPageLoader.tsx @@ -7,6 +7,10 @@ import { import { useQueryLoader } from "react-relay"; import { CoreRelayProvider } from "/providers/CoreRelayProvider"; import type { DomainSettingsPageQuery } from "/__generated__/core/DomainSettingsPageQuery.graphql"; +import { PermissionsProviderLoader } from "/providers/NewPermissionsProvider"; +import { IAMRelayProvider } from "/providers/IAMRelayProvider"; +import { domainSettingsPagePermissionsQuery } from "./domainSettingsPage.iam"; +import type { domainSettingsPage_permissionsQuery } from "/__generated__/iam/domainSettingsPage_permissionsQuery.graphql"; function DomainSettingsPageLoader() { const organizationId = useOrganizationId(); @@ -29,10 +33,16 @@ function DomainSettingsPageLoader() { export default function () { return ( - - - - - + + + query={domainSettingsPagePermissionsQuery} + > + + + + + + + ); } diff --git a/apps/console/src/pages/organizations/settings/_components/DomainCard.tsx b/apps/console/src/pages/organizations/settings/_components/DomainCard.tsx index c8a695f09..13cfb7ab2 100644 --- a/apps/console/src/pages/organizations/settings/_components/DomainCard.tsx +++ b/apps/console/src/pages/organizations/settings/_components/DomainCard.tsx @@ -9,6 +9,8 @@ import { graphql } from "relay-runtime"; import { useFragment } from "react-relay"; import type { DomainCardFragment$key } from "/__generated__/core/DomainCardFragment.graphql"; import { DeleteDomainDialog } from "./DeleteDomainDialog"; +import { use } from "react"; +import { PermissionsContext } from "/providers/NewPermissionsContext"; const fragment = graphql` fragment DomainCardFragment on CustomDomain { @@ -22,6 +24,7 @@ export function DomainCard(props: { fKey: DomainCardFragment$key }) { const { fKey } = props; const { __ } = useTranslate(); + const { canDeleteCustomDomain } = use(PermissionsContext); const domain = useFragment(fragment, fKey); @@ -50,11 +53,11 @@ export function DomainCard(props: { fKey: DomainCardFragment$key }) { - {/* {permissions.canDeleteCustomDomain && ( */} - - - - {/* )} */} + {canDeleteCustomDomain && ( + + + + )} diff --git a/apps/console/src/pages/organizations/settings/domainSettingsPage.iam.ts b/apps/console/src/pages/organizations/settings/domainSettingsPage.iam.ts new file mode 100644 index 000000000..3679b12ab --- /dev/null +++ b/apps/console/src/pages/organizations/settings/domainSettingsPage.iam.ts @@ -0,0 +1,16 @@ +import { graphql } from "relay-runtime"; + +export const domainSettingsPagePermissionsQuery = graphql` + query domainSettingsPage_permissionsQuery($organizationId: ID!) { + viewer @required(action: THROW) { + canCreateCustomDomain: permission( + action: "core:custom-domain:create" + id: $organizationId + ) + canDeleteCustomDomain: permission( + action: "core:custom-domain:delete" + id: $organizationId + ) + } + } +`; diff --git a/apps/console/src/providers/NewPermissionsContext.tsx b/apps/console/src/providers/NewPermissionsContext.tsx new file mode 100644 index 000000000..1422883b9 --- /dev/null +++ b/apps/console/src/providers/NewPermissionsContext.tsx @@ -0,0 +1,5 @@ +import { createContext } from "react"; + +export type PermissionsContextType = Record; + +export const PermissionsContext = createContext({}); diff --git a/apps/console/src/providers/NewPermissionsProvider.tsx b/apps/console/src/providers/NewPermissionsProvider.tsx new file mode 100644 index 000000000..83a178357 --- /dev/null +++ b/apps/console/src/providers/NewPermissionsProvider.tsx @@ -0,0 +1,72 @@ +import { useEffect, type PropsWithChildren } from "react"; +import { PermissionsContext } from "./NewPermissionsContext"; +import { + usePreloadedQuery, + useQueryLoader, + type PreloadedQuery, +} from "react-relay"; +import type { GraphQLTaggedNode, OperationType } from "relay-runtime"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +type PermissionsQueryType = OperationType & { + response: { viewer: Record }; +}; + +type PermissionsProviderLoaderProps = PropsWithChildren<{ + query: GraphQLTaggedNode; +}>; + +export function PermissionsProviderLoader( + props: PermissionsProviderLoaderProps, +) { + const { children, query } = props; + + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader(query); + + useEffect(() => { + loadQuery({ + organizationId, + }); + }, [loadQuery, organizationId]); + + if (!queryRef) { + // While permissions are loading, consider there are no permissions + return {children}; + } + + return ( + + {children} + + ); +} + +type PermissionsProviderProps = + PermissionsProviderLoaderProps & { + queryRef: PreloadedQuery; + }; + +function PermissionsProvider( + props: PermissionsProviderProps, +) { + const { children, query, queryRef } = props; + + const { viewer: permissions } = usePreloadedQuery(query, queryRef); + + const handler = { + get(target: Record, prop: string) { + if (!(prop in target)) { + throw new Error(`Field ${prop} is not defined in IAM viewer node`); + } + + return Reflect.get(target, prop); + }, + }; + + return ( + + {children} + + ); +} diff --git a/apps/console/vite.config.ts b/apps/console/vite.config.ts index 444a3c4c9..dd572a0c3 100644 --- a/apps/console/vite.config.ts +++ b/apps/console/vite.config.ts @@ -7,7 +7,7 @@ import { fileURLToPath, URL } from "node:url"; export default defineConfig({ plugins: [ react({ - exclude: "src/pages/iam/**/*", + exclude: ["src/pages/iam/**/*", "src/pages/**/*.iam.ts"], babel: { plugins: [ [ @@ -21,7 +21,7 @@ export default defineConfig({ }, }), react({ - include: "src/pages/iam/**/*", + include: ["src/pages/iam/**/*", "src/pages/**/*.iam.ts"], babel: { plugins: [ [