From b5473e63fdac3d9756a16786f902d643e53197a8 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Sun, 20 Sep 2026 22:57:01 +0530 Subject: [PATCH] feat(permissions): add AI module to enterprise custom roles --- .../components/dashboard/settings/ai-form.tsx | 62 +++++++++++-------- apps/dokploy/components/layouts/side.tsx | 2 +- .../proprietary/roles/manage-custom-roles.tsx | 25 ++++++++ apps/dokploy/server/api/routers/ai.ts | 40 ++++++------ packages/server/src/lib/access-control.ts | 7 ++- 5 files changed, 89 insertions(+), 47 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx index 797aad23c..abe1a1c3e 100644 --- a/apps/dokploy/components/dashboard/settings/ai-form.tsx +++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx @@ -18,7 +18,9 @@ import { HandleAiProviders } from "./handle-ai-providers"; export const AiForm = () => { const { data: aiConfigs, refetch, isPending } = api.ai.getAll.useQuery(); const { mutateAsync, isPending: isRemoving } = api.ai.delete.useMutation(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: currentUser } = api.user.get.useQuery(); + const isOrgAdmin = currentUser?.role === "owner" || currentUser?.role === "admin"; @@ -36,7 +38,9 @@ export const AiForm = () => {
{isOrgAdmin && } - {aiConfigs && aiConfigs?.length > 0 && } + {permissions?.ai.create && aiConfigs && aiConfigs?.length > 0 && ( + + )}
@@ -53,7 +57,7 @@ export const AiForm = () => { You don't have any AI configurations - + {permissions?.ai.create && } ) : (
@@ -70,33 +74,37 @@ export const AiForm = () => { {config.model}
- - { - await mutateAsync({ - aiId: config.aiId, - }) - .then(() => { - toast.success("AI deleted successfully"); - refetch(); + {permissions?.ai.update && ( + + )} + {permissions?.ai.delete && ( + { + await mutateAsync({ + aiId: config.aiId, }) - .catch(() => { - toast.error("Error deleting AI"); - }); - }} - > - - + + + )}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 59e628aa0..cec02762a 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -351,7 +351,7 @@ const MENU: Menu = { icon: BotIcon, url: "/dashboard/settings/ai", isSingle: true, - isEnabled: ({ permissions }) => !!permissions?.organization.update, + isEnabled: ({ permissions }) => !!permissions?.ai.read, }, { isSingle: true, diff --git a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx index e7d7ae316..22f1dd507 100644 --- a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx +++ b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx @@ -185,6 +185,11 @@ const RESOURCE_META: Record = { description: "Manage the providers (Grafana Loki, Prometheus, Datadog, New Relic, etc.) that container logs and metrics are shipped to", }, + ai: { + label: "AI Settings", + description: + "Manage AI providers, models, and use AI features (log analysis, suggestions)", + }, }; /** Descriptions for each action within a resource */ @@ -495,6 +500,25 @@ const ACTION_META: Record< description: "Remove logs and metrics providers", }, }, + ai: { + read: { + label: "Read", + description: + "View AI providers and use AI features (log analysis, suggestions)", + }, + create: { + label: "Create", + description: "Add new AI provider configurations", + }, + update: { + label: "Update", + description: "Edit AI provider settings and custom providers", + }, + delete: { + label: "Delete", + description: "Remove AI provider configurations", + }, + }, }; /** Resources that should be hidden from the custom role editor (better-auth internals) */ @@ -606,6 +630,7 @@ const ROLE_PRESETS: { logs: ["read"], monitoring: ["read"], auditLog: ["read"], + ai: ["read", "create", "update", "delete"], }, }, ]; diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index 169a32b07..2bf3135a4 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -38,18 +38,18 @@ import { slugify } from "@/lib/slug"; import { adminProcedure, createTRPCRouter, - protectedProcedure, + withPermission, } from "@/server/api/trpc"; import { generatePassword } from "@/templates/utils"; export const aiRouter = createTRPCRouter({ - one: adminProcedure + one: withPermission("ai", "read") .input(z.object({ aiId: z.string() })) .query(async ({ input }) => { return await getAiSettingById(input.aiId); }), - getModels: protectedProcedure + getModels: withPermission("ai", "read") .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() })) .query(async ({ input }) => { try { @@ -177,33 +177,37 @@ export const aiRouter = createTRPCRouter({ }); } }), - create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => { - return await saveAiSettings(ctx.session.activeOrganizationId, input); - }), + create: withPermission("ai", "create") + .input(apiCreateAi) + .mutation(async ({ ctx, input }) => { + return await saveAiSettings(ctx.session.activeOrganizationId, input); + }), - update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => { - return await saveAiSettings(ctx.session.activeOrganizationId, input); - }), + update: withPermission("ai", "update") + .input(apiUpdateAi) + .mutation(async ({ ctx, input }) => { + return await saveAiSettings(ctx.session.activeOrganizationId, input); + }), - getAll: adminProcedure.query(async ({ ctx }) => { + getAll: withPermission("ai", "read").query(async ({ ctx }) => { return await getAiSettingsByOrganizationId( ctx.session.activeOrganizationId, ); }), - get: adminProcedure + get: withPermission("ai", "read") .input(z.object({ aiId: z.string() })) .query(async ({ input }) => { return await getAiSettingById(input.aiId); }), - delete: adminProcedure + delete: withPermission("ai", "delete") .input(z.object({ aiId: z.string() })) .mutation(async ({ input }) => { return await deleteAiSettings(input.aiId); }), - getCustomProviders: protectedProcedure.query(async ({ ctx }) => { + getCustomProviders: withPermission("ai", "read").query(async ({ ctx }) => { return await getCustomAiProviders(ctx.session.activeOrganizationId); }), @@ -216,7 +220,7 @@ export const aiRouter = createTRPCRouter({ ); }), - getEnabledProviders: protectedProcedure.query(async ({ ctx }) => { + getEnabledProviders: withPermission("ai", "read").query(async ({ ctx }) => { const settings = await getAiSettingsByOrganizationId( ctx.session.activeOrganizationId, ); @@ -225,7 +229,7 @@ export const aiRouter = createTRPCRouter({ .map((s) => ({ aiId: s.aiId, name: s.name, model: s.model })); }), - analyzeLogs: protectedProcedure + analyzeLogs: withPermission("ai", "read") .input( z.object({ aiId: z.string().min(1), @@ -283,7 +287,7 @@ ${input.logs}`, } }), - testConnection: protectedProcedure + testConnection: withPermission("ai", "create") .input( z.object({ apiUrl: z.string().min(1), @@ -317,7 +321,7 @@ ${input.logs}`, } }), - suggest: protectedProcedure + suggest: withPermission("ai", "read") .input( z.object({ aiId: z.string(), @@ -338,7 +342,7 @@ ${input.logs}`, }); } }), - deploy: protectedProcedure + deploy: withPermission("ai", "read") .input(deploySuggestionSchema) .mutation(async ({ ctx, input }) => { const environment = await findEnvironmentById(input.environmentId); diff --git a/packages/server/src/lib/access-control.ts b/packages/server/src/lib/access-control.ts index 9f27b16dc..2023075f3 100644 --- a/packages/server/src/lib/access-control.ts +++ b/packages/server/src/lib/access-control.ts @@ -9,7 +9,7 @@ import { createAccessControl } from "better-auth/plugins/access"; * The rest are Dokploy-specific resources. * * Enterprise-only resources (only assignable via custom roles): - * deployment, envVars, server, registry, certificate, backup, domain, logs, monitoring + * deployment, envVars, server, registry, certificate, backup, domain, logs, monitoring, ai */ export const statements = { // better-auth organization plugin defaults @@ -51,6 +51,7 @@ export const statements = { auditLog: ["read"], vaultProvider: ["read", "create", "update", "delete"], dnsProvider: ["read", "create", "update", "delete"], + ai: ["read", "create", "update", "delete"], } as const; /** @@ -79,6 +80,7 @@ export const enterpriseOnlyResources = new Set([ "auditLog", "vaultProvider", "dnsProvider", + "ai", ]); export const ac = createAccessControl(statements); @@ -121,6 +123,7 @@ export const ownerRole = ac.newRole({ vaultProvider: ["read", "create", "update", "delete"], dnsProvider: ["read", "create", "update", "delete"], telemetryProvider: ["read", "create", "delete"], + ai: ["read", "create", "update", "delete"], }); /** @@ -161,6 +164,7 @@ export const adminRole = ac.newRole({ vaultProvider: ["read", "create", "update", "delete"], dnsProvider: ["read", "create", "update", "delete"], telemetryProvider: ["read", "create", "delete"], + ai: ["read", "create", "update", "delete"], }); /** @@ -207,4 +211,5 @@ export const memberRole = ac.newRole({ // Members need provider/secret names for env editor autocomplete; values are never exposed vaultProvider: ["read"], dnsProvider: [], + ai: [], });