From 1b309833075200cd7e4b916861fe28c36b5182fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 5 Jun 2026 10:18:20 +0200 Subject: [PATCH] fix(settings): gate the AI settings page on AI_SETTINGS, not the chat flag (#21239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes #21229. The two AI role permissions behaved **opposite to their labels**. The trap is that the flag's code name is the inverse of its UI label: | `PermissionFlagType` | UI label | Section | Means | |---|---|---|---| | `AI` | **"Ask AI"** | Actions | End-user: chat with AI | | `AI_SETTINGS` | **"AI"** | Member / settings | Admin: configure AI agents | Before this PR (on `main`): - `AI` ("Ask AI", chat) gated **both** the AI chat **and** the AI settings page. - `AI_SETTINGS` ("AI", configure agents) gated **nothing** the user could see. So a chat-only user could reach the whole AI **configuration** page, and toggling the "AI" settings permission did nothing — exactly the misalignment reported in #21229. ## Root cause `PermissionFlagType.AI` *reads* like "the AI permission", so it looks like the natural gate for the AI settings page — but it's actually the **chat** flag. The settings page (nav item + route) had been pointed at `AI` in #21072 to match the Overview stats query (`findWorkspaceAiStats`), which was itself mis-gated on `AI`. Both the stats query and the rest of the settings surface are admin/config features, so they belong on `AI_SETTINGS`. ## Changes All three move the **AI settings surface** from the chat flag (`AI`) to the settings flag (`AI_SETTINGS`); chat keeps following `AI`: - `useSettingsNavigationItems.tsx` — AI nav item → `AI_SETTINGS` - `SettingsRoutes.tsx` — AI settings route group → `AI_SETTINGS` - `ai-workspace-stats.resolver.ts` — `findWorkspaceAiStats` (settings-only, drives the Overview tab) → `AI_SETTINGS` After this: the "AI" permission controls the AI settings page + its Overview; the "Ask AI" permission controls the chat. Both toggles now match their labels. ## Test plan - [ ] Role with **only "Ask AI"** (`AI`): AI chat tabs/pane visible; **Settings → AI is hidden** and the route is not reachable. - [ ] Role with **only "AI"** (`AI_SETTINGS`): Settings → AI is visible, Overview stats load; chat nav is hidden. - [ ] Admin (both flags): everything works as before. ## Known follow-ups (out of scope — pre-existing, shared endpoints) These remain on `AI` because they're shared with non-settings surfaces and need either OR-gating or a resolver split, so a role with `AI_SETTINGS` but **not** `AI` still can't use them yet: - `getAiSystemPromptPreview` (Models/Prompts tabs) lives in the chat resolver, class-gated `AI`; NestJS guards are additive so it can't be cleanly method-overridden — it should be pulled into a settings resolver. - Agent reads `findManyAgents` / `findOneAgent` (agent create/edit forms) are class-gated `AI` and shared with the **Workflow** editor and **Roles** pages; these want a guard that accepts `AI ∨ AI_SETTINGS ∨ WORKFLOWS`. --- .../src/modules/app/components/SettingsRoutes.tsx | 2 +- .../modules/settings/hooks/useSettingsNavigationItems.tsx | 2 +- .../resolvers/ai-workspace-stats.resolver.ts | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 04fb88db16..8218742dbc 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -690,7 +690,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( } > diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index 791dcd2a5e..a9f7dd306e 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -171,7 +171,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { label: t`AI`, path: SettingsPath.AI, Icon: IconSparkles, - isHidden: !permissionMap[PermissionFlagType.AI], + isHidden: !permissionMap[PermissionFlagType.AI_SETTINGS], }, { label: t`Email`, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-workspace-stats/resolvers/ai-workspace-stats.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-workspace-stats/resolvers/ai-workspace-stats.resolver.ts index 3bc49936d7..c211a185ab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-workspace-stats/resolvers/ai-workspace-stats.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-workspace-stats/resolvers/ai-workspace-stats.resolver.ts @@ -13,7 +13,10 @@ import { WorkspaceAiStatsDTO } from 'src/engine/metadata-modules/ai/ai-workspace import { AiWorkspaceStatsService } from 'src/engine/metadata-modules/ai/ai-workspace-stats/services/ai-workspace-stats.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI)) +@UseGuards( + WorkspaceAuthGuard, + SettingsPermissionGuard(PermissionFlagType.AI_SETTINGS), +) @MetadataResolver() export class AiWorkspaceStatsResolver { constructor(