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: [
[