mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
feat(tags): enhance tag management with permission checks
- Integrated user permissions for tag creation, updating, and deletion in the TagManager component. - Updated API routes to enforce permission checks for tag operations. - Added new permissions for managing tags in the roles configuration. - Improved error handling for unauthorized access in tag-related operations.
This commit is contained in:
@@ -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 (
|
||||
<div className="w-full">
|
||||
@@ -47,7 +48,7 @@ export const TagManager = () => {
|
||||
No tags yet. Create your first tag to start organizing
|
||||
projects.
|
||||
</span>
|
||||
<HandleTag />
|
||||
{permissions?.tag.create && <HandleTag />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
@@ -70,8 +71,10 @@ export const TagManager = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<HandleTag tagId={tag.tagId} />
|
||||
<DialogAction
|
||||
{permissions?.tag.update && (
|
||||
<HandleTag tagId={tag.tagId} />
|
||||
)}
|
||||
{permissions?.tag.delete && (<DialogAction
|
||||
title="Delete Tag"
|
||||
description={`Are you sure you want to delete the tag "${tag.name}"? This will remove the tag from all projects. This action cannot be undone.`}
|
||||
type="destructive"
|
||||
@@ -101,15 +104,18 @@ export const TagManager = () => {
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleTag />
|
||||
</div>
|
||||
{permissions?.tag.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleTag />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -143,6 +143,10 @@ const RESOURCE_META: Record<string, { label: string; description: string }> = {
|
||||
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"],
|
||||
|
||||
@@ -656,6 +656,18 @@ export const settingsRouter = createTRPCRouter({
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
"tag",
|
||||
"patch",
|
||||
"server",
|
||||
"volumeBackups",
|
||||
"environment",
|
||||
"auditLog",
|
||||
"customRole",
|
||||
"whitelabeling",
|
||||
"sso",
|
||||
"licenseKey",
|
||||
"organization",
|
||||
"previewDeployment",
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string>([
|
||||
"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: [],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user