diff --git a/apps/dokploy/components/dashboard/settings/tags/tag-manager.tsx b/apps/dokploy/components/dashboard/settings/tags/tag-manager.tsx
index 3c9125a52..08d0a08b6 100644
--- a/apps/dokploy/components/dashboard/settings/tags/tag-manager.tsx
+++ b/apps/dokploy/components/dashboard/settings/tags/tag-manager.tsx
@@ -18,6 +18,7 @@ export const TagManager = () => {
const { data: tags, isPending } = api.tag.all.useQuery();
const { mutateAsync: deleteTag, isPending: isRemoving } =
api.tag.remove.useMutation();
+ const { data: permissions } = api.user.getPermissions.useQuery();
return (
@@ -47,7 +48,7 @@ export const TagManager = () => {
No tags yet. Create your first tag to start organizing
projects.
-
+ {permissions?.tag.create && }
) : (
@@ -70,8 +71,10 @@ export const TagManager = () => {
)}
-
-
+ )}
+ {permissions?.tag.delete && ( {
+ )}
))}
-
-
-
+ {permissions?.tag.create && (
+
+
+
+ )}
)}
>
diff --git a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
index a93cb87c6..51b31b84c 100644
--- a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
+++ b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
@@ -143,6 +143,10 @@ const RESOURCE_META: Record = {
description:
"Manage notification providers (Slack, Discord, Telegram, etc.)",
},
+ tag: {
+ label: "Tags",
+ description: "Manage tags to organize and categorize projects",
+ },
member: {
label: "Users",
description: "Manage organization members, invitations, and roles",
@@ -379,6 +383,12 @@ const ACTION_META: Record<
},
delete: { label: "Delete", description: "Remove notification providers" },
},
+ tag: {
+ read: { label: "Read", description: "View tags" },
+ create: { label: "Create", description: "Create new tags" },
+ update: { label: "Update", description: "Edit existing tags" },
+ delete: { label: "Delete", description: "Delete tags" },
+ },
member: {
read: {
label: "Read",
@@ -447,6 +457,7 @@ const ROLE_PRESETS: {
domain: ["read"],
destination: ["read"],
notification: ["read"],
+ tag: ["read"],
member: ["read"],
logs: ["read"],
monitoring: ["read"],
@@ -515,6 +526,7 @@ const ROLE_PRESETS: {
domain: ["read", "create", "delete"],
destination: ["read", "create", "delete"],
notification: ["read", "create", "delete"],
+ tag: ["read", "create", "update", "delete"],
logs: ["read"],
monitoring: ["read"],
auditLog: ["read"],
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index 4b12abc4c..8e46e9b7d 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -656,6 +656,18 @@ export const settingsRouter = createTRPCRouter({
"github",
"gitlab",
"gitea",
+ "tag",
+ "patch",
+ "server",
+ "volumeBackups",
+ "environment",
+ "auditLog",
+ "customRole",
+ "whitelabeling",
+ "sso",
+ "licenseKey",
+ "organization",
+ "previewDeployment",
],
});
diff --git a/apps/dokploy/server/api/routers/tag.ts b/apps/dokploy/server/api/routers/tag.ts
index 50356448a..8d0f8dc0b 100644
--- a/apps/dokploy/server/api/routers/tag.ts
+++ b/apps/dokploy/server/api/routers/tag.ts
@@ -1,3 +1,4 @@
+import { findMemberByUserId } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
@@ -11,10 +12,10 @@ import {
projectTags,
tags,
} from "@/server/db/schema";
-import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const tagRouter = createTRPCRouter({
- create: protectedProcedure
+ create: withPermission("tag", "create")
.input(apiCreateTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -46,7 +47,7 @@ export const tagRouter = createTRPCRouter({
}
}),
- all: protectedProcedure.query(async ({ ctx }) => {
+ all: withPermission("tag", "read").query(async ({ ctx }) => {
try {
const organizationTags = await db.query.tags.findMany({
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
@@ -63,36 +64,38 @@ export const tagRouter = createTRPCRouter({
}
}),
- one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
- try {
- const tag = await db.query.tags.findFirst({
- where: and(
- eq(tags.tagId, input.tagId),
- eq(tags.organizationId, ctx.session.activeOrganizationId),
- ),
- });
+ one: withPermission("tag", "read")
+ .input(apiFindOneTag)
+ .query(async ({ input, ctx }) => {
+ try {
+ const tag = await db.query.tags.findFirst({
+ where: and(
+ eq(tags.tagId, input.tagId),
+ eq(tags.organizationId, ctx.session.activeOrganizationId),
+ ),
+ });
- if (!tag) {
+ if (!tag) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Tag not found",
+ });
+ }
+
+ return tag;
+ } catch (error) {
+ if (error instanceof TRPCError) {
+ throw error;
+ }
throw new TRPCError({
- code: "NOT_FOUND",
- message: "Tag not found",
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Error fetching tag: ${error instanceof Error ? error.message : error}`,
+ cause: error,
});
}
+ }),
- return tag;
- } catch (error) {
- if (error instanceof TRPCError) {
- throw error;
- }
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: `Error fetching tag: ${error instanceof Error ? error.message : error}`,
- cause: error,
- });
- }
- }),
-
- update: protectedProcedure
+ update: withPermission("tag", "update")
.input(apiUpdateTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -142,7 +145,7 @@ export const tagRouter = createTRPCRouter({
}
}),
- remove: protectedProcedure
+ remove: withPermission("tag", "delete")
.input(apiRemoveTag)
.mutation(async ({ input, ctx }) => {
try {
@@ -186,6 +189,11 @@ export const tagRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
+ const memberRecord = await findMemberByUserId(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
@@ -202,6 +210,18 @@ export const tagRouter = createTRPCRouter({
});
}
+ // Verify the member has access to the project
+ if (
+ memberRecord.role !== "owner" &&
+ memberRecord.role !== "admin" &&
+ !memberRecord.accessedProjects.includes(input.projectId)
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this project",
+ });
+ }
+
// Verify the tag belongs to the user's organization
const tag = await db.query.tags.findFirst({
where: and(
@@ -257,6 +277,11 @@ export const tagRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
+ const memberRecord = await findMemberByUserId(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
@@ -273,6 +298,18 @@ export const tagRouter = createTRPCRouter({
});
}
+ // Verify the member has access to the project
+ if (
+ memberRecord.role !== "owner" &&
+ memberRecord.role !== "admin" &&
+ !memberRecord.accessedProjects.includes(input.projectId)
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this project",
+ });
+ }
+
// Verify the tag belongs to the user's organization
const tag = await db.query.tags.findFirst({
where: and(
@@ -320,6 +357,11 @@ export const tagRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
+ const memberRecord = await findMemberByUserId(
+ ctx.user.id,
+ ctx.session.activeOrganizationId,
+ );
+
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
@@ -336,6 +378,18 @@ export const tagRouter = createTRPCRouter({
});
}
+ // Verify the member has access to the project
+ if (
+ memberRecord.role !== "owner" &&
+ memberRecord.role !== "admin" &&
+ !memberRecord.accessedProjects.includes(input.projectId)
+ ) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You don't have access to this project",
+ });
+ }
+
// Verify all tags belong to the user's organization
if (input.tagIds.length > 0) {
const tagCount = await db.query.tags.findMany({
diff --git a/packages/server/src/db/schema/tag.ts b/packages/server/src/db/schema/tag.ts
index 1de7c0015..624149760 100644
--- a/packages/server/src/db/schema/tag.ts
+++ b/packages/server/src/db/schema/tag.ts
@@ -84,11 +84,9 @@ export const apiCreateTag = createSchema.pick({
color: true,
});
-export const apiFindOneTag = createSchema
- .pick({
- tagId: true,
- })
- .required();
+export const apiFindOneTag = z.object({
+ tagId: z.string().min(1),
+});
export const apiRemoveTag = createSchema
.pick({
diff --git a/packages/server/src/lib/access-control.ts b/packages/server/src/lib/access-control.ts
index d49fd2d45..1289f4116 100644
--- a/packages/server/src/lib/access-control.ts
+++ b/packages/server/src/lib/access-control.ts
@@ -44,6 +44,7 @@ export const statements = {
domain: ["read", "create", "delete"],
destination: ["read", "create", "delete"],
notification: ["read", "create", "update", "delete"],
+ tag: ["read", "create", "update", "delete"],
logs: ["read"],
monitoring: ["read"],
auditLog: ["read"],
@@ -69,6 +70,7 @@ export const enterpriseOnlyResources = new Set([
"domain",
"destination",
"notification",
+ "tag",
"logs",
"monitoring",
"auditLog",
@@ -107,6 +109,7 @@ export const ownerRole = ac.newRole({
domain: ["read", "create", "delete"],
destination: ["read", "create", "delete"],
notification: ["read", "create", "update", "delete"],
+ tag: ["read", "create", "update", "delete"],
logs: ["read"],
monitoring: ["read"],
auditLog: ["read"],
@@ -143,6 +146,7 @@ export const adminRole = ac.newRole({
domain: ["read", "create", "delete"],
destination: ["read", "create", "delete"],
notification: ["read", "create", "update", "delete"],
+ tag: ["read", "create", "update", "delete"],
logs: ["read"],
monitoring: ["read"],
auditLog: ["read"],
@@ -186,5 +190,6 @@ export const memberRole = ac.newRole({
certificate: [],
destination: [],
notification: [],
+ tag: ["read"],
auditLog: [],
});