diff --git a/apps/dokploy/__test__/permissions/check-permission.test.ts b/apps/dokploy/__test__/permissions/check-permission.test.ts new file mode 100644 index 0000000000..7f14e2d0ed --- /dev/null +++ b/apps/dokploy/__test__/permissions/check-permission.test.ts @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockMemberData = ( + role: string, + overrides: Record = {}, +) => ({ + id: "member-1", + role, + userId: "user-1", + organizationId: "org-1", + accessedProjects: [] as string[], + accessedServices: [] as string[], + accessedEnvironments: [] as string[], + canCreateProjects: overrides.canCreateProjects ?? false, + canDeleteProjects: overrides.canDeleteProjects ?? false, + canCreateServices: overrides.canCreateServices ?? false, + canDeleteServices: overrides.canDeleteServices ?? false, + canCreateEnvironments: overrides.canCreateEnvironments ?? false, + canDeleteEnvironments: overrides.canDeleteEnvironments ?? false, + canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false, + canAccessToDocker: overrides.canAccessToDocker ?? false, + canAccessToAPI: overrides.canAccessToAPI ?? false, + canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false, + canAccessToGitProviders: overrides.canAccessToGitProviders ?? false, + user: { id: "user-1", email: "test@test.com" }, +}); + +let memberToReturn: ReturnType = + mockMemberData("member"); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { + member: { + findFirst: vi.fn(() => Promise.resolve(memberToReturn)), + findMany: vi.fn(() => Promise.resolve([])), + }, + organizationRole: { + findFirst: vi.fn(), + findMany: vi.fn(() => Promise.resolve([])), + }, + }, + }, +})); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +const { checkPermission } = await import("@dokploy/server/services/permission"); + +const ctx = { + user: { id: "user-1" }, + session: { activeOrganizationId: "org-1" }, +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("static roles bypass enterprise resources", () => { + it("owner bypasses deployment.read", async () => { + memberToReturn = mockMemberData("owner"); + await expect( + checkPermission(ctx, { deployment: ["read"] }), + ).resolves.toBeUndefined(); + }); + + it("admin bypasses backup.create", async () => { + memberToReturn = mockMemberData("admin"); + await expect( + checkPermission(ctx, { backup: ["create"] }), + ).resolves.toBeUndefined(); + }); + + it("member bypasses schedule.delete", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { schedule: ["delete"] }), + ).resolves.toBeUndefined(); + }); + + it("member bypasses multiple enterprise permissions at once", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { + deployment: ["read"], + backup: ["create"], + domain: ["delete"], + }), + ).resolves.toBeUndefined(); + }); +}); + +describe("static roles validate free-tier resources", () => { + it("owner passes project.create", async () => { + memberToReturn = mockMemberData("owner"); + await expect( + checkPermission(ctx, { project: ["create"] }), + ).resolves.toBeUndefined(); + }); + + it("member fails project.create (no legacy override)", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { project: ["create"] }), + ).rejects.toThrow(); + }); + + it("member passes service.read", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { service: ["read"] }), + ).resolves.toBeUndefined(); + }); + + it("member fails service.create", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { service: ["create"] }), + ).rejects.toThrow(); + }); +}); + +describe("legacy boolean overrides for member", () => { + it("member passes project.create with canCreateProjects=true", async () => { + memberToReturn = mockMemberData("member", { canCreateProjects: true }); + await expect( + checkPermission(ctx, { project: ["create"] }), + ).resolves.toBeUndefined(); + }); + + it("member passes docker.read with canAccessToDocker=true", async () => { + memberToReturn = mockMemberData("member", { canAccessToDocker: true }); + await expect( + checkPermission(ctx, { docker: ["read"] }), + ).resolves.toBeUndefined(); + }); + + it("member fails docker.read with canAccessToDocker=false", async () => { + memberToReturn = mockMemberData("member"); + await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow(); + }); +}); diff --git a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts new file mode 100644 index 0000000000..9568b12afd --- /dev/null +++ b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from "vitest"; +import { + enterpriseOnlyResources, + statements, +} from "@dokploy/server/lib/access-control"; + +const FREE_TIER_RESOURCES = [ + "organization", + "member", + "invitation", + "team", + "ac", + "project", + "service", + "environment", + "docker", + "sshKeys", + "gitProviders", + "traefikFiles", + "api", +]; + +const ENTERPRISE_RESOURCES = [ + "volume", + "deployment", + "envVars", + "projectEnvVars", + "environmentEnvVars", + "server", + "registry", + "certificate", + "backup", + "volumeBackup", + "schedule", + "domain", + "destination", + "notification", + "logs", + "monitoring", + "auditLog", +]; + +describe("enterpriseOnlyResources set", () => { + it("contains all enterprise resources", () => { + for (const resource of ENTERPRISE_RESOURCES) { + expect(enterpriseOnlyResources.has(resource)).toBe(true); + } + }); + + it("does NOT contain free-tier resources", () => { + for (const resource of FREE_TIER_RESOURCES) { + expect(enterpriseOnlyResources.has(resource)).toBe(false); + } + }); + + it("every resource in statements is either free or enterprise", () => { + const allResources = Object.keys(statements); + for (const resource of allResources) { + const isFree = FREE_TIER_RESOURCES.includes(resource); + const isEnterprise = enterpriseOnlyResources.has(resource); + expect(isFree || isEnterprise).toBe(true); + } + }); + + it("free and enterprise sets don't overlap", () => { + for (const resource of FREE_TIER_RESOURCES) { + expect(enterpriseOnlyResources.has(resource)).toBe(false); + } + }); + + it("all statement resources are accounted for", () => { + const allResources = Object.keys(statements); + const categorized = [...FREE_TIER_RESOURCES, ...ENTERPRISE_RESOURCES]; + for (const resource of allResources) { + expect(categorized).toContain(resource); + } + }); +}); diff --git a/apps/dokploy/__test__/permissions/resolve-permissions.test.ts b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts new file mode 100644 index 0000000000..759c8dad8f --- /dev/null +++ b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts @@ -0,0 +1,161 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockMemberData = ( + role: string, + overrides: Record = {}, +) => ({ + id: "member-1", + role, + userId: "user-1", + organizationId: "org-1", + accessedProjects: [] as string[], + accessedServices: [] as string[], + accessedEnvironments: [] as string[], + canCreateProjects: overrides.canCreateProjects ?? false, + canDeleteProjects: overrides.canDeleteProjects ?? false, + canCreateServices: overrides.canCreateServices ?? false, + canDeleteServices: overrides.canDeleteServices ?? false, + canCreateEnvironments: overrides.canCreateEnvironments ?? false, + canDeleteEnvironments: overrides.canDeleteEnvironments ?? false, + canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false, + canAccessToDocker: overrides.canAccessToDocker ?? false, + canAccessToAPI: overrides.canAccessToAPI ?? false, + canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false, + canAccessToGitProviders: overrides.canAccessToGitProviders ?? false, + user: { id: "user-1", email: "test@test.com" }, +}); + +let memberToReturn: ReturnType = + mockMemberData("member"); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { + member: { + findFirst: vi.fn(() => Promise.resolve(memberToReturn)), + findMany: vi.fn(() => Promise.resolve([])), + }, + organizationRole: { + findFirst: vi.fn(), + findMany: vi.fn(() => Promise.resolve([])), + }, + }, + }, +})); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +const { resolvePermissions } = await import( + "@dokploy/server/services/permission" +); +const { enterpriseOnlyResources, statements } = await import( + "@dokploy/server/lib/access-control" +); + +const ctx = { + user: { id: "user-1" }, + session: { activeOrganizationId: "org-1" }, +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("enterprise resources for static roles", () => { + it("owner gets true for all enterprise resources", async () => { + memberToReturn = mockMemberData("owner"); + const perms = await resolvePermissions(ctx); + + for (const resource of enterpriseOnlyResources) { + const actions = statements[resource as keyof typeof statements]; + for (const action of actions) { + expect((perms as any)[resource][action]).toBe(true); + } + } + }); + + it("admin gets true for all enterprise resources", async () => { + memberToReturn = mockMemberData("admin"); + const perms = await resolvePermissions(ctx); + + for (const resource of enterpriseOnlyResources) { + const actions = statements[resource as keyof typeof statements]; + for (const action of actions) { + expect((perms as any)[resource][action]).toBe(true); + } + } + }); + + it("member gets true for service-level enterprise resources", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + + expect(perms.deployment.read).toBe(true); + expect(perms.deployment.create).toBe(true); + expect(perms.domain.read).toBe(true); + expect(perms.backup.read).toBe(true); + expect(perms.logs.read).toBe(true); + expect(perms.monitoring.read).toBe(true); + }); + + it("member gets false for org-level enterprise resources", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + + expect(perms.server.read).toBe(false); + expect(perms.registry.read).toBe(false); + expect(perms.certificate.read).toBe(false); + expect(perms.destination.read).toBe(false); + expect(perms.notification.read).toBe(false); + expect(perms.auditLog.read).toBe(false); + }); +}); + +describe("free-tier resources for member", () => { + it("member gets service.read=true", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + expect(perms.service.read).toBe(true); + }); + + it("member gets project.create=false without legacy override", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + expect(perms.project.create).toBe(false); + }); + + it("member gets project.create=true with canCreateProjects", async () => { + memberToReturn = mockMemberData("member", { canCreateProjects: true }); + const perms = await resolvePermissions(ctx); + expect(perms.project.create).toBe(true); + }); + + it("member gets docker.read=false without legacy override", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + expect(perms.docker.read).toBe(false); + }); + + it("member gets docker.read=true with canAccessToDocker", async () => { + memberToReturn = mockMemberData("member", { canAccessToDocker: true }); + const perms = await resolvePermissions(ctx); + expect(perms.docker.read).toBe(true); + }); +}); + +describe("free-tier resources for owner", () => { + it("owner gets all free-tier permissions as true", async () => { + memberToReturn = mockMemberData("owner"); + const perms = await resolvePermissions(ctx); + expect(perms.project.create).toBe(true); + expect(perms.project.delete).toBe(true); + expect(perms.service.create).toBe(true); + expect(perms.service.read).toBe(true); + expect(perms.service.delete).toBe(true); + expect(perms.docker.read).toBe(true); + expect(perms.traefikFiles.read).toBe(true); + expect(perms.traefikFiles.write).toBe(true); + }); +}); diff --git a/apps/dokploy/__test__/permissions/service-access.test.ts b/apps/dokploy/__test__/permissions/service-access.test.ts new file mode 100644 index 0000000000..b3786807db --- /dev/null +++ b/apps/dokploy/__test__/permissions/service-access.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockMemberData = ( + role: string, + accessedServices: string[] = [], + accessedProjects: string[] = [], +) => ({ + id: "member-1", + role, + userId: "user-1", + organizationId: "org-1", + accessedProjects, + accessedServices, + accessedEnvironments: [] as string[], + canCreateProjects: false, + canDeleteProjects: false, + canCreateServices: false, + canDeleteServices: false, + canCreateEnvironments: false, + canDeleteEnvironments: false, + canAccessToTraefikFiles: false, + canAccessToDocker: false, + canAccessToAPI: false, + canAccessToSSHKeys: false, + canAccessToGitProviders: false, + user: { id: "user-1", email: "test@test.com" }, +}); + +let memberToReturn: ReturnType = + mockMemberData("member"); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: { + member: { + findFirst: vi.fn(() => Promise.resolve(memberToReturn)), + findMany: vi.fn(() => Promise.resolve([])), + }, + organizationRole: { + findFirst: vi.fn(), + findMany: vi.fn(() => Promise.resolve([])), + }, + }, + }, +})); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +const { checkServicePermissionAndAccess, checkServiceAccess } = await import( + "@dokploy/server/services/permission" +); + +const ctx = { + user: { id: "user-1" }, + session: { activeOrganizationId: "org-1" }, +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("checkServicePermissionAndAccess", () => { + it("owner bypasses accessedServices check", async () => { + memberToReturn = mockMemberData("owner", []); + await expect( + checkServicePermissionAndAccess(ctx, "service-123", { + deployment: ["read"], + }), + ).resolves.toBeUndefined(); + }); + + it("admin bypasses accessedServices check", async () => { + memberToReturn = mockMemberData("admin", []); + await expect( + checkServicePermissionAndAccess(ctx, "service-123", { + backup: ["create"], + }), + ).resolves.toBeUndefined(); + }); + + it("member with access to service passes", async () => { + memberToReturn = mockMemberData("member", ["service-123"]); + await expect( + checkServicePermissionAndAccess(ctx, "service-123", { + deployment: ["read"], + }), + ).resolves.toBeUndefined(); + }); + + it("member WITHOUT access to service fails", async () => { + memberToReturn = mockMemberData("member", ["other-service"]); + await expect( + checkServicePermissionAndAccess(ctx, "service-123", { + deployment: ["read"], + }), + ).rejects.toThrow("You don't have access to this service"); + }); + + it("member with empty accessedServices fails", async () => { + memberToReturn = mockMemberData("member", []); + await expect( + checkServicePermissionAndAccess(ctx, "service-123", { + domain: ["delete"], + }), + ).rejects.toThrow("You don't have access to this service"); + }); +}); + +describe("checkServiceAccess", () => { + it("member with service access passes read check", async () => { + memberToReturn = mockMemberData("member", ["app-1"]); + await expect( + checkServiceAccess(ctx, "app-1", "read"), + ).resolves.toBeUndefined(); + }); + + it("member without service access fails read check", async () => { + memberToReturn = mockMemberData("member", []); + await expect(checkServiceAccess(ctx, "app-1", "read")).rejects.toThrow( + "You don't have access to this service", + ); + }); + + it("owner bypasses all access checks", async () => { + memberToReturn = mockMemberData("owner", [], []); + await expect( + checkServiceAccess(ctx, "project-1", "create"), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx index 5d89431976..94efbc285c 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/show-traefik-config.tsx @@ -15,13 +15,17 @@ interface Props { } export const ShowTraefikConfig = ({ applicationId }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canRead = permissions?.traefikFiles.read ?? false; const { data, isPending } = api.application.readTraefikConfig.useQuery( { applicationId, }, - { enabled: !!applicationId }, + { enabled: !!applicationId && canRead }, ); + if (!canRead) return null; + return ( diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index a8ec9053f0..b3646803c8 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -60,6 +60,8 @@ export const validateAndFormatYAML = (yamlText: string) => { }; export const UpdateTraefikConfig = ({ applicationId }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canWrite = permissions?.traefikFiles.write ?? false; const [open, setOpen] = useState(false); const [skipYamlValidation, setSkipYamlValidation] = useState(false); const { data, refetch } = api.application.readTraefikConfig.useQuery( @@ -125,9 +127,11 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => { } }} > - - - + {canWrite && ( + + + + )} Update traefik config diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 92b2591401..bc2329f069 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -21,6 +21,13 @@ interface Props { } export const ShowVolumes = ({ id, type }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canRead = permissions?.volume.read ?? false; + const canCreate = permissions?.volume.create ?? false; + const canDelete = permissions?.volume.delete ?? false; + + if (!canRead) return null; + const queryMap = { postgres: () => api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), @@ -50,7 +57,7 @@ export const ShowVolumes = ({ id, type }: Props) => { - {data && data?.mounts.length > 0 && ( + {canCreate && data && data?.mounts.length > 0 && ( Add Volume @@ -63,9 +70,11 @@ export const ShowVolumes = ({ id, type }: Props) => { No volumes/mounts configured - - Add Volume - + {canCreate && ( + + Add Volume + + )} ) : (
@@ -130,38 +139,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
- - { - await deleteVolume({ - mountId: mount.mountId, - }) - .then(() => { - refetch(); - toast.success("Volume deleted successfully"); + {canCreate && ( + + )} + {canDelete && ( + { + await deleteVolume({ + mountId: mount.mountId, }) - .catch(() => { - toast.error("Error deleting volume"); - }); - }} - > - - + + + )}
diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index c207ba59c2..06428ae217 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -50,6 +50,9 @@ interface Props { } export const ShowDomains = ({ id, type }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canCreateDomain = permissions?.domain.create ?? false; + const canDeleteDomain = permissions?.domain.delete ?? false; const { data: application } = type === "application" ? api.application.one.useQuery( @@ -149,7 +152,7 @@ export const ShowDomains = ({ id, type }: Props) => {
- {data && data?.length > 0 && ( + {canCreateDomain && data && data?.length > 0 && ( - -
+ {canCreateDomain && ( +
+ + + +
+ )} ) : (
@@ -214,47 +219,51 @@ export const ShowDomains = ({ id, type }: Props) => { } /> )} - - - - { - await deleteDomain({ - domainId: item.domainId, - }) - .then((_data) => { - refetch(); - toast.success( - "Domain deleted successfully", - ); + + + )} + {canDeleteDomain && ( + { + await deleteDomain({ + domainId: item.domainId, }) - .catch(() => { - toast.error("Error deleting domain"); - }); - }} - > - - + + + )}
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index 8ff0f6a634..4f695ac88e 100644 --- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -36,6 +36,8 @@ interface Props { } export const ShowEnvironment = ({ id, type }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canWrite = permissions?.envVars.write ?? false; const queryMap = { postgres: () => api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), @@ -185,25 +187,27 @@ PORT=3000 )} /> -
- {hasChanges && ( + {canWrite && ( +
+ {hasChanges && ( + + )} - )} - -
+
+ )} diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index 04b6bc4c93..fcfd81778d 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -31,6 +31,8 @@ interface Props { } export const ShowEnvironment = ({ applicationId }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canWrite = permissions?.envVars.write ?? false; const { mutateAsync, isPending } = api.application.saveEnvironment.useMutation(); @@ -201,27 +203,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => { )} /> )} -
- {hasChanges && ( - + )} + - )} - -
+
+ )}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index d10925eff8..a4fab46d9f 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -1,5 +1,5 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import Link from "next/link"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; @@ -416,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { Watch Paths - -
- ? -
+ +

diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index 624adeb553..37a387bb5a 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -1,5 +1,5 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { KeyRoundIcon, LockIcon, X } from "lucide-react"; +import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -228,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => { Watch Paths - -

- ? -
+ +

diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index ee42caa5e7..01fc9e84ad 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -30,6 +30,9 @@ interface Props { export const ShowGeneralApplication = ({ applicationId }: Props) => { const router = useRouter(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; + const canUpdateService = permissions?.service.create ?? false; const { data, refetch } = api.application.one.useQuery( { applicationId, @@ -57,128 +60,135 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { - { - await deploy({ - applicationId: applicationId, - }) - .then(() => { - toast.success("Application deployed successfully"); - refetch(); - router.push( - `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`, - ); + {canDeploy && ( + { + await deploy({ + applicationId: applicationId, }) - .catch(() => { - toast.error("Error deploying application"); - }); - }} - > - - - { - await reload({ - applicationId: applicationId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Application reloaded successfully"); - refetch(); + + + )} + {canDeploy && ( + { + await reload({ + applicationId: applicationId, + appName: data?.appName || "", }) - .catch(() => { - toast.error("Error reloading application"); - }); - }} - > - - - { - await redeploy({ - applicationId: applicationId, - }) - .then(() => { - toast.success("Application rebuilt successfully"); - refetch(); + + + )} + {canDeploy && ( + { + await redeploy({ + applicationId: applicationId, }) - .catch(() => { - toast.error("Error rebuilding application"); - }); - }} - > - - + + + )} - {data?.applicationStatus === "idle" ? ( + {canDeploy && data?.applicationStatus === "idle" ? ( { - ) : ( + ) : canDeploy ? ( { - )} + ) : null} { Open Terminal -

- Autodeploy - { - await update({ - applicationId, - autoDeploy: enabled, - }) - .then(async () => { - toast.success("Auto Deploy Updated"); - await refetch(); + {canUpdateService && ( +
+ Autodeploy + { + await update({ + applicationId, + autoDeploy: enabled, }) - .catch(() => { - toast.error("Error updating Auto Deploy"); - }); - }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" - /> -
+ .then(async () => { + toast.success("Auto Deploy Updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error updating Auto Deploy"); + }); + }} + className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + /> +
+ )} -
- Clean Cache - { - await update({ - applicationId, - cleanCache: enabled, - }) - .then(async () => { - toast.success("Clean Cache Updated"); - await refetch(); + {canUpdateService && ( +
+ Clean Cache + { + await update({ + applicationId, + cleanCache: enabled, }) - .catch(() => { - toast.error("Error updating Clean Cache"); - }); - }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" - /> -
+ .then(async () => { + toast.success("Clean Cache Updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error updating Clean Cache"); + }); + }} + className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + /> +
+ )} diff --git a/apps/dokploy/components/dashboard/compose/delete-service.tsx b/apps/dokploy/components/dashboard/compose/delete-service.tsx index 9d417ee911..f4db6ad4a1 100644 --- a/apps/dokploy/components/dashboard/compose/delete-service.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-service.tsx @@ -46,6 +46,8 @@ interface Props { } export const DeleteService = ({ id, type }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDelete = permissions?.service.delete ?? false; const [isOpen, setIsOpen] = useState(false); const queryMap = { @@ -123,6 +125,8 @@ export const DeleteService = ({ id, type }: Props) => { data?.applicationStatus === "running") || (data && "composeStatus" in data && data?.composeStatus === "running"); + if (!canDelete) return null; + return ( diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index 8067a7db69..d04725e263 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -19,6 +19,9 @@ interface Props { } export const ComposeActions = ({ composeId }: Props) => { const router = useRouter(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; + const canUpdateService = permissions?.service.create ?? false; const { data, refetch } = api.compose.one.useQuery( { composeId, @@ -35,162 +38,169 @@ export const ComposeActions = ({ composeId }: Props) => { return (
- { - await deploy({ - composeId: composeId, - }) - .then(() => { - toast.success("Compose deployed successfully"); - refetch(); - router.push( - `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`, - ); - }) - .catch(() => { - toast.error("Error deploying compose"); - }); - }} - > - - - { - await redeploy({ - composeId: composeId, - }) - .then(() => { - toast.success("Compose reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading compose"); - }); - }} - > - - - {data?.composeType === "docker-compose" && - data?.composeStatus === "idle" ? ( + {canDeploy && ( { - await start({ + await deploy({ composeId: composeId, }) .then(() => { - toast.success("Compose started successfully"); + toast.success("Compose deployed successfully"); refetch(); + router.push( + `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`, + ); }) .catch(() => { - toast.error("Error starting compose"); + toast.error("Error deploying compose"); }); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await redeploy({ composeId: composeId, }) .then(() => { - toast.success("Compose stopped successfully"); + toast.success("Compose reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping compose"); + toast.error("Error reloading compose"); }); }} > )} + {canDeploy && + (data?.composeType === "docker-compose" && + data?.composeStatus === "idle" ? ( + { + await start({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting compose"); + }); + }} + > + + + ) : ( + { + await stop({ + composeId: composeId, + }) + .then(() => { + toast.success("Compose stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping compose"); + }); + }} + > + + + ))} { Open Terminal -
- Autodeploy - { - await update({ - composeId, - autoDeploy: enabled, - }) - .then(async () => { - toast.success("Auto Deploy Updated"); - await refetch(); + {canUpdateService && ( +
+ Autodeploy + { + await update({ + composeId, + autoDeploy: enabled, }) - .catch(() => { - toast.error("Error updating Auto Deploy"); - }); - }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" - /> -
+ .then(async () => { + toast.success("Auto Deploy Updated"); + await refetch(); + }) + .catch(() => { + toast.error("Error updating Auto Deploy"); + }); + }} + className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + /> +
+ )}
); }; diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index 8193ec8b68..28f958e3ea 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -26,6 +26,8 @@ const AddComposeFile = z.object({ type AddComposeFile = z.infer; export const ComposeFileEditor = ({ composeId }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canUpdate = permissions?.service.create ?? false; const utils = api.useUtils(); const { data, refetch } = api.compose.one.useQuery( { @@ -164,14 +166,16 @@ services:
- + {canUpdate && ( + + )}
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx index 4ad4f741ca..c84a55bb3c 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx @@ -1,5 +1,5 @@ import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { KeyRoundIcon, LockIcon, X } from "lucide-react"; +import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -230,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => { Watch Paths - -
- ? -
+ +

diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx index 9d953279c1..7c89d7b52f 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-general-mariadb.tsx @@ -21,6 +21,8 @@ interface Props { } export const ShowGeneralMariadb = ({ mariadbId }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; const { data, refetch } = api.mariadb.one.useQuery( { mariadbId, @@ -72,154 +74,75 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => { Deploy Settings - - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - - - { - await reload({ - mariadbId: mariadbId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Mariadb reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading Mariadb"); - }); - }} - > - - - - {data?.applicationStatus === "idle" ? ( + {canDeploy && ( { - await start({ - mariadbId: mariadbId, - }) - .then(() => { - toast.success("Mariadb started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting Mariadb"); - }); + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await reload({ mariadbId: mariadbId, + appName: data?.appName || "", }) .then(() => { - toast.success("Mariadb stopped successfully"); + toast.success("Mariadb reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping Mariadb"); + toast.error("Error reloading Mariadb"); }); }} > + + + ) : ( + + { + await stop({ + mariadbId: mariadbId, + }) + .then(() => { + toast.success("Mariadb stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mariadb"); + }); + }} + > + + + + ))} { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; const { data, refetch } = api.mongo.one.useQuery( { mongoId, @@ -73,153 +75,158 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => { - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - { - await reload({ - mongoId: mongoId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Mongo reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading Mongo"); - }); - }} - > - - - {data?.applicationStatus === "idle" ? ( + {canDeploy && ( { - await start({ - mongoId: mongoId, - }) - .then(() => { - toast.success("Mongo started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting Mongo"); - }); + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await reload({ mongoId: mongoId, + appName: data?.appName || "", }) .then(() => { - toast.success("Mongo stopped successfully"); + toast.success("Mongo reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping Mongo"); + toast.error("Error reloading Mongo"); }); }} > )} + {canDeploy && + (data?.applicationStatus === "idle" ? ( + { + await start({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mongo"); + }); + }} + > + + + ) : ( + { + await stop({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mongo"); + }); + }} + > + + + ))} { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; const { data, refetch } = api.mysql.one.useQuery( { mysqlId, @@ -71,153 +73,158 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => { - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - { - await reload({ - mysqlId: mysqlId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("MySQL reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading MySQL"); - }); - }} - > - - - {data?.applicationStatus === "idle" ? ( + {canDeploy && ( { - await start({ - mysqlId: mysqlId, - }) - .then(() => { - toast.success("MySQL started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting MySQL"); - }); + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await reload({ mysqlId: mysqlId, + appName: data?.appName || "", }) .then(() => { - toast.success("MySQL stopped successfully"); + toast.success("MySQL reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping MySQL"); + toast.error("Error reloading MySQL"); }); }} > )} + {canDeploy && + (data?.applicationStatus === "idle" ? ( + { + await start({ + mysqlId: mysqlId, + }) + .then(() => { + toast.success("MySQL started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting MySQL"); + }); + }} + > + + + ) : ( + { + await stop({ + mysqlId: mysqlId, + }) + .then(() => { + toast.success("MySQL stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping MySQL"); + }); + }} + > + + + ))} { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; const { data, refetch } = api.postgres.one.useQuery( { postgresId: postgresId, @@ -73,153 +75,162 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => { - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - { - await reload({ - postgresId: postgresId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("PostgreSQL reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading PostgreSQL"); - }); - }} - > - - - {data?.applicationStatus === "idle" ? ( + {canDeploy && ( { - await start({ - postgresId: postgresId, - }) - .then(() => { - toast.success("PostgreSQL started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting PostgreSQL"); - }); + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await reload({ postgresId: postgresId, + appName: data?.appName || "", }) .then(() => { - toast.success("PostgreSQL stopped successfully"); + toast.success("PostgreSQL reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping PostgreSQL"); + toast.error("Error reloading PostgreSQL"); }); }} > )} + {canDeploy && + (data?.applicationStatus === "idle" ? ( + { + await start({ + postgresId: postgresId, + }) + .then(() => { + toast.success("PostgreSQL started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting PostgreSQL"); + }); + }} + > + + + ) : ( + { + await stop({ + postgresId: postgresId, + }) + .then(() => { + toast.success("PostgreSQL stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping PostgreSQL"); + }); + }} + > + + + ))} { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canRead = permissions?.environmentEnvVars.read ?? false; + const canWrite = permissions?.environmentEnvVars.write ?? false; const [isOpen, setIsOpen] = useState(false); const utils = api.useUtils(); const { mutateAsync, error, isError, isPending } = @@ -97,6 +100,10 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => { }; }, [form, onSubmit, isPending, isOpen]); + if (!canRead) { + return null; + } + return (

@@ -141,6 +148,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => { )} /> - - - + {canWrite && ( + + + + )} diff --git a/apps/dokploy/components/dashboard/projects/project-environment.tsx b/apps/dokploy/components/dashboard/projects/project-environment.tsx index b02f9024a6..46e5d1f544 100644 --- a/apps/dokploy/components/dashboard/projects/project-environment.tsx +++ b/apps/dokploy/components/dashboard/projects/project-environment.tsx @@ -39,6 +39,9 @@ interface Props { } export const ProjectEnvironment = ({ projectId, children }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canRead = permissions?.projectEnvVars.read ?? false; + const canWrite = permissions?.projectEnvVars.write ?? false; const [isOpen, setIsOpen] = useState(false); const utils = api.useUtils(); const { mutateAsync, error, isError, isPending } = @@ -96,6 +99,10 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => { }; }, [form, onSubmit, isPending, isOpen]); + if (!canRead) { + return null; + } + return ( @@ -139,6 +146,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => { )} /> - - - + {canWrite && ( + + + + )} diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index f25fb6d478..7b5797b451 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -61,6 +61,7 @@ export const ShowProjects = () => { const { data: isCloud } = api.settings.isCloud.useQuery(); const { data, isPending } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); const [searchQuery, setSearchQuery] = useState( @@ -168,11 +169,6 @@ export const ShowProjects = () => { - {!isCloud && ( -
- -
- )}
@@ -186,9 +182,7 @@ export const ShowProjects = () => { Create and manage your projects - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canCreateProjects) && ( + {permissions?.project.create && (
@@ -361,8 +355,7 @@ export const ShowProjects = () => {
e.stopPropagation()} > - {(auth?.role === "owner" || - auth?.canDeleteProjects) && ( + {permissions?.project.delete && ( { + const { data: permissions } = api.user.getPermissions.useQuery(); + const canDeploy = permissions?.deployment.create ?? false; const { data, refetch } = api.redis.one.useQuery( { redisId, @@ -72,153 +74,158 @@ export const ShowGeneralRedis = ({ redisId }: Props) => { - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - { - await reload({ - redisId: redisId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Redis reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading Redis"); - }); - }} - > - - - {data?.applicationStatus === "idle" ? ( + {canDeploy && ( { - await start({ - redisId: redisId, - }) - .then(() => { - toast.success("Redis started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting Redis"); - }); + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); }} > - ) : ( + )} + {canDeploy && ( { - await stop({ + await reload({ redisId: redisId, + appName: data?.appName || "", }) .then(() => { - toast.success("Redis stopped successfully"); + toast.success("Redis reloaded successfully"); refetch(); }) .catch(() => { - toast.error("Error stopping Redis"); + toast.error("Error reloading Redis"); }); }} > )} + {canDeploy && + (data?.applicationStatus === "idle" ? ( + { + await start({ + redisId: redisId, + }) + .then(() => { + toast.success("Redis started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Redis"); + }); + }} + > + + + ) : ( + { + await stop({ + redisId: redisId, + }) + .then(() => { + toast.success("Redis stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Redis"); + }); + }} + > + + + ))} { const { mutateAsync, isPending: isRemoving } = api.certificates.remove.useMutation(); const { data, isPending, refetch } = api.certificates.all.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -53,7 +54,7 @@ export const ShowCertificates = () => { You don't have any certificates created - + {permissions?.certificate.create && }
) : (
@@ -101,47 +102,52 @@ export const ShowCertificates = () => {
-
- { - await mutateAsync({ - certificateId: certificate.certificateId, - }) - .then(() => { - toast.success( - "Certificate deleted successfully", - ); - refetch(); + {permissions?.certificate.delete && ( +
+ { + await mutateAsync({ + certificateId: + certificate.certificateId, }) - .catch(() => { - toast.error( - "Error deleting certificate", - ); - }); - }} - > - - -
+ +
+
+ )}
); })} -
- -
+ {permissions?.certificate.create && ( +
+ +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx index cbaf5de2fa..86deb38a76 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -16,6 +16,7 @@ export const ShowRegistry = () => { const { mutateAsync, isPending: isRemoving } = api.registry.remove.useMutation(); const { data, isPending, refetch } = api.registry.all.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -44,7 +45,7 @@ export const ShowRegistry = () => { You don't have any registry configurations - + {permissions?.registry.create && }
) : (
@@ -73,45 +74,49 @@ export const ShowRegistry = () => { registryId={registry.registryId} /> - { - await mutateAsync({ - registryId: registry.registryId, - }) - .then(() => { - toast.success( - "Registry configuration deleted successfully", - ); - refetch(); + {permissions?.registry.delete && ( + { + await mutateAsync({ + registryId: registry.registryId, }) - .catch(() => { - toast.error( - "Error deleting registry configuration", - ); - }); - }} - > - - + + + )}
))} -
- -
+ {permissions?.registry.create && ( +
+ +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index 966c8e5f5b..472d0a8a62 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -37,14 +37,32 @@ import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { S3_PROVIDERS } from "./constants"; +const DESTINATION_TYPES = [ + { key: "s3", name: "S3 Compatible Storage" }, + { key: "gdrive", name: "Google Drive" }, + { key: "sftp", name: "SFTP" }, + { key: "ftp", name: "FTP" }, + { key: "onedrive", name: "OneDrive" }, +]; + const addDestination = z.object({ name: z.string().min(1, "Name is required"), - provider: z.string().min(1, "Provider is required"), - accessKeyId: z.string().min(1, "Access Key Id is required"), - secretAccessKey: z.string().min(1, "Secret Access Key is required"), - bucket: z.string().min(1, "Bucket is required"), - region: z.string(), - endpoint: z.string().min(1, "Endpoint is required"), + destinationType: z.enum(["s3", "gdrive", "sftp", "ftp", "onedrive"]), + // S3 fields + provider: z.string().optional(), + accessKeyId: z.string().optional(), + secretAccessKey: z.string().optional(), + bucket: z.string().optional(), + region: z.string().optional(), + endpoint: z.string().optional(), + // Google Drive / OneDrive + credentials: z.string().optional(), + // SFTP / FTP + host: z.string().optional(), + port: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + remotePath: z.string().optional(), serverId: z.string().optional(), }); @@ -82,6 +100,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { const form = useForm({ defaultValues: { + destinationType: "s3", provider: "", accessKeyId: "", bucket: "", @@ -89,19 +108,35 @@ export const HandleDestinations = ({ destinationId }: Props) => { region: "", secretAccessKey: "", endpoint: "", + credentials: "", + host: "", + port: "", + username: "", + password: "", + remotePath: "", }, resolver: zodResolver(addDestination), }); + + const destinationType = form.watch("destinationType"); + useEffect(() => { if (destination) { form.reset({ + destinationType: (destination.destinationType as "s3" | "gdrive" | "sftp" | "ftp" | "onedrive") || "s3", name: destination.name, provider: destination.provider || "", - accessKeyId: destination.accessKey, - secretAccessKey: destination.secretAccessKey, - bucket: destination.bucket, - region: destination.region, - endpoint: destination.endpoint, + accessKeyId: destination.accessKey || "", + secretAccessKey: destination.secretAccessKey || "", + bucket: destination.bucket || "", + region: destination.region || "", + endpoint: destination.endpoint || "", + credentials: destination.credentials || "", + host: destination.host || "", + port: destination.port || "", + username: destination.username || "", + password: destination.password || "", + remotePath: destination.remotePath || "", }); } else { form.reset(); @@ -110,13 +145,20 @@ export const HandleDestinations = ({ destinationId }: Props) => { const onSubmit = async (data: AddDestination) => { await mutateAsync({ + destinationType: data.destinationType, provider: data.provider || "", - accessKey: data.accessKeyId, - bucket: data.bucket, - endpoint: data.endpoint, + accessKey: data.accessKeyId || "", + bucket: data.bucket || "", + endpoint: data.endpoint || "", name: data.name, - region: data.region, - secretAccessKey: data.secretAccessKey, + region: data.region || "", + secretAccessKey: data.secretAccessKey || "", + credentials: data.credentials || "", + host: data.host || "", + port: data.port || "", + username: data.username || "", + password: data.password || "", + remotePath: data.remotePath || "", destinationId: destinationId || "", }) .then(async () => { @@ -135,49 +177,28 @@ export const HandleDestinations = ({ destinationId }: Props) => { }; const handleTestConnection = async (serverId?: string) => { - const result = await form.trigger([ - "provider", - "accessKeyId", - "secretAccessKey", - "bucket", - "endpoint", - ]); - - if (!result) { - const errors = form.formState.errors; - const errorFields = Object.entries(errors) - .map(([field, error]) => `${field}: ${error?.message}`) - .filter(Boolean) - .join("\n"); - - toast.error("Please fill all required fields", { - description: errorFields, - }); - return; - } - if (isCloud && !serverId) { toast.error("Please select a server"); return; } - const provider = form.getValues("provider"); - const accessKey = form.getValues("accessKeyId"); - const secretKey = form.getValues("secretAccessKey"); - const bucket = form.getValues("bucket"); - const endpoint = form.getValues("endpoint"); - const region = form.getValues("region"); - - const connectionString = `:s3,provider=${provider},access_key_id=${accessKey},secret_access_key=${secretKey},endpoint=${endpoint}${region ? `,region=${region}` : ""}:${bucket}`; + const data = form.getValues(); await testConnection({ - provider, - accessKey, - bucket, - endpoint, + destinationType: data.destinationType, + provider: data.provider || "", + accessKey: data.accessKeyId || "", + bucket: data.bucket || "", + endpoint: data.endpoint || "", name: "Test", - region, - secretAccessKey: secretKey, + region: data.region || "", + secretAccessKey: data.secretAccessKey || "", + credentials: data.credentials || "", + host: data.host || "", + port: data.port || "", + username: data.username || "", + password: data.password || "", + remotePath: data.remotePath || "", serverId, }) .then(() => { @@ -185,7 +206,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { }) .catch((e) => { toast.error("Error connecting to provider", { - description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`, + description: e.message, }); }); }; @@ -214,9 +235,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { {destinationId ? "Update" : "Add"} Destination - In this section, you can configure and add new destinations for your - backups. Please ensure that you provide the correct information to - guarantee secure and efficient storage. + Configure backup destinations. Supports S3, Google Drive, SFTP, FTP, and OneDrive. {(isError || isErrorConnection) && ( @@ -239,7 +258,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { Name - + @@ -248,11 +267,11 @@ export const HandleDestinations = ({ destinationId }: Props) => { /> { return ( - Provider + Destination Type - - - - ); - }} - /> - ( - -
- Secret Access Key -
- - - - -
- )} - /> - ( - -
- Bucket -
- - - - -
- )} - /> - ( - -
- Region -
- - - - -
- )} - /> - ( - - Endpoint - - - - - - )} - /> + {/* S3 Fields */} + {destinationType === "s3" && ( + <> + ( + + Provider + + + + + + )} + /> + ( + + Access Key Id + + + + + + )} + /> + ( + + Secret Access Key + + + + + + )} + /> + ( + + Bucket + + + + + + )} + /> + ( + + Region + + + + + + )} + /> + ( + + Endpoint + + + + + + )} + /> + + )} + + {/* Google Drive / OneDrive Fields */} + {(destinationType === "gdrive" || destinationType === "onedrive") && ( + <> + ( + + + {destinationType === "gdrive" + ? "Service Account JSON" + : "OAuth Token JSON"} + + +