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: [],
});