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