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/constants.ts b/apps/dokploy/components/dashboard/settings/destination/constants.ts index f43e47d1a1..7214138cad 100644 --- a/apps/dokploy/components/dashboard/settings/destination/constants.ts +++ b/apps/dokploy/components/dashboard/settings/destination/constants.ts @@ -131,3 +131,30 @@ export const S3_PROVIDERS: Array<{ name: "Any other S3 compatible provider", }, ]; + +export const DESTINATION_TYPES: Array<{ + key: string; + name: string; + description: string; +}> = [ + { + key: "s3", + name: "S3 Compatible", + description: "AWS S3, Cloudflare R2, MinIO, and other S3-compatible storage", + }, + { + key: "ftp", + name: "FTP", + description: "File Transfer Protocol — plain FTP server", + }, + { + key: "sftp", + name: "SFTP", + description: "SSH File Transfer Protocol — secure file transfer over SSH", + }, + { + key: "google-drive", + name: "Google Drive", + description: "Google Drive cloud storage via OAuth2", + }, +]; diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index 966c8e5f5b..cdcd0313d6 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -35,16 +35,31 @@ import { } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; -import { S3_PROVIDERS } from "./constants"; +import { DESTINATION_TYPES, S3_PROVIDERS } from "./constants"; 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.string().min(1, "Destination type is required"), + // 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(), + // FTP/SFTP fields + ftpHost: z.string().optional(), + ftpPort: z.coerce.number().optional(), + ftpUser: z.string().optional(), + ftpPassword: z.string().optional(), + ftpPath: z.string().optional(), + sftpKeyPath: z.string().optional(), + // Google Drive fields + googleDriveClientId: z.string().optional(), + googleDriveClientSecret: z.string().optional(), + googleDriveRefreshToken: z.string().optional(), + googleDriveFolderId: z.string().optional(), + // Server serverId: z.string().optional(), }); @@ -82,6 +97,7 @@ export const HandleDestinations = ({ destinationId }: Props) => { const form = useForm({ defaultValues: { + destinationType: "s3", provider: "", accessKeyId: "", bucket: "", @@ -89,19 +105,43 @@ export const HandleDestinations = ({ destinationId }: Props) => { region: "", secretAccessKey: "", endpoint: "", + ftpHost: "", + ftpPort: undefined, + ftpUser: "", + ftpPassword: "", + ftpPath: "", + sftpKeyPath: "", + googleDriveClientId: "", + googleDriveClientSecret: "", + googleDriveRefreshToken: "", + googleDriveFolderId: "", }, resolver: zodResolver(addDestination), }); + + const selectedDestType = form.watch("destinationType"); + useEffect(() => { if (destination) { form.reset({ name: destination.name, + destinationType: destination.destinationType || "s3", provider: destination.provider || "", accessKeyId: destination.accessKey, secretAccessKey: destination.secretAccessKey, bucket: destination.bucket, region: destination.region, endpoint: destination.endpoint, + ftpHost: destination.ftpHost || "", + ftpPort: destination.ftpPort || undefined, + ftpUser: destination.ftpUser || "", + ftpPassword: destination.ftpPassword || "", + ftpPath: destination.ftpPath || "", + sftpKeyPath: destination.sftpKeyPath || "", + googleDriveClientId: destination.googleDriveClientId || "", + googleDriveClientSecret: destination.googleDriveClientSecret || "", + googleDriveRefreshToken: destination.googleDriveRefreshToken || "", + googleDriveFolderId: destination.googleDriveFolderId || "", }); } else { form.reset(); @@ -110,14 +150,25 @@ export const HandleDestinations = ({ destinationId }: Props) => { const onSubmit = async (data: AddDestination) => { await mutateAsync({ + destinationType: (data.destinationType as "s3" | "ftp" | "sftp" | "google-drive") || "s3", 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 || "", destinationId: destinationId || "", + ftpHost: data.ftpHost || null, + ftpPort: data.ftpPort || null, + ftpUser: data.ftpUser || null, + ftpPassword: data.ftpPassword || null, + ftpPath: data.ftpPath || null, + sftpKeyPath: data.sftpKeyPath || null, + googleDriveClientId: data.googleDriveClientId || null, + googleDriveClientSecret: data.googleDriveClientSecret || null, + googleDriveRefreshToken: data.googleDriveRefreshToken || null, + googleDriveFolderId: data.googleDriveFolderId || null, }) .then(async () => { toast.success(`Destination ${destinationId ? "Updated" : "Created"}`); @@ -135,57 +186,40 @@ 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; - } + const destType = form.getValues("destinationType"); 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}`; - await testConnection({ - provider, - accessKey, - bucket, - endpoint, + destinationType: (destType as "s3" | "ftp" | "sftp" | "google-drive") || "s3", + provider: form.getValues("provider") || "", + accessKey: form.getValues("accessKeyId") || "", + bucket: form.getValues("bucket") || "", + endpoint: form.getValues("endpoint") || "", name: "Test", - region, - secretAccessKey: secretKey, + region: form.getValues("region") || "", + secretAccessKey: form.getValues("secretAccessKey") || "", + ftpHost: form.getValues("ftpHost") || null, + ftpPort: form.getValues("ftpPort") || null, + ftpUser: form.getValues("ftpUser") || null, + ftpPassword: form.getValues("ftpPassword") || null, + ftpPath: form.getValues("ftpPath") || null, + sftpKeyPath: form.getValues("sftpKeyPath") || null, + googleDriveClientId: form.getValues("googleDriveClientId") || null, + googleDriveClientSecret: form.getValues("googleDriveClientSecret") || null, + googleDriveRefreshToken: form.getValues("googleDriveRefreshToken") || null, + googleDriveFolderId: form.getValues("googleDriveFolderId") || null, serverId, }) .then(() => { toast.success("Connection Success"); }) .catch((e) => { - toast.error("Error connecting to provider", { - description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`, + toast.error("Error connecting to destination", { + description: e.message, }); }); }; @@ -208,15 +242,14 @@ 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 a backup destination. Choose your storage type and provide + the required credentials. {(isError || isErrorConnection) && ( @@ -239,20 +272,21 @@ export const HandleDestinations = ({ destinationId }: Props) => { Name - + ); }} /> + { return ( - Provider + Destination Type - - - - ); - }} - /> - ( - -
- Secret Access Key -
- - - - -
- )} - /> - ( - -
- Bucket -
- - - - -
- )} - /> - ( - -
- Region -
- - - - -
- )} - /> - ( - - Endpoint - - - - - - )} - /> + {/* S3 Fields */} + {selectedDestType === "s3" && ( + <> + { + return ( + + Provider + + + + + + ); + }} + /> + ( + + Access Key Id + + + + + + )} + /> + ( + + Secret Access Key + + + + + + )} + /> + ( + + Bucket + + + + + + )} + /> + ( + + Region + + + + + + )} + /> + ( + + Endpoint + + + + + + )} + /> + + )} + + {/* FTP Fields */} + {selectedDestType === "ftp" && ( + <> + ( + + Host + + + + + + )} + /> + ( + + Port + + + + + + )} + /> + ( + + Username + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + Remote Path + + + + + + )} + /> + + )} + + {/* SFTP Fields */} + {selectedDestType === "sftp" && ( + <> + ( + + Host + + + + + + )} + /> + ( + + Port + + + + + + )} + /> + ( + + Username + + + + + + )} + /> + ( + + Password (or use SSH Key) + + + + + + )} + /> + ( + + SSH Key Path (optional) + + + + + + )} + /> + ( + + Remote Path + + + + + + )} + /> + + )} + + {/* Google Drive Fields */} + {selectedDestType === "google-drive" && ( + <> + ( + + OAuth Client ID + + + + + + )} + /> + ( + + OAuth Client Secret + + + + + + )} + /> + ( + + Refresh Token + + + + + + )} + /> + ( + + Folder ID (optional) + + + + + + )} + /> + + )} { const { data, isPending, refetch } = api.destination.all.useQuery(); const { mutateAsync, isPending: isRemoving } = api.destination.remove.useMutation(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -23,11 +24,11 @@ export const ShowDestinations = () => { - S3 Destinations + Backup Destinations - Add your providers like AWS S3, Cloudflare R2, Wasabi, - DigitalOcean Spaces etc. + Add backup destinations — S3-compatible storage, FTP, SFTP, or + Google Drive. @@ -45,7 +46,7 @@ export const ShowDestinations = () => { To create a backup it is required to set at least 1 provider. - + {permissions?.destination.create && }
) : (
@@ -60,54 +61,65 @@ export const ShowDestinations = () => { {index + 1}. {destination.name} - - Created at:{" "} - {new Date( - destination.createdAt, - ).toLocaleDateString()} - +
+ + {destination.destinationType || "s3"} + + + Created at:{" "} + {new Date( + destination.createdAt, + ).toLocaleDateString()} + +
- { - await mutateAsync({ - destinationId: destination.destinationId, - }) - .then(() => { - toast.success( - "Destination deleted successfully", - ); - refetch(); + {permissions?.destination.delete && ( + { + await mutateAsync({ + destinationId: destination.destinationId, }) - .catch(() => { - toast.error("Error deleting destination"); - }); - }} - > - - + + + )}
))} -
- -
+ {permissions?.destination.create && ( +
+ +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 6ef1e15dbd..c4e5495730 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -737,6 +737,9 @@ export const HandleNotifications = ({ notificationId }: Props) => { }); setVisible(false); await utils.notification.all.invalidate(); + if (notificationId) { + await utils.notification.one.invalidate({ notificationId }); + } }) .catch(() => { toast.error( diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index d8ac31d97d..3d62658aeb 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -26,6 +26,7 @@ export const ShowNotifications = () => { const { data, isPending, refetch } = api.notification.all.useQuery(); const { mutateAsync, isPending: isRemoving } = api.notification.remove.useMutation(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -56,7 +57,9 @@ export const ShowNotifications = () => { To send notifications it is required to set at least 1 provider. - + {permissions?.notification.create && ( + + )}
) : (
@@ -126,45 +129,50 @@ export const ShowNotifications = () => { notificationId={notification.notificationId} /> - { - await mutateAsync({ - notificationId: notification.notificationId, - }) - .then(() => { - toast.success( - "Notification deleted successfully", - ); - refetch(); + {permissions?.notification.delete && ( + { + await mutateAsync({ + notificationId: + notification.notificationId, }) - .catch(() => { - toast.error( - "Error deleting notification", - ); - }); - }} - > - - + + + )}
))} -
- -
+ {permissions?.notification.create && ( +
+ +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index 45ee314d40..859098394d 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -59,6 +59,7 @@ export const ShowServers = () => { const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: canCreateMoreServers } = api.stripe.canCreateMoreServers.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -115,7 +116,7 @@ export const ShowServers = () => { Start adding servers to deploy your applications remotely. - + {permissions?.server.create && }
) : (
@@ -362,66 +363,71 @@ export const ShowServers = () => {
- - -
- - You can not delete this - server because it has - active services. - - You have active services - associated with this - server, please delete - them first. - -
- ) - } - onClick={async () => { - await mutateAsync({ - serverId: server.serverId, - }) - .then(() => { - refetch(); - toast.success( - `Server ${server.name} deleted successfully`, - ); + {permissions?.server.delete && ( + + +
+ + You can not delete this + server because it has + active services. + + You have active + services associated + with this server, + please delete them + first. + +
+ ) + } + onClick={async () => { + await mutateAsync({ + serverId: server.serverId, }) - .catch((err) => { - toast.error(err.message); - }); - }} - > - - -
- - -

- {canDelete - ? "Delete Server" - : "Cannot delete - has active services"} -

-
- + + +
+
+ +

+ {canDelete + ? "Delete Server" + : "Cannot delete - has active services"} +

+
+
+ )}
)} @@ -431,13 +437,15 @@ export const ShowServers = () => { })} -
- {data && data?.length > 0 && ( -
- -
- )} -
+ {permissions?.server.create && ( +
+ {data && data?.length > 0 && ( +
+ +
+ )} +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx index 6fd62e462c..86ea0a2eac 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx @@ -17,6 +17,7 @@ export const ShowDestinations = () => { const { data, isPending, refetch } = api.sshKey.all.useQuery(); const { mutateAsync, isPending: isRemoving } = api.sshKey.remove.useMutation(); + const { data: permissions } = api.user.getPermissions.useQuery(); return (
@@ -46,7 +47,7 @@ export const ShowDestinations = () => { You don't have any SSH keys - + {permissions?.sshKeys.create && }
) : (
@@ -84,43 +85,47 @@ export const ShowDestinations = () => {
- { - await mutateAsync({ - sshKeyId: sshKey.sshKeyId, - }) - .then(() => { - toast.success( - "SSH Key deleted successfully", - ); - refetch(); + {permissions?.sshKeys.delete && ( + { + await mutateAsync({ + sshKeyId: sshKey.sshKeyId, }) - .catch(() => { - toast.error("Error deleting SSH Key"); - }); - }} - > - - + + + )}
))} -
- -
+ {permissions?.sshKeys.create && ( +
+ +
+ )} )} diff --git a/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx index 10b46ef535..f9dce77c9b 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx @@ -32,7 +32,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; const addInvitation = z.object({ @@ -40,7 +39,7 @@ const addInvitation = z.object({ .string() .min(1, "Email is required") .email({ message: "Invalid email" }), - role: z.enum(["member", "admin"]), + role: z.string().min(1, "Role is required"), notificationId: z.string().optional(), }); @@ -49,13 +48,14 @@ type AddInvitation = z.infer; export const AddInvitation = () => { const [open, setOpen] = useState(false); const utils = api.useUtils(); - const [isLoading, setIsLoading] = useState(false); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: emailProviders } = api.notification.getEmailProviders.useQuery(); + const { mutateAsync: inviteMember, isPending: isInviting } = + api.organization.inviteMember.useMutation(); const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation(); + const { data: customRoles } = api.customRole.all.useQuery(); const [error, setError] = useState(null); - const { data: activeOrganization } = api.organization.active.useQuery(); const form = useForm({ defaultValues: { @@ -70,19 +70,15 @@ export const AddInvitation = () => { }, [form, form.formState.isSubmitSuccessful, form.reset]); const onSubmit = async (data: AddInvitation) => { - setIsLoading(true); - const result = await authClient.organization.inviteMember({ - email: data.email.toLowerCase(), - role: data.role, - organizationId: activeOrganization?.id, - }); + try { + const result = await inviteMember({ + email: data.email.toLowerCase(), + role: data.role, + }); - if (result.error) { - setError(result.error.message || ""); - } else { if (!isCloud && data.notificationId) { await sendInvitation({ - invitationId: result.data.id, + invitationId: result!.id, notificationId: data.notificationId || "", }) .then(() => { @@ -96,10 +92,11 @@ export const AddInvitation = () => { } setError(null); setOpen(false); + } catch (error: any) { + setError(error.message || "Failed to create invitation"); } utils.organization.allInvitations.invalidate(); - setIsLoading(false); }; return ( @@ -159,6 +156,11 @@ export const AddInvitation = () => { Member Admin + {customRoles?.map((role) => ( + + {role.role} + + ))} @@ -212,7 +214,7 @@ export const AddInvitation = () => { )} +
+ + + Metadata + + + +
+ ); +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {format(new Date(row.getValue("createdAt")), "MMM d, yyyy HH:mm")} + + ), + }, + { + accessorKey: "userEmail", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.getValue("userEmail")} + ), + }, + { + accessorKey: "action", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const action = row.getValue("action") as string; + const config = ACTION_CONFIG[action]; + if (!config) { + return {action}; + } + const Icon = config.icon; + return ( + + + {config.label} + + ); + }, + }, + { + accessorKey: "resourceType", + header: "Resource", + cell: ({ row }) => ( + + {RESOURCE_LABELS[row.getValue("resourceType") as string] ?? + row.getValue("resourceType")} + + ), + }, + { + accessorKey: "resourceName", + header: "Name", + cell: ({ row }) => ( + + {(row.getValue("resourceName") as string) ?? "—"} + + ), + }, + { + accessorKey: "userRole", + header: "Role", + cell: ({ row }) => ( + + {row.getValue("userRole")} + + ), + }, + { + accessorKey: "metadata", + header: "Metadata", + cell: ({ row }) => , + }, +]; diff --git a/apps/dokploy/components/proprietary/audit-logs/data-table.tsx b/apps/dokploy/components/proprietary/audit-logs/data-table.tsx new file mode 100644 index 0000000000..dec9678917 --- /dev/null +++ b/apps/dokploy/components/proprietary/audit-logs/data-table.tsx @@ -0,0 +1,400 @@ +"use client"; + +import type { AuditLog } from "@dokploy/server/db/schema"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, +} from "@tanstack/react-table"; +import { format } from "date-fns"; +import { CalendarIcon, ChevronDown, X } from "lucide-react"; +import React from "react"; +import type { DateRange } from "react-day-picker"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const ACTION_OPTIONS = [ + { value: "create", label: "Created" }, + { value: "update", label: "Updated" }, + { value: "delete", label: "Deleted" }, + { value: "deploy", label: "Deployed" }, + { value: "cancel", label: "Cancelled" }, + { value: "redeploy", label: "Redeployed" }, + { value: "login", label: "Login" }, + { value: "logout", label: "Logout" }, +]; + +const RESOURCE_OPTIONS = [ + { value: "project", label: "Projects" }, + { value: "service", label: "Applications / Services" }, + { value: "environment", label: "Environments" }, + { value: "deployment", label: "Deployments" }, + { value: "user", label: "Users" }, + { value: "customRole", label: "Custom Roles" }, + { value: "domain", label: "Domains" }, + { value: "certificate", label: "Certificates" }, + { value: "registry", label: "Registries" }, + { value: "server", label: "Remote Servers" }, + { value: "sshKey", label: "SSH Keys" }, + { value: "gitProvider", label: "Git Providers" }, + { value: "notification", label: "Notifications" }, + { value: "settings", label: "Settings" }, + { value: "session", label: "Sessions (Login/Logout)" }, +]; + +const PAGE_SIZE_OPTIONS = [25, 50, 100, 200]; + +type AuditAction = + | "create" + | "update" + | "delete" + | "deploy" + | "cancel" + | "redeploy" + | "login" + | "logout"; +type AuditResourceType = + | "project" + | "service" + | "environment" + | "deployment" + | "user" + | "customRole" + | "domain" + | "certificate" + | "registry" + | "server" + | "sshKey" + | "gitProvider" + | "notification" + | "settings" + | "session"; + +export interface AuditLogFilters { + userEmail: string; + resourceName: string; + action: AuditAction | ""; + resourceType: AuditResourceType | ""; + dateRange: DateRange | undefined; +} + +interface DataTableProps { + columns: ColumnDef[]; + data: AuditLog[]; + total: number; + pageIndex: number; + pageSize: number; + filters: AuditLogFilters; + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; + onFilterChange: ( + key: K, + value: AuditLogFilters[K], + ) => void; + isLoading?: boolean; +} + +export function DataTable({ + columns, + data, + total, + pageIndex, + pageSize, + filters, + onPageChange, + onPageSizeChange, + onFilterChange, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([ + { id: "createdAt", desc: true }, + ]); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onColumnVisibilityChange: setColumnVisibility, + manualPagination: true, + manualFiltering: true, + rowCount: total, + state: { + sorting, + columnVisibility, + }, + }); + + const pageCount = Math.ceil(total / pageSize); + const hasFilters = + filters.userEmail || + filters.resourceName || + filters.action || + filters.resourceType || + filters.dateRange; + + return ( +
+
+ onFilterChange("userEmail", e.target.value)} + className="max-w-xs" + /> + onFilterChange("resourceName", e.target.value)} + className="max-w-xs" + /> + + + + + + + + onFilterChange("dateRange", range)} + numberOfMonths={2} + initialFocus + /> + + + {hasFilters && ( + + )} + + + + + + {table + .getAllColumns() + .filter((col) => col.getCanHide()) + .map((col) => ( + col.toggleVisibility(!!value)} + > + {col.id} + + ))} + + +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {isLoading ? ( + + + Loading... + + + ) : table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No audit logs found. + + + )} + +
+
+ +
+ + {total} {total === 1 ? "entry" : "entries"} total + +
+
+ Rows per page + +
+ + Page {pageIndex + 1} of {Math.max(1, pageCount)} + +
+ + +
+
+
+
+ ); +} diff --git a/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx b/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx new file mode 100644 index 0000000000..7f18514936 --- /dev/null +++ b/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx @@ -0,0 +1,112 @@ +import { ClipboardList } from "lucide-react"; +import React from "react"; +import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { columns } from "./columns"; +import { type AuditLogFilters, DataTable } from "./data-table"; + +function AuditLogsContent() { + const [pageIndex, setPageIndex] = React.useState(0); + const [pageSize, setPageSize] = React.useState(50); + const [filters, setFilters] = React.useState({ + userEmail: "", + resourceName: "", + action: "", + resourceType: "", + dateRange: undefined, + }); + + const [debouncedText, setDebouncedText] = React.useState({ + userEmail: "", + resourceName: "", + }); + + React.useEffect(() => { + const t = setTimeout(() => { + setDebouncedText({ + userEmail: filters.userEmail, + resourceName: filters.resourceName, + }); + setPageIndex(0); + }, 400); + return () => clearTimeout(t); + }, [filters.userEmail, filters.resourceName]); + + const handleFilterChange = ( + key: K, + value: AuditLogFilters[K], + ) => { + setFilters((prev) => ({ ...prev, [key]: value })); + if (key !== "userEmail" && key !== "resourceName") { + setPageIndex(0); + } + }; + + const handlePageSizeChange = (size: number) => { + setPageSize(size); + setPageIndex(0); + }; + + const { data, isLoading } = api.auditLog.all.useQuery({ + userEmail: debouncedText.userEmail || undefined, + resourceName: debouncedText.resourceName || undefined, + action: filters.action || undefined, + resourceType: filters.resourceType || undefined, + from: filters.dateRange?.from, + to: filters.dateRange?.to, + limit: pageSize, + offset: pageIndex * pageSize, + }); + + return ( + + ); +} + +export function ShowAuditLogs() { + return ( + +
+ + + + + Audit Logs + + + Track all actions performed by members in your organization. + + + + + + +
+
+ ); +} diff --git a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx new file mode 100644 index 0000000000..620e938036 --- /dev/null +++ b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx @@ -0,0 +1,891 @@ +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { Loader2, PlusIcon, ShieldCheck, TrashIcon, Users } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate"; +import { Button } from "@/components/ui/button"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +/** Labels and descriptions for each resource */ +const RESOURCE_META: Record = { + project: { + label: "Projects", + description: "Manage project creation and deletion", + }, + service: { + label: "Services", + description: + "Manage services (applications, databases, compose) within projects", + }, + environment: { + label: "Environments", + description: "Manage environment creation, viewing, and deletion", + }, + docker: { + label: "Docker", + description: "Access to Docker containers, images, and volumes management", + }, + sshKeys: { + label: "SSH Keys", + description: "Manage SSH key configurations for servers and repositories", + }, + gitProviders: { + label: "Git Providers", + description: "Access to Git providers (GitHub, GitLab, Bitbucket, Gitea)", + }, + traefikFiles: { + label: "Traefik Files", + description: "Access to the Traefik file system configuration", + }, + api: { + label: "API / CLI", + description: "Access to API keys and CLI usage", + }, + // Enterprise-only resources + volume: { + label: "Volumes", + description: "Manage persistent volumes and mounts attached to services", + }, + deployment: { + label: "Deployments", + description: "Trigger, view, and cancel service deployments", + }, + envVars: { + label: "Service Env Vars", + description: "View and edit environment variables of services", + }, + projectEnvVars: { + label: "Project Shared Env Vars", + description: "View and edit shared environment variables at project level", + }, + environmentEnvVars: { + label: "Environment Shared Env Vars", + description: + "View and edit shared environment variables at environment level", + }, + server: { + label: "Servers", + description: "Manage remote servers and nodes", + }, + registry: { + label: "Registries", + description: "Manage Docker image registries", + }, + certificate: { + label: "Certificates", + description: "Manage SSL/TLS certificates", + }, + backup: { + label: "Backups", + description: "Manage database backups and restores", + }, + volumeBackup: { + label: "Volume Backups", + description: "Manage Docker volume backups and restores", + }, + schedule: { + label: "Schedules", + description: "Manage scheduled jobs (commands, deployments, scripts)", + }, + domain: { + label: "Domains", + description: "Manage custom domains assigned to services", + }, + destination: { + label: "S3 Destinations", + description: + "Manage S3-compatible backup destinations (AWS, Cloudflare R2, etc.)", + }, + notification: { + label: "Notifications", + description: + "Manage notification providers (Slack, Discord, Telegram, etc.)", + }, + member: { + label: "Users", + description: "Manage organization members, invitations, and roles", + }, + logs: { + label: "Logs", + description: "View service and deployment logs", + }, + monitoring: { + label: "Monitoring", + description: "View server and service metrics (CPU, RAM, disk)", + }, + auditLog: { + label: "Audit Logs", + description: "View the audit log of actions performed in the organization", + }, +}; + +/** Descriptions for each action within a resource */ +const ACTION_META: Record< + string, + Record +> = { + project: { + create: { label: "Create", description: "Create new projects" }, + delete: { + label: "Delete", + description: "Delete projects and all their content", + }, + }, + service: { + create: { + label: "Create", + description: "Create new services inside projects", + }, + read: { + label: "Read", + description: "View services, logs, and deployments", + }, + delete: { + label: "Delete", + description: "Delete services from projects", + }, + }, + environment: { + create: { + label: "Create", + description: "Create new environments in projects", + }, + read: { + label: "Read", + description: "View environments and their services", + }, + delete: { + label: "Delete", + description: "Delete environments and their content", + }, + }, + docker: { + read: { + label: "Read", + description: "View Docker containers, images, networks, and volumes", + }, + }, + sshKeys: { + read: { + label: "Read", + description: "View SSH key configurations", + }, + create: { + label: "Create", + description: "Create and edit SSH keys", + }, + delete: { + label: "Delete", + description: "Remove SSH keys", + }, + }, + gitProviders: { + read: { + label: "Read", + description: "View Git provider connections", + }, + create: { + label: "Create", + description: "Create and update Git provider connections", + }, + delete: { + label: "Delete", + description: "Remove Git provider connections", + }, + }, + traefikFiles: { + read: { + label: "Read", + description: "View Traefik configuration files", + }, + write: { + label: "Write", + description: "Edit and save Traefik configuration files", + }, + }, + api: { + read: { + label: "Read", + description: "Create and manage API keys for CLI access", + }, + }, + volume: { + read: { + label: "Read", + description: "View volumes and mounts attached to services", + }, + create: { label: "Create", description: "Add and edit volumes and mounts" }, + delete: { + label: "Delete", + description: "Remove volumes and mounts from services", + }, + }, + deployment: { + read: { label: "Read", description: "View deployment history and status" }, + create: { + label: "Deploy", + description: "Trigger new deployments manually", + }, + cancel: { label: "Cancel", description: "Cancel running deployments" }, + }, + envVars: { + read: { label: "Read", description: "View environment variable values" }, + write: { + label: "Write", + description: "Create, update, and delete environment variables", + }, + }, + projectEnvVars: { + read: { + label: "Read", + description: "View project-level shared environment variables", + }, + write: { + label: "Write", + description: "Edit project-level shared environment variables", + }, + }, + environmentEnvVars: { + read: { + label: "Read", + description: "View environment-level shared environment variables", + }, + write: { + label: "Write", + description: "Edit environment-level shared environment variables", + }, + }, + server: { + read: { + label: "Read", + description: "View server list and connection details", + }, + create: { label: "Create", description: "Add new remote servers" }, + delete: { + label: "Delete", + description: "Remove servers from the organization", + }, + }, + registry: { + read: { label: "Read", description: "View configured Docker registries" }, + create: { label: "Create", description: "Add new Docker registries" }, + delete: { label: "Delete", description: "Remove Docker registries" }, + }, + certificate: { + read: { label: "Read", description: "View SSL/TLS certificates" }, + create: { + label: "Create", + description: "Issue and configure new certificates", + }, + delete: { label: "Delete", description: "Remove certificates" }, + }, + backup: { + read: { label: "Read", description: "View backup history and status" }, + create: { label: "Create", description: "Trigger manual backups" }, + delete: { label: "Delete", description: "Delete backup files" }, + restore: { + label: "Restore", + description: "Restore a database from a backup", + }, + }, + volumeBackup: { + read: { + label: "Read", + description: "View volume backup history and status", + }, + create: { + label: "Create", + description: "Create and trigger volume backups", + }, + update: { + label: "Update", + description: "Update volume backup configuration", + }, + delete: { label: "Delete", description: "Delete volume backup files" }, + restore: { + label: "Restore", + description: "Restore a Docker volume from a backup", + }, + }, + schedule: { + read: { + label: "Read", + description: "View scheduled jobs and their history", + }, + create: { label: "Create", description: "Create and run scheduled jobs" }, + update: { + label: "Update", + description: "Update scheduled job configuration", + }, + delete: { label: "Delete", description: "Delete scheduled jobs" }, + }, + domain: { + read: { label: "Read", description: "View domains assigned to services" }, + create: { label: "Create", description: "Assign new domains to services" }, + delete: { label: "Delete", description: "Remove domains from services" }, + }, + destination: { + read: { label: "Read", description: "View S3 backup destinations" }, + create: { label: "Create", description: "Add and edit S3 destinations" }, + delete: { label: "Delete", description: "Remove S3 destinations" }, + }, + notification: { + read: { label: "Read", description: "View notification providers" }, + create: { + label: "Create", + description: "Add and edit notification providers", + }, + delete: { label: "Delete", description: "Remove notification providers" }, + }, + member: { + read: { + label: "Read", + description: "View the list of organization members", + }, + create: { + label: "Create", + description: "Invite new members to the organization", + }, + update: { + label: "Update", + description: "Change member roles and permissions", + }, + delete: { + label: "Delete", + description: "Remove members from the organization", + }, + }, + logs: { + read: { label: "Read", description: "View real-time and historical logs" }, + }, + monitoring: { + read: { + label: "Read", + description: "View CPU, RAM, disk, and network metrics", + }, + }, + auditLog: { + read: { label: "Read", description: "View the audit log history" }, + }, +}; + +/** Resources that should be hidden from the custom role editor (better-auth internals) */ +const HIDDEN_RESOURCES = ["organization", "invitation", "team", "ac"]; + +const createRoleSchema = z.object({ + roleName: z + .string() + .min(1, "Role name is required") + .max(50, "Role name must be 50 characters or less") + .regex( + /^[a-zA-Z0-9_-]+$/, + "Only letters, numbers, hyphens, and underscores allowed", + ), +}); + +type CreateRoleSchema = z.infer; + +export const ManageCustomRoles = () => { + return ( + +
+ + + + Custom Roles + + + Create and manage custom roles with fine-grained permissions + + + + + + + +
+
+ ); +}; + +interface HandleCustomRoleProps { + roleName?: string; + initialPermissions?: Record; + onSuccess: () => void; +} + +function HandleCustomRole({ + roleName, + initialPermissions, + onSuccess, +}: HandleCustomRoleProps) { + const [open, setOpen] = useState(false); + const [permissions, setPermissions] = useState>({}); + const { data: statements } = api.customRole.getStatements.useQuery(); + const isEdit = !!roleName; + + const form = useForm({ + defaultValues: { roleName: "" }, + resolver: zodResolver(createRoleSchema), + }); + + useEffect(() => { + if (open) { + setPermissions(initialPermissions ? { ...initialPermissions } : {}); + form.reset({ roleName: isEdit ? (roleName ?? "") : "" }); + } + }, [open]); + + const { mutateAsync: createRole, isPending: isCreating } = + api.customRole.create.useMutation(); + const { mutateAsync: updateRole, isPending: isUpdating } = + api.customRole.update.useMutation(); + + const visibleResources = statements + ? Object.entries(statements).filter( + ([key]) => !HIDDEN_RESOURCES.includes(key), + ) + : []; + + const togglePermission = (resource: string, action: string) => { + setPermissions((prev) => { + const current = prev[resource] || []; + const has = current.includes(action); + return { + ...prev, + [resource]: has + ? current.filter((a) => a !== action) + : [...current, action], + }; + }); + }; + + const handleSubmit = async (data: CreateRoleSchema) => { + try { + if (isEdit) { + const newName = data.roleName !== roleName ? data.roleName : undefined; + await updateRole({ + roleName: roleName!, + newRoleName: newName, + permissions, + }); + toast.success(`Role "${newName ?? roleName}" updated`); + } else { + await createRole({ roleName: data.roleName, permissions }); + toast.success(`Role "${data.roleName}" created`); + } + if (!isEdit) { + setOpen(false); + } + onSuccess(); + } catch (error) { + let message = `Error ${isEdit ? "updating" : "creating"} role`; + if (error instanceof Error) { + try { + const parsed = JSON.parse(error.message); + if (Array.isArray(parsed) && parsed[0]?.message) { + message = parsed[0].message; + } else { + message = error.message; + } + } catch { + message = error.message; + } + } + toast.error(message); + } + }; + + return ( + + + {isEdit ? ( + + ) : ( + + )} + + + + + {isEdit ? "Edit Role" : "Create Custom Role"} + + + {isEdit + ? "Update permissions for this role" + : "Define a new role with specific permissions"} + + +
+ + ( + + Role Name + + + + + + )} + /> + + + + + + +
+
+ ); +} + +const CustomRolesContent = () => { + const { + data: customRoles, + isPending, + refetch, + } = api.customRole.all.useQuery(); + const { mutateAsync: deleteRole } = api.customRole.remove.useMutation(); + + const handleDelete = async (roleName: string) => { + try { + await deleteRole({ roleName }); + toast.success(`Role "${roleName}" deleted`); + refetch(); + } catch (error) { + let message = "Error deleting role"; + if (error instanceof Error) { + try { + const parsed = JSON.parse(error.message); + message = + Array.isArray(parsed) && parsed[0]?.message + ? parsed[0].message + : error.message; + } catch { + message = error.message; + } + } + toast.error(message); + } + }; + + if (isPending) { + return ( +
+ Loading... + +
+ ); + } + + return ( +
+
+ +
+ + {customRoles?.length === 0 ? ( +
+
+ +
+
+

No custom roles yet

+

+ Create a role to define fine-grained access for your team members. +

+
+
+ ) : ( +
+ {customRoles?.map((role) => { + const totalPermissions = Object.values(role.permissions).flat() + .length; + const enabledResources = Object.entries(role.permissions).filter( + ([, actions]) => (actions as string[]).length > 0, + ); + return ( +
+
+
+
+ +
+
+
+

+ {role.role} +

+ {role.memberCount > 0 && ( + + )} +
+

+ {enabledResources.length} resource + {enabledResources.length !== 1 ? "s" : ""} ·{" "} + {totalPermissions} permission + {totalPermissions !== 1 ? "s" : ""} +

+
+
+
+ + + {role.memberCount > 0 && ( + + + {role.memberCount} member + {role.memberCount !== 1 ? "s are" : " is"}{" "} + currently assigned + {" "} + to this role. Reassign them before deleting. + + )} + + Are you sure you want to delete the{" "} + "{role.role}" role? This action + cannot be undone. + +
+ } + disabled={role.memberCount > 0} + type="destructive" + onClick={() => handleDelete(role.role)} + > + + +
+
+ + {enabledResources.length > 0 && ( +
+ {enabledResources.map(([resource, actions]) => ( +
+ + {RESOURCE_META[resource]?.label || resource} + + · + + {(actions as string[]) + .map((a) => ACTION_META[resource]?.[a]?.label || a) + .join(", ")} + +
+ ))} +
+ )} +
+ ); + })} +
+ )} + + ); +}; + +function MembersBadge({ + roleName, + count, +}: { + roleName: string; + count: number; +}) { + const [open, setOpen] = useState(false); + const { data: members, isLoading } = api.customRole.membersByRole.useQuery( + { roleName }, + { enabled: open }, + ); + return ( + + + + + +

+ Assigned members +

+ {isLoading ? ( +
+ +
+ ) : members && members.length > 0 ? ( +
    + {members.map((m) => ( +
  • +
    + {(m.firstName?.[0] || m.email?.[0] || "?").toUpperCase()} +
    +
    + {(m.firstName || m.lastName) && ( +

    + {[m.firstName, m.lastName].filter(Boolean).join(" ")} +

    + )} +

    + {m.email} +

    +
    +
  • + ))} +
+ ) : ( +

+ No members found. +

+ )} +
+
+ ); +} + +/** Reusable permission toggle grid with descriptions */ +function PermissionEditor({ + resources, + permissions, + onToggle, +}: { + resources: [string, readonly string[]][]; + permissions: Record; + onToggle: (resource: string, action: string) => void; +}) { + return ( +
+

Permissions

+
+ {resources.map(([resource, actions]) => { + const meta = RESOURCE_META[resource]; + return ( +
+
+

{meta?.label || resource}

+ {meta?.description && ( +

+ {meta.description} +

+ )} +
+
+ {actions.map((action) => { + const actionMeta = ACTION_META[resource]?.[action]; + return ( +
onToggle(resource, action)} + > + onToggle(resource, action)} + /> +
+ + {actionMeta?.label || action} + +
+
+ ); + })} +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx index 9ba28850cf..16b185e0f1 100644 --- a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx +++ b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx @@ -17,6 +17,8 @@ import { } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; import { SidebarTrigger } from "@/components/ui/sidebar"; +import { TimeBadge } from "@/components/ui/time-badge"; +import { api } from "@/utils/api"; interface BreadcrumbEntry { name: string; @@ -32,9 +34,11 @@ interface Props { } export const BreadcrumbSidebar = ({ list }: Props) => { + const { data: isCloud } = api.settings.isCloud.useQuery(); + return (
-
+
@@ -75,6 +79,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
+ {!isCloud && }
); diff --git a/apps/dokploy/components/ui/sidebar.tsx b/apps/dokploy/components/ui/sidebar.tsx index bb9d940a1c..bca3e2b3ea 100644 --- a/apps/dokploy/components/ui/sidebar.tsx +++ b/apps/dokploy/components/ui/sidebar.tsx @@ -213,7 +213,9 @@ const Sidebar = React.forwardRef< } side={side} > -
{children}
+
+ {children} +
); @@ -412,7 +414,7 @@ const SidebarContent = React.forwardRef< ref={ref} data-sidebar="content" className={cn( - "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-y-auto", className, )} {...props} diff --git a/apps/dokploy/drizzle/0149_rare_radioactive_man.sql b/apps/dokploy/drizzle/0149_rare_radioactive_man.sql new file mode 100644 index 0000000000..fcf195427d --- /dev/null +++ b/apps/dokploy/drizzle/0149_rare_radioactive_man.sql @@ -0,0 +1,31 @@ +CREATE TABLE "organization_role" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "role" text NOT NULL, + "permission" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "audit_log" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text, + "user_id" text, + "user_email" text NOT NULL, + "user_role" text NOT NULL, + "action" text NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text, + "resource_name" text, + "metadata" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "organizationRole_organizationId_idx" ON "organization_role" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "organizationRole_role_idx" ON "organization_role" USING btree ("role");--> statement-breakpoint +CREATE INDEX "auditLog_organizationId_idx" ON "audit_log" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "auditLog_userId_idx" ON "audit_log" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "auditLog_createdAt_idx" ON "audit_log" USING btree ("created_at"); \ No newline at end of file diff --git a/apps/dokploy/drizzle/0150_add_destination_types.sql b/apps/dokploy/drizzle/0150_add_destination_types.sql new file mode 100644 index 0000000000..5002e57c11 --- /dev/null +++ b/apps/dokploy/drizzle/0150_add_destination_types.sql @@ -0,0 +1,22 @@ +-- Add destinationType enum +CREATE TYPE "public"."destinationType" AS ENUM('s3', 'ftp', 'sftp', 'google-drive');--> statement-breakpoint + +-- Add new columns to destination table +ALTER TABLE "destination" ADD COLUMN "destinationType" "destinationType" DEFAULT 's3' NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "ftpHost" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "ftpPort" integer;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "ftpUser" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "ftpPassword" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "ftpPath" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "sftpKeyPath" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "googleDriveClientId" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "googleDriveClientSecret" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "googleDriveRefreshToken" text;--> statement-breakpoint +ALTER TABLE "destination" ADD COLUMN "googleDriveFolderId" text;--> statement-breakpoint + +-- Make S3-specific columns nullable for non-S3 destinations +ALTER TABLE "destination" ALTER COLUMN "accessKey" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "secretAccessKey" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "bucket" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "region" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "destination" ALTER COLUMN "endpoint" DROP NOT NULL; diff --git a/apps/dokploy/drizzle/meta/0149_snapshot.json b/apps/dokploy/drizzle/meta/0149_snapshot.json new file mode 100644 index 0000000000..643bb6faac --- /dev/null +++ b/apps/dokploy/drizzle/meta/0149_snapshot.json @@ -0,0 +1,7715 @@ +{ + "id": "e6eacbcd-0e09-4fa0-91be-730d3cc20d84", + "prevId": "a293a443-ceaf-418e-8e34-ff46e183995f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizationRole_organizationId_idx": { + "name": "organizationRole_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizationRole_role_idx": { + "name": "organizationRole_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auditLog_organizationId_idx": { + "name": "auditLog_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_userId_idx": { + "name": "auditLog_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_createdAt_idx": { + "name": "auditLog_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_organization_id_organization_id_fk": { + "name": "audit_log_organization_id_organization_id_fk", + "tableFrom": "audit_log", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Dockerfile'" + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_userId_user_id_fk": { + "name": "schedule_userId_user_id_fk", + "tableFrom": "schedule", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "whitelabelingConfig": { + "name": "whitelabelingConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"metaTitle\":null,\"footerText\":null}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 12e4346607..e0a631afb3 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1044,6 +1044,20 @@ "when": 1773129798212, "tag": "0148_futuristic_bullseye", "breakpoints": true + }, + { + "idx": 149, + "version": "7", + "when": 1773637297592, + "tag": "0149_rare_radioactive_man", + "breakpoints": true + }, + { + "idx": 150, + "version": "7", + "when": 1774095480000, + "tag": "0150_add_destination_types", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/lib/auth-client.ts b/apps/dokploy/lib/auth-client.ts index 47f0d2294f..6d786cc11c 100644 --- a/apps/dokploy/lib/auth-client.ts +++ b/apps/dokploy/lib/auth-client.ts @@ -1,7 +1,7 @@ import { ssoClient } from "@better-auth/sso/client"; +import { apiKeyClient } from "@better-auth/api-key/client"; import { adminClient, - apiKeyClient, inferAdditionalFields, organizationClient, twoFactorClient, diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 50a7fa44d8..fd74866745 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -46,7 +46,8 @@ "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai-compatible": "^2.0.30", - "@better-auth/sso": "1.5.0-beta.16", + "@better-auth/api-key": "1.5.4", + "@better-auth/sso": "1.5.4", "@codemirror/autocomplete": "^6.18.6", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-json": "^6.0.1", @@ -99,7 +100,7 @@ "ai": "^6.0.86", "ai-sdk-ollama": "^3.7.0", "bcrypt": "5.1.1", - "better-auth": "1.5.0-beta.16", + "better-auth": "1.5.4", "bl": "6.0.11", "boxen": "^7.1.1", "bullmq": "5.67.3", diff --git a/apps/dokploy/pages/dashboard/deployments.tsx b/apps/dokploy/pages/dashboard/deployments.tsx index 744301abf2..c39e4b1563 100644 --- a/apps/dokploy/pages/dashboard/deployments.tsx +++ b/apps/dokploy/pages/dashboard/deployments.tsx @@ -1,4 +1,5 @@ import { validateRequest } from "@dokploy/server/lib/auth"; +import { hasPermission } from "@dokploy/server/services/permission"; import { Rocket } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; @@ -79,7 +80,7 @@ DeploymentsPage.getLayout = (page: ReactElement) => { }; export async function getServerSideProps(ctx: GetServerSidePropsContext) { - const { user } = await validateRequest(ctx.req); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -88,6 +89,24 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) { }, }; } + + const canView = await hasPermission( + { + user: { id: user.id }, + session: { activeOrganizationId: session?.activeOrganizationId || "" }, + }, + { deployment: ["read"] }, + ); + + if (!canView) { + return { + redirect: { + permanent: false, + destination: "/dashboard/projects", + }, + }; + } + return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/docker.tsx b/apps/dokploy/pages/dashboard/docker.tsx index a61931a33d..384380a4d1 100644 --- a/apps/dokploy/pages/dashboard/docker.tsx +++ b/apps/dokploy/pages/dashboard/docker.tsx @@ -53,19 +53,15 @@ export async function getServerSideProps( try { await helpers.project.all.prefetch(); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToDocker) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.docker.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { props: { diff --git a/apps/dokploy/pages/dashboard/monitoring.tsx b/apps/dokploy/pages/dashboard/monitoring.tsx index 6858b1e1a7..02e7da48ed 100644 --- a/apps/dokploy/pages/dashboard/monitoring.tsx +++ b/apps/dokploy/pages/dashboard/monitoring.tsx @@ -1,5 +1,6 @@ import { IS_CLOUD } from "@dokploy/server/constants"; import { validateRequest } from "@dokploy/server/lib/auth"; +import { hasPermission } from "@dokploy/server/services/permission"; import { Loader2 } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; import type { ReactElement } from "react"; @@ -99,7 +100,7 @@ export async function getServerSideProps( }, }; } - const { user } = await validateRequest(ctx.req); + const { user, session } = await validateRequest(ctx.req); if (!user) { return { redirect: { @@ -109,6 +110,23 @@ export async function getServerSideProps( }; } + const canView = await hasPermission( + { + user: { id: user.id }, + session: { activeOrganizationId: session?.activeOrganizationId || "" }, + }, + { monitoring: ["read"] }, + ); + + if (!canView) { + return { + redirect: { + permanent: false, + destination: "/dashboard/projects", + }, + }; + } + return { props: {}, }; diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 41c44d2a84..d7ac393def 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -272,6 +272,7 @@ const EnvironmentPage = ( const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const { projectId, environmentId } = props; const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ projectId: projectId, @@ -905,9 +906,7 @@ const EnvironmentPage = ( - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canCreateServices) && ( + {permissions?.service.create && ( - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.delete && ( <>
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -242,24 +243,47 @@ const Service = (
General - Environment - Domains - Deployments - - Preview Deployments - - Schedules - - Volume Backups - - Logs + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.domain.read && ( + Domains + )} + {permissions?.deployment.read && ( + + Deployments + + )} + {permissions?.deployment.read && ( + + Preview Deployments + + )} + {permissions?.schedule.read && ( + Schedules + )} + {permissions?.volumeBackup.read && ( + + Volume Backups + + )} + {permissions?.logs.read && ( + Logs + )} {data?.sourceType !== "docker" && ( Patches )} - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced )} - Advanced
@@ -268,26 +292,29 @@ const Service = ( - -
- -
-
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} - -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - {/* {monitoring?.enabledFeatures && + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && isCloud && data?.serverId && (
@@ -301,7 +328,7 @@ const Service = (
)} */} - {/* {toggleMonitoring ? ( + {/* {toggleMonitoring ? ( ) : ( */} -
- -
- {/* )} */} - - )} +
+ +
+ {/* )} */} + + )} +
-
- + + )} - -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
+ {permissions?.logs.read && ( + +
+ +
+
+ )} + {permissions?.schedule.read && ( + +
+ +
+
+ )} + {permissions?.deployment.read && ( + +
+ +
+
+ )} + {permissions?.volumeBackup.read && ( + +
+ +
+
+ )} + {permissions?.deployment.read && ( + +
+ +
+
+ )} + {permissions?.domain.read && ( + +
+ +
+
+ )}
- -
- - - - - - - - - -
-
+ {permissions?.service.create && ( + +
+ + + + + + + + + +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx index c168959ecf..9056e186dd 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx @@ -81,6 +81,7 @@ const Service = ( const { data } = api.compose.one.useQuery({ composeId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ projectId: data?.environment?.projectId || "", @@ -185,11 +186,11 @@ const Service = ( )}
- + {permissions?.service.create && ( + + )} - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.delete && ( )}
@@ -232,22 +233,45 @@ const Service = (
General - Environment - Domains - Deployments - Backups - Schedules - - Volume Backups - - Logs + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.domain.read && ( + Domains + )} + {permissions?.deployment.read && ( + + Deployments + + )} + {permissions?.service.create && ( + Backups + )} + {permissions?.schedule.read && ( + Schedules + )} + {permissions?.volumeBackup.read && ( + + Volume Backups + + )} + {permissions?.logs.read && ( + Logs + )} {data?.sourceType !== "raw" && ( Patches )} - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced )} - Advanced
@@ -256,47 +280,56 @@ const Service = (
- -
- -
-
- -
- -
-
+ {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.service.create && ( + +
+ +
+
+ )} - -
- -
-
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - {/* {monitoring?.enabledFeatures && + {permissions?.schedule.read && ( + +
+ +
+
+ )} + {permissions?.volumeBackup.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && isCloud && data?.serverId && (
@@ -320,53 +353,60 @@ const Service = ( appType={data?.composeType || "docker-compose"} /> ) : ( */} - {/*
*/} - - {/*
*/} - {/* )} */} - + {/*
*/} + + {/*
*/} + {/* )} */} + + )} +
+
+ + )} + + {permissions?.logs.read && ( + +
+ {data?.composeType === "docker-compose" ? ( + + ) : ( + )}
-
-
+ + )} - -
- {data?.composeType === "docker-compose" ? ( - +
+ - ) : ( - - )} -
- - - -
- -
-
+
+
+ )} - -
- -
-
+ {permissions?.domain.read && ( + +
+ +
+
+ )}
@@ -374,14 +414,16 @@ const Service = (
- -
- - - - -
-
+ {permissions?.service.create && ( + +
+ + + + +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx index a5ff414e9b..8e210c92b0 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx @@ -60,6 +60,7 @@ const Mariadb = ( const [tab, setSab] = useState(activeTab); const { data } = api.mariadb.one.useQuery({ mariadbId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -159,10 +160,10 @@ const Mariadb = ( )}
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -214,13 +215,24 @@ const Mariadb = ( )} > General - Environment - Logs - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} Backups - Advanced + {permissions?.service.create && ( + Advanced + )}
@@ -231,25 +243,28 @@ const Mariadb = (
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - {/* {monitoring?.enabledFeatures && ( + {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && (
*/} + {/* )} */} + + )} +
-
- - -
- -
-
+ + )} + {permissions?.logs.read && ( + +
+ +
+
+ )}
- -
- -
-
+ {permissions?.service.create && ( + +
+ +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx index 4567893322..3d72601d5d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx @@ -60,6 +60,7 @@ const Mongo = ( const { data } = api.mongo.one.useQuery({ mongoId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ @@ -159,10 +160,10 @@ const Mongo = (
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -214,13 +215,24 @@ const Mongo = ( )} > General - Environment - Logs - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} Backups - Advanced + {permissions?.service.create && ( + Advanced + )} @@ -231,25 +243,28 @@ const Mongo = (
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - {/* {monitoring?.enabledFeatures && ( + {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && (
*/} + {/* )} */} + + )} +
-
- - -
- -
-
+ + )} + {permissions?.logs.read && ( + +
+ +
+
+ )}
- -
- -
-
+ {permissions?.service.create && ( + +
+ +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx index 76af699a54..c04b689d77 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx @@ -59,6 +59,7 @@ const MySql = ( const [tab, setSab] = useState(activeTab); const { data } = api.mysql.one.useQuery({ mysqlId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ @@ -159,10 +160,10 @@ const MySql = (
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -214,17 +215,24 @@ const MySql = ( )} > General - - Environment - - Logs - {((data?.serverId && isCloud) || !data?.server) && ( - - Monitoring + {permissions?.envVars.read && ( + + Environment )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} Backups - Advanced + {permissions?.service.create && ( + Advanced + )} @@ -235,40 +243,47 @@ const MySql = (
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - +
+ +
+ + )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + - - )} + ) : ( + <> + + + )} +
-
- - -
- -
-
+ + )} + {permissions?.logs.read && ( + +
+ +
+
+ )}
- -
- -
-
+ {permissions?.service.create && ( + +
+ +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx index d97ada5b05..d697496e20 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx @@ -59,6 +59,7 @@ const Postgresql = ( const [tab, setSab] = useState(activeTab); const { data } = api.postgres.one.useQuery({ postgresId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ @@ -158,10 +159,10 @@ const Postgresql = (
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -215,13 +216,24 @@ const Postgresql = ( )} > General - Environment - Logs - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} Backups - Advanced + {permissions?.service.create && ( + Advanced + )} @@ -236,44 +248,50 @@ const Postgresql = ( />
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - +
+ +
+ + )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + - - )} + ) : ( + <> + + + )} +
-
- - -
- -
-
+ + )} + {permissions?.logs.read && ( + +
+ +
+
+ )}
- -
- -
-
+ {permissions?.service.create && ( + +
+ +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx index 95058d4a64..b41337d16a 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx @@ -59,6 +59,7 @@ const Redis = ( const { data } = api.redis.one.useQuery({ redisId }); const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: environments } = api.environment.byProjectId.useQuery({ @@ -158,10 +159,10 @@ const Redis = (
- - {(auth?.role === "owner" || - auth?.role === "admin" || - auth?.canDeleteServices) && ( + {permissions?.service.create && ( + + )} + {permissions?.service.delete && ( )}
@@ -213,12 +214,23 @@ const Redis = ( )} > General - Environment - Logs - {((data?.serverId && isCloud) || !data?.server) && ( - Monitoring + {permissions?.envVars.read && ( + + Environment + + )} + {permissions?.logs.read && ( + Logs + )} + {permissions?.monitoring.read && + ((data?.serverId && isCloud) || !data?.server) && ( + + Monitoring + + )} + {permissions?.service.create && ( + Advanced )} - Advanced @@ -229,25 +241,28 @@ const Redis = (
- -
- -
-
- -
-
- {data?.serverId && isCloud ? ( - - ) : ( - <> - {/* {monitoring?.enabledFeatures && ( + {permissions?.envVars.read && ( + +
+ +
+
+ )} + {permissions?.monitoring.read && ( + +
+
+ {data?.serverId && isCloud ? ( + + ) : ( + <> + {/* {monitoring?.enabledFeatures && (
*/} + {/* )} */} + + )} +
-
- - -
- -
-
- -
- -
-
+ + )} + {permissions?.logs.read && ( + +
+ +
+
+ )} + {permissions?.service.create && ( + +
+ +
+
+ )} )} diff --git a/apps/dokploy/pages/dashboard/settings/audit-logs.tsx b/apps/dokploy/pages/dashboard/settings/audit-logs.tsx new file mode 100644 index 0000000000..1172b2c024 --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/audit-logs.tsx @@ -0,0 +1,66 @@ +import { validateRequest } from "@dokploy/server"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import type { ReactElement } from "react"; +import superjson from "superjson"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs"; +import { appRouter } from "@/server/api/root"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return {page}; +}; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { req, res } = ctx; + const { user, session } = await validateRequest(req); + + if (!user) { + return { + redirect: { destination: "/", permanent: true }, + }; + } + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session as any, + user: user as any, + }, + transformer: superjson, + }); + + try { + const userPermissions = await helpers.user.getPermissions.fetch(); + + if (!userPermissions?.auditLog.read) { + return { + redirect: { + destination: "/dashboard/settings/profile", + permanent: false, + }, + }; + } + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch { + return { props: {} }; + } +} diff --git a/apps/dokploy/pages/dashboard/settings/git-providers.tsx b/apps/dokploy/pages/dashboard/settings/git-providers.tsx index 6b54f45a5e..492f0b8502 100644 --- a/apps/dokploy/pages/dashboard/settings/git-providers.tsx +++ b/apps/dokploy/pages/dashboard/settings/git-providers.tsx @@ -48,19 +48,15 @@ export async function getServerSideProps( try { await helpers.project.all.prefetch(); await helpers.settings.isCloud.prefetch(); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToGitProviders) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.gitProviders.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { props: { diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 22077fb416..b02d59c0e3 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -11,7 +11,7 @@ import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; const Page = () => { - const { data } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); return ( @@ -19,9 +19,7 @@ const Page = () => {
{isCloud && } - {(data?.canAccessToAPI || - data?.role === "owner" || - data?.role === "admin") && } + {permissions?.api.read && }
); diff --git a/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx b/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx index 928c45a124..72ac60280d 100644 --- a/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx +++ b/apps/dokploy/pages/dashboard/settings/ssh-keys.tsx @@ -49,19 +49,15 @@ export async function getServerSideProps( await helpers.project.all.prefetch(); await helpers.settings.isCloud.prefetch(); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToSSHKeys) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.sshKeys.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { props: { diff --git a/apps/dokploy/pages/dashboard/settings/users.tsx b/apps/dokploy/pages/dashboard/settings/users.tsx index 29941679d5..43c0142790 100644 --- a/apps/dokploy/pages/dashboard/settings/users.tsx +++ b/apps/dokploy/pages/dashboard/settings/users.tsx @@ -3,16 +3,24 @@ import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; import type { ReactElement } from "react"; import superjson from "superjson"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles"; import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations"; import { ShowUsers } from "@/components/dashboard/settings/users/show-users"; -import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; +import { api } from "@/utils/api"; const Page = () => { + const { data: auth } = api.user.get.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); + const isOwnerOrAdmin = auth?.role === "owner" || auth?.role === "admin"; + const canCreateMembers = permissions?.member.create ?? false; + return (
- + {canCreateMembers && } + {isOwnerOrAdmin && }
); }; @@ -28,7 +36,7 @@ export async function getServerSideProps( const { req, res } = ctx; const { user, session } = await validateRequest(req); - if (!user || user.role === "member") { + if (!user) { return { redirect: { permanent: true, @@ -48,12 +56,30 @@ export async function getServerSideProps( }, transformer: superjson, }); - await helpers.user.get.prefetch(); - await helpers.settings.isCloud.prefetch(); - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; + try { + await helpers.user.get.prefetch(); + await helpers.settings.isCloud.prefetch(); + + const userPermissions = await helpers.user.getPermissions.fetch(); + + if (!userPermissions?.member.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch { + return { + props: {}, + }; + } } diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 0711d843e2..3ded13c28d 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -53,19 +53,15 @@ export async function getServerSideProps( try { await helpers.project.all.prefetch(); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToDocker) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.docker.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { props: { diff --git a/apps/dokploy/pages/dashboard/traefik.tsx b/apps/dokploy/pages/dashboard/traefik.tsx index d46eb63a7b..7342de6e46 100644 --- a/apps/dokploy/pages/dashboard/traefik.tsx +++ b/apps/dokploy/pages/dashboard/traefik.tsx @@ -53,19 +53,15 @@ export async function getServerSideProps( try { await helpers.project.all.prefetch(); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToTraefikFiles) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.traefikFiles.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { props: { diff --git a/apps/dokploy/pages/swagger.tsx b/apps/dokploy/pages/swagger.tsx index 1b962c945b..b461a85eca 100644 --- a/apps/dokploy/pages/swagger.tsx +++ b/apps/dokploy/pages/swagger.tsx @@ -98,19 +98,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, transformer: superjson, }); - if (user.role === "member") { - const userR = await helpers.user.one.fetch({ - userId: user.id, - }); + const userPermissions = await helpers.user.getPermissions.fetch(); - if (!userR?.canAccessToAPI) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } + if (!userPermissions?.api.read) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; } return { diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index e80bf9e379..5f44e8446c 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -23,6 +23,8 @@ import { mysqlRouter } from "./routers/mysql"; import { notificationRouter } from "./routers/notification"; import { organizationRouter } from "./routers/organization"; import { patchRouter } from "./routers/patch"; +import { auditLogRouter } from "./routers/proprietary/audit-log"; +import { customRoleRouter } from "./routers/proprietary/custom-role"; import { licenseKeyRouter } from "./routers/proprietary/license-key"; import { ssoRouter } from "./routers/proprietary/sso"; import { whitelabelingRouter } from "./routers/proprietary/whitelabeling"; @@ -89,6 +91,8 @@ export const appRouter = createTRPCRouter({ licenseKey: licenseKeyRouter, sso: ssoRouter, whitelabeling: whitelabelingRouter, + customRole: customRoleRouter, + auditLog: auditLogRouter, schedule: scheduleRouter, rollback: rollbackRouter, volumeBackups: volumeBackupsRouter, diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index ff2d1ee8a8..b46fcf99a2 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -21,7 +21,7 @@ import { findProjectById } from "@dokploy/server/services/project"; import { addNewService, checkServiceAccess, -} from "@dokploy/server/services/user"; +} from "@dokploy/server/services/permission"; import { getProviderHeaders, getProviderName, @@ -38,17 +38,10 @@ import { import { generatePassword } from "@/templates/utils"; export const aiRouter = createTRPCRouter({ - one: protectedProcedure + one: adminProcedure .input(z.object({ aiId: z.string() })) - .query(async ({ ctx, input }) => { - const aiSetting = await getAiSettingById(input.aiId); - if (aiSetting.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You don't have access to this AI configuration", - }); - } - return aiSetting; + .query(async ({ input }) => { + return await getAiSettingById(input.aiId); }), getModels: protectedProcedure @@ -159,11 +152,9 @@ export const aiRouter = createTRPCRouter({ return await saveAiSettings(ctx.session.activeOrganizationId, input); }), - update: protectedProcedure - .input(apiUpdateAi) - .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); + }), getAll: adminProcedure.query(async ({ ctx }) => { return await getAiSettingsByOrganizationId( @@ -171,29 +162,15 @@ export const aiRouter = createTRPCRouter({ ); }), - get: protectedProcedure + get: adminProcedure .input(z.object({ aiId: z.string() })) - .query(async ({ ctx, input }) => { - const aiSetting = await getAiSettingById(input.aiId); - if (aiSetting.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You don't have access to this AI configuration", - }); - } - return aiSetting; + .query(async ({ input }) => { + return await getAiSettingById(input.aiId); }), - delete: protectedProcedure + delete: adminProcedure .input(z.object({ aiId: z.string() })) - .mutation(async ({ ctx, input }) => { - const aiSetting = await getAiSettingById(input.aiId); - if (aiSetting.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You don't have access to this AI configuration", - }); - } + .mutation(async ({ input }) => { return await deleteAiSettings(input.aiId); }), @@ -223,13 +200,7 @@ export const aiRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.session.activeOrganizationId, - environment.projectId, - "create", - ); - } + await checkServiceAccess(ctx, environment.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -275,13 +246,7 @@ export const aiRouter = createTRPCRouter({ } } - if (ctx.user.role === "member") { - await addNewService( - ctx.session.activeOrganizationId, - ctx.user.ownerId, - compose.composeId, - ); - } + await addNewService(ctx, compose.composeId); return null; }), diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index df3e81c82b..22023e6e18 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1,13 +1,10 @@ import { - addNewService, - checkServiceAccess, clearOldDeployments, createApplication, deleteAllMiddlewares, findApplicationById, findEnvironmentById, findGitProviderById, - findMemberById, findProjectById, getApplicationStats, IS_CLOUD, @@ -29,14 +26,24 @@ import { updateDeploymentStatus, writeConfig, writeConfigRemote, - // uploadFileSchema } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateApplication, apiDeployApplication, @@ -72,18 +79,10 @@ export const applicationRouter = createTRPCRouter({ .input(apiCreateApplication) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -101,13 +100,13 @@ export const applicationRouter = createTRPCRouter({ const newApplication = await createApplication(input); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newApplication.applicationId, - project.organizationId, - ); - } + await addNewService(ctx, newApplication.applicationId); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newApplication.applicationId, + resourceName: newApplication.appName, + }); return newApplication; } catch (error: unknown) { console.log("error", error); @@ -124,14 +123,7 @@ export const applicationRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.applicationId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.applicationId, "read"); const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== @@ -186,22 +178,21 @@ export const applicationRouter = createTRPCRouter({ reload: protectedProcedure .input(apiReloadApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const application = await findApplicationById(input.applicationId); try { - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this application", - }); - } - await updateApplicationStatus(input.applicationId, "idle"); await mechanizeDockerContainer(application); await updateApplicationStatus(input.applicationId, "done"); + await audit(ctx, { + action: "reload", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; } catch (error) { await updateApplicationStatus(input.applicationId, "error"); @@ -216,14 +207,7 @@ export const applicationRouter = createTRPCRouter({ delete: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.applicationId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.applicationId, "delete"); const application = await findApplicationById(input.applicationId); if ( @@ -272,69 +256,66 @@ export const applicationRouter = createTRPCRouter({ } catch (_) {} } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: application.applicationId, + resourceName: application.appName, + }); return application; }), stop: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const service = await findApplicationById(input.applicationId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this application", - }); - } if (service.serverId) { await stopServiceRemote(service.serverId, service.appName); } else { await stopService(service.appName); } await updateApplicationStatus(input.applicationId, "idle"); - + await audit(ctx, { + action: "stop", + resourceType: "application", + resourceId: service.applicationId, + resourceName: service.appName, + }); return service; }), start: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const service = await findApplicationById(input.applicationId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this application", - }); - } - if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { await startService(service.appName); } await updateApplicationStatus(input.applicationId, "done"); - + await audit(ctx, { + action: "start", + resourceType: "application", + resourceId: service.applicationId, + resourceName: service.appName, + }); return service; }), redeploy: protectedProcedure .input(apiRedeployApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to redeploy this application", - }); - } const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: input.title || "Rebuild deployment", @@ -349,6 +330,12 @@ export const applicationRouter = createTRPCRouter({ deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); + await audit(ctx, { + action: "rebuild", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; } await myQueue.add( @@ -359,41 +346,40 @@ export const applicationRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "rebuild", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); }), saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariables) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + envVars: ["write"], + }); await updateApplication(input.applicationId, { env: input.env, buildArgs: input.buildArgs, buildSecrets: input.buildSecrets, createEnvFile: input.createEnvFile, }); + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveBuildType: protectedProcedure .input(apiSaveBuildType) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this build type", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { buildType: input.buildType, dockerfile: input.dockerfile, @@ -404,22 +390,21 @@ export const applicationRouter = createTRPCRouter({ isStaticSpa: input.isStaticSpa, railpackVersion: input.railpackVersion, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveGithubProvider: protectedProcedure .input(apiSaveGithubProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this github provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { repository: input.repository, branch: input.branch, @@ -432,22 +417,21 @@ export const applicationRouter = createTRPCRouter({ triggerType: input.triggerType, enableSubmodules: input.enableSubmodules, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveGitlabProvider: protectedProcedure .input(apiSaveGitlabProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this gitlab provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { gitlabRepository: input.gitlabRepository, gitlabOwner: input.gitlabOwner, @@ -461,22 +445,21 @@ export const applicationRouter = createTRPCRouter({ watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveBitbucketProvider: protectedProcedure .input(apiSaveBitbucketProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this bitbucket provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { bitbucketRepository: input.bitbucketRepository, bitbucketRepositorySlug: input.bitbucketRepositorySlug, @@ -489,22 +472,21 @@ export const applicationRouter = createTRPCRouter({ watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveGiteaProvider: protectedProcedure .input(apiSaveGiteaProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this gitea provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { giteaRepository: input.giteaRepository, giteaOwner: input.giteaOwner, @@ -516,22 +498,21 @@ export const applicationRouter = createTRPCRouter({ watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveDockerProvider: protectedProcedure .input(apiSaveDockerProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this docker provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { dockerImage: input.dockerImage, username: input.username, @@ -540,22 +521,21 @@ export const applicationRouter = createTRPCRouter({ applicationStatus: "idle", registryUrl: input.registryUrl, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), saveGitProvider: protectedProcedure .input(apiSaveGitProvider) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this git provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { customGitBranch: input.customGitBranch, customGitBuildPath: input.customGitBuildPath, @@ -566,26 +546,22 @@ export const applicationRouter = createTRPCRouter({ watchPaths: input.watchPaths, enableSubmodules: input.enableSubmodules, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), disconnectGitProvider: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to disconnect this git provider", - }); - } - - // Reset all git provider related fields + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { - // GitHub fields repository: null, branch: null, owner: null, @@ -593,7 +569,6 @@ export const applicationRouter = createTRPCRouter({ githubId: null, triggerType: "push", - // GitLab fields gitlabRepository: null, gitlabOwner: null, gitlabBranch: null, @@ -602,63 +577,58 @@ export const applicationRouter = createTRPCRouter({ gitlabProjectId: null, gitlabPathNamespace: null, - // Bitbucket fields bitbucketRepository: null, bitbucketOwner: null, bitbucketBranch: null, bitbucketBuildPath: null, bitbucketId: null, - // Gitea fields giteaRepository: null, giteaOwner: null, giteaBranch: null, giteaBuildPath: null, giteaId: null, - // Custom Git fields customGitBranch: null, customGitBuildPath: null, customGitUrl: null, customGitSSHKeyId: null, - // Common fields sourceType: "github", // Reset to default applicationStatus: "idle", watchPaths: null, enableSubmodules: false, }); - + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), markRunning: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to mark this application as running", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); await updateApplicationStatus(input.applicationId, "running"); + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "deploy", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); }), update: protectedProcedure .input(apiUpdateApplication) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); const { applicationId, ...rest } = input; const updateApp = await updateApplication(applicationId, { ...rest, @@ -670,40 +640,39 @@ export const applicationRouter = createTRPCRouter({ message: "Error updating application", }); } - + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: updateApp.applicationId, + resourceName: updateApp.appName, + }); return true; }), refreshToken: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to refresh this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); await updateApplication(input.applicationId, { refreshToken: nanoid(), }); + const application = await findApplicationById(input.applicationId); + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), deploy: protectedProcedure .input(apiDeployApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: input.title || "Manual deployment", @@ -717,7 +686,12 @@ export const applicationRouter = createTRPCRouter({ deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); - + await audit(ctx, { + action: "deploy", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; } await myQueue.add( @@ -728,69 +702,60 @@ export const applicationRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "deploy", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); }), cleanQueues: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to clean this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["cancel"], + }); await cleanQueuesByApplication(input.applicationId); }), clearDeployments: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["create"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: - "You are not authorized to clear deployments for this application", - }); - } await clearOldDeployments(application.appName, application.serverId); + await audit(ctx, { + action: "delete", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), killBuild: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["cancel"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to kill this build", - }); - } await killDockerBuild("application", application.serverId); + await audit(ctx, { + action: "stop", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); }), readTraefikConfig: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + traefikFiles: ["read"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to read this application", - }); - } - let traefikConfig = null; if (application.serverId) { traefikConfig = await readRemoteConfig( @@ -820,18 +785,11 @@ export const applicationRouter = createTRPCRouter({ const applicationId = formData.get("applicationId") as string; const dropBuildPath = formData.get("dropBuildPath") as string | null; + await checkServicePermissionAndAccess(ctx, applicationId, { + deployment: ["create"], + }); const app = await findApplicationById(applicationId); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } - await updateApplication(applicationId, { sourceType: "drop", dropBuildPath: dropBuildPath || "", @@ -862,23 +820,21 @@ export const applicationRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "deploy", + resourceType: "application", + resourceId: app.applicationId, + resourceName: app.appName, + }); return true; }), updateTraefikConfig: protectedProcedure .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + traefikFiles: ["write"], + }); const application = await findApplicationById(input.applicationId); - - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } - if (application.serverId) { await writeConfigRemote( application.serverId, @@ -888,9 +844,15 @@ export const applicationRouter = createTRPCRouter({ } else { writeConfig(application.appName, input.traefikConfig); } + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return true; }), - readAppMonitoring: protectedProcedure + readAppMonitoring: withPermission("monitoring", "read") .input(apiFindMonitoringStats) .query(async ({ input }) => { if (IS_CLOUD) { @@ -911,31 +873,10 @@ export const applicationRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this application", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); - // Update the application's projectId const updatedApplication = await db .update(applications) .set({ @@ -951,23 +892,22 @@ export const applicationRouter = createTRPCRouter({ message: "Failed to move application", }); } - + await audit(ctx, { + action: "update", + resourceType: "application", + resourceId: updatedApplication.applicationId, + resourceName: updatedApplication.appName, + }); return updatedApplication; }), cancelDeployment: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["cancel"], + }); const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to cancel this deployment", - }); - } if (IS_CLOUD && application.serverId) { try { @@ -984,7 +924,12 @@ export const applicationRouter = createTRPCRouter({ applicationId: input.applicationId, applicationType: "application", }); - + await audit(ctx, { + action: "stop", + resourceType: "application", + resourceId: application.applicationId, + resourceName: application.appName, + }); return { success: true, message: "Deployment cancellation requested", @@ -1085,19 +1030,17 @@ export const applicationRouter = createTRPCRouter({ ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${applications.applicationId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${applications.applicationId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); const where = and(...baseConditions); diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 2425408939..950eb9b9b6 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -27,7 +27,8 @@ import { import { findDestinationById } from "@dokploy/server/services/destination"; import { runComposeBackup } from "@dokploy/server/utils/backups/compose"; import { - getS3Credentials, + getRcloneFlags, + getRcloneRemotePath, normalizeS3Path, } from "@dokploy/server/utils/backups/utils"; import { @@ -44,7 +45,13 @@ import { } from "@dokploy/server/utils/restore"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateBackup, apiFindOneBackup, @@ -69,10 +76,21 @@ interface RcloneFile { export const backupRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { - const newBackup = await createBackup(input); + const serviceId = + input.postgresId || + input.mysqlId || + input.mariadbId || + input.mongoId || + input.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + backup: ["create"], + }); + } + const newBackup = await createBackup(input); const backup = await findBackupById(newBackup.backupId); if (IS_CLOUD && backup.enabled) { @@ -110,6 +128,11 @@ export const backupRouter = createTRPCRouter({ scheduleBackup(backup); } } + await audit(ctx, { + action: "create", + resourceType: "backup", + resourceId: backup.backupId, + }); } catch (error) { console.error(error); throw new TRPCError({ @@ -122,15 +145,42 @@ export const backupRouter = createTRPCRouter({ }); } }), - one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => { - const backup = await findBackupById(input.backupId); + one: protectedProcedure + .input(apiFindOneBackup) + .query(async ({ input, ctx }) => { + const backup = await findBackupById(input.backupId); + + const serviceId = + backup.postgresId || + backup.mysqlId || + backup.mariadbId || + backup.mongoId || + backup.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + backup: ["read"], + }); + } - return backup; - }), + return backup; + }), update: protectedProcedure .input(apiUpdateBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { + const existing = await findBackupById(input.backupId); + const serviceId = + existing.postgresId || + existing.mysqlId || + existing.mariadbId || + existing.mongoId || + existing.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + backup: ["update"], + }); + } + await updateBackupById(input.backupId, input); const backup = await findBackupById(input.backupId); @@ -156,6 +206,11 @@ export const backupRouter = createTRPCRouter({ removeScheduleBackup(input.backupId); } } + await audit(ctx, { + action: "update", + resourceType: "backup", + resourceId: backup.backupId, + }); } catch (error) { const message = error instanceof Error ? error.message : "Error updating this Backup"; @@ -167,8 +222,21 @@ export const backupRouter = createTRPCRouter({ }), remove: protectedProcedure .input(apiRemoveBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { + const backup = await findBackupById(input.backupId); + const serviceId = + backup.postgresId || + backup.mysqlId || + backup.mariadbId || + backup.mongoId || + backup.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + backup: ["delete"], + }); + } + const value = await removeBackupById(input.backupId); if (IS_CLOUD && value) { removeJob({ @@ -179,6 +247,11 @@ export const backupRouter = createTRPCRouter({ } else if (!IS_CLOUD) { removeScheduleBackup(input.backupId); } + await audit(ctx, { + action: "delete", + resourceType: "backup", + resourceId: input.backupId, + }); return value; } catch (error) { const message = @@ -191,13 +264,22 @@ export const backupRouter = createTRPCRouter({ }), manualBackupPostgres: protectedProcedure .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const backup = await findBackupById(input.backupId); + if (backup.postgresId) { + await checkServicePermissionAndAccess(ctx, backup.postgresId, { + backup: ["create"], + }); + } const postgres = await findPostgresByBackupId(backup.backupId); await runPostgresBackup(postgres, backup); - await keepLatestNBackups(backup, postgres?.serverId); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; } catch (error) { const message = @@ -213,12 +295,22 @@ export const backupRouter = createTRPCRouter({ manualBackupMySql: protectedProcedure .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const backup = await findBackupById(input.backupId); + if (backup.mysqlId) { + await checkServicePermissionAndAccess(ctx, backup.mysqlId, { + backup: ["create"], + }); + } const mysql = await findMySqlByBackupId(backup.backupId); await runMySqlBackup(mysql, backup); await keepLatestNBackups(backup, mysql?.serverId); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; } catch (error) { throw new TRPCError({ @@ -230,12 +322,22 @@ export const backupRouter = createTRPCRouter({ }), manualBackupMariadb: protectedProcedure .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const backup = await findBackupById(input.backupId); + if (backup.mariadbId) { + await checkServicePermissionAndAccess(ctx, backup.mariadbId, { + backup: ["create"], + }); + } const mariadb = await findMariadbByBackupId(backup.backupId); await runMariadbBackup(mariadb, backup); await keepLatestNBackups(backup, mariadb?.serverId); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; } catch (error) { throw new TRPCError({ @@ -247,12 +349,22 @@ export const backupRouter = createTRPCRouter({ }), manualBackupCompose: protectedProcedure .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const backup = await findBackupById(input.backupId); + if (backup.composeId) { + await checkServicePermissionAndAccess(ctx, backup.composeId, { + backup: ["create"], + }); + } const compose = await findComposeByBackupId(backup.backupId); await runComposeBackup(compose, backup); await keepLatestNBackups(backup, compose?.serverId); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; } catch (error) { throw new TRPCError({ @@ -264,12 +376,22 @@ export const backupRouter = createTRPCRouter({ }), manualBackupMongo: protectedProcedure .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const backup = await findBackupById(input.backupId); + if (backup.mongoId) { + await checkServicePermissionAndAccess(ctx, backup.mongoId, { + backup: ["create"], + }); + } const mongo = await findMongoByBackupId(backup.backupId); await runMongoBackup(mongo, backup); await keepLatestNBackups(backup, mongo?.serverId); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; } catch (error) { throw new TRPCError({ @@ -279,15 +401,20 @@ export const backupRouter = createTRPCRouter({ }); } }), - manualBackupWebServer: protectedProcedure + manualBackupWebServer: withPermission("backup", "create") .input(apiFindOneBackup) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const backup = await findBackupById(input.backupId); await runWebServerBackup(backup); await keepLatestNBackups(backup); + await audit(ctx, { + action: "run", + resourceType: "backup", + resourceId: backup.backupId, + }); return true; }), - listBackupFiles: protectedProcedure + listBackupFiles: withPermission("backup", "read") .input( z.object({ destinationId: z.string(), @@ -298,8 +425,8 @@ export const backupRouter = createTRPCRouter({ .query(async ({ input }) => { try { const destination = await findDestinationById(input.destinationId); - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const lastSlashIndex = input.search.lastIndexOf("/"); const baseDir = @@ -374,7 +501,12 @@ export const backupRouter = createTRPCRouter({ }, }) .input(apiRestoreBackup) - .subscription(async function* ({ input, signal }) { + .subscription(async function* ({ input, ctx, signal }) { + if (input.databaseId) { + await checkServicePermissionAndAccess(ctx, input.databaseId, { + backup: ["restore"], + }); + } const destination = await findDestinationById(input.destinationId); const queue: string[] = []; const done = false; diff --git a/apps/dokploy/server/api/routers/bitbucket.ts b/apps/dokploy/server/api/routers/bitbucket.ts index e4c58aafcc..bfaa68540f 100644 --- a/apps/dokploy/server/api/routers/bitbucket.ts +++ b/apps/dokploy/server/api/routers/bitbucket.ts @@ -8,7 +8,12 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiBitbucketTestConnection, apiCreateBitbucket, @@ -18,15 +23,23 @@ import { } from "@/server/db/schema"; export const bitbucketRouter = createTRPCRouter({ - create: protectedProcedure + create: withPermission("gitProviders", "create") .input(apiCreateBitbucket) .mutation(async ({ input, ctx }) => { try { - return await createBitbucket( + const result = await createBitbucket( input, ctx.session.activeOrganizationId, ctx.session.userId, ); + + await audit(ctx, { + action: "create", + resourceType: "gitProvider", + resourceName: input.name, + }); + + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -37,19 +50,8 @@ export const bitbucketRouter = createTRPCRouter({ }), one: protectedProcedure .input(apiFindOneBitbucket) - .query(async ({ input, ctx }) => { - const bitbucketProvider = await findBitbucketById(input.bitbucketId); - if ( - bitbucketProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - bitbucketProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this bitbucket provider", - }); - } - return bitbucketProvider; + .query(async ({ input }) => { + return await findBitbucketById(input.bitbucketId); }), bitbucketProviders: protectedProcedure.query(async ({ ctx }) => { let result = await db.query.bitbucket.findMany({ @@ -73,53 +75,18 @@ export const bitbucketRouter = createTRPCRouter({ getBitbucketRepositories: protectedProcedure .input(apiFindOneBitbucket) - .query(async ({ input, ctx }) => { - const bitbucketProvider = await findBitbucketById(input.bitbucketId); - if ( - bitbucketProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - bitbucketProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this bitbucket provider", - }); - } + .query(async ({ input }) => { return await getBitbucketRepositories(input.bitbucketId); }), getBitbucketBranches: protectedProcedure .input(apiFindBitbucketBranches) - .query(async ({ input, ctx }) => { - const bitbucketProvider = await findBitbucketById( - input.bitbucketId || "", - ); - if ( - bitbucketProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - bitbucketProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this bitbucket provider", - }); - } + .query(async ({ input }) => { return await getBitbucketBranches(input); }), testConnection: protectedProcedure .input(apiBitbucketTestConnection) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { - const bitbucketProvider = await findBitbucketById(input.bitbucketId); - if ( - bitbucketProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - bitbucketProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this bitbucket provider", - }); - } const result = await testBitbucketConnection(input); return `Found ${result} repositories`; @@ -130,23 +97,21 @@ export const bitbucketRouter = createTRPCRouter({ }); } }), - update: protectedProcedure + update: withPermission("gitProviders", "create") .input(apiUpdateBitbucket) .mutation(async ({ input, ctx }) => { - const bitbucketProvider = await findBitbucketById(input.bitbucketId); - if ( - bitbucketProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - bitbucketProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this bitbucket provider", - }); - } - return await updateBitbucket(input.bitbucketId, { + const result = await updateBitbucket(input.bitbucketId, { ...input, organizationId: ctx.session.activeOrganizationId, }); + + await audit(ctx, { + action: "update", + resourceType: "gitProvider", + resourceId: input.bitbucketId, + resourceName: input.name, + }); + + return result; }), }); diff --git a/apps/dokploy/server/api/routers/certificate.ts b/apps/dokploy/server/api/routers/certificate.ts index 095967565a..0ebd33e7f7 100644 --- a/apps/dokploy/server/api/routers/certificate.ts +++ b/apps/dokploy/server/api/routers/certificate.ts @@ -7,7 +7,8 @@ import { import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { adminProcedure, createTRPCRouter } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; +import { createTRPCRouter, withPermission } from "@/server/api/trpc"; import { apiCreateCertificate, apiFindCertificate, @@ -15,7 +16,7 @@ import { } from "@/server/db/schema"; export const certificateRouter = createTRPCRouter({ - create: adminProcedure + create: withPermission("certificate", "create") .input(apiCreateCertificate) .mutation(async ({ input, ctx }) => { if (IS_CLOUD && !input.serverId) { @@ -24,10 +25,20 @@ export const certificateRouter = createTRPCRouter({ message: "Please set a server to create a certificate", }); } - return await createCertificate(input, ctx.session.activeOrganizationId); + const cert = await createCertificate( + input, + ctx.session.activeOrganizationId, + ); + await audit(ctx, { + action: "create", + resourceType: "certificate", + resourceId: cert.certificateId, + resourceName: cert.name, + }); + return cert; }), - one: adminProcedure + one: withPermission("certificate", "read") .input(apiFindCertificate) .query(async ({ input, ctx }) => { const certificates = await findCertificateById(input.certificateId); @@ -39,7 +50,7 @@ export const certificateRouter = createTRPCRouter({ } return certificates; }), - remove: adminProcedure + remove: withPermission("certificate", "delete") .input(apiFindCertificate) .mutation(async ({ input, ctx }) => { const certificates = await findCertificateById(input.certificateId); @@ -49,10 +60,16 @@ export const certificateRouter = createTRPCRouter({ message: "You are not allowed to delete this certificate", }); } + await audit(ctx, { + action: "delete", + resourceType: "certificate", + resourceId: certificates.certificateId, + resourceName: certificates.name, + }); await removeCertificateById(input.certificateId); return true; }), - all: adminProcedure.query(async ({ ctx }) => { + all: withPermission("certificate", "read").query(async ({ ctx }) => { return await db.query.certificates.findMany({ where: eq(certificates.organizationId, ctx.session.activeOrganizationId), }); diff --git a/apps/dokploy/server/api/routers/cluster.ts b/apps/dokploy/server/api/routers/cluster.ts index 6c118d8028..afd8a0e928 100644 --- a/apps/dokploy/server/api/routers/cluster.ts +++ b/apps/dokploy/server/api/routers/cluster.ts @@ -7,10 +7,12 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { getLocalServerIp } from "@/server/wss/terminal"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createTRPCRouter, withPermission } from "../trpc"; + export const clusterRouter = createTRPCRouter({ - getNodes: protectedProcedure + getNodes: withPermission("server", "read") .input( z.object({ serverId: z.string().optional(), @@ -19,17 +21,17 @@ export const clusterRouter = createTRPCRouter({ .query(async ({ input }) => { const docker = await getRemoteDocker(input.serverId); const workers: DockerNode[] = await docker.listNodes(); - return workers; }), - removeWorker: protectedProcedure + + removeWorker: withPermission("server", "delete") .input( z.object({ nodeId: z.string(), serverId: z.string().optional(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { const drainCommand = `docker node update --availability drain ${input.nodeId}`; const removeCommand = `docker node rm ${input.nodeId} --force`; @@ -41,6 +43,12 @@ export const clusterRouter = createTRPCRouter({ await execAsync(drainCommand); await execAsync(removeCommand); } + await audit(ctx, { + action: "delete", + resourceType: "cluster", + resourceId: input.nodeId, + resourceName: input.nodeId, + }); return true; } catch (error) { throw new TRPCError({ @@ -50,7 +58,8 @@ export const clusterRouter = createTRPCRouter({ }); } }), - addWorker: protectedProcedure + + addWorker: withPermission("server", "create") .input( z.object({ serverId: z.string().optional(), @@ -68,13 +77,12 @@ export const clusterRouter = createTRPCRouter({ } return { - command: `docker swarm join --token ${ - result.JoinTokens.Worker - } ${ip}:2377`, + command: `docker swarm join --token ${result.JoinTokens.Worker} ${ip}:2377`, version: docker_version.Version, }; }), - addManager: protectedProcedure + + addManager: withPermission("server", "create") .input( z.object({ serverId: z.string().optional(), @@ -91,9 +99,7 @@ export const clusterRouter = createTRPCRouter({ ip = server?.ipAddress; } return { - command: `docker swarm join --token ${ - result.JoinTokens.Manager - } ${ip}:2377`, + command: `docker swarm join --token ${result.JoinTokens.Manager} ${ip}:2377`, version: docker_version.Version, }; }), diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index e3c803cd4f..0d3782eab1 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -1,7 +1,5 @@ import { addDomainToCompose, - addNewService, - checkServiceAccess, clearOldDeployments, cloneCompose, createCommand, @@ -16,7 +14,6 @@ import { findDomainsByComposeId, findEnvironmentById, findGitProviderById, - findMemberById, findProjectById, findServerById, getComposeContainer, @@ -34,6 +31,12 @@ import { updateCompose, updateDeploymentStatus, } from "@dokploy/server"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { type CompleteTemplate, @@ -72,6 +75,7 @@ import { } from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; import { generatePassword } from "@/templates/utils"; +import { audit } from "../utils/audit"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const composeRouter = createTRPCRouter({ @@ -79,18 +83,10 @@ export const composeRouter = createTRPCRouter({ .input(apiCreateCompose) .mutation(async ({ ctx, input }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -108,14 +104,14 @@ export const composeRouter = createTRPCRouter({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newService.composeId, - project.organizationId, - ); - } + await addNewService(ctx, newService.composeId); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newService.composeId, + resourceName: newService.appName, + }); return newService; } catch (error) { throw error; @@ -125,14 +121,7 @@ export const composeRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindCompose) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.composeId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.composeId, "read"); const compose = await findComposeById(input.composeId); if ( @@ -188,29 +177,22 @@ export const composeRouter = createTRPCRouter({ update: protectedProcedure .input(apiUpdateCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this compose", - }); - } - return updateCompose(input.composeId, input); + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); + const updated = await updateCompose(input.composeId, input); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: updated?.name, + }); + return updated; }), delete: protectedProcedure .input(apiDeleteCompose) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.composeId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.composeId, "delete"); const composeResult = await findComposeById(input.composeId); if ( @@ -249,70 +231,55 @@ export const composeRouter = createTRPCRouter({ } catch (_) {} } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: composeResult.composeId, + resourceName: composeResult.appName, + }); return composeResult; }), cleanQueues: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to clean this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); await cleanQueuesByCompose(input.composeId); return { success: true, message: "Queues cleaned successfully" }; }), clearDeployments: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: - "You are not authorized to clear deployments for this compose", - }); - } await clearOldDeployments(compose.appName, compose.serverId); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return true; }), killBuild: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["cancel"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to kill this build", - }); - } await killDockerBuild("compose", compose.serverId); }), loadServices: protectedProcedure .input(apiFetchServices) .query(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to load this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); return await loadServices(input.composeId, input.type); }), loadMountsByService: protectedProcedure @@ -323,16 +290,10 @@ export const composeRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to load this compose", - }); - } const container = await getComposeContainer(compose, input.serviceName); const mounts = container?.Mounts.filter( (mount) => mount.Type === "volume" && mount.Source !== "", @@ -343,18 +304,11 @@ export const composeRouter = createTRPCRouter({ .input(apiFindCompose) .mutation(async ({ input, ctx }) => { try { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to fetch this compose", - }); - } - const command = await cloneCompose(compose); if (compose.serverId) { await execAsyncRemote(compose.serverId, command); @@ -374,49 +328,45 @@ export const composeRouter = createTRPCRouter({ randomizeCompose: protectedProcedure .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); + const result = await randomizeComposeFile(input.composeId, input.suffix); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to randomize this compose", - }); - } - return await randomizeComposeFile(input.composeId, input.suffix); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); + return result; }), isolatedDeployment: protectedProcedure .input(apiRandomizeCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to randomize this compose", - }); - } - return await randomizeIsolatedDeploymentComposeFile( + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); + const result = await randomizeIsolatedDeploymentComposeFile( input.composeId, input.suffix, ); + const compose = await findComposeById(input.composeId); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); + return result; }), getConvertedCompose: protectedProcedure .input(apiFindCompose) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to get this compose", - }); - } const domains = await findDomainsByComposeId(input.composeId); const composeFile = await addDomainToCompose(compose, domains); return stringify(composeFile, { @@ -427,17 +377,11 @@ export const composeRouter = createTRPCRouter({ deploy: protectedProcedure .input(apiDeployCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this compose", - }); - } const jobData: DeploymentJob = { composeId: input.composeId, titleLog: input.title || "Manual deployment", @@ -452,6 +396,12 @@ export const composeRouter = createTRPCRouter({ deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); + await audit(ctx, { + action: "deploy", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return true; } await myQueue.add( @@ -462,6 +412,12 @@ export const composeRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "deploy", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return { success: true, message: "Deployment queued", @@ -471,16 +427,10 @@ export const composeRouter = createTRPCRouter({ redeploy: protectedProcedure .input(apiRedeployCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to redeploy this compose", - }); - } const jobData: DeploymentJob = { composeId: input.composeId, titleLog: input.title || "Rebuild deployment", @@ -494,6 +444,12 @@ export const composeRouter = createTRPCRouter({ deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); + await audit(ctx, { + action: "deploy", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return true; } await myQueue.add( @@ -504,6 +460,12 @@ export const composeRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "deploy", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return { success: true, message: "Redeployment queued", @@ -513,70 +475,61 @@ export const composeRouter = createTRPCRouter({ stop: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); await stopCompose(input.composeId); - + const composeForStop = await findComposeById(input.composeId); + await audit(ctx, { + action: "stop", + resourceType: "compose", + resourceId: input.composeId, + resourceName: composeForStop.name, + }); return true; }), start: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["create"], + }); await startCompose(input.composeId); - + const composeForStart = await findComposeById(input.composeId); + await audit(ctx, { + action: "start", + resourceType: "compose", + resourceId: input.composeId, + resourceName: composeForStart.name, + }); return true; }), getDefaultCommand: protectedProcedure .input(apiFindCompose) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); - - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to get this compose", - }); - } const command = createCommand(compose); return `docker ${command}`; }), refreshToken: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to refresh this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); await updateCompose(input.composeId, { refreshToken: nanoid(), }); + const composeForToken = await findComposeById(input.composeId); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: composeForToken.name, + }); return true; }), deployTemplate: protectedProcedure @@ -591,14 +544,7 @@ export const composeRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const environment = await findEnvironmentById(input.environmentId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - environment.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, environment.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -648,13 +594,7 @@ export const composeRouter = createTRPCRouter({ isolatedDeployment: true, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - compose.composeId, - ctx.session.activeOrganizationId, - ); - } + await addNewService(ctx, compose.composeId); if (generate.mounts && generate.mounts?.length > 0) { for (const mount of generate.mounts) { @@ -681,6 +621,12 @@ export const composeRouter = createTRPCRouter({ } } + await audit(ctx, { + action: "create", + resourceType: "compose", + resourceId: compose.composeId, + resourceName: compose.name, + }); return compose; }), @@ -714,20 +660,11 @@ export const composeRouter = createTRPCRouter({ disconnectGitProvider: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to disconnect this git provider", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); - // Reset all git provider related fields await updateCompose(input.composeId, { - // GitHub fields repository: null, branch: null, owner: null, @@ -735,7 +672,6 @@ export const composeRouter = createTRPCRouter({ githubId: null, triggerType: "push", - // GitLab fields gitlabRepository: null, gitlabOwner: null, gitlabBranch: null, @@ -743,30 +679,33 @@ export const composeRouter = createTRPCRouter({ gitlabProjectId: null, gitlabPathNamespace: null, - // Bitbucket fields bitbucketRepository: null, bitbucketOwner: null, bitbucketBranch: null, bitbucketId: null, - // Gitea fields giteaRepository: null, giteaOwner: null, giteaBranch: null, giteaId: null, - // Custom Git fields customGitBranch: null, customGitUrl: null, customGitSSHKeyId: null, - // Common fields sourceType: "github", // Reset to default composeStatus: "idle", watchPaths: null, enableSubmodules: false, }); + const composeForDisconnect = await findComposeById(input.composeId); + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: composeForDisconnect.name, + }); return true; }), @@ -778,29 +717,9 @@ export const composeRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this compose", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const updatedCompose = await db .update(composeTable) @@ -818,6 +737,12 @@ export const composeRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: updatedCompose.name, + }); return updatedCompose; }), @@ -830,18 +755,11 @@ export const composeRouter = createTRPCRouter({ ) .mutation(async ({ input, ctx }) => { try { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this compose", - }); - } - const decodedData = Buffer.from(input.base64, "base64").toString( "utf-8", ); @@ -901,21 +819,14 @@ export const composeRouter = createTRPCRouter({ ) .mutation(async ({ input, ctx }) => { try { + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["create"], + }); const compose = await findComposeById(input.composeId); const decodedData = Buffer.from(input.base64, "base64").toString( "utf-8", ); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this compose", - }); - } - for (const mount of compose.mounts) { await deleteMount(mount.mountId); } @@ -993,6 +904,12 @@ export const composeRouter = createTRPCRouter({ } } + await audit(ctx, { + action: "update", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.appName, + }); return { success: true, message: "Template imported successfully", @@ -1008,16 +925,10 @@ export const composeRouter = createTRPCRouter({ cancelDeployment: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["cancel"], + }); const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to cancel this deployment", - }); - } if (IS_CLOUD && compose.serverId) { try { @@ -1037,6 +948,12 @@ export const composeRouter = createTRPCRouter({ applicationType: "compose", }); + await audit(ctx, { + action: "stop", + resourceType: "compose", + resourceId: input.composeId, + resourceName: compose.name, + }); return { success: true, message: "Deployment cancellation requested", @@ -1113,19 +1030,17 @@ export const composeRouter = createTRPCRouter({ ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${composeTable.composeId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${composeTable.composeId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); const where = and(...baseConditions); diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index 7b2b414c00..03cd3c9357 100644 --- a/apps/dokploy/server/api/routers/deployment.ts +++ b/apps/dokploy/server/api/routers/deployment.ts @@ -5,20 +5,21 @@ import { findAllDeploymentsByComposeId, findAllDeploymentsByServerId, findAllDeploymentsCentralized, - findApplicationById, - findComposeById, findDeploymentById, - findMemberById, - findServerById, IS_CLOUD, removeDeployment, resolveServicePath, updateDeploymentStatus, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { apiFindAllByApplication, apiFindAllByCompose, @@ -29,65 +30,46 @@ import { } from "@/server/db/schema"; import { myQueue } from "@/server/queues/queueSetup"; import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; export const deploymentRouter = createTRPCRouter({ all: protectedProcedure .input(apiFindAllByApplication) .query(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["read"], + }); return await findAllDeploymentsByApplicationId(input.applicationId); }), allByCompose: protectedProcedure .input(apiFindAllByCompose) .query(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + deployment: ["read"], + }); return await findAllDeploymentsByComposeId(input.composeId); }), - allByServer: protectedProcedure + allByServer: withPermission("deployment", "read") .input(apiFindAllByServer) - .query(async ({ input, ctx }) => { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this server", - }); - } + .query(async ({ input }) => { return await findAllDeploymentsByServerId(input.serverId); }), - allCentralized: protectedProcedure.query(async ({ ctx }) => { - const orgId = ctx.session.activeOrganizationId; - const accessedServices = - ctx.user.role === "member" - ? (await findMemberById(ctx.user.id, orgId)).accessedServices - : null; - if (accessedServices !== null && accessedServices.length === 0) { - return []; - } - return findAllDeploymentsCentralized(orgId, accessedServices); - }), + allCentralized: withPermission("deployment", "read").query( + async ({ ctx }) => { + const orgId = ctx.session.activeOrganizationId; + const accessedServices = + ctx.user.role !== "owner" && ctx.user.role !== "admin" + ? (await findMemberByUserId(ctx.user.id, orgId)).accessedServices + : null; + if (accessedServices !== null && accessedServices.length === 0) { + return []; + } + return findAllDeploymentsCentralized(orgId, accessedServices); + }, + ), - queueList: protectedProcedure.query(async ({ ctx }) => { + queueList: withPermission("deployment", "read").query(async ({ ctx }) => { const orgId = ctx.session.activeOrganizationId; let rows: QueueJobRow[]; @@ -135,7 +117,10 @@ export const deploymentRouter = createTRPCRouter({ allByType: protectedProcedure .input(apiFindAllByType) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.id, { + deployment: ["read"], + }); const deploymentsList = await db.query.deployments.findMany({ where: eq(deployments[`${input.type}Id`], input.id), orderBy: desc(deployments.createdAt), @@ -151,8 +136,14 @@ export const deploymentRouter = createTRPCRouter({ deploymentId: z.string().min(1), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const deployment = await findDeploymentById(input.deploymentId); + const serviceId = deployment.applicationId || deployment.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + deployment: ["cancel"], + }); + } if (!deployment.pid) { throw new TRPCError({ @@ -169,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({ } await updateDeploymentStatus(deployment.deploymentId, "error"); + await audit(ctx, { + action: "cancel", + resourceType: "deployment", + resourceId: deployment.deploymentId, + }); }), removeDeployment: protectedProcedure @@ -177,7 +173,20 @@ export const deploymentRouter = createTRPCRouter({ deploymentId: z.string().min(1), }), ) - .mutation(async ({ input }) => { - return await removeDeployment(input.deploymentId); + .mutation(async ({ input, ctx }) => { + const deployment = await findDeploymentById(input.deploymentId); + const serviceId = deployment.applicationId || deployment.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + deployment: ["cancel"], + }); + } + const result = await removeDeployment(input.deploymentId); + await audit(ctx, { + action: "delete", + resourceType: "deployment", + resourceId: deployment.deploymentId, + }); + return result; }), }); diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index c1298a5614..4973fbb7ba 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -10,11 +10,8 @@ import { import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; -import { - adminProcedure, - createTRPCRouter, - protectedProcedure, -} from "@/server/api/trpc"; +import { createTRPCRouter, withPermission } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateDestination, apiFindOneDestination, @@ -24,14 +21,21 @@ import { } from "@/server/db/schema"; export const destinationRouter = createTRPCRouter({ - create: adminProcedure + create: withPermission("destination", "create") .input(apiCreateDestination) .mutation(async ({ input, ctx }) => { try { - return await createDestintation( + const result = await createDestintation( input, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "create", + resourceType: "destination", + resourceId: result.destinationId, + resourceName: input.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -40,28 +44,90 @@ export const destinationRouter = createTRPCRouter({ }); } }), - testConnection: adminProcedure + testConnection: withPermission("destination", "create") .input(apiCreateDestination) .mutation(async ({ input }) => { - const { secretAccessKey, bucket, region, endpoint, accessKey, provider } = - input; try { - const rcloneFlags = [ - `--s3-access-key-id="${accessKey}"`, - `--s3-secret-access-key="${secretAccessKey}"`, - `--s3-region="${region}"`, - `--s3-endpoint="${endpoint}"`, - "--s3-no-check-bucket", - "--s3-force-path-style", - "--retries 1", - "--low-level-retries 1", - "--timeout 10s", - "--contimeout 5s", - ]; - if (provider) { - rcloneFlags.unshift(`--s3-provider="${provider}"`); + const destType = input.destinationType || "s3"; + let rcloneFlags: string[] = []; + let rcloneDestination = ""; + + switch (destType) { + case "ftp": { + rcloneFlags = [ + `--ftp-host="${input.ftpHost || ""}"`, + `--ftp-user="${input.ftpUser || ""}"`, + `--ftp-pass="$(rclone obscure '${input.ftpPassword || ""}')"`, + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", + ]; + if (input.ftpPort) { + rcloneFlags.push(`--ftp-port="${input.ftpPort}"`); + } + rcloneDestination = `:ftp:${input.ftpPath || "/"}`; + break; + } + case "sftp": { + rcloneFlags = [ + `--sftp-host="${input.ftpHost || ""}"`, + `--sftp-user="${input.ftpUser || ""}"`, + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", + ]; + if (input.ftpPort) { + rcloneFlags.push(`--sftp-port="${input.ftpPort}"`); + } + if (input.sftpKeyPath) { + rcloneFlags.push(`--sftp-key-file="${input.sftpKeyPath}"`); + } else if (input.ftpPassword) { + rcloneFlags.push(`--sftp-pass="$(rclone obscure '${input.ftpPassword}')"`) + } + rcloneDestination = `:sftp:${input.ftpPath || "/"}`; + break; + } + case "google-drive": { + rcloneFlags = [ + `--drive-client-id="${input.googleDriveClientId || ""}"`, + `--drive-client-secret="${input.googleDriveClientSecret || ""}"`, + `--drive-token='{"access_token":"","token_type":"Bearer","refresh_token":"${input.googleDriveRefreshToken || ""}","expiry":"2000-01-01T00:00:00.000Z"}'`, + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", + ]; + rcloneDestination = input.googleDriveFolderId + ? `:drive:${input.googleDriveFolderId}` + : `:drive:`; + break; + } + case "s3": + default: { + const { secretAccessKey, bucket, region, endpoint, accessKey, provider } = + input; + rcloneFlags = [ + `--s3-access-key-id="${accessKey}"`, + `--s3-secret-access-key="${secretAccessKey}"`, + `--s3-region="${region}"`, + `--s3-endpoint="${endpoint}"`, + "--s3-no-check-bucket", + "--s3-force-path-style", + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", + ]; + if (provider) { + rcloneFlags.unshift(`--s3-provider="${provider}"`); + } + rcloneDestination = `:s3:${bucket}`; + break; + } } - const rcloneDestination = `:s3:${bucket}`; + const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; if (IS_CLOUD && !input.serverId) { @@ -82,12 +148,12 @@ export const destinationRouter = createTRPCRouter({ message: error instanceof Error ? error?.message - : "Error connecting to bucket", + : "Error connecting to destination", cause: error, }); } }), - one: protectedProcedure + one: withPermission("destination", "read") .input(apiFindOneDestination) .query(async ({ input, ctx }) => { const destination = await findDestinationById(input.destinationId); @@ -99,13 +165,13 @@ export const destinationRouter = createTRPCRouter({ } return destination; }), - all: protectedProcedure.query(async ({ ctx }) => { + all: withPermission("destination", "read").query(async ({ ctx }) => { return await db.query.destinations.findMany({ where: eq(destinations.organizationId, ctx.session.activeOrganizationId), orderBy: [desc(destinations.createdAt)], }); }), - remove: adminProcedure + remove: withPermission("destination", "delete") .input(apiRemoveDestination) .mutation(async ({ input, ctx }) => { try { @@ -117,15 +183,22 @@ export const destinationRouter = createTRPCRouter({ message: "You are not allowed to delete this destination", }); } - return await removeDestinationById( + const result = await removeDestinationById( input.destinationId, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "delete", + resourceType: "destination", + resourceId: input.destinationId, + resourceName: destination.name, + }); + return result; } catch (error) { throw error; } }), - update: adminProcedure + update: withPermission("destination", "create") .input(apiUpdateDestination) .mutation(async ({ input, ctx }) => { try { @@ -136,10 +209,17 @@ export const destinationRouter = createTRPCRouter({ message: "You are not allowed to update this destination", }); } - return await updateDestinationById(input.destinationId, { + const result = await updateDestinationById(input.destinationId, { ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "destination", + resourceId: input.destinationId, + resourceName: input.name, + }); + return result; } catch (error) { throw error; } diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index 372d939b5c..d4a7ddd57b 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -10,12 +10,13 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { audit } from "@/server/api/utils/audit"; +import { createTRPCRouter, withPermission } from "../trpc"; export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; export const dockerRouter = createTRPCRouter({ - getContainers: protectedProcedure + getContainers: withPermission("docker", "read") .input( z.object({ serverId: z.string().optional(), @@ -31,7 +32,7 @@ export const dockerRouter = createTRPCRouter({ return await getContainers(input.serverId); }), - restartContainer: protectedProcedure + restartContainer: withPermission("docker", "read") .input( z.object({ containerId: z @@ -40,11 +41,18 @@ export const dockerRouter = createTRPCRouter({ .regex(containerIdRegex, "Invalid container id."), }), ) - .mutation(async ({ input }) => { - return await containerRestart(input.containerId); + .mutation(async ({ input, ctx }) => { + const result = await containerRestart(input.containerId); + await audit(ctx, { + action: "start", + resourceType: "docker", + resourceId: input.containerId, + resourceName: input.containerId, + }); + return result; }), - getConfig: protectedProcedure + getConfig: withPermission("docker", "read") .input( z.object({ containerId: z @@ -64,7 +72,7 @@ export const dockerRouter = createTRPCRouter({ return await getConfig(input.containerId, input.serverId); }), - getContainersByAppNameMatch: protectedProcedure + getContainersByAppNameMatch: withPermission("service", "read") .input( z.object({ appType: z.enum(["stack", "docker-compose"]).optional(), @@ -86,7 +94,7 @@ export const dockerRouter = createTRPCRouter({ ); }), - getContainersByAppLabel: protectedProcedure + getContainersByAppLabel: withPermission("docker", "read") .input( z.object({ appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), @@ -108,7 +116,7 @@ export const dockerRouter = createTRPCRouter({ ); }), - getStackContainersByAppName: protectedProcedure + getStackContainersByAppName: withPermission("docker", "read") .input( z.object({ appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), @@ -125,7 +133,7 @@ export const dockerRouter = createTRPCRouter({ return await getStackContainersByAppName(input.appName, input.serverId); }), - getServiceContainersByAppName: protectedProcedure + getServiceContainersByAppName: withPermission("docker", "read") .input( z.object({ appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."), diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 861c3b9b6f..8210fcf8a5 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -1,7 +1,6 @@ import { createDomain, findApplicationById, - findComposeById, findDomainById, findDomainsByApplicationId, findDomainsByComposeId, @@ -15,9 +14,15 @@ import { updateDomainById, validateDomain, } from "@dokploy/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateDomain, apiFindCompose, @@ -32,29 +37,22 @@ export const domainRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { if (input.domainType === "compose" && input.composeId) { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + domain: ["create"], + }); } else if (input.domainType === "application" && input.applicationId) { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + domain: ["create"], + }); } - return await createDomain(input); + const domain = await createDomain(input); + await audit(ctx, { + action: "create", + resourceType: "domain", + resourceId: domain.domainId, + resourceName: domain.host, + }); + return domain; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -69,34 +67,20 @@ export const domainRouter = createTRPCRouter({ byApplicationId: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + domain: ["read"], + }); return await findDomainsByApplicationId(input.applicationId); }), byComposeId: protectedProcedure .input(apiFindCompose) .query(async ({ input, ctx }) => { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + await checkServicePermissionAndAccess(ctx, input.composeId, { + domain: ["read"], + }); return await findDomainsByComposeId(input.composeId); }), - generateDomain: protectedProcedure + generateDomain: withPermission("domain", "create") .input(z.object({ appName: z.string(), serverId: z.string().optional() })) .mutation(async ({ input, ctx }) => { return generateTraefikMeDomain( @@ -105,7 +89,7 @@ export const domainRouter = createTRPCRouter({ input.serverId, ); }), - canGenerateTraefikMeDomains: protectedProcedure + canGenerateTraefikMeDomains: withPermission("domain", "read") .input(z.object({ serverId: z.string() })) .query(async ({ input }) => { if (input.serverId) { @@ -120,45 +104,28 @@ export const domainRouter = createTRPCRouter({ .input(apiUpdateDomain) .mutation(async ({ input, ctx }) => { const currentDomain = await findDomainById(input.domainId); - - if (currentDomain.applicationId) { - const newApp = await findApplicationById(currentDomain.applicationId); - if ( - newApp.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (currentDomain.composeId) { - const newCompose = await findComposeById(currentDomain.composeId); - if ( - newCompose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + const serviceId = currentDomain.applicationId || currentDomain.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + domain: ["create"], + }); } else if (currentDomain.previewDeploymentId) { - const newPreviewDeployment = await findPreviewDeploymentById( + const preview = await findPreviewDeploymentById( currentDomain.previewDeploymentId, ); - if ( - newPreviewDeployment.application.environment.project - .organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this preview deployment", - }); - } + await checkServicePermissionAndAccess(ctx, preview.applicationId, { + domain: ["create"], + }); } + const result = await updateDomainById(input.domainId, input); const domain = await findDomainById(input.domainId); + await audit(ctx, { + action: "update", + resourceType: "domain", + resourceId: domain.domainId, + resourceName: domain.host, + }); if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); await manageDomain(application, domain); @@ -176,59 +143,46 @@ export const domainRouter = createTRPCRouter({ }), one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => { const domain = await findDomainById(input.domainId); - if (domain.applicationId) { - const application = await findApplicationById(domain.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (domain.composeId) { - const compose = await findComposeById(domain.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + const serviceId = domain.applicationId || domain.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + domain: ["read"], + }); + } else if (domain.previewDeploymentId) { + const preview = await findPreviewDeploymentById( + domain.previewDeploymentId, + ); + await checkServicePermissionAndAccess(ctx, preview.applicationId, { + domain: ["read"], + }); } - return await findDomainById(input.domainId); + return domain; }), delete: protectedProcedure .input(apiFindDomain) .mutation(async ({ input, ctx }) => { const domain = await findDomainById(input.domainId); - if (domain.applicationId) { - const application = await findApplicationById(domain.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (domain.composeId) { - const compose = await findComposeById(domain.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + const serviceId = domain.applicationId || domain.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + domain: ["delete"], + }); + } else if (domain.previewDeploymentId) { + const preview = await findPreviewDeploymentById( + domain.previewDeploymentId, + ); + await checkServicePermissionAndAccess(ctx, preview.applicationId, { + domain: ["delete"], + }); } + const result = await removeDomainById(input.domainId); + await audit(ctx, { + action: "delete", + resourceType: "domain", + resourceId: domain.domainId, + resourceName: domain.host, + }); if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); @@ -238,7 +192,7 @@ export const domainRouter = createTRPCRouter({ return result; }), - validateDomain: protectedProcedure + validateDomain: withPermission("domain", "read") .input( z.object({ domain: z.string(), diff --git a/apps/dokploy/server/api/routers/environment.ts b/apps/dokploy/server/api/routers/environment.ts index 16376e9e01..c773d8ef9f 100644 --- a/apps/dokploy/server/api/routers/environment.ts +++ b/apps/dokploy/server/api/routers/environment.ts @@ -1,31 +1,35 @@ import { - addNewEnvironment, - checkEnvironmentAccess, - checkEnvironmentCreationPermission, - checkEnvironmentDeletionPermission, createEnvironment, deleteEnvironment, duplicateEnvironment, findEnvironmentById, findEnvironmentsByProjectId, - findMemberById, updateEnvironmentById, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + addNewEnvironment, + checkEnvironmentAccess, + checkEnvironmentCreationPermission, + checkEnvironmentDeletionPermission, + checkPermission, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateEnvironment, apiDuplicateEnvironment, apiFindOneEnvironment, apiRemoveEnvironment, apiUpdateEnvironment, + environments, + projects, } from "@/server/db/schema"; -import { environments, projects } from "@/server/db/schema"; -// Helper function to filter services within an environment based on user permissions const filterEnvironmentServices = ( environment: any, accessedServices: string[], @@ -59,12 +63,7 @@ export const environmentRouter = createTRPCRouter({ .input(apiCreateEnvironment) .mutation(async ({ input, ctx }) => { try { - // Check if user has permission to create environments - await checkEnvironmentCreationPermission( - ctx.user.id, - input.projectId, - ctx.session.activeOrganizationId, - ); + await checkEnvironmentCreationPermission(ctx, input.projectId); if (input.name === "production") { throw new TRPCError({ @@ -74,16 +73,15 @@ export const environmentRouter = createTRPCRouter({ }); } - // Allow users to create environments with any name, including "production" const environment = await createEnvironment(input); - if (ctx.user.role === "member") { - await addNewEnvironment( - ctx.user.id, - environment.environmentId, - ctx.session.activeOrganizationId, - ); - } + await addNewEnvironment(ctx, environment.environmentId); + await audit(ctx, { + action: "create", + resourceType: "environment", + resourceId: environment.environmentId, + resourceName: environment.name, + }); return environment; } catch (error) { if (error instanceof TRPCError) { @@ -100,54 +98,39 @@ export const environmentRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneEnvironment) .query(async ({ input, ctx }) => { - try { - if (ctx.user.role === "member") { - await checkEnvironmentAccess( + const environment = await findEnvironmentById(input.environmentId); + if ( + environment.project.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You are not allowed to access this environment", + }); + } + + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedEnvironments, accessedServices } = + await findMemberByUserId( ctx.user.id, - input.environmentId, ctx.session.activeOrganizationId, - "access", ); - } - const environment = await findEnvironmentById(input.environmentId); - if ( - environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { + + if (!accessedEnvironments.includes(environment.environmentId)) { throw new TRPCError({ code: "FORBIDDEN", message: "You are not allowed to access this environment", }); } - // Check environment access and filter services for members - if (ctx.user.role === "member") { - const { accessedEnvironments, accessedServices } = - await findMemberById(ctx.user.id, ctx.session.activeOrganizationId); - - if (!accessedEnvironments.includes(environment.environmentId)) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You are not allowed to access this environment", - }); - } - - // Filter services based on member permissions - const filteredEnvironment = filterEnvironmentServices( - environment, - accessedServices, - ); - - return filteredEnvironment; - } + const filteredEnvironment = filterEnvironmentServices( + environment, + accessedServices, + ); - return environment; - } catch (error) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Environment not found", - }); + return filteredEnvironment; } + + return environment; }), byProjectId: protectedProcedure @@ -156,7 +139,6 @@ export const environmentRouter = createTRPCRouter({ try { const environments = await findEnvironmentsByProjectId(input.projectId); - // Check organization access if ( environments.some( (environment) => @@ -170,12 +152,13 @@ export const environmentRouter = createTRPCRouter({ }); } - // Filter environments for members based on their permissions - if (ctx.user.role === "member") { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { const { accessedEnvironments, accessedServices } = - await findMemberById(ctx.user.id, ctx.session.activeOrganizationId); + await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); - // Filter environments to only show those the member has access to const filteredEnvironments = environments .filter((environment) => accessedEnvironments.includes(environment.environmentId), @@ -211,7 +194,6 @@ export const environmentRouter = createTRPCRouter({ }); } - // Prevent deletion of the default environment if (environment.isDefault) { throw new TRPCError({ code: "BAD_REQUEST", @@ -219,24 +201,17 @@ export const environmentRouter = createTRPCRouter({ }); } - // Check environment deletion permission - await checkEnvironmentDeletionPermission( - ctx.user.id, - environment.projectId, - ctx.session.activeOrganizationId, - ); + await checkEnvironmentDeletionPermission(ctx, environment.projectId); - // Additional check for environment access for members - if (ctx.user.role === "member") { - await checkEnvironmentAccess( - ctx.user.id, - input.environmentId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkEnvironmentAccess(ctx, input.environmentId, "read"); const deletedEnvironment = await deleteEnvironment(input.environmentId); + await audit(ctx, { + action: "delete", + resourceType: "environment", + resourceId: deletedEnvironment?.environmentId, + resourceName: deletedEnvironment?.name, + }); return deletedEnvironment; } catch (error) { if (error instanceof TRPCError) { @@ -256,18 +231,14 @@ export const environmentRouter = createTRPCRouter({ try { const { environmentId, ...updateData } = input; - // Allow users to rename environments to any name, including "production" - if (ctx.user.role === "member") { - await checkEnvironmentAccess( - ctx.user.id, - environmentId, - ctx.session.activeOrganizationId, - "access", - ); + await checkEnvironmentAccess(ctx, environmentId, "read"); + + if (updateData.env !== undefined) { + await checkPermission(ctx, { environmentEnvVars: ["write"] }); } + const currentEnvironment = await findEnvironmentById(environmentId); - // Prevent renaming the default environment, but allow updating env and description if (currentEnvironment.isDefault && updateData.name !== undefined) { throw new TRPCError({ code: "BAD_REQUEST", @@ -284,9 +255,8 @@ export const environmentRouter = createTRPCRouter({ }); } - // Check environment access for members - if (ctx.user.role === "member") { - const { accessedEnvironments } = await findMemberById( + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedEnvironments } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); @@ -305,6 +275,14 @@ export const environmentRouter = createTRPCRouter({ environmentId, updateData, ); + if (environment) { + await audit(ctx, { + action: "update", + resourceType: "environment", + resourceId: environment.environmentId, + resourceName: environment.name, + }); + } return environment; } catch (error) { throw new TRPCError({ @@ -318,14 +296,7 @@ export const environmentRouter = createTRPCRouter({ .input(apiDuplicateEnvironment) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.role === "member") { - await checkEnvironmentAccess( - ctx.user.id, - input.environmentId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkEnvironmentAccess(ctx, input.environmentId, "read"); const environment = await findEnvironmentById(input.environmentId); if ( environment.project.organizationId !== @@ -337,9 +308,8 @@ export const environmentRouter = createTRPCRouter({ }); } - // Check environment access for members - if (ctx.user.role === "member") { - const { accessedEnvironments } = await findMemberById( + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedEnvironments } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); @@ -353,6 +323,13 @@ export const environmentRouter = createTRPCRouter({ } const duplicatedEnvironment = await duplicateEnvironment(input); + await audit(ctx, { + action: "create", + resourceType: "environment", + resourceId: duplicatedEnvironment.environmentId, + resourceName: duplicatedEnvironment.name, + metadata: { duplicatedFrom: input.environmentId }, + }); return duplicatedEnvironment; } catch (error) { throw new TRPCError({ @@ -404,8 +381,8 @@ export const environmentRouter = createTRPCRouter({ ); } - if (ctx.user.role === "member") { - const { accessedEnvironments } = await findMemberById( + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedEnvironments } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); diff --git a/apps/dokploy/server/api/routers/git-provider.ts b/apps/dokploy/server/api/routers/git-provider.ts index 51d216043d..b0aa8627dd 100644 --- a/apps/dokploy/server/api/routers/git-provider.ts +++ b/apps/dokploy/server/api/routers/git-provider.ts @@ -2,7 +2,12 @@ import { findGitProviderById, removeGitProvider } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq } from "drizzle-orm"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema"; export const gitProviderRouter = createTRPCRouter({ @@ -21,7 +26,7 @@ export const gitProviderRouter = createTRPCRouter({ ), }); }), - remove: protectedProcedure + remove: withPermission("gitProviders", "delete") .input(apiRemoveGitProvider) .mutation(async ({ input, ctx }) => { try { @@ -33,6 +38,12 @@ export const gitProviderRouter = createTRPCRouter({ message: "You are not allowed to delete this Git provider", }); } + await audit(ctx, { + action: "delete", + resourceType: "gitProvider", + resourceId: gitProvider.gitProviderId, + resourceName: gitProvider.name ?? gitProvider.gitProviderId, + }); return await removeGitProvider(input.gitProviderId); } catch (error) { const message = diff --git a/apps/dokploy/server/api/routers/gitea.ts b/apps/dokploy/server/api/routers/gitea.ts index a32a8c7d9a..1ab6cd6479 100644 --- a/apps/dokploy/server/api/routers/gitea.ts +++ b/apps/dokploy/server/api/routers/gitea.ts @@ -10,7 +10,12 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateGitea, apiFindGiteaBranches, @@ -20,15 +25,24 @@ import { } from "@/server/db/schema"; export const giteaRouter = createTRPCRouter({ - create: protectedProcedure + create: withPermission("gitProviders", "create") .input(apiCreateGitea) .mutation(async ({ input, ctx }) => { try { - return await createGitea( + const result = await createGitea( input, ctx.session.activeOrganizationId, ctx.session.userId, ); + + await audit(ctx, { + action: "create", + resourceType: "gitProvider", + resourceId: result.giteaId, + resourceName: input.name, + }); + + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -38,24 +52,11 @@ export const giteaRouter = createTRPCRouter({ } }), - one: protectedProcedure - .input(apiFindOneGitea) - .query(async ({ input, ctx }) => { - const giteaProvider = await findGiteaById(input.giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } - return giteaProvider; - }), + one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => { + return await findGiteaById(input.giteaId); + }), - giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => { + giteaProviders: protectedProcedure.query(async ({ ctx }) => { let result = await db.query.gitea.findMany({ with: { gitProvider: true, @@ -85,7 +86,7 @@ export const giteaRouter = createTRPCRouter({ getGiteaRepositories: protectedProcedure .input(apiFindOneGitea) - .query(async ({ input, ctx }) => { + .query(async ({ input }) => { const { giteaId } = input; if (!giteaId) { @@ -95,18 +96,6 @@ export const giteaRouter = createTRPCRouter({ }); } - const giteaProvider = await findGiteaById(giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } - try { const repositories = await getGiteaRepositories(giteaId); return repositories; @@ -121,7 +110,7 @@ export const giteaRouter = createTRPCRouter({ getGiteaBranches: protectedProcedure .input(apiFindGiteaBranches) - .query(async ({ input, ctx }) => { + .query(async ({ input }) => { const { giteaId, owner, repositoryName } = input; if (!giteaId || !owner || !repositoryName) { @@ -132,18 +121,6 @@ export const giteaRouter = createTRPCRouter({ }); } - const giteaProvider = await findGiteaById(giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } - try { return await getGiteaBranches({ giteaId, @@ -161,22 +138,10 @@ export const giteaRouter = createTRPCRouter({ testConnection: protectedProcedure .input(apiGiteaTestConnection) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { const giteaId = input.giteaId ?? ""; try { - const giteaProvider = await findGiteaById(giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } - const result = await testGiteaConnection({ giteaId, }); @@ -191,21 +156,9 @@ export const giteaRouter = createTRPCRouter({ } }), - update: protectedProcedure + update: withPermission("gitProviders", "create") .input(apiUpdateGitea) .mutation(async ({ input, ctx }) => { - const giteaProvider = await findGiteaById(input.giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } - if (input.name) { await updateGitProvider(input.gitProviderId, { name: input.name, @@ -221,12 +174,19 @@ export const giteaRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "gitProvider", + resourceId: input.giteaId, + resourceName: input.name, + }); + return { success: true }; }), getGiteaUrl: protectedProcedure .input(apiFindOneGitea) - .query(async ({ input, ctx }) => { + .query(async ({ input }) => { const { giteaId } = input; if (!giteaId) { @@ -237,16 +197,6 @@ export const giteaRouter = createTRPCRouter({ } const giteaProvider = await findGiteaById(giteaId); - if ( - giteaProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - giteaProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitea provider", - }); - } // Return the base URL of the Gitea instance return giteaProvider.giteaUrl; diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index 29d5d25139..e10d057716 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -8,7 +8,12 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiFindGithubBranches, apiFindOneGithub, @@ -16,53 +21,17 @@ import { } from "@/server/db/schema"; export const githubRouter = createTRPCRouter({ - one: protectedProcedure - .input(apiFindOneGithub) - .query(async ({ input, ctx }) => { - const githubProvider = await findGithubById(input.githubId); - if ( - githubProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - githubProvider.gitProvider.userId === ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this github provider", - }); - } - return githubProvider; - }), + one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => { + return await findGithubById(input.githubId); + }), getGithubRepositories: protectedProcedure .input(apiFindOneGithub) - .query(async ({ input, ctx }) => { - const githubProvider = await findGithubById(input.githubId); - if ( - githubProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - githubProvider.gitProvider.userId === ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this github provider", - }); - } + .query(async ({ input }) => { return await getGithubRepositories(input.githubId); }), getGithubBranches: protectedProcedure .input(apiFindGithubBranches) - .query(async ({ input, ctx }) => { - const githubProvider = await findGithubById(input.githubId || ""); - if ( - githubProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - githubProvider.gitProvider.userId === ctx.session.userId - ) { - //TODO: Remove this line when the cloud version is ready - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this github provider", - }); - } + .query(async ({ input }) => { return await getGithubBranches(input); }), githubProviders: protectedProcedure.query(async ({ ctx }) => { @@ -95,19 +64,8 @@ export const githubRouter = createTRPCRouter({ testConnection: protectedProcedure .input(apiFindOneGithub) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { - const githubProvider = await findGithubById(input.githubId); - if ( - githubProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - githubProvider.gitProvider.userId === ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this github provider", - }); - } const result = await getGithubRepositories(input.githubId); return `Found ${result.length} repositories`; } catch (err) { @@ -117,20 +75,9 @@ export const githubRouter = createTRPCRouter({ }); } }), - update: protectedProcedure + update: withPermission("gitProviders", "create") .input(apiUpdateGithub) .mutation(async ({ input, ctx }) => { - const githubProvider = await findGithubById(input.githubId); - if ( - githubProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - githubProvider.gitProvider.userId === ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this github provider", - }); - } await updateGitProvider(input.gitProviderId, { name: input.name, organizationId: ctx.session.activeOrganizationId, @@ -139,5 +86,12 @@ export const githubRouter = createTRPCRouter({ await updateGithub(input.githubId, { ...input, }); + + await audit(ctx, { + action: "update", + resourceType: "gitProvider", + resourceId: input.gitProviderId, + resourceName: input.name, + }); }), }); diff --git a/apps/dokploy/server/api/routers/gitlab.ts b/apps/dokploy/server/api/routers/gitlab.ts index b51f056ae1..e4efbd87bb 100644 --- a/apps/dokploy/server/api/routers/gitlab.ts +++ b/apps/dokploy/server/api/routers/gitlab.ts @@ -10,7 +10,12 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateGitlab, apiFindGitlabBranches, @@ -20,15 +25,23 @@ import { } from "@/server/db/schema"; export const gitlabRouter = createTRPCRouter({ - create: protectedProcedure + create: withPermission("gitProviders", "create") .input(apiCreateGitlab) .mutation(async ({ input, ctx }) => { try { - return await createGitlab( + const result = await createGitlab( input, ctx.session.activeOrganizationId, ctx.session.userId, ); + + await audit(ctx, { + action: "create", + resourceType: "gitProvider", + resourceName: input.name, + }); + + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -37,22 +50,9 @@ export const gitlabRouter = createTRPCRouter({ }); } }), - one: protectedProcedure - .input(apiFindOneGitlab) - .query(async ({ input, ctx }) => { - const gitlabProvider = await findGitlabById(input.gitlabId); - if ( - gitlabProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - gitlabProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitlab provider", - }); - } - return gitlabProvider; - }), + one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => { + return await findGitlabById(input.gitlabId); + }), gitlabProviders: protectedProcedure.query(async ({ ctx }) => { let result = await db.query.gitlab.findMany({ with: { @@ -83,52 +83,19 @@ export const gitlabRouter = createTRPCRouter({ }), getGitlabRepositories: protectedProcedure .input(apiFindOneGitlab) - .query(async ({ input, ctx }) => { - const gitlabProvider = await findGitlabById(input.gitlabId); - if ( - gitlabProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - gitlabProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitlab provider", - }); - } + .query(async ({ input }) => { return await getGitlabRepositories(input.gitlabId); }), getGitlabBranches: protectedProcedure .input(apiFindGitlabBranches) - .query(async ({ input, ctx }) => { - const gitlabProvider = await findGitlabById(input.gitlabId || ""); - if ( - gitlabProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - gitlabProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitlab provider", - }); - } + .query(async ({ input }) => { return await getGitlabBranches(input); }), testConnection: protectedProcedure .input(apiGitlabTestConnection) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }) => { try { - const gitlabProvider = await findGitlabById(input.gitlabId || ""); - if ( - gitlabProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - gitlabProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitlab provider", - }); - } const result = await testGitlabConnection(input); return `Found ${result} repositories`; @@ -139,20 +106,9 @@ export const gitlabRouter = createTRPCRouter({ }); } }), - update: protectedProcedure + update: withPermission("gitProviders", "create") .input(apiUpdateGitlab) .mutation(async ({ input, ctx }) => { - const gitlabProvider = await findGitlabById(input.gitlabId); - if ( - gitlabProvider.gitProvider.organizationId !== - ctx.session.activeOrganizationId && - gitlabProvider.gitProvider.userId !== ctx.session.userId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not allowed to access this Gitlab provider", - }); - } if (input.name) { await updateGitProvider(input.gitProviderId, { name: input.name, @@ -167,5 +123,12 @@ export const gitlabRouter = createTRPCRouter({ ...input, }); } + + await audit(ctx, { + action: "update", + resourceType: "gitProvider", + resourceId: input.gitProviderId, + resourceName: input.name, + }); }), }); diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index 567cd4ad86..bb739a43b8 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -1,14 +1,11 @@ import { - addNewService, checkPortInUse, - checkServiceAccess, createMariadb, createMount, deployMariadb, findBackupsByDbId, findEnvironmentById, findMariadbById, - findMemberById, findProjectById, IS_CLOUD, rebuildDatabase, @@ -21,11 +18,18 @@ import { updateMariadbById, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiChangeMariaDBStatus, apiCreateMariaDB, @@ -36,27 +40,20 @@ import { apiSaveEnvironmentVariablesMariaDB, apiSaveExternalPortMariaDB, apiUpdateMariaDB, + environments, mariadb as mariadbTable, + projects, } from "@/server/db/schema"; -import { environments, projects } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; export const mariadbRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateMariaDB) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -74,13 +71,7 @@ export const mariadbRouter = createTRPCRouter({ const newMariadb = await createMariadb({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newMariadb.mariadbId, - project.organizationId, - ); - } + await addNewService(ctx, newMariadb.mariadbId); await createMount({ serviceId: newMariadb.mariadbId, @@ -90,6 +81,12 @@ export const mariadbRouter = createTRPCRouter({ type: "volume", }); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newMariadb.mariadbId, + resourceName: newMariadb.appName, + }); return newMariadb; } catch (error) { if (error instanceof TRPCError) { @@ -101,14 +98,7 @@ export const mariadbRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMariaDB) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mariadbId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.mariadbId, "read"); const mariadb = await findMariadbById(input.mariadbId); if ( mariadb.environment.project.organizationId !== @@ -125,16 +115,10 @@ export const mariadbRouter = createTRPCRouter({ start: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); const service = await findMariadbById(input.mariadbId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this Mariadb", - }); - } if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { @@ -144,11 +128,20 @@ export const mariadbRouter = createTRPCRouter({ applicationStatus: "done", }); + await audit(ctx, { + action: "start", + resourceType: "service", + resourceId: service.mariadbId, + resourceName: service.appName, + }); return service; }), stop: protectedProcedure .input(apiFindOneMariaDB) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); const mariadb = await findMariadbById(input.mariadbId); if (mariadb.serverId) { @@ -160,21 +153,21 @@ export const mariadbRouter = createTRPCRouter({ applicationStatus: "idle", }); + await audit(ctx, { + action: "stop", + resourceType: "service", + resourceId: mariadb.mariadbId, + resourceName: mariadb.appName, + }); return mariadb; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortMariaDB) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + service: ["create"], + }); const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this external port", - }); - } if (input.externalPort) { const portCheck = await checkPortInUse( @@ -193,22 +186,28 @@ export const mariadbRouter = createTRPCRouter({ externalPort: input.externalPort, }); await deployMariadb(input.mariadbId); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mariadb.mariadbId, + resourceName: mariadb.appName, + }); return mariadb; }), deploy: protectedProcedure .input(apiDeployMariaDB) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Mariadb", - }); - } + await audit(ctx, { + action: "deploy", + resourceType: "service", + resourceId: mariadb.mariadbId, + resourceName: mariadb.appName, + }); return deployMariadb(input.mariadbId); }), deployWithLogs: protectedProcedure @@ -222,16 +221,9 @@ export const mariadbRouter = createTRPCRouter({ }) .input(apiDeployMariaDB) .subscription(async ({ input, ctx }) => { - const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Mariadb", - }); - } + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); return observable((emit) => { deployMariadb(input.mariadbId, (log) => { @@ -242,32 +234,25 @@ export const mariadbRouter = createTRPCRouter({ changeStatus: protectedProcedure .input(apiChangeMariaDBStatus) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); const mongo = await findMariadbById(input.mariadbId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to change this Mariadb status", - }); - } await updateMariadbById(input.mariadbId, { applicationStatus: input.applicationStatus, }); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongo.mariadbId, + resourceName: mongo.appName, + }); return mongo; }), remove: protectedProcedure .input(apiFindOneMariaDB) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mariadbId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.mariadbId, "delete"); const mongo = await findMariadbById(input.mariadbId); if ( @@ -280,6 +265,12 @@ export const mariadbRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: mongo.mariadbId, + resourceName: mongo.appName, + }); const backups = await findBackupsByDbId(input.mariadbId, "mariadb"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), @@ -298,16 +289,9 @@ export const mariadbRouter = createTRPCRouter({ saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesMariaDB) .mutation(async ({ input, ctx }) => { - const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + envVars: ["write"], + }); const service = await updateMariadbById(input.mariadbId, { env: input.env, }); @@ -319,21 +303,20 @@ export const mariadbRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: input.mariadbId, + }); return true; }), reload: protectedProcedure .input(apiResetMariadb) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this Mariadb", - }); - } if (mariadb.serverId) { await stopServiceRemote(mariadb.serverId, mariadb.appName); } else { @@ -351,22 +334,21 @@ export const mariadbRouter = createTRPCRouter({ await updateMariadbById(input.mariadbId, { applicationStatus: "done", }); + await audit(ctx, { + action: "reload", + resourceType: "service", + resourceId: mariadb.mariadbId, + resourceName: mariadb.appName, + }); return true; }), update: protectedProcedure .input(apiUpdateMariaDB) .mutation(async ({ input, ctx }) => { const { mariadbId, ...rest } = input; - const mariadb = await findMariadbById(mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this Mariadb", - }); - } + await checkServicePermissionAndAccess(ctx, mariadbId, { + service: ["create"], + }); const service = await updateMariadbById(mariadbId, { ...rest, }); @@ -378,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mariadbId, + resourceName: service.appName, + }); return true; }), move: protectedProcedure @@ -388,31 +376,10 @@ export const mariadbRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this mariadb", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + service: ["create"], + }); - // Update the mariadb's projectId const updatedMariadb = await db .update(mariadbTable) .set({ @@ -429,23 +396,27 @@ export const mariadbRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "move", + resourceType: "service", + resourceId: updatedMariadb.mariadbId, + resourceName: updatedMariadb.appName, + }); return updatedMariadb; }), rebuild: protectedProcedure .input(apiRebuildMariadb) .mutation(async ({ input, ctx }) => { - const mariadb = await findMariadbById(input.mariadbId); - if ( - mariadb.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rebuild this MariaDB database", - }); - } + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + deployment: ["create"], + }); - await rebuildDatabase(mariadb.mariadbId, "mariadb"); + await rebuildDatabase(input.mariadbId, "mariadb"); + await audit(ctx, { + action: "rebuild", + resourceType: "service", + resourceId: input.mariadbId, + }); return true; }), search: protectedProcedure @@ -499,19 +470,18 @@ export const mariadbRouter = createTRPCRouter({ ), ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${mariadbTable.mariadbId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${mariadbTable.mariadbId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index ec0a4041c7..a64535e245 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -1,13 +1,10 @@ import { - addNewService, checkPortInUse, - checkServiceAccess, createMongo, createMount, deployMongo, findBackupsByDbId, findEnvironmentById, - findMemberById, findMongoById, findProjectById, IS_CLOUD, @@ -20,10 +17,17 @@ import { stopServiceRemote, updateMongoById, } from "@dokploy/server"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangeMongoStatus, @@ -44,18 +48,10 @@ export const mongoRouter = createTRPCRouter({ .input(apiCreateMongo) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -73,13 +69,7 @@ export const mongoRouter = createTRPCRouter({ const newMongo = await createMongo({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newMongo.mongoId, - project.organizationId, - ); - } + await addNewService(ctx, newMongo.mongoId); await createMount({ serviceId: newMongo.mongoId, @@ -89,6 +79,12 @@ export const mongoRouter = createTRPCRouter({ type: "volume", }); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newMongo.mongoId, + resourceName: newMongo.appName, + }); return newMongo; } catch (error) { if (error instanceof TRPCError) { @@ -104,14 +100,7 @@ export const mongoRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMongo) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mongoId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.mongoId, "read"); const mongo = await findMongoById(input.mongoId); if ( @@ -129,18 +118,11 @@ export const mongoRouter = createTRPCRouter({ start: protectedProcedure .input(apiFindOneMongo) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const service = await findMongoById(input.mongoId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this mongo", - }); - } - if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { @@ -150,23 +132,22 @@ export const mongoRouter = createTRPCRouter({ applicationStatus: "done", }); + await audit(ctx, { + action: "start", + resourceType: "service", + resourceId: service.mongoId, + resourceName: service.appName, + }); return service; }), stop: protectedProcedure .input(apiFindOneMongo) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this mongo", - }); - } - if (mongo.serverId) { await stopServiceRemote(mongo.serverId, mongo.appName); } else { @@ -176,21 +157,21 @@ export const mongoRouter = createTRPCRouter({ applicationStatus: "idle", }); + await audit(ctx, { + action: "stop", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); return mongo; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortMongo) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + service: ["create"], + }); const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this external port", - }); - } if (input.externalPort) { const portCheck = await checkPortInUse( @@ -209,21 +190,27 @@ export const mongoRouter = createTRPCRouter({ externalPort: input.externalPort, }); await deployMongo(input.mongoId); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); return mongo; }), deploy: protectedProcedure .input(apiDeployMongo) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this mongo", - }); - } + await audit(ctx, { + action: "deploy", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); return deployMongo(input.mongoId); }), deployWithLogs: protectedProcedure @@ -237,16 +224,9 @@ export const mongoRouter = createTRPCRouter({ }) .input(apiDeployMongo) .subscription(async function* ({ input, ctx, signal }) { - const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this mongo", - }); - } + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const queue: string[] = []; const done = false; @@ -270,34 +250,28 @@ export const mongoRouter = createTRPCRouter({ changeStatus: protectedProcedure .input(apiChangeMongoStatus) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to change this mongo status", - }); - } await updateMongoById(input.mongoId, { applicationStatus: input.applicationStatus, }); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); return mongo; }), reload: protectedProcedure .input(apiResetMongo) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this mongo", - }); - } if (mongo.serverId) { await stopServiceRemote(mongo.serverId, mongo.appName); } else { @@ -315,19 +289,18 @@ export const mongoRouter = createTRPCRouter({ await updateMongoById(input.mongoId, { applicationStatus: "done", }); + await audit(ctx, { + action: "reload", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); return true; }), remove: protectedProcedure .input(apiFindOneMongo) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mongoId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.mongoId, "delete"); const mongo = await findMongoById(input.mongoId); @@ -340,6 +313,12 @@ export const mongoRouter = createTRPCRouter({ message: "You are not authorized to delete this mongo", }); } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: mongo.mongoId, + resourceName: mongo.appName, + }); const backups = await findBackupsByDbId(input.mongoId, "mongo"); const cleanupOperations = [ @@ -359,16 +338,9 @@ export const mongoRouter = createTRPCRouter({ saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesMongo) .mutation(async ({ input, ctx }) => { - const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mongoId, { + envVars: ["write"], + }); const service = await updateMongoById(input.mongoId, { env: input.env, }); @@ -380,22 +352,20 @@ export const mongoRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: input.mongoId, + }); return true; }), update: protectedProcedure .input(apiUpdateMongo) .mutation(async ({ input, ctx }) => { const { mongoId, ...rest } = input; - const mongo = await findMongoById(mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this mongo", - }); - } + await checkServicePermissionAndAccess(ctx, mongoId, { + service: ["create"], + }); const service = await updateMongoById(mongoId, { ...rest, }); @@ -407,6 +377,12 @@ export const mongoRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongoId, + resourceName: service.appName, + }); return true; }), move: protectedProcedure @@ -417,31 +393,10 @@ export const mongoRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this mongo", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mongoId, { + service: ["create"], + }); - // Update the mongo's projectId const updatedMongo = await db .update(mongoTable) .set({ @@ -458,24 +413,28 @@ export const mongoRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "move", + resourceType: "service", + resourceId: updatedMongo.mongoId, + resourceName: updatedMongo.appName, + }); return updatedMongo; }), rebuild: protectedProcedure .input(apiRebuildMongo) .mutation(async ({ input, ctx }) => { - const mongo = await findMongoById(input.mongoId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rebuild this MongoDB database", - }); - } + await checkServicePermissionAndAccess(ctx, input.mongoId, { + deployment: ["create"], + }); - await rebuildDatabase(mongo.mongoId, "mongo"); + await rebuildDatabase(input.mongoId, "mongo"); + await audit(ctx, { + action: "rebuild", + resourceType: "service", + resourceId: input.mongoId, + }); return true; }), search: protectedProcedure @@ -524,19 +483,18 @@ export const mongoRouter = createTRPCRouter({ ilike(mongoTable.description ?? "", `%${input.description.trim()}%`), ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${mongoTable.mongoId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${mongoTable.mongoId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db diff --git a/apps/dokploy/server/api/routers/mount.ts b/apps/dokploy/server/api/routers/mount.ts index 34e29ab728..f641f21069 100644 --- a/apps/dokploy/server/api/routers/mount.ts +++ b/apps/dokploy/server/api/routers/mount.ts @@ -1,5 +1,4 @@ import { - checkServiceAccess, createMount, deleteMount, findApplicationById, @@ -7,7 +6,6 @@ import { findMariadbById, findMongoById, findMountById, - findMountOrganizationId, findMountsByApplicationId, findMySqlById, findPostgresById, @@ -15,6 +13,10 @@ import { getServiceContainer, updateMount, } from "@dokploy/server"; +import { + checkServiceAccess, + checkServicePermissionAndAccess, +} from "@dokploy/server/services/permission"; import type { ServiceType } from "@dokploy/server/db/schema/mount"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -26,6 +28,7 @@ import { apiUpdateMount, } from "@/server/db/schema"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { audit } from "@/server/api/utils/audit"; async function getServiceOrganizationId( serviceId: string, @@ -68,49 +71,94 @@ async function getServiceOrganizationId( export const mountRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateMount) - .mutation(async ({ input }) => { - return await createMount(input); + .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.serviceId, { + volume: ["create"], + }); + const mount = await createMount(input); + await audit(ctx, { + action: "create", + resourceType: "mount", + resourceId: mount.mountId, + resourceName: input.mountPath, + }); + return mount; }), remove: protectedProcedure .input(apiRemoveMount) .mutation(async ({ input, ctx }) => { - const organizationId = await findMountOrganizationId(input.mountId); - if (organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this mount", + const mount = await findMountById(input.mountId); + const serviceId = + mount.applicationId || + mount.postgresId || + mount.mariadbId || + mount.mongoId || + mount.mysqlId || + mount.redisId || + mount.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volume: ["delete"], }); } + await audit(ctx, { + action: "delete", + resourceType: "mount", + resourceId: input.mountId, + }); return await deleteMount(input.mountId); }), one: protectedProcedure .input(apiFindOneMount) .query(async ({ input, ctx }) => { - const organizationId = await findMountOrganizationId(input.mountId); - if (organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this mount", + const mount = await findMountById(input.mountId); + const serviceId = + mount.applicationId || + mount.postgresId || + mount.mariadbId || + mount.mongoId || + mount.mysqlId || + mount.redisId || + mount.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volume: ["read"], }); } - return await findMountById(input.mountId); + return mount; }), update: protectedProcedure .input(apiUpdateMount) .mutation(async ({ input, ctx }) => { - const organizationId = await findMountOrganizationId(input.mountId); - if (organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this mount", + const mount = await findMountById(input.mountId); + const serviceId = + mount.applicationId || + mount.postgresId || + mount.mariadbId || + mount.mongoId || + mount.mysqlId || + mount.redisId || + mount.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volume: ["create"], }); } + await audit(ctx, { + action: "update", + resourceType: "mount", + resourceId: input.mountId, + resourceName: input.mountPath, + }); return await updateMount(input.mountId, input); }), allNamedByApplicationId: protectedProcedure .input(z.object({ applicationId: z.string().min(1) })) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.applicationId, { + volume: ["read"], + }); const app = await findApplicationById(input.applicationId); const container = await getServiceContainer(app.appName, app.serverId); const mounts = container?.Mounts.filter( @@ -122,14 +170,7 @@ export const mountRouter = createTRPCRouter({ .input(apiFindMountByApplicationId) .query(async ({ input, ctx }) => { console.log("input", input); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.serviceId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.serviceId, "read"); const organizationId = await getServiceOrganizationId( input.serviceId, input.serviceType, diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index 5a00ef0d01..abb0f97a79 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -1,13 +1,10 @@ import { - addNewService, checkPortInUse, - checkServiceAccess, createMount, createMysql, deployMySql, findBackupsByDbId, findEnvironmentById, - findMemberById, findMySqlById, findProjectById, IS_CLOUD, @@ -20,10 +17,17 @@ import { stopServiceRemote, updateMySqlById, } from "@dokploy/server"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangeMySqlStatus, @@ -46,18 +50,10 @@ export const mysqlRouter = createTRPCRouter({ .input(apiCreateMySql) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -76,13 +72,7 @@ export const mysqlRouter = createTRPCRouter({ const newMysql = await createMysql({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newMysql.mysqlId, - project.organizationId, - ); - } + await addNewService(ctx, newMysql.mysqlId); await createMount({ serviceId: newMysql.mysqlId, @@ -92,6 +82,12 @@ export const mysqlRouter = createTRPCRouter({ type: "volume", }); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newMysql.mysqlId, + resourceName: newMysql.appName, + }); return newMysql; } catch (error) { if (error instanceof TRPCError) { @@ -107,14 +103,7 @@ export const mysqlRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneMySql) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mysqlId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.mysqlId, "read"); const mysql = await findMySqlById(input.mysqlId); if ( mysql.environment.project.organizationId !== @@ -131,16 +120,10 @@ export const mysqlRouter = createTRPCRouter({ start: protectedProcedure .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const service = await findMySqlById(input.mysqlId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this MySQL", - }); - } if (service.serverId) { await startServiceRemote(service.serverId, service.appName); @@ -151,21 +134,21 @@ export const mysqlRouter = createTRPCRouter({ applicationStatus: "done", }); + await audit(ctx, { + action: "start", + resourceType: "service", + resourceId: service.mysqlId, + resourceName: service.appName, + }); return service; }), stop: protectedProcedure .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const mongo = await findMySqlById(input.mysqlId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this MySQL", - }); - } if (mongo.serverId) { await stopServiceRemote(mongo.serverId, mongo.appName); } else { @@ -175,21 +158,21 @@ export const mysqlRouter = createTRPCRouter({ applicationStatus: "idle", }); + await audit(ctx, { + action: "stop", + resourceType: "service", + resourceId: mongo.mysqlId, + resourceName: mongo.appName, + }); return mongo; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortMySql) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + service: ["create"], + }); const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this external port", - }); - } if (input.externalPort) { const portCheck = await checkPortInUse( @@ -208,21 +191,27 @@ export const mysqlRouter = createTRPCRouter({ externalPort: input.externalPort, }); await deployMySql(input.mysqlId); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mysql.mysqlId, + resourceName: mysql.appName, + }); return mysql; }), deploy: protectedProcedure .input(apiDeployMySql) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this MySQL", - }); - } + await audit(ctx, { + action: "deploy", + resourceType: "service", + resourceId: mysql.mysqlId, + resourceName: mysql.appName, + }); return deployMySql(input.mysqlId); }), deployWithLogs: protectedProcedure @@ -236,16 +225,9 @@ export const mysqlRouter = createTRPCRouter({ }) .input(apiDeployMySql) .subscription(async function* ({ input, ctx, signal }) { - const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this MySQL", - }); - } + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const queue: string[] = []; const done = false; @@ -269,34 +251,28 @@ export const mysqlRouter = createTRPCRouter({ changeStatus: protectedProcedure .input(apiChangeMySqlStatus) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const mongo = await findMySqlById(input.mysqlId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to change this MySQL status", - }); - } await updateMySqlById(input.mysqlId, { applicationStatus: input.applicationStatus, }); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongo.mysqlId, + resourceName: mongo.appName, + }); return mongo; }), reload: protectedProcedure .input(apiResetMysql) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this MySQL", - }); - } if (mysql.serverId) { await stopServiceRemote(mysql.serverId, mysql.appName); } else { @@ -313,19 +289,18 @@ export const mysqlRouter = createTRPCRouter({ await updateMySqlById(input.mysqlId, { applicationStatus: "done", }); + await audit(ctx, { + action: "reload", + resourceType: "service", + resourceId: mysql.mysqlId, + resourceName: mysql.appName, + }); return true; }), remove: protectedProcedure .input(apiFindOneMySql) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.mysqlId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.mysqlId, "delete"); const mongo = await findMySqlById(input.mysqlId); if ( mongo.environment.project.organizationId !== @@ -337,6 +312,12 @@ export const mysqlRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: mongo.mysqlId, + resourceName: mongo.appName, + }); const backups = await findBackupsByDbId(input.mysqlId, "mysql"); const cleanupOperations = [ async () => await removeService(mongo?.appName, mongo.serverId), @@ -355,16 +336,9 @@ export const mysqlRouter = createTRPCRouter({ saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesMySql) .mutation(async ({ input, ctx }) => { - const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + envVars: ["write"], + }); const service = await updateMySqlById(input.mysqlId, { env: input.env, }); @@ -376,22 +350,20 @@ export const mysqlRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: input.mysqlId, + }); return true; }), update: protectedProcedure .input(apiUpdateMySql) .mutation(async ({ input, ctx }) => { const { mysqlId, ...rest } = input; - const mysql = await findMySqlById(mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this MySQL", - }); - } + await checkServicePermissionAndAccess(ctx, mysqlId, { + service: ["create"], + }); const service = await updateMySqlById(mysqlId, { ...rest, }); @@ -403,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mysqlId, + resourceName: service.appName, + }); return true; }), move: protectedProcedure @@ -413,31 +391,10 @@ export const mysqlRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this mysql", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + service: ["create"], + }); - // Update the mysql's projectId const updatedMysql = await db .update(mysqlTable) .set({ @@ -454,24 +411,28 @@ export const mysqlRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "move", + resourceType: "service", + resourceId: updatedMysql.mysqlId, + resourceName: updatedMysql.appName, + }); return updatedMysql; }), rebuild: protectedProcedure .input(apiRebuildMysql) .mutation(async ({ input, ctx }) => { - const mysql = await findMySqlById(input.mysqlId); - if ( - mysql.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rebuild this MySQL database", - }); - } + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + deployment: ["create"], + }); - await rebuildDatabase(mysql.mysqlId, "mysql"); + await rebuildDatabase(input.mysqlId, "mysql"); + await audit(ctx, { + action: "rebuild", + resourceType: "service", + resourceId: input.mysqlId, + }); return true; }), search: protectedProcedure @@ -520,19 +481,18 @@ export const mysqlRouter = createTRPCRouter({ ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`), ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${mysqlTable.mysqlId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${mysqlTable.mysqlId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index 26811b194f..a7bcd11994 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -43,11 +43,11 @@ import { TRPCError } from "@trpc/server"; import { desc, eq, sql } from "drizzle-orm"; import { z } from "zod"; import { - adminProcedure, createTRPCRouter, - protectedProcedure, publicProcedure, + withPermission, } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateCustom, apiCreateDiscord, @@ -88,15 +88,18 @@ import { } from "@/server/db/schema"; export const notificationRouter = createTRPCRouter({ - createSlack: adminProcedure + createSlack: withPermission("notification", "create") .input(apiCreateSlack) .mutation(async ({ input, ctx }) => { try { - return await createSlackNotification( - input, - ctx.session.activeOrganizationId, - ); + await createSlackNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { + console.log(error); throw new TRPCError({ code: "BAD_REQUEST", message: "Error creating the notification", @@ -104,7 +107,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateSlack: adminProcedure + updateSlack: withPermission("notification", "update") .input(apiUpdateSlack) .mutation(async ({ input, ctx }) => { try { @@ -115,15 +118,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateSlackNotification({ + const result = await updateSlackNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testSlackConnection: adminProcedure + testSlackConnection: withPermission("notification", "create") .input(apiTestSlackConnection) .mutation(async ({ input }) => { try { @@ -140,14 +150,19 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTelegram: adminProcedure + createTelegram: withPermission("notification", "create") .input(apiCreateTelegram) .mutation(async ({ input, ctx }) => { try { - return await createTelegramNotification( + await createTelegramNotification( input, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -157,7 +172,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateTelegram: adminProcedure + updateTelegram: withPermission("notification", "update") .input(apiUpdateTelegram) .mutation(async ({ input, ctx }) => { try { @@ -168,10 +183,17 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateTelegramNotification({ + const result = await updateTelegramNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -180,7 +202,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - testTelegramConnection: adminProcedure + testTelegramConnection: withPermission("notification", "create") .input(apiTestTelegramConnection) .mutation(async ({ input }) => { try { @@ -194,14 +216,19 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createDiscord: adminProcedure + createDiscord: withPermission("notification", "create") .input(apiCreateDiscord) .mutation(async ({ input, ctx }) => { try { - return await createDiscordNotification( + await createDiscordNotification( input, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -211,7 +238,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateDiscord: adminProcedure + updateDiscord: withPermission("notification", "update") .input(apiUpdateDiscord) .mutation(async ({ input, ctx }) => { try { @@ -222,10 +249,17 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateDiscordNotification({ + const result = await updateDiscordNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -235,7 +269,7 @@ export const notificationRouter = createTRPCRouter({ } }), - testDiscordConnection: adminProcedure + testDiscordConnection: withPermission("notification", "create") .input(apiTestDiscordConnection) .mutation(async ({ input }) => { try { @@ -257,14 +291,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createEmail: adminProcedure + createEmail: withPermission("notification", "create") .input(apiCreateEmail) .mutation(async ({ input, ctx }) => { try { - return await createEmailNotification( - input, - ctx.session.activeOrganizationId, - ); + await createEmailNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -273,7 +309,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateEmail: adminProcedure + updateEmail: withPermission("notification", "update") .input(apiUpdateEmail) .mutation(async ({ input, ctx }) => { try { @@ -284,10 +320,17 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateEmailNotification({ + const result = await updateEmailNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -296,7 +339,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - testEmailConnection: adminProcedure + testEmailConnection: withPermission("notification", "create") .input(apiTestEmailConnection) .mutation(async ({ input }) => { try { @@ -314,14 +357,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createResend: adminProcedure + createResend: withPermission("notification", "create") .input(apiCreateResend) .mutation(async ({ input, ctx }) => { try { - return await createResendNotification( - input, - ctx.session.activeOrganizationId, - ); + await createResendNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -330,7 +375,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateResend: adminProcedure + updateResend: withPermission("notification", "update") .input(apiUpdateResend) .mutation(async ({ input, ctx }) => { try { @@ -341,10 +386,17 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateResendNotification({ + const result = await updateResendNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -353,7 +405,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - testResendConnection: adminProcedure + testResendConnection: withPermission("notification", "create") .input(apiTestResendConnection) .mutation(async ({ input }) => { try { @@ -371,7 +423,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - remove: adminProcedure + remove: withPermission("notification", "delete") .input(apiFindOneNotification) .mutation(async ({ input, ctx }) => { try { @@ -382,6 +434,11 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to delete this notification", }); } + await audit(ctx, { + action: "delete", + resourceType: "notification", + resourceName: notification.name, + }); return await removeNotificationById(input.notificationId); } catch (error) { const message = @@ -394,7 +451,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - one: protectedProcedure + one: withPermission("notification", "read") .input(apiFindOneNotification) .query(async ({ input, ctx }) => { const notification = await findNotificationById(input.notificationId); @@ -406,7 +463,7 @@ export const notificationRouter = createTRPCRouter({ } return notification; }), - all: adminProcedure.query(async ({ ctx }) => { + all: withPermission("notification", "read").query(async ({ ctx }) => { return await db.query.notifications.findMany({ with: { slack: true, @@ -453,8 +510,6 @@ export const notificationRouter = createTRPCRouter({ }); } - // For Dokploy server type, we don't have a specific organizationId - // This might need to be adjusted based on your business logic organizationId = ""; ServerName = "Dokploy"; } else { @@ -488,14 +543,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createGotify: adminProcedure + createGotify: withPermission("notification", "create") .input(apiCreateGotify) .mutation(async ({ input, ctx }) => { try { - return await createGotifyNotification( - input, - ctx.session.activeOrganizationId, - ); + await createGotifyNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -504,7 +561,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateGotify: adminProcedure + updateGotify: withPermission("notification", "update") .input(apiUpdateGotify) .mutation(async ({ input, ctx }) => { try { @@ -518,15 +575,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateGotifyNotification({ + const result = await updateGotifyNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testGotifyConnection: adminProcedure + testGotifyConnection: withPermission("notification", "create") .input(apiTestGotifyConnection) .mutation(async ({ input }) => { try { @@ -544,14 +608,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createNtfy: adminProcedure + createNtfy: withPermission("notification", "create") .input(apiCreateNtfy) .mutation(async ({ input, ctx }) => { try { - return await createNtfyNotification( - input, - ctx.session.activeOrganizationId, - ); + await createNtfyNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -560,7 +626,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateNtfy: adminProcedure + updateNtfy: withPermission("notification", "update") .input(apiUpdateNtfy) .mutation(async ({ input, ctx }) => { try { @@ -574,15 +640,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateNtfyNotification({ + const result = await updateNtfyNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testNtfyConnection: adminProcedure + testNtfyConnection: withPermission("notification", "create") .input(apiTestNtfyConnection) .mutation(async ({ input }) => { try { @@ -602,14 +675,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createCustom: adminProcedure + createCustom: withPermission("notification", "create") .input(apiCreateCustom) .mutation(async ({ input, ctx }) => { try { - return await createCustomNotification( - input, - ctx.session.activeOrganizationId, - ); + await createCustomNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -618,7 +693,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateCustom: adminProcedure + updateCustom: withPermission("notification", "update") .input(apiUpdateCustom) .mutation(async ({ input, ctx }) => { try { @@ -629,15 +704,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateCustomNotification({ + const result = await updateCustomNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testCustomConnection: adminProcedure + testCustomConnection: withPermission("notification", "create") .input(apiTestCustomConnection) .mutation(async ({ input }) => { try { @@ -655,14 +737,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createLark: adminProcedure + createLark: withPermission("notification", "create") .input(apiCreateLark) .mutation(async ({ input, ctx }) => { try { - return await createLarkNotification( - input, - ctx.session.activeOrganizationId, - ); + await createLarkNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -671,7 +755,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateLark: adminProcedure + updateLark: withPermission("notification", "update") .input(apiUpdateLark) .mutation(async ({ input, ctx }) => { try { @@ -685,15 +769,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateLarkNotification({ + const result = await updateLarkNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testLarkConnection: adminProcedure + testLarkConnection: withPermission("notification", "create") .input(apiTestLarkConnection) .mutation(async ({ input }) => { try { @@ -712,14 +803,16 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTeams: adminProcedure + createTeams: withPermission("notification", "create") .input(apiCreateTeams) .mutation(async ({ input, ctx }) => { try { - return await createTeamsNotification( - input, - ctx.session.activeOrganizationId, - ); + await createTeamsNotification(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -728,7 +821,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateTeams: adminProcedure + updateTeams: withPermission("notification", "update") .input(apiUpdateTeams) .mutation(async ({ input, ctx }) => { try { @@ -742,15 +835,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updateTeamsNotification({ + const result = await updateTeamsNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testTeamsConnection: adminProcedure + testTeamsConnection: withPermission("notification", "create") .input(apiTestTeamsConnection) .mutation(async ({ input }) => { try { @@ -767,14 +867,19 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createPushover: adminProcedure + createPushover: withPermission("notification", "create") .input(apiCreatePushover) .mutation(async ({ input, ctx }) => { try { - return await createPushoverNotification( + await createPushoverNotification( input, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "create", + resourceType: "notification", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -783,7 +888,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updatePushover: adminProcedure + updatePushover: withPermission("notification", "update") .input(apiUpdatePushover) .mutation(async ({ input, ctx }) => { try { @@ -797,15 +902,22 @@ export const notificationRouter = createTRPCRouter({ message: "You are not authorized to update this notification", }); } - return await updatePushoverNotification({ + const result = await updatePushoverNotification({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "update", + resourceType: "notification", + resourceId: input.notificationId, + resourceName: notification.name, + }); + return result; } catch (error) { throw error; } }), - testPushoverConnection: adminProcedure + testPushoverConnection: withPermission("notification", "create") .input(apiTestPushoverConnection) .mutation(async ({ input }) => { try { @@ -823,13 +935,18 @@ export const notificationRouter = createTRPCRouter({ }); } }), - getEmailProviders: adminProcedure.query(async ({ ctx }) => { - return await db.query.notifications.findMany({ - where: eq(notifications.organizationId, ctx.session.activeOrganizationId), - with: { - email: true, - resend: true, - }, - }); - }), + getEmailProviders: withPermission("notification", "read").query( + async ({ ctx }) => { + return await db.query.notifications.findMany({ + where: eq( + notifications.organizationId, + ctx.session.activeOrganizationId, + ), + with: { + email: true, + resend: true, + }, + }); + }, + ), }); diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 825290b0bf..2f7d452875 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -1,11 +1,18 @@ import { db } from "@dokploy/server/db"; import { IS_CLOUD } from "@dokploy/server/index"; +import { audit } from "@/server/api/utils/audit"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, exists } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { invitation, member, organization } from "@/server/db/schema"; -import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +import { + invitation, + member, + organization, + organizationRole, + user, +} from "@/server/db/schema"; +import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; export const organizationRouter = createTRPCRouter({ create: protectedProcedure .input( @@ -50,6 +57,12 @@ export const organizationRouter = createTRPCRouter({ createdAt: new Date(), userId: ctx.user.id, }); + await audit(ctx, { + action: "create", + resourceType: "organization", + resourceId: result.id, + resourceName: result.name, + }); return result; }), all: protectedProcedure.query(async ({ ctx }) => { @@ -156,6 +169,12 @@ export const organizationRouter = createTRPCRouter({ }) .where(eq(organization.id, input.organizationId)) .returning(); + await audit(ctx, { + action: "update", + resourceType: "organization", + resourceId: input.organizationId, + resourceName: input.name, + }); return result[0]; }), delete: protectedProcedure @@ -220,15 +239,109 @@ export const organizationRouter = createTRPCRouter({ .delete(organization) .where(eq(organization.id, input.organizationId)); + await audit(ctx, { + action: "delete", + resourceType: "organization", + resourceId: input.organizationId, + resourceName: org.name, + }); return result; }), - allInvitations: adminProcedure.query(async ({ ctx }) => { + inviteMember: withPermission("member", "create") + .input( + z.object({ + email: z.string().email(), + role: z.string().min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + const orgId = ctx.session.activeOrganizationId; + const email = input.email.toLowerCase(); + + // Check if user is already a member + const existingUser = await db.query.user.findFirst({ + where: eq(user.email, email), + }); + + if (existingUser) { + const existingMember = await db.query.member.findFirst({ + where: and( + eq(member.organizationId, orgId), + eq(member.userId, existingUser.id), + ), + }); + + if (existingMember) { + throw new TRPCError({ + code: "CONFLICT", + message: "User is already a member of this organization", + }); + } + } + + // Check for pending invitation + const existingInvitation = await db.query.invitation.findFirst({ + where: and( + eq(invitation.organizationId, orgId), + eq(invitation.email, email), + eq(invitation.status, "pending"), + ), + }); + + if (existingInvitation) { + throw new TRPCError({ + code: "CONFLICT", + message: "An invitation has already been sent to this email", + }); + } + + // If assigning a custom role, verify it exists + if (!["owner", "admin", "member"].includes(input.role)) { + const customRole = await db.query.organizationRole.findFirst({ + where: and( + eq(organizationRole.organizationId, orgId), + eq(organizationRole.role, input.role), + ), + }); + + if (!customRole) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Role "${input.role}" not found`, + }); + } + } + + const [created] = await db + .insert(invitation) + .values({ + id: nanoid(), + organizationId: orgId, + email, + role: input.role as any, + status: "pending", + expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000), + inviterId: ctx.user.id, + }) + .returning(); + + await audit(ctx, { + action: "create", + resourceType: "organization", + resourceId: created?.id, + resourceName: email, + metadata: { type: "inviteMember", role: input.role }, + }); + return created; + }), + + allInvitations: withPermission("member", "create").query(async ({ ctx }) => { return await db.query.invitation.findMany({ where: eq(invitation.organizationId, ctx.session.activeOrganizationId), orderBy: [desc(invitation.status), desc(invitation.expiresAt)], }); }), - removeInvitation: adminProcedure + removeInvitation: withPermission("member", "create") .input(z.object({ invitationId: z.string() })) .mutation(async ({ ctx, input }) => { const invitationResult = await db.query.invitation.findFirst({ @@ -251,15 +364,23 @@ export const organizationRouter = createTRPCRouter({ }); } - return await db + const result = await db .delete(invitation) .where(eq(invitation.id, input.invitationId)); + await audit(ctx, { + action: "delete", + resourceType: "organization", + resourceId: input.invitationId, + resourceName: invitationResult.email, + metadata: { type: "removeInvitation" }, + }); + return result; }), - updateMemberRole: adminProcedure + updateMemberRole: withPermission("member", "update") .input( z.object({ memberId: z.string(), - role: z.enum(["admin", "member"]), + role: z.string().min(1), }), ) .mutation(async ({ ctx, input }) => { @@ -289,7 +410,7 @@ export const organizationRouter = createTRPCRouter({ } // Owner role is intransferible - cannot change to or from owner - if (target.role === "owner") { + if (target.role === "owner" || input.role === "owner") { throw new TRPCError({ code: "FORBIDDEN", message: "The owner role is intransferible", @@ -306,12 +427,39 @@ export const organizationRouter = createTRPCRouter({ }); } + // If assigning a custom role (not admin/member), verify it exists + if (input.role !== "admin" && input.role !== "member") { + const customRole = await db.query.organizationRole.findFirst({ + where: and( + eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + eq(organizationRole.role, input.role), + ), + }); + + if (!customRole) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Custom role "${input.role}" not found`, + }); + } + } + // Update the target member's role await db .update(member) .set({ role: input.role }) .where(eq(member.id, input.memberId)); + await audit(ctx, { + action: "update", + resourceType: "user", + resourceId: target.userId, + resourceName: target.user.email, + metadata: { before: target.role, after: input.role }, + }); return true; }), setDefault: protectedProcedure @@ -353,6 +501,12 @@ export const organizationRouter = createTRPCRouter({ ), ); + await audit(ctx, { + action: "update", + resourceType: "organization", + resourceId: input.organizationId, + metadata: { type: "setDefault" }, + }); return { success: true }; }), active: protectedProcedure.query(async ({ ctx }) => { diff --git a/apps/dokploy/server/api/routers/patch.ts b/apps/dokploy/server/api/routers/patch.ts index 28dc2c8e85..333dd1856a 100644 --- a/apps/dokploy/server/api/routers/patch.ts +++ b/apps/dokploy/server/api/routers/patch.ts @@ -1,5 +1,4 @@ import { - checkServiceAccess, cleanPatchRepos, createPatch, deletePatch, @@ -14,6 +13,7 @@ import { readPatchRepoFile, updatePatch, } from "@dokploy/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { @@ -21,6 +21,7 @@ import { createTRPCRouter, protectedProcedure, } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreatePatch, apiDeletePatch, @@ -29,47 +30,56 @@ import { apiUpdatePatch, } from "@/server/db/schema"; +/** + * Resolves the serviceId from a patch record (applicationId or composeId). + * Throws if neither is set. + */ +const resolvePatchServiceId = (patch: { + applicationId: string | null; + composeId: string | null; +}): string => { + const serviceId = patch.applicationId ?? patch.composeId; + if (!serviceId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Patch has no associated service", + }); + } + return serviceId; +}; + export const patchRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePatch) .mutation(async ({ input, ctx }) => { - if (input.applicationId) { - const app = await findApplicationById(input.applicationId); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.applicationId, - ctx.session.activeOrganizationId, - "access", - ); - } - } else if (input.composeId) { - const compose = await findComposeById(input.composeId); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } + const serviceId = input.applicationId ?? input.composeId; + if (!serviceId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Either applicationId or composeId must be provided", + }); } - - return await createPatch(input); + await checkServicePermissionAndAccess(ctx, serviceId, { + service: ["create"], + }); + const result = await createPatch(input); + await audit(ctx, { + action: "create", + resourceType: "settings", + resourceId: result.patchId, + resourceName: result.filePath, + metadata: { type: "patch" }, + }); + return result; }), - one: protectedProcedure.input(apiFindPatch).query(async ({ input }) => { - return await findPatchById(input.patchId); + one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => { + const patch = await findPatchById(input.patchId); + const serviceId = resolvePatchServiceId(patch); + await checkServicePermissionAndAccess(ctx, serviceId, { + service: ["read"], + }); + return patch; }), byEntityId: protectedProcedure @@ -77,51 +87,70 @@ export const patchRouter = createTRPCRouter({ z.object({ id: z.string(), type: z.enum(["application", "compose"]) }), ) .query(async ({ input, ctx }) => { - if (input.type === "application") { - const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (input.type === "compose") { - const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } - } - const result = await findPatchesByEntityId(input.id, input.type); - - return result; + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["read"], + }); + return await findPatchesByEntityId(input.id, input.type); }), update: protectedProcedure .input(apiUpdatePatch) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const patch = await findPatchById(input.patchId); + const serviceId = resolvePatchServiceId(patch); + await checkServicePermissionAndAccess(ctx, serviceId, { + service: ["create"], + }); const { patchId, ...data } = input; - return await updatePatch(patchId, data); + const result = await updatePatch(patchId, data); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceId: patchId, + resourceName: patch.filePath, + metadata: { type: "patch" }, + }); + return result; }), delete: protectedProcedure .input(apiDeletePatch) - .mutation(async ({ input }) => { - return await deletePatch(input.patchId); + .mutation(async ({ input, ctx }) => { + const patch = await findPatchById(input.patchId); + const serviceId = resolvePatchServiceId(patch); + await checkServicePermissionAndAccess(ctx, serviceId, { + service: ["delete"], + }); + const result = await deletePatch(input.patchId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceId: input.patchId, + resourceName: patch.filePath, + metadata: { type: "patch" }, + }); + return result; }), toggleEnabled: protectedProcedure .input(apiTogglePatchEnabled) - .mutation(async ({ input }) => { - return await updatePatch(input.patchId, { enabled: input.enabled }); + .mutation(async ({ input, ctx }) => { + const patch = await findPatchById(input.patchId); + const serviceId = resolvePatchServiceId(patch); + await checkServicePermissionAndAccess(ctx, serviceId, { + service: ["create"], + }); + const result = await updatePatch(input.patchId, { + enabled: input.enabled, + }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceId: input.patchId, + resourceName: patch.filePath, + metadata: { type: "patch", enabled: input.enabled }, + }); + return result; }), // Repository Operations @@ -132,11 +161,21 @@ export const patchRouter = createTRPCRouter({ type: z.enum(["application", "compose"]), }), ) - .mutation(async ({ input }) => { - return await ensurePatchRepo({ + .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["create"], + }); + const result = await ensurePatchRepo({ type: input.type, id: input.id, }); + await audit(ctx, { + action: "create", + resourceType: "settings", + resourceId: input.id, + metadata: { type: "ensurePatchRepo", serviceType: input.type }, + }); + return result; }), readRepoDirectories: protectedProcedure @@ -148,36 +187,17 @@ export const patchRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["read"], + }); let serverId: string | null = null; - if (input.type === "application") { const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } serverId = app.serverId; - } - - if (input.type === "compose") { + } else { const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } serverId = compose.serverId; } - return await readPatchRepoDirectory(input.repoPath, serverId); }), @@ -190,44 +210,22 @@ export const patchRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["read"], + }); let serverId: string | null = null; - if (input.type === "application") { const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } serverId = app.serverId; - } else if (input.type === "compose") { + } else { const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } serverId = compose.serverId; - } else { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Either applicationId or composeId must be provided", - }); } const existingPatch = await findPatchByFilePath( input.filePath, input.id, input.type, ); - // For delete patches, show current file content from repo (what will be deleted) if (existingPatch?.type === "delete") { try { @@ -253,55 +251,43 @@ export const patchRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.type === "application") { - const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (input.type === "compose") { - const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } - } else { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Either application or compose must be provided", - }); - } - + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["create"], + }); const existingPatch = await findPatchByFilePath( input.filePath, input.id, input.type, ); - if (!existingPatch) { - return await createPatch({ + const result = await createPatch({ filePath: input.filePath, content: input.content, type: input.patchType, applicationId: input.type === "application" ? input.id : undefined, composeId: input.type === "compose" ? input.id : undefined, }); + await audit(ctx, { + action: "create", + resourceType: "settings", + resourceId: result.patchId, + resourceName: input.filePath, + metadata: { type: "saveFileAsPatch" }, + }); + return result; } - - return await updatePatch(existingPatch.patchId, { + const result = await updatePatch(existingPatch.patchId, { content: input.content, type: input.patchType, }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceId: existingPatch.patchId, + resourceName: input.filePath, + metadata: { type: "saveFileAsPatch" }, + }); + return result; }), markFileForDeletion: protectedProcedure @@ -313,36 +299,34 @@ export const patchRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.type === "application") { - const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - } else if (input.type === "compose") { - const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } - } - - return await markPatchForDeletion(input.filePath, input.id, input.type); + await checkServicePermissionAndAccess(ctx, input.id, { + service: ["create"], + }); + const result = await markPatchForDeletion( + input.filePath, + input.id, + input.type, + ); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceId: input.id, + resourceName: input.filePath, + metadata: { type: "markFileForDeletion" }, + }); + return result; }), + cleanPatchRepos: adminProcedure .input(z.object({ serverId: z.string().optional() })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanPatchRepos(input.serverId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceId: input.serverId || "local", + metadata: { type: "cleanPatchRepos" }, + }); return true; }), }); diff --git a/apps/dokploy/server/api/routers/port.ts b/apps/dokploy/server/api/routers/port.ts index bbd9498047..c98081e86b 100644 --- a/apps/dokploy/server/api/routers/port.ts +++ b/apps/dokploy/server/api/routers/port.ts @@ -4,8 +4,10 @@ import { removePortById, updatePortById, } from "@dokploy/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreatePort, apiFindOnePort, @@ -15,10 +17,19 @@ import { export const portRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePort) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { - await createPort(input); - return true; + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); + const port = await createPort(input); + await audit(ctx, { + action: "create", + resourceType: "port", + resourceId: port.portId, + resourceName: `${port.publishedPort}:${port.targetPort}`, + }); + return port; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -32,15 +43,11 @@ export const portRouter = createTRPCRouter({ .query(async ({ input, ctx }) => { try { const port = await finPortById(input.portId); - if ( - port.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this port", - }); - } + await checkServicePermissionAndAccess( + ctx, + port.application.applicationId, + { service: ["read"] }, + ); return port; } catch (error) { throw new TRPCError({ @@ -54,17 +61,20 @@ export const portRouter = createTRPCRouter({ .input(apiFindOnePort) .mutation(async ({ input, ctx }) => { const port = await finPortById(input.portId); - if ( - port.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this port", - }); - } + await checkServicePermissionAndAccess( + ctx, + port.application.applicationId, + { service: ["delete"] }, + ); try { - return await removePortById(input.portId); + const result = await removePortById(input.portId); + await audit(ctx, { + action: "delete", + resourceType: "port", + resourceId: port.portId, + resourceName: `${port.publishedPort}:${port.targetPort}`, + }); + return result; } catch (error) { const message = error instanceof Error ? error.message : "Error input: Deleting port"; @@ -78,17 +88,20 @@ export const portRouter = createTRPCRouter({ .input(apiUpdatePort) .mutation(async ({ input, ctx }) => { const port = await finPortById(input.portId); - if ( - port.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this port", - }); - } + await checkServicePermissionAndAccess( + ctx, + port.application.applicationId, + { service: ["create"] }, + ); try { - return await updatePortById(input.portId, input); + const result = await updatePortById(input.portId, input); + await audit(ctx, { + action: "update", + resourceType: "port", + resourceId: port.portId, + resourceName: `${port.publishedPort}:${port.targetPort}`, + }); + return result; } catch (error) { const message = error instanceof Error ? error.message : "Error updating the port"; diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 48de9d5a2a..3b3cfd2086 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -1,13 +1,10 @@ import { - addNewService, checkPortInUse, - checkServiceAccess, createMount, createPostgres, deployPostgres, findBackupsByDbId, findEnvironmentById, - findMemberById, findPostgresById, findProjectById, getMountPath, @@ -21,10 +18,17 @@ import { stopServiceRemote, updatePostgresById, } from "@dokploy/server"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangePostgresStatus, @@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({ .input(apiCreatePostgres) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({ const newPostgres = await createPostgres({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newPostgres.postgresId, - project.organizationId, - ); - } + await addNewService(ctx, newPostgres.postgresId); const mountPath = getMountPath(input.dockerImage); @@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({ type: "volume", }); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newPostgres.postgresId, + resourceName: newPostgres.appName, + }); return newPostgres; } catch (error) { if (error instanceof TRPCError) { @@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOnePostgres) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.postgresId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.postgresId, "read"); const postgres = await findPostgresById(input.postgresId); if ( @@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({ start: protectedProcedure .input(apiFindOnePostgres) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const service = await findPostgresById(input.postgresId); - if ( - service.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this Postgres", - }); - } - if (service.serverId) { await startServiceRemote(service.serverId, service.appName); } else { @@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({ applicationStatus: "done", }); + await audit(ctx, { + action: "start", + resourceType: "service", + resourceId: service.postgresId, + resourceName: service.appName, + }); return service; }), stop: protectedProcedure .input(apiFindOnePostgres) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this Postgres", - }); - } if (postgres.serverId) { await stopServiceRemote(postgres.serverId, postgres.appName); } else { @@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({ applicationStatus: "idle", }); + await audit(ctx, { + action: "stop", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); return postgres; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortPostgres) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + service: ["create"], + }); const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this external port", - }); - } - if (input.externalPort) { const portCheck = await checkPortInUse( input.externalPort, @@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({ externalPort: input.externalPort, }); await deployPostgres(input.postgresId); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); return postgres; }), deploy: protectedProcedure .input(apiDeployPostgres) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Postgres", - }); - } + await audit(ctx, { + action: "deploy", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); return deployPostgres(input.postgresId); }), @@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({ }) .input(apiDeployPostgres) .subscription(async function* ({ input, ctx, signal }) { - const postgres = await findPostgresById(input.postgresId); - - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Postgres", - }); - } + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const queue: string[] = []; const done = false; @@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({ changeStatus: protectedProcedure .input(apiChangePostgresStatus) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to change this Postgres status", - }); - } await updatePostgresById(input.postgresId, { applicationStatus: input.applicationStatus, }); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); return postgres; }), remove: protectedProcedure .input(apiFindOnePostgres) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.postgresId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.postgresId, "delete"); const postgres = await findPostgresById(input.postgresId); if ( @@ -314,6 +286,12 @@ export const postgresRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); const backups = await findBackupsByDbId(input.postgresId, "postgres"); const cleanupOperations = [ @@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({ saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesPostgres) .mutation(async ({ input, ctx }) => { - const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.postgresId, { + envVars: ["write"], + }); const service = await updatePostgresById(input.postgresId, { env: input.env, }); @@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: input.postgresId, + }); return true; }), reload: protectedProcedure .input(apiResetPostgres) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this Postgres", - }); - } if (postgres.serverId) { await stopServiceRemote(postgres.serverId, postgres.appName); } else { @@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({ await updatePostgresById(input.postgresId, { applicationStatus: "done", }); + await audit(ctx, { + action: "reload", + resourceType: "service", + resourceId: postgres.postgresId, + resourceName: postgres.appName, + }); return true; }), update: protectedProcedure .input(apiUpdatePostgres) .mutation(async ({ input, ctx }) => { const { postgresId, ...rest } = input; - const postgres = await findPostgresById(postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this Postgres", - }); - } + await checkServicePermissionAndAccess(ctx, postgresId, { + service: ["create"], + }); const service = await updatePostgresById(postgresId, { ...rest, @@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: postgresId, + resourceName: service.appName, + }); return true; }), move: protectedProcedure @@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this postgres", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.postgresId, { + service: ["create"], + }); - // Update the postgres's projectId const updatedPostgres = await db .update(postgresTable) .set({ @@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "move", + resourceType: "service", + resourceId: updatedPostgres.postgresId, + resourceName: updatedPostgres.appName, + }); return updatedPostgres; }), rebuild: protectedProcedure .input(apiRebuildPostgres) .mutation(async ({ input, ctx }) => { - const postgres = await findPostgresById(input.postgresId); - if ( - postgres.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rebuild this Postgres database", - }); - } + await checkServicePermissionAndAccess(ctx, input.postgresId, { + deployment: ["create"], + }); - await rebuildDatabase(postgres.postgresId, "postgres"); + await rebuildDatabase(input.postgresId, "postgres"); + await audit(ctx, { + action: "rebuild", + resourceType: "service", + resourceId: input.postgresId, + }); return true; }), search: protectedProcedure @@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({ ), ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${postgresTable.postgresId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${postgresTable.postgresId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db diff --git a/apps/dokploy/server/api/routers/preview-deployment.ts b/apps/dokploy/server/api/routers/preview-deployment.ts index 0c325a9c68..a45ef80c53 100644 --- a/apps/dokploy/server/api/routers/preview-deployment.ts +++ b/apps/dokploy/server/api/routers/preview-deployment.ts @@ -5,8 +5,9 @@ import { IS_CLOUD, removePreviewDeployment, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { apiFindAllByApplication } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; @@ -17,53 +18,46 @@ export const previewDeploymentRouter = createTRPCRouter({ all: protectedProcedure .input(apiFindAllByApplication) .query(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } + await checkServicePermissionAndAccess(ctx, input.applicationId, { + deployment: ["read"], + }); return await findPreviewDeploymentsByApplicationId(input.applicationId); }), - delete: protectedProcedure + + one: protectedProcedure .input(z.object({ previewDeploymentId: z.string() })) - .mutation(async ({ input, ctx }) => { + .query(async ({ input, ctx }) => { const previewDeployment = await findPreviewDeploymentById( input.previewDeploymentId, ); - if ( - previewDeployment.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this preview deployment", - }); - } - await removePreviewDeployment(input.previewDeploymentId); - return true; + await checkServicePermissionAndAccess( + ctx, + previewDeployment.applicationId, + { deployment: ["read"] }, + ); + return previewDeployment; }), - one: protectedProcedure + + delete: protectedProcedure .input(z.object({ previewDeploymentId: z.string() })) - .query(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }) => { const previewDeployment = await findPreviewDeploymentById( input.previewDeploymentId, ); - if ( - previewDeployment.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this preview deployment", - }); - } - return previewDeployment; + await checkServicePermissionAndAccess( + ctx, + previewDeployment.applicationId, + { deployment: ["cancel"] }, + ); + await removePreviewDeployment(input.previewDeploymentId); + await audit(ctx, { + action: "delete", + resourceType: "previewDeployment", + resourceId: input.previewDeploymentId, + }); + return true; }), + redeploy: protectedProcedure .input( z.object({ @@ -76,15 +70,11 @@ export const previewDeploymentRouter = createTRPCRouter({ const previewDeployment = await findPreviewDeploymentById( input.previewDeploymentId, ); - if ( - previewDeployment.application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to redeploy this preview deployment", - }); - } + await checkServicePermissionAndAccess( + ctx, + previewDeployment.applicationId, + { deployment: ["create"] }, + ); const application = await findApplicationById( previewDeployment.applicationId, ); @@ -103,6 +93,11 @@ export const previewDeploymentRouter = createTRPCRouter({ deploy(jobData).catch((error) => { console.error("Background deployment failed:", error); }); + await audit(ctx, { + action: "redeploy", + resourceType: "previewDeployment", + resourceId: input.previewDeploymentId, + }); return true; } await myQueue.add( @@ -113,6 +108,11 @@ export const previewDeploymentRouter = createTRPCRouter({ removeOnFail: true, }, ); + await audit(ctx, { + action: "redeploy", + resourceType: "previewDeployment", + resourceId: input.previewDeploymentId, + }); return true; }), }); diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index e270ee4b40..6bb523f920 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -1,7 +1,4 @@ import { - addNewEnvironment, - addNewProject, - checkProjectAccess, createApplication, createBackup, createCompose, @@ -22,7 +19,6 @@ import { findComposeById, findEnvironmentById, findMariadbById, - findMemberById, findMongoById, findMySqlById, findPostgresById, @@ -32,15 +28,23 @@ import { IS_CLOUD, updateProjectById, } from "@dokploy/server"; +import { + addNewEnvironment, + addNewProject, + checkPermission, + checkProjectAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import type { AnyPgColumn } from "drizzle-orm/pg-core"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { - adminProcedure, createTRPCRouter, protectedProcedure, + withPermission, } from "@/server/api/trpc"; import { apiCreateProject, @@ -63,13 +67,7 @@ export const projectRouter = createTRPCRouter({ .input(apiCreateProject) .mutation(async ({ ctx, input }) => { try { - if (ctx.user.role === "member") { - await checkProjectAccess( - ctx.user.id, - "create", - ctx.session.activeOrganizationId, - ); - } + await checkProjectAccess(ctx, "create"); const admin = await findUserById(ctx.user.ownerId); @@ -84,20 +82,16 @@ export const projectRouter = createTRPCRouter({ input, ctx.session.activeOrganizationId, ); - if (ctx.user.role === "member") { - await addNewProject( - ctx.user.id, - project.project.projectId, - ctx.session.activeOrganizationId, - ); + await addNewProject(ctx, project.project.projectId); - await addNewEnvironment( - ctx.user.id, - project?.environment?.environmentId || "", - ctx.session.activeOrganizationId, - ); - } + await addNewEnvironment(ctx, project?.environment?.environmentId || ""); + await audit(ctx, { + action: "create", + resourceType: "project", + resourceId: project.project.projectId, + resourceName: project.project.name, + }); return project; } catch (error) { throw new TRPCError({ @@ -111,18 +105,18 @@ export const projectRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneProject) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedServices, accessedProjects } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); - await checkProjectAccess( - ctx.user.id, - "access", - ctx.session.activeOrganizationId, - input.projectId, - ); + if (!accessedProjects.includes(input.projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } const project = await db.query.projects.findFirst({ where: and( @@ -189,15 +183,14 @@ export const projectRouter = createTRPCRouter({ return project; }), all: protectedProcedure.query(async ({ ctx }) => { - if (ctx.user.role === "member") { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { const { accessedProjects, accessedEnvironments, accessedServices } = - await findMemberById(ctx.user.id, ctx.session.activeOrganizationId); + await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId); if (accessedProjects.length === 0) { return []; } - // Build environment filter const environmentFilter = accessedEnvironments.length === 0 ? sql`false` @@ -348,105 +341,106 @@ export const projectRouter = createTRPCRouter({ }); }), - /** All projects with full environments and services for the admin permissions UI. Admin only. */ - allForPermissions: adminProcedure.query(async ({ ctx }) => { - return await db.query.projects.findMany({ - where: eq(projects.organizationId, ctx.session.activeOrganizationId), - orderBy: desc(projects.createdAt), - columns: { - projectId: true, - name: true, - }, - with: { - environments: { - columns: { - environmentId: true, - name: true, - isDefault: true, - }, - with: { - applications: { - columns: { - applicationId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, - }, + allForPermissions: withPermission("member", "update").query( + async ({ ctx }) => { + return await db.query.projects.findMany({ + where: eq(projects.organizationId, ctx.session.activeOrganizationId), + orderBy: desc(projects.createdAt), + columns: { + projectId: true, + name: true, + }, + with: { + environments: { + columns: { + environmentId: true, + name: true, + isDefault: true, }, - mariadb: { - columns: { - mariadbId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, + with: { + applications: { + columns: { + applicationId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, }, - }, - postgres: { - columns: { - postgresId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, + mariadb: { + columns: { + mariadbId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, }, - }, - mysql: { - columns: { - mysqlId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, + postgres: { + columns: { + postgresId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, }, - }, - mongo: { - columns: { - mongoId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, + mysql: { + columns: { + mysqlId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, }, - }, - redis: { - columns: { - redisId: true, - appName: true, - name: true, - createdAt: true, - applicationStatus: true, - description: true, - serverId: true, + mongo: { + columns: { + mongoId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, }, - }, - compose: { - columns: { - composeId: true, - appName: true, - name: true, - createdAt: true, - composeStatus: true, - description: true, - serverId: true, + redis: { + columns: { + redisId: true, + appName: true, + name: true, + createdAt: true, + applicationStatus: true, + description: true, + serverId: true, + }, + }, + compose: { + columns: { + composeId: true, + appName: true, + name: true, + createdAt: true, + composeStatus: true, + description: true, + serverId: true, + }, }, }, }, }, - }, - }); - }), + }); + }, + ), search: protectedProcedure .input( @@ -482,8 +476,8 @@ export const projectRouter = createTRPCRouter({ ); } - if (ctx.user.role === "member") { - const { accessedProjects } = await findMemberById( + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedProjects } = await findMemberByUserId( ctx.user.id, ctx.session.activeOrganizationId, ); @@ -529,13 +523,6 @@ export const projectRouter = createTRPCRouter({ .input(apiRemoveProject) .mutation(async ({ input, ctx }) => { try { - if (ctx.user.role === "member") { - await checkProjectAccess( - ctx.user.id, - "delete", - ctx.session.activeOrganizationId, - ); - } const currentProject = await findProjectById(input.projectId); if ( currentProject.organizationId !== ctx.session.activeOrganizationId @@ -545,8 +532,15 @@ export const projectRouter = createTRPCRouter({ message: "You are not authorized to delete this project", }); } + await checkProjectAccess(ctx, "delete", input.projectId); const deletedProject = await deleteProject(input.projectId); + await audit(ctx, { + action: "delete", + resourceType: "project", + resourceId: currentProject.projectId, + resourceName: currentProject.name, + }); return deletedProject; } catch (error) { throw error; @@ -565,10 +559,36 @@ export const projectRouter = createTRPCRouter({ message: "You are not authorized to update this project", }); } + + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + const { accessedProjects } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (!accessedProjects.includes(input.projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } + + if (input.env !== undefined) { + await checkPermission(ctx, { projectEnvVars: ["write"] }); + } + const project = await updateProjectById(input.projectId, { ...input, }); + if (project) { + await audit(ctx, { + action: "update", + resourceType: "project", + resourceId: input.projectId, + resourceName: project.name, + }); + } return project; } catch (error) { throw error; @@ -602,15 +622,8 @@ export const projectRouter = createTRPCRouter({ ) .mutation(async ({ ctx, input }) => { try { - if (ctx.user.role === "member") { - await checkProjectAccess( - ctx.user.id, - "create", - ctx.session.activeOrganizationId, - ); - } + await checkProjectAccess(ctx, "create"); - // Get source project const sourceEnvironment = input.duplicateInSameProject ? await findEnvironmentById(input.sourceEnvironmentId) : null; @@ -626,7 +639,24 @@ export const projectRouter = createTRPCRouter({ }); } - // Create new project or use existing one + if ( + input.duplicateInSameProject && + sourceEnvironment && + ctx.user.role !== "owner" && + ctx.user.role !== "admin" + ) { + const { accessedProjects } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (!accessedProjects.includes(sourceEnvironment.project.projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } + const targetProject = input.duplicateInSameProject ? sourceEnvironment : await createProject( @@ -643,7 +673,6 @@ export const projectRouter = createTRPCRouter({ if (input.includeServices) { const servicesToDuplicate = input.selectedServices || []; - // Helper function to duplicate a service const duplicateService = async (id: string, type: string) => { switch (type) { case "application": { @@ -947,20 +976,22 @@ export const projectRouter = createTRPCRouter({ } }; - // Duplicate selected services for (const service of servicesToDuplicate) { await duplicateService(service.id, service.type); } } - if (!input.duplicateInSameProject && ctx.user.role === "member") { - await addNewProject( - ctx.user.id, - targetProject?.projectId || "", - ctx.session.activeOrganizationId, - ); + if (!input.duplicateInSameProject) { + await addNewProject(ctx, targetProject?.projectId || ""); } + await audit(ctx, { + action: "create", + resourceType: "project", + resourceId: targetProject?.projectId || "", + resourceName: input.name, + metadata: { duplicatedFrom: input.sourceEnvironmentId }, + }); return targetProject; } catch (error) { throw new TRPCError({ diff --git a/apps/dokploy/server/api/routers/proprietary/audit-log.ts b/apps/dokploy/server/api/routers/proprietary/audit-log.ts new file mode 100644 index 0000000000..3ff814f1cd --- /dev/null +++ b/apps/dokploy/server/api/routers/proprietary/audit-log.ts @@ -0,0 +1,67 @@ +import { getAuditLogs } from "@dokploy/server/services/proprietary/audit-log"; +import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { createTRPCRouter, withPermission } from "../../trpc"; + +export const auditLogRouter = createTRPCRouter({ + all: withPermission("auditLog", "read") + .use(async ({ ctx, next }) => { + const licensed = await hasValidLicense(ctx.session.activeOrganizationId); + if (!licensed) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Valid enterprise license required", + }); + } + return next(); + }) + .input( + z.object({ + userId: z.string().optional(), + userEmail: z.string().optional(), + resourceName: z.string().optional(), + action: z + .enum([ + "create", + "update", + "delete", + "deploy", + "cancel", + "redeploy", + "login", + "logout", + ]) + .optional(), + resourceType: z + .enum([ + "project", + "service", + "environment", + "deployment", + "user", + "customRole", + "domain", + "certificate", + "registry", + "server", + "sshKey", + "gitProvider", + "notification", + "settings", + "session", + ]) + .optional(), + from: z.date().optional(), + to: z.date().optional(), + limit: z.number().min(1).max(500).default(50), + offset: z.number().min(0).default(0), + }), + ) + .query(async ({ ctx, input }) => { + return getAuditLogs({ + organizationId: ctx.session.activeOrganizationId, + ...input, + }); + }), +}); diff --git a/apps/dokploy/server/api/routers/proprietary/custom-role.ts b/apps/dokploy/server/api/routers/proprietary/custom-role.ts new file mode 100644 index 0000000000..ed97a25c03 --- /dev/null +++ b/apps/dokploy/server/api/routers/proprietary/custom-role.ts @@ -0,0 +1,321 @@ +import { db } from "@dokploy/server/db"; +import { member, organizationRole, user } from "@dokploy/server/db/schema"; +import { statements } from "@dokploy/server/lib/access-control"; +import { TRPCError } from "@trpc/server"; +import { and, count, eq } from "drizzle-orm"; +import { z } from "zod"; +import { + createTRPCRouter, + enterpriseProcedure, + protectedProcedure, +} from "../../trpc"; +import { audit } from "../../utils/audit"; + +const permissionsSchema = z.record(z.string(), z.array(z.string())); + +export const customRoleRouter = createTRPCRouter({ + all: protectedProcedure.query(async ({ ctx }) => { + const [roles, memberCounts] = await Promise.all([ + db.query.organizationRole.findMany({ + where: eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + }), + db + .select({ role: member.role, count: count() }) + .from(member) + .where(eq(member.organizationId, ctx.session.activeOrganizationId)) + .groupBy(member.role), + ]); + + const memberCountByRole = new Map( + memberCounts.map((r) => [r.role, r.count]), + ); + + const roleMap = new Map< + string, + { + role: string; + permissions: Record; + createdAt: Date; + ids: string[]; + memberCount: number; + } + >(); + + for (const entry of roles) { + const existing = roleMap.get(entry.role); + const parsed = JSON.parse(entry.permission) as Record; + + if (existing) { + for (const [resource, actions] of Object.entries(parsed)) { + existing.permissions[resource] = [ + ...new Set([...(existing.permissions[resource] ?? []), ...actions]), + ]; + } + existing.ids.push(entry.id); + } else { + roleMap.set(entry.role, { + role: entry.role, + permissions: parsed, + createdAt: entry.createdAt, + ids: [entry.id], + memberCount: memberCountByRole.get(entry.role) ?? 0, + }); + } + } + + return Array.from(roleMap.values()); + }), + + create: enterpriseProcedure + .input( + z.object({ + roleName: z + .string() + .min(1) + .max(50) + .refine( + (name) => !["owner", "admin", "member"].includes(name), + "Cannot use reserved role names (owner, admin, member)", + ), + permissions: permissionsSchema, + }), + ) + .mutation(async ({ input, ctx }) => { + const existingRoles = await db.query.organizationRole.findMany({ + where: eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + }); + + const uniqueRoleNames = new Set(existingRoles.map((r) => r.role)); + + if (uniqueRoleNames.size >= 10) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Maximum of 10 custom roles per organization reached", + }); + } + + if (uniqueRoleNames.has(input.roleName)) { + throw new TRPCError({ + code: "CONFLICT", + message: `Role "${input.roleName}" already exists`, + }); + } + + validatePermissions(input.permissions); + + const [created] = await db + .insert(organizationRole) + .values({ + organizationId: ctx.session.activeOrganizationId, + role: input.roleName, + permission: JSON.stringify(input.permissions), + }) + .returning(); + + await audit(ctx, { + action: "create", + resourceType: "customRole", + resourceName: input.roleName, + }); + return created; + }), + + update: enterpriseProcedure + .input( + z.object({ + roleName: z.string().min(1), + newRoleName: z + .string() + .min(1) + .max(50) + .refine( + (name) => !["owner", "admin", "member"].includes(name), + "Cannot use reserved role names (owner, admin, member)", + ) + .optional(), + permissions: permissionsSchema, + }), + ) + .mutation(async ({ input, ctx }) => { + if (["owner", "admin", "member"].includes(input.roleName)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot modify built-in roles", + }); + } + + const effectiveRoleName = input.newRoleName ?? input.roleName; + + if (input.newRoleName && input.newRoleName !== input.roleName) { + const existing = await db.query.organizationRole.findFirst({ + where: and( + eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + eq(organizationRole.role, input.newRoleName), + ), + }); + if (existing) { + throw new TRPCError({ + code: "CONFLICT", + message: `Role "${input.newRoleName}" already exists`, + }); + } + + await db + .update(member) + .set({ role: input.newRoleName }) + .where( + and( + eq(member.organizationId, ctx.session.activeOrganizationId), + eq(member.role, input.roleName), + ), + ); + } + + validatePermissions(input.permissions); + + const [updated] = await db + .update(organizationRole) + .set({ + role: effectiveRoleName, + permission: JSON.stringify(input.permissions), + }) + .where( + and( + eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + eq(organizationRole.role, input.roleName), + ), + ) + .returning(); + + await audit(ctx, { + action: "update", + resourceType: "customRole", + resourceName: effectiveRoleName, + }); + return updated; + }), + + remove: enterpriseProcedure + .input( + z.object({ + roleName: z.string().min(1), + }), + ) + .mutation(async ({ input, ctx }) => { + if (["owner", "admin", "member"].includes(input.roleName)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot delete built-in roles", + }); + } + + const assignedMembers = await db.query.member.findMany({ + where: and( + eq(member.organizationId, ctx.session.activeOrganizationId), + eq(member.role, input.roleName), + ), + }); + + if (assignedMembers.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Cannot delete role "${input.roleName}": ${assignedMembers.length} member(s) are currently assigned to it. Reassign them first.`, + }); + } + + const deleted = await db + .delete(organizationRole) + .where( + and( + eq( + organizationRole.organizationId, + ctx.session.activeOrganizationId, + ), + eq(organizationRole.role, input.roleName), + ), + ) + .returning(); + + if (deleted.length === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Role "${input.roleName}" not found`, + }); + } + + await audit(ctx, { + action: "delete", + resourceType: "customRole", + resourceName: input.roleName, + }); + return { deleted: deleted.length }; + }), + + membersByRole: protectedProcedure + .input(z.object({ roleName: z.string().min(1) })) + .query(async ({ input, ctx }) => { + const members = await db + .select({ + id: member.id, + userId: member.userId, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }) + .from(member) + .innerJoin(user, eq(member.userId, user.id)) + .where( + and( + eq(member.organizationId, ctx.session.activeOrganizationId), + eq(member.role, input.roleName), + ), + ); + return members; + }), + + getStatements: protectedProcedure.query(() => { + return statements; + }), +}); + +const INTERNAL_RESOURCES = ["organization", "invitation", "team", "ac"]; + +function validatePermissions(permissions: Record) { + for (const [resource, actions] of Object.entries(permissions)) { + if (INTERNAL_RESOURCES.includes(resource)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Resource "${resource}" is managed internally and cannot be assigned to custom roles`, + }); + } + + if (!(resource in statements)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Unknown resource: ${resource}`, + }); + } + + const validActions = statements[resource as keyof typeof statements]; + for (const action of actions) { + if (!validActions.includes(action as never)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Invalid action "${action}" for resource "${resource}". Valid actions: ${validActions.join(", ")}`, + }); + } + } + } +} diff --git a/apps/dokploy/server/api/routers/proprietary/license-key.ts b/apps/dokploy/server/api/routers/proprietary/license-key.ts index 9bf8cb2869..2b2154f9b9 100644 --- a/apps/dokploy/server/api/routers/proprietary/license-key.ts +++ b/apps/dokploy/server/api/routers/proprietary/license-key.ts @@ -4,7 +4,11 @@ import { hasValidLicense, validateLicenseKey } from "@dokploy/server/index"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { z } from "zod"; -import { adminProcedure, createTRPCRouter } from "@/server/api/trpc"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, +} from "@/server/api/trpc"; import { activateLicenseKey, deactivateLicenseKey, @@ -183,7 +187,7 @@ export const licenseKeyRouter = createTRPCRouter({ licenseKey: currentUser.licenseKey ?? "", }; }), - haveValidLicenseKey: adminProcedure.query(async ({ ctx }) => { + haveValidLicenseKey: protectedProcedure.query(async ({ ctx }) => { return await hasValidLicense(ctx.session.activeOrganizationId); }), updateEnterpriseSettings: adminProcedure diff --git a/apps/dokploy/server/api/routers/redirects.ts b/apps/dokploy/server/api/routers/redirects.ts index f8b7014f2c..4b95d4fe5c 100644 --- a/apps/dokploy/server/api/routers/redirects.ts +++ b/apps/dokploy/server/api/routers/redirects.ts @@ -1,80 +1,74 @@ import { createRedirect, - findApplicationById, findRedirectById, removeRedirectById, updateRedirectById, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateRedirect, apiFindOneRedirect, apiUpdateRedirect, } from "@/server/db/schema"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; export const redirectsRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateRedirect) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return await createRedirect(input); + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); + await createRedirect(input); + await audit(ctx, { + action: "create", + resourceType: "redirect", + resourceId: input.applicationId, + resourceName: input.regex, + }); + return true; }), + one: protectedProcedure .input(apiFindOneRedirect) .query(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); - const application = await findApplicationById(redirect.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return findRedirectById(input.redirectId); + await checkServicePermissionAndAccess(ctx, redirect.applicationId, { + service: ["read"], + }); + return redirect; }), + delete: protectedProcedure .input(apiFindOneRedirect) .mutation(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); - const application = await findApplicationById(redirect.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return removeRedirectById(input.redirectId); + await checkServicePermissionAndAccess(ctx, redirect.applicationId, { + service: ["delete"], + }); + const result = await removeRedirectById(input.redirectId); + await audit(ctx, { + action: "delete", + resourceType: "redirect", + resourceId: input.redirectId, + }); + return result; }), + update: protectedProcedure .input(apiUpdateRedirect) .mutation(async ({ input, ctx }) => { const redirect = await findRedirectById(input.redirectId); - const application = await findApplicationById(redirect.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return updateRedirectById(input.redirectId, input); + await checkServicePermissionAndAccess(ctx, redirect.applicationId, { + service: ["create"], + }); + const result = await updateRedirectById(input.redirectId, input); + await audit(ctx, { + action: "update", + resourceType: "redirect", + resourceId: input.redirectId, + }); + return result; }), }); diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 94939bd208..01d922aa4e 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -1,12 +1,9 @@ import { - addNewService, checkPortInUse, - checkServiceAccess, createMount, createRedis, deployRedis, findEnvironmentById, - findMemberById, findProjectById, findRedisById, IS_CLOUD, @@ -19,10 +16,17 @@ import { stopServiceRemote, updateRedisById, } from "@dokploy/server"; +import { + addNewService, + checkServiceAccess, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, ilike, or, sql } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { apiChangeRedisStatus, @@ -42,18 +46,10 @@ export const redisRouter = createTRPCRouter({ .input(apiCreateRedis) .mutation(async ({ input, ctx }) => { try { - // Get project from environment const environment = await findEnvironmentById(input.environmentId); const project = await findProjectById(environment.projectId); - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - project.projectId, - ctx.session.activeOrganizationId, - "create", - ); - } + await checkServiceAccess(ctx, project.projectId, "create"); if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -71,13 +67,7 @@ export const redisRouter = createTRPCRouter({ const newRedis = await createRedis({ ...input, }); - if (ctx.user.role === "member") { - await addNewService( - ctx.user.id, - newRedis.redisId, - project.organizationId, - ); - } + await addNewService(ctx, newRedis.redisId); await createMount({ serviceId: newRedis.redisId, @@ -87,6 +77,12 @@ export const redisRouter = createTRPCRouter({ type: "volume", }); + await audit(ctx, { + action: "create", + resourceType: "service", + resourceId: newRedis.redisId, + resourceName: newRedis.appName, + }); return newRedis; } catch (error) { throw error; @@ -95,14 +91,7 @@ export const redisRouter = createTRPCRouter({ one: protectedProcedure .input(apiFindOneRedis) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.redisId, - ctx.session.activeOrganizationId, - "access", - ); - } + await checkServiceAccess(ctx, input.redisId, "read"); const redis = await findRedisById(input.redisId); if ( @@ -120,16 +109,10 @@ export const redisRouter = createTRPCRouter({ start: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this Redis", - }); - } if (redis.serverId) { await startServiceRemote(redis.serverId, redis.appName); @@ -140,21 +123,21 @@ export const redisRouter = createTRPCRouter({ applicationStatus: "done", }); + await audit(ctx, { + action: "start", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); return redis; }), reload: protectedProcedure .input(apiResetRedis) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this Redis", - }); - } if (redis.serverId) { await stopServiceRemote(redis.serverId, redis.appName); } else { @@ -172,22 +155,22 @@ export const redisRouter = createTRPCRouter({ await updateRedisById(input.redisId, { applicationStatus: "done", }); + await audit(ctx, { + action: "reload", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); return true; }), stop: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this Redis", - }); - } if (redis.serverId) { await stopServiceRemote(redis.serverId, redis.appName); } else { @@ -197,21 +180,21 @@ export const redisRouter = createTRPCRouter({ applicationStatus: "idle", }); + await audit(ctx, { + action: "stop", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); return redis; }), saveExternalPort: protectedProcedure .input(apiSaveExternalPortRedis) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + service: ["create"], + }); const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this external port", - }); - } if (input.externalPort) { const portCheck = await checkPortInUse( @@ -230,21 +213,27 @@ export const redisRouter = createTRPCRouter({ externalPort: input.externalPort, }); await deployRedis(input.redisId); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); return redis; }), deploy: protectedProcedure .input(apiDeployRedis) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Redis", - }); - } + await audit(ctx, { + action: "deploy", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); return deployRedis(input.redisId); }), deployWithLogs: protectedProcedure @@ -258,16 +247,9 @@ export const redisRouter = createTRPCRouter({ }) .input(apiDeployRedis) .subscription(async function* ({ input, ctx, signal }) { - const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this Redis", - }); - } + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const queue: string[] = []; const done = false; @@ -290,32 +272,25 @@ export const redisRouter = createTRPCRouter({ changeStatus: protectedProcedure .input(apiChangeRedisStatus) .mutation(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); const mongo = await findRedisById(input.redisId); - if ( - mongo.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to change this Redis status", - }); - } await updateRedisById(input.redisId, { applicationStatus: input.applicationStatus, }); + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: mongo.redisId, + resourceName: mongo.appName, + }); return mongo; }), remove: protectedProcedure .input(apiFindOneRedis) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - await checkServiceAccess( - ctx.user.id, - input.redisId, - ctx.session.activeOrganizationId, - "delete", - ); - } + await checkServiceAccess(ctx, input.redisId, "delete"); const redis = await findRedisById(input.redisId); @@ -328,6 +303,12 @@ export const redisRouter = createTRPCRouter({ message: "You are not authorized to delete this Redis", }); } + await audit(ctx, { + action: "delete", + resourceType: "service", + resourceId: redis.redisId, + resourceName: redis.appName, + }); const cleanupOperations = [ async () => await removeService(redis?.appName, redis.serverId), async () => await removeRedisById(input.redisId), @@ -344,16 +325,9 @@ export const redisRouter = createTRPCRouter({ saveEnvironment: protectedProcedure .input(apiSaveEnvironmentVariablesRedis) .mutation(async ({ input, ctx }) => { - const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.redisId, { + envVars: ["write"], + }); const updatedRedis = await updateRedisById(input.redisId, { env: input.env, }); @@ -365,12 +339,20 @@ export const redisRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: input.redisId, + }); return true; }), update: protectedProcedure .input(apiUpdateRedis) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const { redisId, ...rest } = input; + await checkServicePermissionAndAccess(ctx, redisId, { + service: ["create"], + }); const redis = await updateRedisById(redisId, { ...rest, }); @@ -382,6 +364,12 @@ export const redisRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "service", + resourceId: redisId, + resourceName: redis.appName, + }); return true; }), move: protectedProcedure @@ -392,31 +380,10 @@ export const redisRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move this redis", - }); - } - - const targetEnvironment = await findEnvironmentById( - input.targetEnvironmentId, - ); - if ( - targetEnvironment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to move to this environment", - }); - } + await checkServicePermissionAndAccess(ctx, input.redisId, { + service: ["create"], + }); - // Update the redis's projectId const updatedRedis = await db .update(redisTable) .set({ @@ -433,23 +400,27 @@ export const redisRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "move", + resourceType: "service", + resourceId: updatedRedis.redisId, + resourceName: updatedRedis.appName, + }); return updatedRedis; }), rebuild: protectedProcedure .input(apiRebuildRedis) .mutation(async ({ input, ctx }) => { - const redis = await findRedisById(input.redisId); - if ( - redis.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rebuild this Redis database", - }); - } + await checkServicePermissionAndAccess(ctx, input.redisId, { + deployment: ["create"], + }); - await rebuildDatabase(redis.redisId, "redis"); + await rebuildDatabase(input.redisId, "redis"); + await audit(ctx, { + action: "rebuild", + resourceType: "service", + resourceId: input.redisId, + }); return true; }), search: protectedProcedure @@ -498,19 +469,18 @@ export const redisRouter = createTRPCRouter({ ilike(redisTable.description ?? "", `%${input.description.trim()}%`), ); } - if (ctx.user.role === "member") { - const { accessedServices } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - if (accessedServices.length === 0) return { items: [], total: 0 }; - baseConditions.push( - sql`${redisTable.redisId} IN (${sql.join( - accessedServices.map((id) => sql`${id}`), - sql`, `, - )})`, - ); - } + const { accessedServices } = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (accessedServices.length === 0) return { items: [], total: 0 }; + baseConditions.push( + sql`${redisTable.redisId} IN (${sql.join( + accessedServices.map((id) => sql`${id}`), + sql`, `, + )})`, + ); + const where = and(...baseConditions); const [items, countResult] = await Promise.all([ db diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index 87f20c92e8..7e2174419a 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -19,14 +19,22 @@ import { apiUpdateRegistry, registry, } from "@/server/db/schema"; -import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +import { audit } from "@/server/api/utils/audit"; +import { createTRPCRouter, withPermission } from "../trpc"; export const registryRouter = createTRPCRouter({ - create: adminProcedure + create: withPermission("registry", "create") .input(apiCreateRegistry) .mutation(async ({ ctx, input }) => { - return await createRegistry(input, ctx.session.activeOrganizationId); + const reg = await createRegistry(input, ctx.session.activeOrganizationId); + await audit(ctx, { + action: "create", + resourceType: "registry", + resourceId: reg.registryId, + resourceName: reg.registryName, + }); + return reg; }), - remove: adminProcedure + remove: withPermission("registry", "delete") .input(apiRemoveRegistry) .mutation(async ({ ctx, input }) => { const registry = await findRegistryById(input.registryId); @@ -36,9 +44,15 @@ export const registryRouter = createTRPCRouter({ message: "You are not allowed to delete this registry", }); } + await audit(ctx, { + action: "delete", + resourceType: "registry", + resourceId: registry.registryId, + resourceName: registry.registryName, + }); return await removeRegistry(input.registryId); }), - update: protectedProcedure + update: withPermission("registry", "create") .input(apiUpdateRegistry) .mutation(async ({ input, ctx }) => { const { registryId, ...rest } = input; @@ -60,15 +74,21 @@ export const registryRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "update", + resourceType: "registry", + resourceId: registryId, + resourceName: registry.registryName, + }); return true; }), - all: protectedProcedure.query(async ({ ctx }) => { + all: withPermission("registry", "read").query(async ({ ctx }) => { const registryResponse = await db.query.registry.findMany({ where: eq(registry.organizationId, ctx.session.activeOrganizationId), }); return registryResponse; }), - one: adminProcedure + one: withPermission("registry", "read") .input(apiFindOneRegistry) .query(async ({ input, ctx }) => { const registry = await findRegistryById(input.registryId); @@ -80,7 +100,7 @@ export const registryRouter = createTRPCRouter({ } return registry; }), - testRegistry: protectedProcedure + testRegistry: withPermission("registry", "read") .input(apiTestRegistry) .mutation(async ({ input }) => { try { @@ -122,11 +142,10 @@ export const registryRouter = createTRPCRouter({ }); } }), - testRegistryById: protectedProcedure + testRegistryById: withPermission("registry", "read") .input(apiTestRegistryById) .mutation(async ({ input, ctx }) => { try { - // Get the full registry with password from database const registryData = await db.query.registry.findFirst({ where: eq(registry.registryId, input.registryId ?? ""), }); diff --git a/apps/dokploy/server/api/routers/rollbacks.ts b/apps/dokploy/server/api/routers/rollbacks.ts index d9e6180fb7..20c6412581 100644 --- a/apps/dokploy/server/api/routers/rollbacks.ts +++ b/apps/dokploy/server/api/routers/rollbacks.ts @@ -3,16 +3,31 @@ import { removeRollbackById, rollback, } from "@dokploy/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiFindOneRollback } from "@/server/db/schema"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; export const rollbackRouter = createTRPCRouter({ delete: protectedProcedure .input(apiFindOneRollback) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { - return removeRollbackById(input.rollbackId); + const rb = await findRollbackById(input.rollbackId); + const serviceId = rb.deployment.applicationId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + deployment: ["create"], + }); + } + const result = await removeRollbackById(input.rollbackId); + await audit(ctx, { + action: "delete", + resourceType: "deployment", + resourceId: input.rollbackId, + }); + return result; } catch (error) { const message = error instanceof Error @@ -28,17 +43,20 @@ export const rollbackRouter = createTRPCRouter({ .input(apiFindOneRollback) .mutation(async ({ input, ctx }) => { try { - const currentRollback = await findRollbackById(input.rollbackId); - if ( - currentRollback?.deployment?.application?.environment?.project - .organizationId !== ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to rollback this deployment", + const rb = await findRollbackById(input.rollbackId); + const serviceId = rb.deployment.applicationId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + deployment: ["create"], }); } - return await rollback(input.rollbackId); + const result = await rollback(input.rollbackId); + await audit(ctx, { + action: "restore", + resourceType: "deployment", + resourceId: input.rollbackId, + }); + return result; } catch (error) { console.error(error); throw new TRPCError({ diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index 3bf284b763..2563fd7acf 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -16,12 +16,20 @@ import { import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { removeJob, schedule } from "@/server/utils/backup"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const scheduleRouter = createTRPCRouter({ create: protectedProcedure .input(createScheduleSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const serviceId = input.applicationId || input.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + schedule: ["create"], + }); + } const newSchedule = await createSchedule(input); if (newSchedule?.enabled) { @@ -36,12 +44,26 @@ export const scheduleRouter = createTRPCRouter({ scheduleJob(newSchedule); } } + await audit(ctx, { + action: "create", + resourceType: "schedule", + resourceId: newSchedule?.scheduleId, + resourceName: newSchedule?.name, + }); return newSchedule; }), update: protectedProcedure .input(updateScheduleSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const existingSchedule = await findScheduleById(input.scheduleId); + const serviceId = + existingSchedule.applicationId || existingSchedule.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + schedule: ["update"], + }); + } const updatedSchedule = await updateSchedule(input); if (IS_CLOUD) { @@ -67,24 +89,42 @@ export const scheduleRouter = createTRPCRouter({ removeScheduleJob(updatedSchedule.scheduleId); } } + await audit(ctx, { + action: "update", + resourceType: "schedule", + resourceId: updatedSchedule.scheduleId, + resourceName: updatedSchedule.name, + }); return updatedSchedule; }), delete: protectedProcedure .input(z.object({ scheduleId: z.string() })) - .mutation(async ({ input }) => { - const schedule = await findScheduleById(input.scheduleId); + .mutation(async ({ input, ctx }) => { + const scheduleItem = await findScheduleById(input.scheduleId); + const serviceId = scheduleItem.applicationId || scheduleItem.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + schedule: ["delete"], + }); + } await deleteSchedule(input.scheduleId); if (IS_CLOUD) { await removeJob({ - cronSchedule: schedule.cronExpression, - scheduleId: schedule.scheduleId, + cronSchedule: scheduleItem.cronExpression, + scheduleId: scheduleItem.scheduleId, type: "schedule", }); } else { - removeScheduleJob(schedule.scheduleId); + removeScheduleJob(scheduleItem.scheduleId); } + await audit(ctx, { + action: "delete", + resourceType: "schedule", + resourceId: scheduleItem.scheduleId, + resourceName: scheduleItem.name, + }); return true; }), @@ -100,7 +140,15 @@ export const scheduleRouter = createTRPCRouter({ ]), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + if ( + input.scheduleType === "application" || + input.scheduleType === "compose" + ) { + await checkServicePermissionAndAccess(ctx, input.id, { + schedule: ["read"], + }); + } const where = { application: eq(schedules.applicationId, input.id), compose: eq(schedules.composeId, input.id), @@ -122,15 +170,34 @@ export const scheduleRouter = createTRPCRouter({ one: protectedProcedure .input(z.object({ scheduleId: z.string() })) - .query(async ({ input }) => { - return await findScheduleById(input.scheduleId); + .query(async ({ input, ctx }) => { + const schedule = await findScheduleById(input.scheduleId); + const serviceId = schedule.applicationId || schedule.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + schedule: ["read"], + }); + } + return schedule; }), runManually: protectedProcedure .input(z.object({ scheduleId: z.string().min(1) })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const scheduleItem = await findScheduleById(input.scheduleId); + const serviceId = scheduleItem.applicationId || scheduleItem.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + schedule: ["create"], + }); + } try { await runCommand(input.scheduleId); + await audit(ctx, { + action: "run", + resourceType: "schedule", + resourceId: input.scheduleId, + }); return true; } catch (error) { throw new TRPCError({ diff --git a/apps/dokploy/server/api/routers/security.ts b/apps/dokploy/server/api/routers/security.ts index 5dd5a6ddcb..f2c8fc5ebc 100644 --- a/apps/dokploy/server/api/routers/security.ts +++ b/apps/dokploy/server/api/routers/security.ts @@ -1,80 +1,74 @@ import { createSecurity, deleteSecurityById, - findApplicationById, findSecurityById, updateSecurityById, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; import { apiCreateSecurity, apiFindOneSecurity, apiUpdateSecurity, } from "@/server/db/schema"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; export const securityRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateSecurity) .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return await createSecurity(input); + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["create"], + }); + await createSecurity(input); + await audit(ctx, { + action: "create", + resourceType: "security", + resourceId: input.applicationId, + resourceName: input.username, + }); + return true; }), + one: protectedProcedure .input(apiFindOneSecurity) .query(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); - const application = await findApplicationById(security.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } + await checkServicePermissionAndAccess(ctx, security.applicationId, { + service: ["read"], + }); return security; }), + delete: protectedProcedure .input(apiFindOneSecurity) .mutation(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); - const application = await findApplicationById(security.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return await deleteSecurityById(input.securityId); + await checkServicePermissionAndAccess(ctx, security.applicationId, { + service: ["delete"], + }); + const result = await deleteSecurityById(input.securityId); + await audit(ctx, { + action: "delete", + resourceType: "security", + resourceId: input.securityId, + }); + return result; }), + update: protectedProcedure .input(apiUpdateSecurity) .mutation(async ({ input, ctx }) => { const security = await findSecurityById(input.securityId); - const application = await findApplicationById(security.applicationId); - if ( - application.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return await updateSecurityById(input.securityId, input); + await checkServicePermissionAndAccess(ctx, security.applicationId, { + service: ["create"], + }); + const result = await updateSecurityById(input.securityId, input); + await audit(ctx, { + action: "update", + resourceType: "security", + resourceId: input.securityId, + }); + return result; }), }); diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 9df8a57896..7fabe242dd 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -21,7 +21,12 @@ import { observable } from "@trpc/server/observable"; import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm"; import { z } from "zod"; import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; +import { + createTRPCRouter, + protectedProcedure, + withPermission, +} from "@/server/api/trpc"; import { apiCreateServer, apiFindOneServer, @@ -40,7 +45,7 @@ import { } from "@/server/db/schema"; export const serverRouter = createTRPCRouter({ - create: protectedProcedure + create: withPermission("server", "create") .input(apiCreateServer) .mutation(async ({ ctx, input }) => { try { @@ -56,6 +61,12 @@ export const serverRouter = createTRPCRouter({ input, ctx.session.activeOrganizationId, ); + await audit(ctx, { + action: "create", + resourceType: "server", + resourceId: project.serverId, + resourceName: project.name, + }); return project; } catch (error) { throw new TRPCError({ @@ -66,7 +77,7 @@ export const serverRouter = createTRPCRouter({ } }), - one: protectedProcedure + one: withPermission("server", "read") .input(apiFindOneServer) .query(async ({ input, ctx }) => { const server = await findServerById(input.serverId); @@ -79,14 +90,14 @@ export const serverRouter = createTRPCRouter({ return server; }), - getDefaultCommand: protectedProcedure + getDefaultCommand: withPermission("server", "read") .input(apiFindOneServer) .query(async ({ input }) => { const server = await findServerById(input.serverId); const isBuildServer = server.serverType === "build"; return defaultCommand(isBuildServer); }), - all: protectedProcedure.query(async ({ ctx }) => { + all: withPermission("server", "read").query(async ({ ctx }) => { const result = await db .select({ ...getTableColumns(server), @@ -118,7 +129,7 @@ export const serverRouter = createTRPCRouter({ return servers.length ?? 0; }), - withSSHKey: protectedProcedure.query(async ({ ctx }) => { + withSSHKey: withPermission("server", "read").query(async ({ ctx }) => { const result = await db.query.server.findMany({ orderBy: desc(server.createdAt), where: IS_CLOUD @@ -136,7 +147,7 @@ export const serverRouter = createTRPCRouter({ }); return result; }), - buildServers: protectedProcedure.query(async ({ ctx }) => { + buildServers: withPermission("server", "read").query(async ({ ctx }) => { const result = await db.query.server.findMany({ orderBy: desc(server.createdAt), where: IS_CLOUD @@ -154,7 +165,7 @@ export const serverRouter = createTRPCRouter({ }); return result; }), - setup: protectedProcedure + setup: withPermission("server", "create") .input(apiFindOneServer) .mutation(async ({ input, ctx }) => { try { @@ -166,12 +177,18 @@ export const serverRouter = createTRPCRouter({ }); } const currentServer = await serverSetup(input.serverId); + await audit(ctx, { + action: "update", + resourceType: "server", + resourceId: input.serverId, + resourceName: server.name, + }); return currentServer; } catch (error) { throw error; } }), - setupWithLogs: protectedProcedure + setupWithLogs: withPermission("server", "create") .meta({ openapi: { path: "/deploy/server-with-logs", @@ -199,7 +216,7 @@ export const serverRouter = createTRPCRouter({ throw error; } }), - validate: protectedProcedure + validate: withPermission("server", "read") .input(apiFindOneServer) .query(async ({ input, ctx }) => { try { @@ -245,7 +262,7 @@ export const serverRouter = createTRPCRouter({ } }), - security: protectedProcedure + security: withPermission("server", "read") .input(apiFindOneServer) .query(async ({ input, ctx }) => { try { @@ -295,7 +312,7 @@ export const serverRouter = createTRPCRouter({ }); } }), - setupMonitoring: protectedProcedure + setupMonitoring: withPermission("server", "create") .input(apiUpdateServerMonitoring) .mutation(async ({ input, ctx }) => { try { @@ -332,22 +349,21 @@ export const serverRouter = createTRPCRouter({ }, }); const currentServer = await setupMonitoring(input.serverId); + await audit(ctx, { + action: "update", + resourceType: "server", + resourceId: input.serverId, + resourceName: server.name, + }); return currentServer; } catch (error) { throw error; } }), - remove: protectedProcedure + remove: withPermission("server", "delete") .input(apiRemoveServer) .mutation(async ({ input, ctx }) => { try { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session.activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this server", - }); - } const activeServers = await haveActiveServices(input.serverId); if (activeServers) { @@ -357,6 +373,12 @@ export const serverRouter = createTRPCRouter({ }); } const currentServer = await findServerById(input.serverId); + await audit(ctx, { + action: "delete", + resourceType: "server", + resourceId: currentServer.serverId, + resourceName: currentServer.name, + }); await removeDeploymentsByServerId(currentServer); await deleteServer(input.serverId); @@ -371,7 +393,7 @@ export const serverRouter = createTRPCRouter({ throw error; } }), - update: protectedProcedure + update: withPermission("server", "create") .input(apiUpdateServer) .mutation(async ({ input, ctx }) => { try { @@ -393,6 +415,12 @@ export const serverRouter = createTRPCRouter({ ...input, }); + await audit(ctx, { + action: "update", + resourceType: "server", + resourceId: input.serverId, + resourceName: server.name, + }); return currentServer; } catch (error) { throw error; @@ -414,7 +442,7 @@ export const serverRouter = createTRPCRouter({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }; }), - getServerMetrics: protectedProcedure + getServerMetrics: withPermission("monitoring", "read") .input( z.object({ url: z.string(), diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 30cb522ba7..e52842f731 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -1,6 +1,5 @@ import { CLEANUP_CRON_JOB, - canAccessToTraefikFiles, checkGPUStatus, checkPortInUse, cleanupAll, @@ -45,6 +44,7 @@ import { writeTraefikConfigInPath, writeTraefikSetup, } from "@dokploy/server"; +import { checkPermission } from "@dokploy/server/services/permission"; import { db } from "@dokploy/server/db"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { TRPCError } from "@trpc/server"; @@ -52,6 +52,7 @@ import { eq, sql } from "drizzle-orm"; import { scheduledJobs, scheduleJob } from "node-schedule"; import { parse, stringify } from "yaml"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { apiAssignDomain, apiEnableDashboard, @@ -84,14 +85,19 @@ export const settingsRouter = createTRPCRouter({ const settings = await getWebServerSettings(); return settings; }), - reloadServer: adminProcedure.mutation(async () => { + reloadServer: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } await reloadDockerResource("dokploy", undefined, packageInfo.version); + await audit(ctx, { + action: "reload", + resourceType: "settings", + resourceName: "dokploy", + }); return true; }), - cleanRedis: adminProcedure.mutation(async () => { + cleanRedis: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } @@ -107,36 +113,56 @@ export const settingsRouter = createTRPCRouter({ const redisContainerId = containerId.trim(); await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "clean-redis", + }); return true; }), - reloadRedis: adminProcedure.mutation(async () => { + reloadRedis: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } await reloadDockerResource("dokploy-redis"); - + await audit(ctx, { + action: "reload", + resourceType: "settings", + resourceName: "dokploy-redis", + }); return true; }), - cleanAllDeploymentQueue: adminProcedure.mutation(async () => { + cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } - return cleanAllDeploymentQueue(); + const result = cleanAllDeploymentQueue(); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "clean-deployment-queue", + }); + return result; }), reloadTraefik: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { // Run in background so the request returns immediately; avoids proxy timeouts. void reloadDockerResource("dokploy-traefik", input?.serverId).catch( (err) => { console.error("reloadTraefik background:", err); }, ); + await audit(ctx, { + action: "reload", + resourceType: "settings", + resourceName: "dokploy-traefik", + }); return true; }), toggleDashboard: adminProcedure .input(apiEnableDashboard) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const ports = await readPorts("dokploy-traefik", input.serverId); const env = await readEnvironmentVariables( "dokploy-traefik", @@ -175,70 +201,112 @@ export const settingsRouter = createTRPCRouter({ }).catch((err) => { console.error("toggleDashboard background writeTraefikSetup:", err); }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "toggle-dashboard", + }); return true; }), cleanUnusedImages: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanupImages(input?.serverId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-unused-images", + }); return true; }), cleanUnusedVolumes: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanupVolumes(input?.serverId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-unused-volumes", + }); return true; }), cleanStoppedContainers: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanupContainers(input?.serverId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-stopped-containers", + }); return true; }), cleanDockerBuilder: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanupBuilders(input?.serverId); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-docker-builder", + }); }), cleanDockerPrune: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { await cleanupSystem(input?.serverId); await cleanupBuilders(input?.serverId); - + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-docker-prune", + }); return true; }), cleanAll: adminProcedure .input(apiServerSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { // Execute cleanup in background and return immediately to avoid gateway timeouts const result = await cleanupAllBackground(input?.serverId); - + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-all", + }); return result; }), - cleanMonitoring: adminProcedure.mutation(async () => { + cleanMonitoring: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } const { MONITORING_PATH } = paths(); await recreateDirectory(MONITORING_PATH); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "clean-monitoring", + }); return true; }), saveSSHPrivateKey: adminProcedure .input(apiSaveSSHKey) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } await updateWebServerSettings({ sshPrivateKey: input.sshPrivateKey, }); - + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "ssh-private-key", + }); return true; }), assignDomainServer: adminProcedure .input(apiAssignDomain) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } @@ -261,15 +329,25 @@ export const settingsRouter = createTRPCRouter({ updateLetsEncryptEmail(input.letsEncryptEmail); } + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "assign-domain-server", + }); return settings; }), - cleanSSHPrivateKey: adminProcedure.mutation(async () => { + cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } await updateWebServerSettings({ sshPrivateKey: null, }); + await audit(ctx, { + action: "delete", + resourceType: "settings", + resourceName: "ssh-private-key", + }); return true; }), updateDockerCleanup: adminProcedure @@ -349,6 +427,11 @@ export const settingsRouter = createTRPCRouter({ } } + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "docker-cleanup", + }); return true; }), @@ -362,11 +445,16 @@ export const settingsRouter = createTRPCRouter({ updateTraefikConfig: adminProcedure .input(apiTraefikConfig) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } writeMainConfig(input.traefikConfig); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "traefik-config", + }); return true; }), @@ -379,11 +467,16 @@ export const settingsRouter = createTRPCRouter({ }), updateWebServerTraefikConfig: adminProcedure .input(apiTraefikConfig) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } writeConfig("dokploy", input.traefikConfig); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "web-server-traefik-config", + }); return true; }), @@ -397,11 +490,16 @@ export const settingsRouter = createTRPCRouter({ updateMiddlewareTraefikConfig: adminProcedure .input(apiTraefikConfig) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } writeConfig("middlewares", input.traefikConfig); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "middleware-traefik-config", + }); return true; }), getUpdateData: protectedProcedure.mutation(async () => { @@ -411,7 +509,7 @@ export const settingsRouter = createTRPCRouter({ return await getUpdateData(packageInfo.version); }), - updateServer: adminProcedure.mutation(async () => { + updateServer: adminProcedure.mutation(async ({ ctx }) => { if (IS_CLOUD) { return true; } @@ -426,6 +524,11 @@ export const settingsRouter = createTRPCRouter({ `dokploy/dokploy:${data.latestVersion}`, "dokploy", ]); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "dokploy-version", + }); } return true; @@ -441,16 +544,7 @@ export const settingsRouter = createTRPCRouter({ .input(apiServerSchema) .query(async ({ ctx, input }) => { try { - if (ctx.user.role === "member") { - const canAccess = await canAccessToTraefikFiles( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await checkPermission(ctx, { traefikFiles: ["read"] }); const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); return result || []; @@ -462,37 +556,24 @@ export const settingsRouter = createTRPCRouter({ updateTraefikFile: protectedProcedure .input(apiModifyTraefikConfig) .mutation(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - const canAccess = await canAccessToTraefikFiles( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await checkPermission(ctx, { traefikFiles: ["write"] }); await writeTraefikConfigInPath( input.path, input.traefikConfig, input?.serverId, ); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "traefik-file", + }); return true; }), readTraefikFile: protectedProcedure .input(apiReadTraefikConfig) .query(async ({ input, ctx }) => { - if (ctx.user.role === "member") { - const canAccess = await canAccessToTraefikFiles( - ctx.user.id, - ctx.session.activeOrganizationId, - ); - - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await checkPermission(ctx, { traefikFiles: ["read"] }); if (input.serverId) { const server = await findServerById(input.serverId); @@ -517,13 +598,18 @@ export const settingsRouter = createTRPCRouter({ serverIp: z.string(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } const settings = await updateWebServerSettings({ serverIp: input.serverIp, }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "server-ip", + }); return settings; }), @@ -610,7 +696,7 @@ export const settingsRouter = createTRPCRouter({ writeTraefikEnv: adminProcedure .input(z.object({ env: z.string(), serverId: z.string().optional() })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const envs = prepareEnvironmentVariables(input.env); const ports = await readPorts("dokploy-traefik", input?.serverId); @@ -622,6 +708,11 @@ export const settingsRouter = createTRPCRouter({ }).catch((err) => { console.error("writeTraefikEnv background writeTraefikSetup:", err); }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "traefik-env", + }); return true; }), haveTraefikDashboardPortEnabled: adminProcedure @@ -715,7 +806,7 @@ export const settingsRouter = createTRPCRouter({ enable: z.boolean(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } @@ -742,7 +833,11 @@ export const settingsRouter = createTRPCRouter({ } writeMainConfig(stringify(currentConfig)); - + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "toggle-requests", + }); return true; }), isCloud: publicProcedure.query(async () => { @@ -775,13 +870,18 @@ export const settingsRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD && !input.serverId) { throw new Error("Select a server to enable the GPU Setup"); } try { await setupGPUSupport(input.serverId); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "setup-gpu", + }); return { success: true }; } catch (error) { console.error("GPU Setup Error:", error); @@ -835,7 +935,7 @@ export const settingsRouter = createTRPCRouter({ ), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -873,6 +973,11 @@ export const settingsRouter = createTRPCRouter({ err, ); }); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "traefik-ports", + }); return true; } catch (error) { throw new TRPCError({ @@ -897,14 +1002,22 @@ export const settingsRouter = createTRPCRouter({ cronExpression: z.string().nullable(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { if (IS_CLOUD) { return true; } + let result: boolean; if (input.cronExpression) { - return startLogCleanup(input.cronExpression); + result = await startLogCleanup(input.cronExpression); + } else { + result = await stopLogCleanup(); } - return stopLogCleanup(); + await audit(ctx, { + action: "update", + resourceType: "settings", + resourceName: "log-cleanup", + }); + return result; }), getLogCleanupStatus: protectedProcedure.query(async () => { diff --git a/apps/dokploy/server/api/routers/ssh-key.ts b/apps/dokploy/server/api/routers/ssh-key.ts index 45ca68bd90..74aeb5e583 100644 --- a/apps/dokploy/server/api/routers/ssh-key.ts +++ b/apps/dokploy/server/api/routers/ssh-key.ts @@ -8,7 +8,8 @@ import { import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; -import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { audit } from "@/server/api/utils/audit"; +import { createTRPCRouter, withPermission } from "@/server/api/trpc"; import { apiCreateSshKey, apiFindOneSshKey, @@ -19,7 +20,7 @@ import { } from "@/server/db/schema"; export const sshRouter = createTRPCRouter({ - create: protectedProcedure + create: withPermission("sshKeys", "create") .input(apiCreateSshKey) .mutation(async ({ input, ctx }) => { try { @@ -27,6 +28,11 @@ export const sshRouter = createTRPCRouter({ ...input, organizationId: ctx.session.activeOrganizationId, }); + await audit(ctx, { + action: "create", + resourceType: "sshKey", + resourceName: input.name, + }); } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -35,7 +41,7 @@ export const sshRouter = createTRPCRouter({ }); } }), - remove: protectedProcedure + remove: withPermission("sshKeys", "delete") .input(apiRemoveSshKey) .mutation(async ({ input, ctx }) => { try { @@ -47,12 +53,18 @@ export const sshRouter = createTRPCRouter({ }); } + await audit(ctx, { + action: "delete", + resourceType: "sshKey", + resourceId: sshKey.sshKeyId, + resourceName: sshKey.name, + }); return await removeSSHKeyById(input.sshKeyId); } catch (error) { throw error; } }), - one: protectedProcedure + one: withPermission("sshKeys", "read") .input(apiFindOneSshKey) .query(async ({ input, ctx }) => { const sshKey = await findSSHKeyById(input.sshKeyId); @@ -65,18 +77,18 @@ export const sshRouter = createTRPCRouter({ } return sshKey; }), - all: protectedProcedure.query(async ({ ctx }) => { + all: withPermission("sshKeys", "read").query(async ({ ctx }) => { return await db.query.sshKeys.findMany({ where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId), orderBy: desc(sshKeys.createdAt), }); }), - generate: protectedProcedure + generate: withPermission("sshKeys", "read") .input(apiGenerateSSHKey) .mutation(async ({ input }) => { return await generateSSHKey(input.type); }), - update: protectedProcedure + update: withPermission("sshKeys", "create") .input(apiUpdateSshKey) .mutation(async ({ input, ctx }) => { try { @@ -87,7 +99,14 @@ export const sshRouter = createTRPCRouter({ message: "You are not allowed to update this SSH key", }); } - return await updateSSHKeyById(input); + const result = await updateSSHKeyById(input); + await audit(ctx, { + action: "update", + resourceType: "sshKey", + resourceId: sshKey.sshKeyId, + resourceName: sshKey.name, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index cd3b042e95..c5ad7656e0 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -1,58 +1,38 @@ import { - findServerById, getApplicationInfo, getNodeApplications, getNodeInfo, getSwarmNodes, } from "@dokploy/server"; -import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { createTRPCRouter, withPermission } from "../trpc"; import { containerIdRegex } from "./docker"; export const swarmRouter = createTRPCRouter({ - getNodes: protectedProcedure + getNodes: withPermission("server", "read") .input( z.object({ serverId: z.string().optional(), }), ) - .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + .query(async ({ input }) => { return await getSwarmNodes(input.serverId); }), - getNodeInfo: protectedProcedure + getNodeInfo: withPermission("server", "read") .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) - .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + .query(async ({ input }) => { return await getNodeInfo(input.nodeId, input.serverId); }), - getNodeApps: protectedProcedure + getNodeApps: withPermission("server", "read") .input( z.object({ serverId: z.string().optional(), }), ) - .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + .query(async ({ input }) => { return getNodeApplications(input.serverId); }), - getAppInfos: protectedProcedure + getAppInfos: withPermission("server", "read") .meta({ openapi: { path: "/drop-deployment", @@ -71,13 +51,7 @@ export const swarmRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + .query(async ({ input }) => { return await getApplicationInfo(input.appName, input.serverId); }), }); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 067f57bbc4..56b37b7afb 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -22,15 +22,21 @@ import { invitation, member, } from "@dokploy/server/db/schema"; +import { + hasPermission, + resolvePermissions, +} from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { and, asc, eq, gt } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure, + withPermission, } from "../trpc"; const apiCreateApiKey = z.object({ @@ -51,7 +57,7 @@ const apiCreateApiKey = z.object({ }); export const userRouter = createTRPCRouter({ - all: adminProcedure.query(async ({ ctx }) => { + all: withPermission("member", "read").query(async ({ ctx }) => { return await db.query.member.findMany({ where: eq(member.organizationId, ctx.session.activeOrganizationId), with: { @@ -87,16 +93,20 @@ export const userRouter = createTRPCRouter({ // Allow access if: // 1. User is requesting their own information - // 2. User has owner role (admin permissions) AND user is in the same organization + // 2. User is owner/admin + // 3. User has member.update permission (custom roles managing permissions) if ( memberResult.userId !== ctx.user.id && ctx.user.role !== "owner" && ctx.user.role !== "admin" ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this user", - }); + const canUpdate = await hasPermission(ctx, { member: ["update"] }); + if (!canUpdate) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this user", + }); + } } return memberResult; @@ -131,6 +141,9 @@ export const userRouter = createTRPCRouter({ return memberResult; }), + getPermissions: protectedProcedure.query(async ({ ctx }) => { + return resolvePermissions(ctx); + }), haveRootAccess: protectedProcedure.query(async ({ ctx }) => { if (!IS_CLOUD) { return false; @@ -166,19 +179,21 @@ export const userRouter = createTRPCRouter({ return memberResult?.user; }), - getServerMetrics: protectedProcedure.query(async ({ ctx }) => { - const memberResult = await db.query.member.findFirst({ - where: and( - eq(member.userId, ctx.user.id), - eq(member.organizationId, ctx.session?.activeOrganizationId || ""), - ), - with: { - user: true, - }, - }); + getServerMetrics: withPermission("monitoring", "read").query( + async ({ ctx }) => { + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.userId, ctx.user.id), + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + with: { + user: true, + }, + }); - return memberResult?.user; - }), + return memberResult?.user; + }, + ), update: protectedProcedure .input(apiUpdateUser) .mutation(async ({ input, ctx }) => { @@ -213,7 +228,14 @@ export const userRouter = createTRPCRouter({ } try { - return await updateUser(ctx.user.id, input); + const result = await updateUser(ctx.user.id, input); + await audit(ctx, { + action: "update", + resourceType: "user", + resourceId: ctx.user.id, + resourceName: ctx.user.email, + }); + return result; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -227,15 +249,17 @@ export const userRouter = createTRPCRouter({ .query(async ({ input }) => { return await getUserByToken(input.token); }), - getMetricsToken: protectedProcedure.query(async ({ ctx }) => { - const user = await findUserById(ctx.user.ownerId); - const settings = await getWebServerSettings(); - return { - serverIp: settings?.serverIp, - enabledFeatures: user.enablePaidFeatures, - metricsConfig: settings?.metricsConfig, - }; - }), + getMetricsToken: withPermission("monitoring", "read").query( + async ({ ctx }) => { + const user = await findUserById(ctx.user.ownerId); + const settings = await getWebServerSettings(); + return { + serverIp: settings?.serverIp, + enabledFeatures: user.enablePaidFeatures, + metricsConfig: settings?.metricsConfig, + }; + }, + ), remove: protectedProcedure .input( z.object({ @@ -297,9 +321,15 @@ export const userRouter = createTRPCRouter({ }); } - return await removeUserById(input.userId); + const result = await removeUserById(input.userId); + await audit(ctx, { + action: "delete", + resourceType: "user", + resourceId: input.userId, + }); + return result; }), - assignPermissions: adminProcedure + assignPermissions: withPermission("member", "update") .input(apiAssignPermissions) .mutation(async ({ input, ctx }) => { try { @@ -330,6 +360,12 @@ export const userRouter = createTRPCRouter({ ), ), ); + await audit(ctx, { + action: "update", + resourceType: "user", + resourceId: input.id, + metadata: { permissions: rest }, + }); } catch (error) { throw error; } @@ -347,7 +383,7 @@ export const userRouter = createTRPCRouter({ }); }), - getContainerMetrics: protectedProcedure + getContainerMetrics: withPermission("monitoring", "read") .input( z.object({ url: z.string(), @@ -437,6 +473,12 @@ export const userRouter = createTRPCRouter({ } await db.delete(apikey).where(eq(apikey.id, input.apiKeyId)); + await audit(ctx, { + action: "delete", + resourceType: "user", + resourceId: input.apiKeyId, + resourceName: apiKeyToDelete.name || undefined, + }); return true; } catch (error) { throw error; @@ -464,6 +506,12 @@ export const userRouter = createTRPCRouter({ } const apiKey = await createApiKey(ctx.user.id, input); + await audit(ctx, { + action: "create", + resourceType: "user", + resourceId: apiKey.id, + resourceName: input.name, + }); return apiKey; }), @@ -508,7 +556,7 @@ export const userRouter = createTRPCRouter({ return organizations.length; }), - sendInvitation: adminProcedure + sendInvitation: withPermission("member", "create") .input( z.object({ invitationId: z.string().min(1), @@ -574,6 +622,13 @@ export const userRouter = createTRPCRouter({ console.log(error); throw error; } + await audit(ctx, { + action: "create", + resourceType: "user", + resourceId: input.invitationId, + resourceName: currentInvitation?.email || "", + metadata: { type: "sendInvitation" }, + }); return inviteLink; }), }); diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index de147a8bf4..f9675456b1 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -23,8 +23,10 @@ import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { desc, eq } from "drizzle-orm"; import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; import { removeJob, schedule, updateJob } from "@/server/utils/backup"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; export const volumeBackupsRouter = createTRPCRouter({ list: protectedProcedure @@ -42,7 +44,10 @@ export const volumeBackupsRouter = createTRPCRouter({ ]), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await checkServicePermissionAndAccess(ctx, input.id, { + volumeBackup: ["read"], + }); return await db.query.volumeBackups.findMany({ where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id), with: { @@ -59,7 +64,20 @@ export const volumeBackupsRouter = createTRPCRouter({ }), create: protectedProcedure .input(createVolumeBackupSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const serviceId = + input.applicationId || + input.postgresId || + input.mysqlId || + input.mariadbId || + input.mongoId || + input.redisId || + input.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volumeBackup: ["create"], + }); + } const newVolumeBackup = await createVolumeBackup(input); if (newVolumeBackup?.enabled) { @@ -73,6 +91,11 @@ export const volumeBackupsRouter = createTRPCRouter({ await scheduleVolumeBackup(newVolumeBackup.volumeBackupId); } } + await audit(ctx, { + action: "create", + resourceType: "volumeBackup", + resourceId: newVolumeBackup?.volumeBackupId, + }); return newVolumeBackup; }), one: protectedProcedure @@ -81,8 +104,22 @@ export const volumeBackupsRouter = createTRPCRouter({ volumeBackupId: z.string().min(1), }), ) - .query(async ({ input }) => { - return await findVolumeBackupById(input.volumeBackupId); + .query(async ({ input, ctx }) => { + const vb = await findVolumeBackupById(input.volumeBackupId); + const serviceId = + vb.applicationId || + vb.postgresId || + vb.mysqlId || + vb.mariadbId || + vb.mongoId || + vb.redisId || + vb.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volumeBackup: ["read"], + }); + } + return vb; }), delete: protectedProcedure .input( @@ -90,12 +127,46 @@ export const volumeBackupsRouter = createTRPCRouter({ volumeBackupId: z.string().min(1), }), ) - .mutation(async ({ input }) => { - return await removeVolumeBackup(input.volumeBackupId); + .mutation(async ({ input, ctx }) => { + const vb = await findVolumeBackupById(input.volumeBackupId); + const serviceId = + vb.applicationId || + vb.postgresId || + vb.mysqlId || + vb.mariadbId || + vb.mongoId || + vb.redisId || + vb.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volumeBackup: ["delete"], + }); + } + const result = await removeVolumeBackup(input.volumeBackupId); + await audit(ctx, { + action: "delete", + resourceType: "volumeBackup", + resourceId: input.volumeBackupId, + }); + return result; }), update: protectedProcedure .input(updateVolumeBackupSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const existingVb = await findVolumeBackupById(input.volumeBackupId); + const serviceId = + existingVb.applicationId || + existingVb.postgresId || + existingVb.mysqlId || + existingVb.mariadbId || + existingVb.mongoId || + existingVb.redisId || + existingVb.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volumeBackup: ["update"], + }); + } const updatedVolumeBackup = await updateVolumeBackup( input.volumeBackupId, input, @@ -130,20 +201,45 @@ export const volumeBackupsRouter = createTRPCRouter({ removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId); } } + await audit(ctx, { + action: "update", + resourceType: "volumeBackup", + resourceId: updatedVolumeBackup.volumeBackupId, + }); return updatedVolumeBackup; }), runManually: protectedProcedure .input(z.object({ volumeBackupId: z.string().min(1) })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + const vb = await findVolumeBackupById(input.volumeBackupId); + const serviceId = + vb.applicationId || + vb.postgresId || + vb.mysqlId || + vb.mariadbId || + vb.mongoId || + vb.redisId || + vb.composeId; + if (serviceId) { + await checkServicePermissionAndAccess(ctx, serviceId, { + volumeBackup: ["create"], + }); + } try { - return await runVolumeBackup(input.volumeBackupId); + const result = await runVolumeBackup(input.volumeBackupId); + await audit(ctx, { + action: "run", + resourceType: "volumeBackup", + resourceId: input.volumeBackupId, + }); + return result; } catch (error) { console.error(error); return false; } }), - restoreVolumeBackupWithLogs: protectedProcedure + restoreVolumeBackupWithLogs: withPermission("volumeBackup", "restore") .meta({ openapi: { enabled: false, diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index 933da5b52a..486640349c 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -10,7 +10,9 @@ // import { getServerAuthSession } from "@/server/auth"; import { db } from "@dokploy/server/db"; import { hasValidLicense } from "@dokploy/server/index"; +import type { statements } from "@dokploy/server/lib/access-control"; import { validateRequest } from "@dokploy/server/lib/auth"; +import { checkPermission } from "@dokploy/server/services/permission"; import type { OpenApiMeta } from "@dokploy/trpc-openapi"; import { initTRPC, TRPCError } from "@trpc/server"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; @@ -18,6 +20,9 @@ import type { Session, User } from "better-auth"; import superjson from "superjson"; import { ZodError } from "zod"; +type Resource = keyof typeof statements; +type ActionOf = (typeof statements)[R][number]; + /** * 1. CONTEXT * @@ -235,3 +240,26 @@ export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => { }, }); }); + +/** + * Permission-checked procedure factory. + * + * Verifies the caller has the required resource+action permission before the + * handler runs. Works for all role types: + * - owner / admin → always granted (static roles, no license needed) + * - member → legacy boolean fields (no license needed) + * - custom role → enterprise license verified automatically inside resolveRole + * + * Usage: + * create: withPermission("project", "create") + * .input(...) + * .mutation(async ({ ctx, input }) => { ... }) + */ +export const withPermission = ( + resource: R, + action: ActionOf, +) => + protectedProcedure.use(async ({ ctx, next }) => { + await checkPermission(ctx, { [resource]: [action] } as any); + return next(); + }); diff --git a/apps/dokploy/server/api/utils/audit.ts b/apps/dokploy/server/api/utils/audit.ts new file mode 100644 index 0000000000..9f73befbf4 --- /dev/null +++ b/apps/dokploy/server/api/utils/audit.ts @@ -0,0 +1,31 @@ +import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log"; +import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema"; + +interface AuditCtx { + user: { id: string; email: string; role: string }; + session: { activeOrganizationId: string }; +} + +interface AuditEvent { + action: AuditAction; + resourceType: AuditResourceType; + resourceId?: string; + resourceName?: string; + metadata?: Record; +} + +/** + * Creates an audit log entry from a tRPC context. + * Extracts userId, userEmail, userRole and organizationId automatically. + * + * Usage: + * await audit(ctx, { action: "create", resourceType: "project", resourceName: "my-app" }); + */ +export const audit = (ctx: AuditCtx, event: AuditEvent) => + createAuditLog({ + organizationId: ctx.session.activeOrganizationId, + userId: ctx.user.id, + userEmail: ctx.user.email, + userRole: ctx.user.role, + ...event, + }); diff --git a/packages/server/auth-schema2.ts b/packages/server/auth-schema2.ts index 671c4ab7a8..9c163c8207 100644 --- a/packages/server/auth-schema2.ts +++ b/packages/server/auth-schema2.ts @@ -1,274 +1,299 @@ -// import { relations } from "drizzle-orm"; -// import { -// pgTable, -// text, -// timestamp, -// boolean, -// integer, -// index, -// uniqueIndex, -// } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; +import { + pgTable, + text, + timestamp, + boolean, + integer, + index, + uniqueIndex, +} from "drizzle-orm/pg-core"; -// export const user = pgTable("user", { -// id: text("id").primaryKey(), -// firstName: text("first_name").notNull(), -// email: text("email").notNull().unique(), -// emailVerified: boolean("email_verified").default(false).notNull(), -// image: text("image"), -// createdAt: timestamp("created_at").defaultNow().notNull(), -// updatedAt: timestamp("updated_at") -// .defaultNow() -// .$onUpdate(() => /* @__PURE__ */ new Date()) -// .notNull(), -// twoFactorEnabled: boolean("two_factor_enabled").default(false), -// role: text("role"), -// ownerId: text("owner_id"), -// allowImpersonation: boolean("allow_impersonation").default(false), -// lastName: text("last_name").default(""), -// }); +export const user = pgTable("user", { + id: text("id").primaryKey(), + firstName: text("first_name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + twoFactorEnabled: boolean("two_factor_enabled").default(false), + role: text("role"), + ownerId: text("owner_id"), + allowImpersonation: boolean("allow_impersonation").default(false), + lastName: text("last_name").default(""), + enableEnterpriseFeatures: boolean("enable_enterprise_features"), + isValidEnterpriseLicense: boolean("is_valid_enterprise_license"), +}); -// export const session = pgTable( -// "session", -// { -// id: text("id").primaryKey(), -// expiresAt: timestamp("expires_at").notNull(), -// token: text("token").notNull().unique(), -// createdAt: timestamp("created_at").defaultNow().notNull(), -// updatedAt: timestamp("updated_at") -// .$onUpdate(() => /* @__PURE__ */ new Date()) -// .notNull(), -// ipAddress: text("ip_address"), -// userAgent: text("user_agent"), -// userId: text("user_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// activeOrganizationId: text("active_organization_id"), -// }, -// (table) => [index("session_userId_idx").on(table.userId)], -// ); +export const session = pgTable( + "session", + { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + activeOrganizationId: text("active_organization_id"), + }, + (table) => [index("session_userId_idx").on(table.userId)], +); -// export const account = pgTable( -// "account", -// { -// id: text("id").primaryKey(), -// accountId: text("account_id").notNull(), -// providerId: text("provider_id").notNull(), -// userId: text("user_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// accessToken: text("access_token"), -// refreshToken: text("refresh_token"), -// idToken: text("id_token"), -// accessTokenExpiresAt: timestamp("access_token_expires_at"), -// refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), -// scope: text("scope"), -// password: text("password"), -// createdAt: timestamp("created_at").defaultNow().notNull(), -// updatedAt: timestamp("updated_at") -// .$onUpdate(() => /* @__PURE__ */ new Date()) -// .notNull(), -// }, -// (table) => [index("account_userId_idx").on(table.userId)], -// ); +export const account = pgTable( + "account", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("account_userId_idx").on(table.userId)], +); -// export const verification = pgTable( -// "verification", -// { -// id: text("id").primaryKey(), -// identifier: text("identifier").notNull(), -// value: text("value").notNull(), -// expiresAt: timestamp("expires_at").notNull(), -// createdAt: timestamp("created_at").defaultNow().notNull(), -// updatedAt: timestamp("updated_at") -// .defaultNow() -// .$onUpdate(() => /* @__PURE__ */ new Date()) -// .notNull(), -// }, -// (table) => [index("verification_identifier_idx").on(table.identifier)], -// ); +export const verification = pgTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)], +); -// export const apikey = pgTable( -// "apikey", -// { -// id: text("id").primaryKey(), -// name: text("name"), -// start: text("start"), -// prefix: text("prefix"), -// key: text("key").notNull(), -// userId: text("user_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// refillInterval: integer("refill_interval"), -// refillAmount: integer("refill_amount"), -// lastRefillAt: timestamp("last_refill_at"), -// enabled: boolean("enabled").default(true), -// rateLimitEnabled: boolean("rate_limit_enabled").default(true), -// rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000), -// rateLimitMax: integer("rate_limit_max").default(10), -// requestCount: integer("request_count").default(0), -// remaining: integer("remaining"), -// lastRequest: timestamp("last_request"), -// expiresAt: timestamp("expires_at"), -// createdAt: timestamp("created_at").notNull(), -// updatedAt: timestamp("updated_at").notNull(), -// permissions: text("permissions"), -// metadata: text("metadata"), -// }, -// (table) => [ -// index("apikey_key_idx").on(table.key), -// index("apikey_userId_idx").on(table.userId), -// ], -// ); +export const apikey = pgTable( + "apikey", + { + id: text("id").primaryKey(), + configId: text("config_id").default("default").notNull(), + name: text("name"), + start: text("start"), + referenceId: text("reference_id").notNull(), + prefix: text("prefix"), + key: text("key").notNull(), + refillInterval: integer("refill_interval"), + refillAmount: integer("refill_amount"), + lastRefillAt: timestamp("last_refill_at"), + enabled: boolean("enabled").default(true), + rateLimitEnabled: boolean("rate_limit_enabled").default(true), + rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000), + rateLimitMax: integer("rate_limit_max").default(10), + requestCount: integer("request_count").default(0), + remaining: integer("remaining"), + lastRequest: timestamp("last_request"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").notNull(), + updatedAt: timestamp("updated_at").notNull(), + permissions: text("permissions"), + metadata: text("metadata"), + }, + (table) => [ + index("apikey_configId_idx").on(table.configId), + index("apikey_referenceId_idx").on(table.referenceId), + index("apikey_key_idx").on(table.key), + ], +); -// export const ssoProvider = pgTable("sso_provider", { -// id: text("id").primaryKey(), -// issuer: text("issuer").notNull(), -// oidcConfig: text("oidc_config"), -// samlConfig: text("saml_config"), -// userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), -// providerId: text("provider_id").notNull().unique(), -// organizationId: text("organization_id"), -// domain: text("domain").notNull(), -// }); +export const ssoProvider = pgTable("sso_provider", { + id: text("id").primaryKey(), + issuer: text("issuer").notNull(), + oidcConfig: text("oidc_config"), + samlConfig: text("saml_config"), + userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), + providerId: text("provider_id").notNull().unique(), + organizationId: text("organization_id"), + domain: text("domain").notNull(), +}); -// export const twoFactor = pgTable( -// "two_factor", -// { -// id: text("id").primaryKey(), -// secret: text("secret").notNull(), -// backupCodes: text("backup_codes").notNull(), -// userId: text("user_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// }, -// (table) => [ -// index("twoFactor_secret_idx").on(table.secret), -// index("twoFactor_userId_idx").on(table.userId), -// ], -// ); +export const twoFactor = pgTable( + "two_factor", + { + id: text("id").primaryKey(), + secret: text("secret").notNull(), + backupCodes: text("backup_codes").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("twoFactor_secret_idx").on(table.secret), + index("twoFactor_userId_idx").on(table.userId), + ], +); -// export const organization = pgTable( -// "organization", -// { -// id: text("id").primaryKey(), -// name: text("name").notNull(), -// slug: text("slug").notNull().unique(), -// logo: text("logo"), -// createdAt: timestamp("created_at").notNull(), -// metadata: text("metadata"), -// }, -// (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], -// ); +export const organization = pgTable( + "organization", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + slug: text("slug").notNull().unique(), + logo: text("logo"), + createdAt: timestamp("created_at").notNull(), + metadata: text("metadata"), + }, + (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], +); -// export const member = pgTable( -// "member", -// { -// id: text("id").primaryKey(), -// organizationId: text("organization_id") -// .notNull() -// .references(() => organization.id, { onDelete: "cascade" }), -// userId: text("user_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// role: text("role").default("member").notNull(), -// createdAt: timestamp("created_at").notNull(), -// }, -// (table) => [ -// index("member_organizationId_idx").on(table.organizationId), -// index("member_userId_idx").on(table.userId), -// ], -// ); +export const organizationRole = pgTable( + "organization_role", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + role: text("role").notNull(), + permission: text("permission").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate( + () => /* @__PURE__ */ new Date(), + ), + }, + (table) => [ + index("organizationRole_organizationId_idx").on(table.organizationId), + index("organizationRole_role_idx").on(table.role), + ], +); -// export const invitation = pgTable( -// "invitation", -// { -// id: text("id").primaryKey(), -// organizationId: text("organization_id") -// .notNull() -// .references(() => organization.id, { onDelete: "cascade" }), -// email: text("email").notNull(), -// role: text("role"), -// status: text("status").default("pending").notNull(), -// expiresAt: timestamp("expires_at").notNull(), -// createdAt: timestamp("created_at").defaultNow().notNull(), -// inviterId: text("inviter_id") -// .notNull() -// .references(() => user.id, { onDelete: "cascade" }), -// }, -// (table) => [ -// index("invitation_organizationId_idx").on(table.organizationId), -// index("invitation_email_idx").on(table.email), -// ], -// ); +export const member = pgTable( + "member", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + role: text("role").default("member").notNull(), + createdAt: timestamp("created_at").notNull(), + }, + (table) => [ + index("member_organizationId_idx").on(table.organizationId), + index("member_userId_idx").on(table.userId), + ], +); -// export const userRelations = relations(user, ({ many }) => ({ -// sessions: many(session), -// accounts: many(account), -// apikeys: many(apikey), -// ssoProviders: many(ssoProvider), -// twoFactors: many(twoFactor), -// members: many(member), -// invitations: many(invitation), -// })); +export const invitation = pgTable( + "invitation", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role"), + status: text("status").default("pending").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("invitation_organizationId_idx").on(table.organizationId), + index("invitation_email_idx").on(table.email), + ], +); -// export const sessionRelations = relations(session, ({ one }) => ({ -// user: one(user, { -// fields: [session.userId], -// references: [user.id], -// }), -// })); +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), + ssoProviders: many(ssoProvider), + twoFactors: many(twoFactor), + members: many(member), + invitations: many(invitation), +})); -// export const accountRelations = relations(account, ({ one }) => ({ -// user: one(user, { -// fields: [account.userId], -// references: [user.id], -// }), -// })); +export const sessionRelations = relations(session, ({ one }) => ({ + user: one(user, { + fields: [session.userId], + references: [user.id], + }), +})); -// export const apikeyRelations = relations(apikey, ({ one }) => ({ -// user: one(user, { -// fields: [apikey.userId], -// references: [user.id], -// }), -// })); +export const accountRelations = relations(account, ({ one }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), +})); -// export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({ -// user: one(user, { -// fields: [ssoProvider.userId], -// references: [user.id], -// }), -// })); +export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({ + user: one(user, { + fields: [ssoProvider.userId], + references: [user.id], + }), +})); -// export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ -// user: one(user, { -// fields: [twoFactor.userId], -// references: [user.id], -// }), -// })); +export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ + user: one(user, { + fields: [twoFactor.userId], + references: [user.id], + }), +})); -// export const organizationRelations = relations(organization, ({ many }) => ({ -// members: many(member), -// invitations: many(invitation), -// })); +export const organizationRelations = relations(organization, ({ many }) => ({ + organizationRoles: many(organizationRole), + members: many(member), + invitations: many(invitation), +})); -// export const memberRelations = relations(member, ({ one }) => ({ -// organization: one(organization, { -// fields: [member.organizationId], -// references: [organization.id], -// }), -// user: one(user, { -// fields: [member.userId], -// references: [user.id], -// }), -// })); +export const organizationRoleRelations = relations( + organizationRole, + ({ one }) => ({ + organization: one(organization, { + fields: [organizationRole.organizationId], + references: [organization.id], + }), + }), +); -// export const invitationRelations = relations(invitation, ({ one }) => ({ -// organization: one(organization, { -// fields: [invitation.organizationId], -// references: [organization.id], -// }), -// user: one(user, { -// fields: [invitation.inviterId], -// references: [user.id], -// }), -// })); +export const memberRelations = relations(member, ({ one }) => ({ + organization: one(organization, { + fields: [member.organizationId], + references: [organization.id], + }), + user: one(user, { + fields: [member.userId], + references: [user.id], + }), +})); + +export const invitationRelations = relations(invitation, ({ one }) => ({ + organization: one(organization, { + fields: [invitation.organizationId], + references: [organization.id], + }), + user: one(user, { + fields: [invitation.inviterId], + references: [user.id], + }), +})); diff --git a/packages/server/package.json b/packages/server/package.json index 049a5ea81d..7fe2eaba02 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -27,7 +27,7 @@ "esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ", "typecheck": "tsc --noEmit", "dbml:generate": "npx tsx src/db/schema/dbml.ts", - "generate:drizzle": "pnpm dlx @better-auth/cli generate --output auth-schema2.ts --config src/lib/auth.ts" + "generate:drizzle": "pnpm dlx @better-auth/cli generate --output auth-schema2.ts --config src/lib/auth-cli.ts" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.44", @@ -37,6 +37,8 @@ "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai-compatible": "^2.0.30", + "@better-auth/api-key": "1.5.4", + "@better-auth/sso": "1.5.4", "@better-auth/utils": "0.3.1", "@faker-js/faker": "^8.4.1", "@octokit/auth-app": "^6.1.3", @@ -44,13 +46,13 @@ "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@react-email/components": "^0.0.21", - "@better-auth/sso": "1.5.0-beta.16", "@trpc/server": "11.10.0", "adm-zip": "^0.5.16", "ai": "^6.0.86", "ai-sdk-ollama": "^3.7.0", "bcrypt": "5.1.1", - "better-auth": "1.5.0-beta.16", + "better-auth": "1.5.4", + "better-call": "2.0.2", "bl": "6.0.11", "boxen": "^7.1.1", "date-fns": "3.6.0", @@ -59,7 +61,6 @@ "drizzle-dbml-generator": "0.10.0", "drizzle-orm": "0.45.1", "drizzle-zod": "0.5.1", - "yaml": "2.8.1", "lodash": "4.17.21", "micromatch": "4.0.8", "nanoid": "3.3.11", @@ -76,19 +77,17 @@ "react": "18.2.0", "react-dom": "18.2.0", "resend": "^6.0.2", + "semver": "7.7.3", "shell-quote": "^1.8.1", "slugify": "^1.6.6", "ssh2": "1.15.0", "toml": "3.0.0", "ws": "8.16.0", - "zod": "^4.3.6", - "semver": "7.7.3", - "better-call": "1.3.2" + "yaml": "2.8.1", + "zod": "^4.3.6" }, "devDependencies": { - "rimraf": "6.1.3", - "@better-auth/cli": "1.5.0-beta.13", - "@types/semver": "7.7.1", + "@better-auth/cli": "1.4.21", "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", "@types/dockerode": "3.3.23", @@ -100,6 +99,7 @@ "@types/qrcode": "^1.5.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/semver": "7.7.1", "@types/shell-quote": "^1.7.5", "@types/ssh2": "1.15.1", "@types/ws": "8.5.10", @@ -107,6 +107,7 @@ "esbuild": "0.20.2", "esbuild-plugin-alias": "0.2.1", "postcss": "^8.5.3", + "rimraf": "6.1.3", "tailwindcss": "^3.4.17", "tsc-alias": "1.8.10", "tsx": "^4.16.2", diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index 6814613e51..5d8bfb99df 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -1,6 +1,7 @@ import { relations, sql } from "drizzle-orm"; import { boolean, + index, integer, pgTable, text, @@ -69,6 +70,36 @@ export const organization = pgTable("organization", { .references(() => user.id, { onDelete: "cascade" }), }); +export const organizationRole = pgTable( + "organization_role", + { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + role: text("role").notNull(), + permission: text("permission").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate(() => new Date()), + }, + (table) => [ + index("organizationRole_organizationId_idx").on(table.organizationId), + index("organizationRole_role_idx").on(table.role), + ], +); + +export const organizationRoleRelations = relations( + organizationRole, + ({ one }) => ({ + organization: one(organization, { + fields: [organizationRole.organizationId], + references: [organization.id], + }), + }), +); + export const organizationRelations = relations( organization, ({ one, many }) => ({ @@ -80,6 +111,7 @@ export const organizationRelations = relations( projects: many(projects), members: many(member), ssoProviders: many(ssoProvider), + roles: many(organizationRole), }), ); @@ -93,7 +125,9 @@ export const member = pgTable("member", { userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), - role: text("role").notNull().$type<"owner" | "member" | "admin">(), + role: text("role") + .notNull() + .$type<"owner" | "member" | "admin" | (string & {})>(), createdAt: timestamp("created_at").notNull(), teamId: text("team_id"), isDefault: boolean("is_default").notNull().default(false), @@ -148,7 +182,7 @@ export const invitation = pgTable("invitation", { .notNull() .references(() => organization.id, { onDelete: "cascade" }), email: text("email").notNull(), - role: text("role").$type<"owner" | "member" | "admin">(), + role: text("role").$type<"owner" | "member" | "admin" | (string & {})>(), status: text("status").notNull(), expiresAt: timestamp("expires_at").notNull(), inviterId: text("inviter_id") diff --git a/packages/server/src/db/schema/audit-log.ts b/packages/server/src/db/schema/audit-log.ts new file mode 100644 index 0000000000..2b08bde63b --- /dev/null +++ b/packages/server/src/db/schema/audit-log.ts @@ -0,0 +1,94 @@ +import { relations } from "drizzle-orm"; +import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { nanoid } from "nanoid"; +import { organization } from "./account"; +import { user } from "./user"; + +export const auditLog = pgTable( + "audit_log", + { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + organizationId: text("organization_id").references(() => organization.id, { + onDelete: "set null", + }), + userId: text("user_id").references(() => user.id, { onDelete: "set null" }), + userEmail: text("user_email").notNull(), + userRole: text("user_role").notNull(), + action: text("action").notNull(), + resourceType: text("resource_type").notNull(), + resourceId: text("resource_id"), + resourceName: text("resource_name"), + metadata: text("metadata"), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (t) => ({ + orgIdx: index("auditLog_organizationId_idx").on(t.organizationId), + userIdx: index("auditLog_userId_idx").on(t.userId), + createdAtIdx: index("auditLog_createdAt_idx").on(t.createdAt), + }), +); + +export const auditLogRelations = relations(auditLog, ({ one }) => ({ + organization: one(organization, { + fields: [auditLog.organizationId], + references: [organization.id], + }), + user: one(user, { + fields: [auditLog.userId], + references: [user.id], + }), +})); + +export type AuditLog = typeof auditLog.$inferSelect; +export type NewAuditLog = typeof auditLog.$inferInsert; + +export type AuditAction = + | "create" + | "update" + | "delete" + | "deploy" + | "cancel" + | "redeploy" + | "login" + | "logout" + | "restore" + | "run" + | "start" + | "stop" + | "reload" + | "rebuild" + | "move"; + +export type AuditResourceType = + | "project" + | "service" + | "environment" + | "deployment" + | "user" + | "customRole" + | "domain" + | "certificate" + | "registry" + | "server" + | "sshKey" + | "gitProvider" + | "destination" + | "notification" + | "settings" + | "session" + | "port" + | "redirect" + | "security" + | "schedule" + | "backup" + | "volumeBackup" + | "docker" + | "swarm" + | "previewDeployment" + | "organization" + | "cluster" + | "mount" + | "application" + | "compose"; diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 8e51aef919..3dd22e2a63 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -1,23 +1,48 @@ import { relations } from "drizzle-orm"; -import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { pgEnum, pgTable, text, integer, timestamp } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; import { organization } from "./account"; import { backups } from "./backups"; +export const destinationTypeEnum = pgEnum("destinationType", [ + "s3", + "ftp", + "sftp", + "google-drive", +]); + export const destinations = pgTable("destination", { destinationId: text("destinationId") .notNull() .primaryKey() .$defaultFn(() => nanoid()), name: text("name").notNull(), + destinationType: destinationTypeEnum("destinationType") + .notNull() + .default("s3"), + // S3 fields (nullable for non-S3 destinations) provider: text("provider"), - accessKey: text("accessKey").notNull(), - secretAccessKey: text("secretAccessKey").notNull(), - bucket: text("bucket").notNull(), - region: text("region").notNull(), - endpoint: text("endpoint").notNull(), + accessKey: text("accessKey"), + secretAccessKey: text("secretAccessKey"), + bucket: text("bucket"), + region: text("region"), + endpoint: text("endpoint"), + // FTP/SFTP fields + ftpHost: text("ftpHost"), + ftpPort: integer("ftpPort"), + ftpUser: text("ftpUser"), + ftpPassword: text("ftpPassword"), + ftpPath: text("ftpPath"), + // SFTP-specific: SSH key + sftpKeyPath: text("sftpKeyPath"), + // Google Drive fields + googleDriveClientId: text("googleDriveClientId"), + googleDriveClientSecret: text("googleDriveClientSecret"), + googleDriveRefreshToken: text("googleDriveRefreshToken"), + googleDriveFolderId: text("googleDriveFolderId"), + // Common organizationId: text("organizationId") .notNull() .references(() => organization.id, { onDelete: "cascade" }), @@ -38,25 +63,47 @@ export const destinationsRelations = relations( const createSchema = createInsertSchema(destinations, { destinationId: z.string(), name: z.string().min(1), + destinationType: z.enum(["s3", "ftp", "sftp", "google-drive"]).default("s3"), provider: z.string(), accessKey: z.string(), bucket: z.string(), endpoint: z.string(), secretAccessKey: z.string(), region: z.string(), + ftpHost: z.string().optional().nullable(), + ftpPort: z.number().optional().nullable(), + ftpUser: z.string().optional().nullable(), + ftpPassword: z.string().optional().nullable(), + ftpPath: z.string().optional().nullable(), + sftpKeyPath: z.string().optional().nullable(), + googleDriveClientId: z.string().optional().nullable(), + googleDriveClientSecret: z.string().optional().nullable(), + googleDriveRefreshToken: z.string().optional().nullable(), + googleDriveFolderId: z.string().optional().nullable(), }); export const apiCreateDestination = createSchema .pick({ name: true, + destinationType: true, provider: true, accessKey: true, bucket: true, region: true, endpoint: true, secretAccessKey: true, + ftpHost: true, + ftpPort: true, + ftpUser: true, + ftpPassword: true, + ftpPath: true, + sftpKeyPath: true, + googleDriveClientId: true, + googleDriveClientSecret: true, + googleDriveRefreshToken: true, + googleDriveFolderId: true, }) - .required() + .required({ name: true }) .extend({ serverId: z.string().optional(), }); @@ -74,6 +121,7 @@ export const apiRemoveDestination = createSchema export const apiUpdateDestination = createSchema .pick({ name: true, + destinationType: true, accessKey: true, bucket: true, region: true, @@ -81,8 +129,18 @@ export const apiUpdateDestination = createSchema secretAccessKey: true, destinationId: true, provider: true, + ftpHost: true, + ftpPort: true, + ftpUser: true, + ftpPassword: true, + ftpPath: true, + sftpKeyPath: true, + googleDriveClientId: true, + googleDriveClientSecret: true, + googleDriveRefreshToken: true, + googleDriveFolderId: true, }) - .required() + .required({ name: true, destinationId: true }) .extend({ serverId: z.string().optional(), }); diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index fece17f02c..a4bce3aa49 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -1,5 +1,6 @@ export * from "./account"; export * from "./ai"; +export * from "./audit-log"; export * from "./application"; export * from "./backups"; export * from "./bitbucket"; diff --git a/packages/server/src/db/schema/mariadb.ts b/packages/server/src/db/schema/mariadb.ts index ac4b0e8235..2659c29786 100644 --- a/packages/server/src/db/schema/mariadb.ts +++ b/packages/server/src/db/schema/mariadb.ts @@ -202,6 +202,7 @@ export const apiUpdateMariaDB = createSchema .partial() .extend({ mariadbId: z.string().min(1), + dockerImage: z.string().optional(), }) .omit({ serverId: true }); diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts index ff315bbf6b..4599cedb2c 100644 --- a/packages/server/src/db/schema/mongo.ts +++ b/packages/server/src/db/schema/mongo.ts @@ -191,6 +191,7 @@ export const apiUpdateMongo = createSchema .partial() .extend({ mongoId: z.string().min(1), + dockerImage: z.string().optional(), }) .omit({ serverId: true }); diff --git a/packages/server/src/db/schema/mysql.ts b/packages/server/src/db/schema/mysql.ts index a5f066de88..90b38e6fa2 100644 --- a/packages/server/src/db/schema/mysql.ts +++ b/packages/server/src/db/schema/mysql.ts @@ -199,6 +199,7 @@ export const apiUpdateMySql = createSchema .partial() .extend({ mysqlId: z.string().min(1), + dockerImage: z.string().optional(), }) .omit({ serverId: true }); diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts index 1079976faa..5cb3015ce9 100644 --- a/packages/server/src/db/schema/postgres.ts +++ b/packages/server/src/db/schema/postgres.ts @@ -192,6 +192,7 @@ export const apiUpdatePostgres = createSchema .partial() .extend({ postgresId: z.string().min(1), + dockerImage: z.string().optional(), }) .omit({ serverId: true }); diff --git a/packages/server/src/db/schema/redis.ts b/packages/server/src/db/schema/redis.ts index e44ecff2a7..f024378053 100644 --- a/packages/server/src/db/schema/redis.ts +++ b/packages/server/src/db/schema/redis.ts @@ -178,6 +178,7 @@ export const apiUpdateRedis = createSchema .partial() .extend({ redisId: z.string().min(1), + dockerImage: z.string().optional(), }) .omit({ serverId: true }); diff --git a/packages/server/src/lib/access-control.ts b/packages/server/src/lib/access-control.ts new file mode 100644 index 0000000000..d49fd2d45f --- /dev/null +++ b/packages/server/src/lib/access-control.ts @@ -0,0 +1,190 @@ +import { createAccessControl } from "better-auth/plugins/access"; + +/** + * Dokploy Access Control Statements + * + * Defines all resources and their possible actions across the platform. + * The first 5 (organization, member, invitation, team, ac) are better-auth defaults + * used internally by the organization plugin. + * The rest are Dokploy-specific resources. + * + * Enterprise-only resources (only assignable via custom roles): + * deployment, envVars, server, registry, certificate, backup, domain, logs, monitoring + */ +export const statements = { + // better-auth organization plugin defaults + organization: ["update", "delete"], + member: ["read", "create", "update", "delete"], + invitation: ["create", "cancel"], + team: ["create", "update", "delete"], + ac: ["create", "read", "update", "delete"], + + // Dokploy core resources (free tier) + project: ["create", "delete"], + service: ["create", "read", "delete"], + environment: ["create", "read", "delete"], + docker: ["read"], + sshKeys: ["read", "create", "delete"], + gitProviders: ["read", "create", "delete"], + traefikFiles: ["read", "write"], + api: ["read"], + + // Enterprise-only resources (custom roles only) + volume: ["read", "create", "delete"], + deployment: ["read", "create", "cancel"], + envVars: ["read", "write"], + projectEnvVars: ["read", "write"], + environmentEnvVars: ["read", "write"], + server: ["read", "create", "delete"], + registry: ["read", "create", "delete"], + certificate: ["read", "create", "delete"], + backup: ["read", "create", "update", "delete", "restore"], + volumeBackup: ["read", "create", "update", "delete", "restore"], + schedule: ["read", "create", "update", "delete"], + domain: ["read", "create", "delete"], + destination: ["read", "create", "delete"], + notification: ["read", "create", "update", "delete"], + logs: ["read"], + monitoring: ["read"], + auditLog: ["read"], +} as const; + +/** + * Enterprise-only resources. For static roles (owner/admin/member), + * permission checks on these resources are bypassed — they only apply + * when using custom roles with an enterprise license. + */ +export const enterpriseOnlyResources = new Set([ + "volume", + "deployment", + "envVars", + "projectEnvVars", + "environmentEnvVars", + "server", + "registry", + "certificate", + "backup", + "volumeBackup", + "schedule", + "domain", + "destination", + "notification", + "logs", + "monitoring", + "auditLog", +]); + +export const ac = createAccessControl(statements); + +/** + * Owner role — full access to everything + */ +export const ownerRole = ac.newRole({ + organization: ["update", "delete"], + member: ["read", "create", "update", "delete"], + invitation: ["create", "cancel"], + team: ["create", "update", "delete"], + ac: ["create", "read", "update", "delete"], + project: ["create", "delete"], + service: ["create", "read", "delete"], + environment: ["create", "read", "delete"], + docker: ["read"], + sshKeys: ["read", "create", "delete"], + gitProviders: ["read", "create", "delete"], + traefikFiles: ["read", "write"], + api: ["read"], + volume: ["read", "create", "delete"], + deployment: ["read", "create", "cancel"], + envVars: ["read", "write"], + projectEnvVars: ["read", "write"], + environmentEnvVars: ["read", "write"], + server: ["read", "create", "delete"], + registry: ["read", "create", "delete"], + certificate: ["read", "create", "delete"], + backup: ["read", "create", "update", "delete", "restore"], + volumeBackup: ["read", "create", "update", "delete", "restore"], + schedule: ["read", "create", "update", "delete"], + domain: ["read", "create", "delete"], + destination: ["read", "create", "delete"], + notification: ["read", "create", "update", "delete"], + logs: ["read"], + monitoring: ["read"], + auditLog: ["read"], +}); + +/** + * Admin role — same as owner but cannot delete the organization + */ +export const adminRole = ac.newRole({ + organization: ["update"], + member: ["read", "create", "update", "delete"], + invitation: ["create", "cancel"], + team: ["create", "update", "delete"], + ac: ["create", "read", "update", "delete"], + project: ["create", "delete"], + service: ["create", "read", "delete"], + environment: ["create", "read", "delete"], + docker: ["read"], + sshKeys: ["read", "create", "delete"], + gitProviders: ["read", "create", "delete"], + traefikFiles: ["read", "write"], + api: ["read"], + volume: ["read", "create", "delete"], + deployment: ["read", "create", "cancel"], + envVars: ["read", "write"], + projectEnvVars: ["read", "write"], + environmentEnvVars: ["read", "write"], + server: ["read", "create", "delete"], + registry: ["read", "create", "delete"], + certificate: ["read", "create", "delete"], + backup: ["read", "create", "update", "delete", "restore"], + volumeBackup: ["read", "create", "update", "delete", "restore"], + schedule: ["read", "create", "update", "delete"], + domain: ["read", "create", "delete"], + destination: ["read", "create", "delete"], + notification: ["read", "create", "update", "delete"], + logs: ["read"], + monitoring: ["read"], + auditLog: ["read"], +}); + +/** + * Member role (free tier) — read-only base permissions. + * Members can read projects/services/environments they have access to, + * but cannot create, delete, or access admin resources. + * Enterprise resources are not available to the base member role. + */ +export const memberRole = ac.newRole({ + organization: [], + member: [], + invitation: [], + team: [], + ac: ["read"], + project: [], + service: ["read"], + environment: ["read"], + docker: [], + sshKeys: [], + gitProviders: [], + traefikFiles: [], + api: [], + // Service-level enterprise resources — member can do everything within services they have access to + volume: ["read", "create", "delete"], + deployment: ["read", "create", "cancel"], + envVars: ["read", "write"], + projectEnvVars: ["read", "write"], + environmentEnvVars: ["read", "write"], + backup: ["read", "create", "update", "delete", "restore"], + volumeBackup: ["read", "create", "update", "delete", "restore"], + schedule: ["read", "create", "update", "delete"], + domain: ["read", "create", "delete"], + logs: ["read"], + monitoring: ["read"], + // Org-level enterprise resources — member cannot manage these + server: [], + registry: [], + certificate: [], + destination: [], + notification: [], + auditLog: [], +}); diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 21e1ff19fa..0721bbf85f 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -1,10 +1,11 @@ import type { IncomingMessage } from "node:http"; +import { apiKey } from "@better-auth/api-key"; import { sso } from "@better-auth/sso"; import * as bcrypt from "bcrypt"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { APIError } from "better-auth/api"; -import { admin, apiKey, organization, twoFactor } from "better-auth/plugins"; +import { admin, organization, twoFactor } from "better-auth/plugins"; import { and, desc, eq } from "drizzle-orm"; import { BETTER_AUTH_SECRET, IS_CLOUD } from "../constants"; import { db } from "../db"; @@ -14,6 +15,7 @@ import { getTrustedProviders, getUserByToken, } from "../services/admin"; +import { createAuditLog } from "../services/proprietary/audit-log"; import { getWebServerSettings, updateWebServerSettings, @@ -21,6 +23,7 @@ import { import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot"; import { sendEmail } from "../verification/send-verification-email"; import { getPublicIpWithFallback } from "../wss/utils"; +import { ac, adminRole, memberRole, ownerRole } from "./access-control"; const { handler, api } = betterAuth({ database: drizzleAdapter(db, { @@ -269,6 +272,52 @@ const { handler, api } = betterAuth({ }, }; }, + after: async (session) => { + const orgId = ( + session as typeof session & { activeOrganizationId?: string } + ).activeOrganizationId; + if (!orgId) return; + const memberRecord = await db.query.member.findFirst({ + where: and( + eq(schema.member.userId, session.userId), + eq(schema.member.organizationId, orgId), + ), + with: { user: true }, + }); + if (!memberRecord) return; + await createAuditLog({ + organizationId: orgId, + userId: session.userId, + userEmail: memberRecord.user.email, + userRole: memberRecord.role, + action: "login", + resourceType: "session", + }); + }, + }, + delete: { + after: async (session) => { + const orgId = ( + session as typeof session & { activeOrganizationId?: string } + ).activeOrganizationId; + if (!orgId) return; + const memberRecord = await db.query.member.findFirst({ + where: and( + eq(schema.member.userId, session.userId), + eq(schema.member.organizationId, orgId), + ), + with: { user: true }, + }); + if (!memberRecord) return; + await createAuditLog({ + organizationId: orgId, + userId: session.userId, + userEmail: memberRecord.user.email, + userRole: memberRecord.role, + action: "logout", + resourceType: "session", + }); + }, }, }, }, @@ -322,6 +371,16 @@ const { handler, api } = betterAuth({ sso(), twoFactor(), organization({ + ac, + roles: { + owner: ownerRole, + admin: adminRole, + member: memberRole, + }, + dynamicAccessControl: { + enabled: true, + maximumRolesPerOrganization: 10, + }, async sendInvitationEmail(data, _request) { if (IS_CLOUD) { const host = diff --git a/packages/server/src/services/permission.ts b/packages/server/src/services/permission.ts new file mode 100644 index 0000000000..3088842b6a --- /dev/null +++ b/packages/server/src/services/permission.ts @@ -0,0 +1,431 @@ +import { db } from "@dokploy/server/db"; +import { member, organizationRole } from "@dokploy/server/db/schema"; +import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { + ac, + adminRole, + enterpriseOnlyResources, + memberRole, + ownerRole, + statements, +} from "../lib/access-control"; + +type Statements = typeof statements; +type Resource = keyof Statements; +type Action = Statements[R][number]; +type Permissions = { + [R in Resource]?: Action[]; +}; + +export type PermissionCtx = { + user: { id: string }; + session: { activeOrganizationId: string }; +}; + +export type ResolvedPermissions = { + [R in Resource]: { + [A in Statements[R][number]]: boolean; + }; +}; + +const staticRoles: Record> = { + owner: ownerRole, + admin: adminRole, + member: memberRole, +}; + +const resolveRole = async ( + roleName: string, + organizationId: string, +): Promise | null> => { + if (staticRoles[roleName]) { + return staticRoles[roleName]; + } + + const licensed = await hasValidLicense(organizationId); + if (!licensed) { + return null; + } + + const customRoles = await db.query.organizationRole.findMany({ + where: and( + eq(organizationRole.organizationId, organizationId), + eq(organizationRole.role, roleName), + ), + }); + + if (customRoles.length === 0) { + return null; + } + + const merged: Record = {}; + for (const entry of customRoles) { + const parsed = JSON.parse(entry.permission) as Record; + for (const [resource, actions] of Object.entries(parsed)) { + merged[resource] = [ + ...new Set([...(merged[resource] ?? []), ...actions]), + ]; + } + } + + return ac.newRole(merged as any); +}; + +export const checkPermission = async ( + ctx: PermissionCtx, + permissions: Permissions, +) => { + const { id: userId } = ctx.user; + const { activeOrganizationId: organizationId } = ctx.session; + const memberRecord = await findMemberByUserId(userId, organizationId); + const isStaticRole = memberRecord.role in staticRoles; + + if (isStaticRole) { + const allEnterprise = Object.keys(permissions).every((r) => + enterpriseOnlyResources.has(r), + ); + if (allEnterprise) return; + } + + const role = await resolveRole(memberRecord.role, organizationId); + + if (!role) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid role", + }); + } + + const result = role.authorize(permissions); + if (result.success) { + return; + } + + if (memberRecord.role === "member") { + const overrides = getLegacyOverrides(memberRecord); + const allGranted = Object.entries(permissions).every( + ([resource, actions]) => + (actions as string[]).every( + (action) => + !!(overrides[resource] as Record | undefined)?.[ + action + ], + ), + ); + if (allGranted) { + return; + } + } + + throw new TRPCError({ + code: "UNAUTHORIZED", + message: result.error || "Permission denied", + }); +}; + +export const hasPermission = async ( + ctx: PermissionCtx, + permissions: Permissions, +): Promise => { + try { + await checkPermission(ctx, permissions); + return true; + } catch { + return false; + } +}; + +const getLegacyOverrides = ( + memberRecord: Awaited>, +): Partial>> => { + return { + project: { + create: !!memberRecord.canCreateProjects, + delete: !!memberRecord.canDeleteProjects, + }, + service: { + create: !!memberRecord.canCreateServices, + delete: !!memberRecord.canDeleteServices, + }, + environment: { + create: !!memberRecord.canCreateEnvironments, + delete: !!memberRecord.canDeleteEnvironments, + }, + traefikFiles: { + read: !!memberRecord.canAccessToTraefikFiles, + }, + docker: { + read: !!memberRecord.canAccessToDocker, + }, + api: { + read: !!memberRecord.canAccessToAPI, + }, + sshKeys: { + read: !!memberRecord.canAccessToSSHKeys, + }, + gitProviders: { + read: !!memberRecord.canAccessToGitProviders, + }, + }; +}; + +export const resolvePermissions = async ( + ctx: PermissionCtx, +): Promise => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + const role = await resolveRole(memberRecord.role, organizationId); + + const legacyOverrides = + memberRecord.role === "member" ? getLegacyOverrides(memberRecord) : {}; + + const isPrivilegedRole = + memberRecord.role === "owner" || memberRecord.role === "admin"; + const result = {} as ResolvedPermissions; + + for (const [resource, actions] of Object.entries(statements)) { + const resourcePerms = {} as Record; + for (const action of actions) { + if (isPrivilegedRole && enterpriseOnlyResources.has(resource)) { + resourcePerms[action] = true; + continue; + } + if (!role) { + resourcePerms[action] = false; + continue; + } + const check = role.authorize({ [resource]: [action] }); + resourcePerms[action] = + check.success || + !!(legacyOverrides[resource] as Record | undefined)?.[ + action + ]; + } + (result as any)[resource] = resourcePerms; + } + + return result; +}; + +export const checkProjectAccess = async ( + ctx: PermissionCtx, + action: "create" | "delete", + projectId?: string, +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + + await checkPermission(ctx, { project: [action] }); + + if ( + action !== "create" && + projectId && + memberRecord.role !== "owner" && + memberRecord.role !== "admin" + ) { + if (!memberRecord.accessedProjects.includes(projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } +}; + +export const checkServicePermissionAndAccess = async ( + ctx: PermissionCtx, + serviceId: string, + permissions: Permissions, +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + await checkPermission(ctx, permissions); + if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { + if (!memberRecord.accessedServices.includes(serviceId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this service", + }); + } + } +}; + +export const checkServiceAccess = async ( + ctx: PermissionCtx, + serviceId: string, + action: "create" | "read" | "delete" = "read", +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + + await checkPermission(ctx, { service: [action] }); + + if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { + if (action === "create") { + if (!memberRecord.accessedProjects.includes(serviceId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } else { + if (!memberRecord.accessedServices.includes(serviceId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this service", + }); + } + } + } +}; + +export const checkEnvironmentAccess = async ( + ctx: PermissionCtx, + environmentId: string, + action: "read" | "create" | "delete" = "read", +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + + await checkPermission(ctx, { environment: [action] }); + + if ( + action !== "create" && + memberRecord.role !== "owner" && + memberRecord.role !== "admin" + ) { + if (!memberRecord.accessedEnvironments.includes(environmentId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this environment", + }); + } + } +}; + +export const checkEnvironmentCreationPermission = async ( + ctx: PermissionCtx, + projectId: string, +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + + await checkPermission(ctx, { environment: ["create"] }); + + if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { + if (!memberRecord.accessedProjects.includes(projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } +}; + +export const checkEnvironmentDeletionPermission = async ( + ctx: PermissionCtx, + projectId: string, +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + + await checkPermission(ctx, { environment: ["delete"] }); + + if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { + if (!memberRecord.accessedProjects.includes(projectId)) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + } +}; + +export const addNewProject = async (ctx: PermissionCtx, projectId: string) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + await db + .update(member) + .set({ + accessedProjects: [...memberRecord.accessedProjects, projectId], + }) + .where( + and( + eq(member.id, memberRecord.id), + eq(member.organizationId, organizationId), + ), + ); +}; + +export const addNewEnvironment = async ( + ctx: PermissionCtx, + environmentId: string, +) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + await db + .update(member) + .set({ + accessedEnvironments: [ + ...memberRecord.accessedEnvironments, + environmentId, + ], + }) + .where( + and( + eq(member.id, memberRecord.id), + eq(member.organizationId, organizationId), + ), + ); +}; + +export const addNewService = async (ctx: PermissionCtx, serviceId: string) => { + const userId = ctx.user.id; + const organizationId = ctx.session.activeOrganizationId; + const memberRecord = await findMemberByUserId(userId, organizationId); + await db + .update(member) + .set({ + accessedServices: [...memberRecord.accessedServices, serviceId], + }) + .where( + and( + eq(member.id, memberRecord.id), + eq(member.organizationId, organizationId), + ), + ); +}; + +export const findMemberByUserId = async ( + userId: string, + organizationId: string, +) => { + const result = await db.query.member.findFirst({ + where: and( + eq(member.userId, userId), + eq(member.organizationId, organizationId), + ), + with: { + user: true, + }, + }); + + if (!result) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Permission denied", + }); + } + return result; +}; diff --git a/packages/server/src/services/proprietary/audit-log.ts b/packages/server/src/services/proprietary/audit-log.ts new file mode 100644 index 0000000000..157e75c9b1 --- /dev/null +++ b/packages/server/src/services/proprietary/audit-log.ts @@ -0,0 +1,95 @@ +import { db } from "@dokploy/server/db"; +import { auditLog } from "@dokploy/server/db/schema"; +import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema"; +import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; +import { and, desc, eq, gte, ilike, lte } from "drizzle-orm"; + +export type { AuditAction, AuditResourceType }; + +export interface CreateAuditLogInput { + organizationId: string; + userId: string; + userEmail: string; + userRole: string; + action: AuditAction; + resourceType: AuditResourceType; + resourceId?: string; + resourceName?: string; + metadata?: Record; +} + +/** + * Creates an audit log entry. Fire-and-forget safe — errors are swallowed + * so a logging failure never breaks the main operation. + */ +export const createAuditLog = async (input: CreateAuditLogInput) => { + try { + const licensed = await hasValidLicense(input.organizationId); + if (!licensed) return; + + await db.insert(auditLog).values({ + organizationId: input.organizationId, + userId: input.userId, + userEmail: input.userEmail, + userRole: input.userRole, + action: input.action, + resourceType: input.resourceType, + resourceId: input.resourceId, + resourceName: input.resourceName, + metadata: input.metadata ? JSON.stringify(input.metadata) : undefined, + }); + } catch (err) { + console.error("[audit-log] Failed to create audit log entry:", err); + } +}; + +export interface GetAuditLogsInput { + organizationId: string; + userId?: string; + userEmail?: string; + resourceName?: string; + action?: AuditAction; + resourceType?: AuditResourceType; + from?: Date; + to?: Date; + limit?: number; + offset?: number; +} + +export const getAuditLogs = async (input: GetAuditLogsInput) => { + const { + organizationId, + userId, + userEmail, + resourceName, + action, + resourceType, + from, + to, + limit = 50, + offset = 0, + } = input; + + const conditions = [eq(auditLog.organizationId, organizationId)]; + + if (userId) conditions.push(eq(auditLog.userId, userId)); + if (userEmail) conditions.push(ilike(auditLog.userEmail, `%${userEmail}%`)); + if (resourceName) + conditions.push(ilike(auditLog.resourceName, `%${resourceName}%`)); + if (action) conditions.push(eq(auditLog.action, action)); + if (resourceType) conditions.push(eq(auditLog.resourceType, resourceType)); + if (from) conditions.push(gte(auditLog.createdAt, from)); + if (to) conditions.push(lte(auditLog.createdAt, to)); + + const [logs, total] = await Promise.all([ + db.query.auditLog.findMany({ + where: and(...conditions), + orderBy: [desc(auditLog.createdAt)], + limit, + offset, + }), + db.$count(auditLog, and(...conditions)), + ]); + + return { logs, total }; +}; diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 34f6d2a9b4..63e7acb275 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -8,7 +8,7 @@ import { findEnvironmentById } from "@dokploy/server/services/environment"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { getBackupCommand, getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runComposeBackup = async ( compose: Compose, @@ -29,8 +29,8 @@ export const runComposeBackup = async ( }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; const backupCommand = getBackupCommand( diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index 71eeda7ea5..7d34d44c8e 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -10,7 +10,7 @@ import { startLogCleanup } from "../access-log/handler"; import { cleanupAll } from "../docker/utils"; import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils"; +import { getRcloneFlags, getRcloneRemotePath, normalizeS3Path, scheduleBackup } from "./utils"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); @@ -129,9 +129,9 @@ export const keepLatestNBackups = async ( if (!backup.keepLatestCount) return; try { - const rcloneFlags = getS3Credentials(backup.destination); + const rcloneFlags = getRcloneFlags(backup.destination); const appName = getServiceAppName(backup); - const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`; + const backupFilesPath = getRcloneRemotePath(backup.destination, `${appName}/${normalizeS3Path(backup.prefix)}`); // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 089b3cb046..960a61c31b 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -8,7 +8,7 @@ import type { Mariadb } from "@dokploy/server/services/mariadb"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { getBackupCommand, getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runMariadbBackup = async ( mariadb: Mariadb, @@ -27,8 +27,8 @@ export const runMariadbBackup = async ( description: "MariaDB Backup", }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; const backupCommand = getBackupCommand( diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index d1b04e68b5..aca4b3b211 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -8,7 +8,7 @@ import type { Mongo } from "@dokploy/server/services/mongo"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { getBackupCommand, getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { const { environmentId, name, appName } = mongo; @@ -24,8 +24,8 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { description: "MongoDB Backup", }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; const backupCommand = getBackupCommand( diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index 461a17bf90..bce62d9437 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -8,7 +8,7 @@ import type { MySql } from "@dokploy/server/services/mysql"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { getBackupCommand, getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { const { environmentId, name, appName } = mysql; @@ -25,8 +25,8 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { }); try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index 3371b0cf9a..b8f9d06743 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -8,7 +8,7 @@ import type { Postgres } from "@dokploy/server/services/postgres"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { getBackupCommand, getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runPostgresBackup = async ( postgres: Postgres, @@ -28,8 +28,8 @@ export const runPostgresBackup = async ( const backupFileName = `${new Date().toISOString()}.sql.gz`; const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; try { - const rcloneFlags = getS3Credentials(destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index f30577a53b..9c3cfa075d 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -77,6 +77,87 @@ export const getS3Credentials = (destination: Destination) => { return rcloneFlags; }; +export const getFtpCredentials = (destination: Destination) => { + const { ftpHost, ftpPort, ftpUser, ftpPassword } = destination; + const rcloneFlags = [ + `--ftp-host="${ftpHost}"`, + `--ftp-user="${ftpUser || ""}"`, + `--ftp-pass="$(rclone obscure '${ftpPassword || ""}')"`, + ]; + if (ftpPort) { + rcloneFlags.push(`--ftp-port="${ftpPort}"`); + } + return rcloneFlags; +}; + +export const getSftpCredentials = (destination: Destination) => { + const { ftpHost, ftpPort, ftpUser, ftpPassword, sftpKeyPath } = destination; + const rcloneFlags = [ + `--sftp-host="${ftpHost}"`, + `--sftp-user="${ftpUser || ""}"`, + ]; + if (ftpPort) { + rcloneFlags.push(`--sftp-port="${ftpPort}"`); + } + if (sftpKeyPath) { + rcloneFlags.push(`--sftp-key-file="${sftpKeyPath}"`); + } else if (ftpPassword) { + rcloneFlags.push(`--sftp-pass="$(rclone obscure '${ftpPassword}')"`) + } + return rcloneFlags; +}; + +export const getGoogleDriveCredentials = (destination: Destination) => { + const { + googleDriveClientId, + googleDriveClientSecret, + googleDriveRefreshToken, + } = destination; + const rcloneFlags = [ + `--drive-client-id="${googleDriveClientId || ""}"`, + `--drive-client-secret="${googleDriveClientSecret || ""}"`, + `--drive-token='{"access_token":"","token_type":"Bearer","refresh_token":"${googleDriveRefreshToken || ""}","expiry":"2000-01-01T00:00:00.000Z"}'`, + ]; + return rcloneFlags; +}; + +export const getRcloneFlags = (destination: Destination): string[] => { + const destType = destination.destinationType || "s3"; + switch (destType) { + case "ftp": + return getFtpCredentials(destination); + case "sftp": + return getSftpCredentials(destination); + case "google-drive": + return getGoogleDriveCredentials(destination); + case "s3": + default: + return getS3Credentials(destination); + } +}; + +export const getRcloneRemotePath = ( + destination: Destination, + subPath: string, +): string => { + const destType = destination.destinationType || "s3"; + switch (destType) { + case "ftp": + return `:ftp:${destination.ftpPath || "/"}${subPath}`; + case "sftp": + return `:sftp:${destination.ftpPath || "/"}${subPath}`; + case "google-drive": { + const folderId = destination.googleDriveFolderId; + return folderId + ? `:drive:${folderId}/${subPath}` + : `:drive:${subPath}`; + } + case "s3": + default: + return `:s3:${destination.bucket}/${subPath}`; + } +}; + export const getPostgresBackupCommand = ( database: string, databaseUser: string, @@ -255,16 +336,16 @@ export const getBackupCommand = ( } echo "[$(date)] ✅ backup completed successfully" >> ${logPath}; - echo "[$(date)] Starting upload to S3..." >> ${logPath}; + echo "[$(date)] Starting upload to destination..." >> ${logPath}; # Run the upload command and capture the exit status UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || { - echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath}; + echo "[$(date)] ❌ Error: Upload to destination failed" >> ${logPath}; echo "Error: $UPLOAD_OUTPUT" >> ${logPath}; exit 1; } - echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath}; + echo "[$(date)] ✅ Upload to destination completed successfully" >> ${logPath}; echo "Backup done ✅" >> ${logPath}; `; }; diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 1a51d23ea5..911fd8b72e 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -10,7 +10,7 @@ import { } from "@dokploy/server/services/deployment"; import { findDestinationById } from "@dokploy/server/services/destination"; import { execAsync } from "../process/execAsync"; -import { getS3Credentials, normalizeS3Path } from "./utils"; +import { getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "./utils"; export const runWebServerBackup = async (backup: BackupSchedule) => { if (IS_CLOUD) { @@ -26,12 +26,12 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { try { const destination = await findDestinationById(backup.destinationId); - const rcloneFlags = getS3Credentials(destination); + const rcloneFlags = getRcloneFlags(destination); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const { BASE_PATH } = paths(); const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-")); const backupFileName = `webserver-backup-${timestamp}.zip`; - const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`; + const remotePath = getRcloneRemotePath(destination, `${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`); try { await execAsync(`mkdir -p ${tempDir}/filesystem`); @@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await execAsync(cleanupCommand); await execAsync( - `rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`, + `rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' ${BASE_PATH}/ ${tempDir}/filesystem/`, ); writeStream.write("Copied filesystem to temp directory\n"); @@ -79,10 +79,10 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { writeStream.write("Zipped database and filesystem\n"); - const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; - writeStream.write("Running command to upload backup to S3\n"); + const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${remotePath}"`; + writeStream.write("Running command to upload backup to destination\n"); await execAsync(uploadCommand); - writeStream.write("Uploaded backup to S3 ✅\n"); + writeStream.write("Uploaded backup to destination ✅\n"); writeStream.end(); await updateDeploymentStatus(deployment.deploymentId, "done"); return true; diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 5bf3a790e0..69bde1b999 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -182,7 +182,11 @@ export const mechanizeDockerContainer = async ( }); } catch (error) { console.log(error); - await docker.createService(settings); + if (authConfig) { + await docker.createService(authConfig, settings); + } else { + await docker.createService(settings); + } } }; diff --git a/packages/server/src/utils/crons/enterprise.ts b/packages/server/src/utils/crons/enterprise.ts index 7b07aaefb5..a9026c6417 100644 --- a/packages/server/src/utils/crons/enterprise.ts +++ b/packages/server/src/utils/crons/enterprise.ts @@ -5,9 +5,9 @@ import { db } from "../../db/index"; import { user as userSchema } from "../../db/schema/user"; export const LICENSE_KEY_URL = - process.env.NODE_ENV === "development" - ? "http://localhost:4002" - : "https://licenses-api.dokploy.com"; + // process.env.NODE_ENV === "development" + // ? "http://localhost:4002" + "https://licenses-api.dokploy.com"; export const initEnterpriseBackupCronJobs = async () => { scheduleJob("enterprise-check", "0 0 */3 * *", async () => { diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index 10797a51d6..31d5c4726d 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -2,7 +2,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Compose } from "@dokploy/server/services/compose"; import type { Destination } from "@dokploy/server/services/destination"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -23,8 +23,8 @@ export const restoreComposeBackup = async ( } const { serverId, appName, composeType } = compose; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupInput.backupFile}`; let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; diff --git a/packages/server/src/utils/restore/mariadb.ts b/packages/server/src/utils/restore/mariadb.ts index ffbceba765..3bfc470afd 100644 --- a/packages/server/src/utils/restore/mariadb.ts +++ b/packages/server/src/utils/restore/mariadb.ts @@ -2,7 +2,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Mariadb } from "@dokploy/server/services/mariadb"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -15,8 +15,8 @@ export const restoreMariadbBackup = async ( try { const { appName, serverId, databaseUser, databasePassword } = mariadb; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; diff --git a/packages/server/src/utils/restore/mongo.ts b/packages/server/src/utils/restore/mongo.ts index 4329a49857..408b4f1b2e 100644 --- a/packages/server/src/utils/restore/mongo.ts +++ b/packages/server/src/utils/restore/mongo.ts @@ -2,7 +2,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Mongo } from "@dokploy/server/services/mongo"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -15,8 +15,8 @@ export const restoreMongoBackup = async ( try { const { appName, databasePassword, databaseUser, serverId } = mongo; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupInput.backupFile}`; const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`; diff --git a/packages/server/src/utils/restore/mysql.ts b/packages/server/src/utils/restore/mysql.ts index f5187242cf..6ed43e04f9 100644 --- a/packages/server/src/utils/restore/mysql.ts +++ b/packages/server/src/utils/restore/mysql.ts @@ -2,7 +2,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { MySql } from "@dokploy/server/services/mysql"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -15,8 +15,8 @@ export const restoreMySqlBackup = async ( try { const { appName, databaseRootPassword, serverId } = mysql; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupInput.backupFile}`; const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 19f32989f0..03ab7dd2c8 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -2,7 +2,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Postgres } from "@dokploy/server/services/postgres"; import type { z } from "zod"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getRestoreCommand } from "./utils"; @@ -15,8 +15,8 @@ export const restorePostgresBackup = async ( try { const { appName, databaseUser, serverId } = postgres; - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupInput.backupFile}`; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 683a1898ae..ae2a1e52cd 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -3,7 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Destination } from "@dokploy/server/services/destination"; -import { getS3Credentials } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath } from "../backups/utils"; import { execAsync } from "../process/execAsync"; export const restoreWebServerBackup = async ( @@ -15,8 +15,8 @@ export const restoreWebServerBackup = async ( return; } try { - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupFile}`; const { BASE_PATH } = paths(); diff --git a/packages/server/src/utils/volume-backups/backup.ts b/packages/server/src/utils/volume-backups/backup.ts index e192fd698f..eab6483040 100644 --- a/packages/server/src/utils/volume-backups/backup.ts +++ b/packages/server/src/utils/volume-backups/backup.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { paths } from "@dokploy/server/constants"; import { findComposeById } from "@dokploy/server/services/compose"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; -import { getS3Credentials, normalizeS3Path } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "../backups/utils"; export const getVolumeServiceAppName = ( volumeBackup: Awaited>, @@ -33,13 +33,13 @@ export const backupVolume = async ( const s3AppName = getVolumeServiceAppName(volumeBackup); const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`; - const rcloneFlags = getS3Credentials(volumeBackup.destination); - const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const rcloneFlags = getRcloneFlags(volumeBackup.destination); + const rcloneDestination = getRcloneRemotePath(destination, bucketDestination); const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`; - const baseCommand = ` + const backupCommand = ` set -e echo "Volume name: ${volumeName}" echo "Backup file name: ${backupFileName}" @@ -52,6 +52,9 @@ export const backupVolume = async ( ubuntu \ bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ." echo "Volume backup done ✅" + `; + + const uploadCommand = ` echo "Starting upload to S3..." ${rcloneCommand} echo "Upload to S3 done ✅" @@ -61,7 +64,10 @@ export const backupVolume = async ( `; if (!turnOff) { - return baseCommand; + return ` + ${backupCommand} + ${uploadCommand} + `; } const serviceLockId = @@ -110,9 +116,10 @@ export const backupVolume = async ( ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}") echo "Actual replicas: $ACTUAL_REPLICAS" docker service update --replicas=0 ${volumeBackup.application?.appName} - ${baseCommand} + ${backupCommand} echo "Starting application to $ACTUAL_REPLICAS replicas" docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName} + ${uploadCommand} `); } if (serviceType === "compose") { @@ -147,8 +154,9 @@ export const backupVolume = async ( } return lockWrapper(` ${stopCommand} - ${baseCommand} + ${backupCommand} ${startCommand} + ${uploadCommand} `); } }; diff --git a/packages/server/src/utils/volume-backups/restore.ts b/packages/server/src/utils/volume-backups/restore.ts index 6f6068cafc..f98e061fdf 100644 --- a/packages/server/src/utils/volume-backups/restore.ts +++ b/packages/server/src/utils/volume-backups/restore.ts @@ -3,9 +3,10 @@ import { findApplicationById, findComposeById, findDestinationById, - getS3Credentials, + getRcloneFlags, paths, } from "../.."; +import { getRcloneRemotePath } from "../backups/utils"; export const restoreVolume = async ( id: string, @@ -18,8 +19,8 @@ export const restoreVolume = async ( const destination = await findDestinationById(destinationId); const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName); - const rcloneFlags = getS3Credentials(destination); - const bucketPath = `:s3:${destination.bucket}`; + const rcloneFlags = getRcloneFlags(destination); + const bucketPath = getRcloneRemotePath(destination, ""); const backupPath = `${bucketPath}/${backupFileName}`; // Command to download backup file from S3 diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 6a51e765d7..71792905b5 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -10,7 +10,7 @@ import { execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { scheduledJobs, scheduleJob } from "node-schedule"; -import { getS3Credentials, normalizeS3Path } from "../backups/utils"; +import { getRcloneFlags, getRcloneRemotePath, normalizeS3Path } from "../backups/utils"; import { sendVolumeBackupNotifications } from "../notifications/volume-backup"; import { backupVolume, getVolumeServiceAppName } from "./backup"; @@ -80,9 +80,9 @@ const cleanupOldVolumeBackups = async ( if (!keepLatestCount) return; try { - const rcloneFlags = getS3Credentials(destination); + const rcloneFlags = getRcloneFlags(destination); const s3AppName = getVolumeServiceAppName(volumeBackup); - const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`; + const backupFilesPath = getRcloneRemotePath(destination, `${s3AppName}/${normalizeS3Path(prefix || "")}`); const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 710e0b5480..27c0a37b76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,9 +113,12 @@ importers: '@ai-sdk/openai-compatible': specifier: ^2.0.30 version: 2.0.30(zod@4.3.6) + '@better-auth/api-key': + specifier: 1.5.4 + version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(6416d86fb612cf38d909eeddb781eced)) '@better-auth/sso': - specifier: 1.5.0-beta.16 - version: 1.5.0-beta.16(ecc6d140ded1a4f90ca904fc42a7ea34) + specifier: 1.5.4 + version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(6416d86fb612cf38d909eeddb781eced))(better-call@2.0.2(zod@4.3.6)) '@codemirror/autocomplete': specifier: ^6.18.6 version: 6.20.0 @@ -273,8 +276,8 @@ importers: specifier: 5.1.1 version: 5.1.1 better-auth: - specifier: 1.5.0-beta.16 - version: 1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)) + specifier: 1.5.4 + version: 1.5.4(6416d86fb612cf38d909eeddb781eced) bl: specifier: 6.0.11 version: 6.0.11 @@ -307,10 +310,10 @@ importers: version: 16.4.5 drizzle-orm: specifier: 0.45.1 - version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) drizzle-zod: specifier: 0.8.3 - version: 0.8.3(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6) + version: 0.8.3(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6) fancy-ansi: specifier: ^0.1.3 version: 0.1.3 @@ -548,7 +551,7 @@ importers: version: 16.4.5 drizzle-orm: specifier: 0.45.1 - version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) hono: specifier: ^4.11.7 version: 4.12.2 @@ -613,9 +616,12 @@ importers: '@ai-sdk/openai-compatible': specifier: ^2.0.30 version: 2.0.30(zod@4.3.6) + '@better-auth/api-key': + specifier: 1.5.4 + version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(e98fe91cbd4cf66623811f3957decc8e)) '@better-auth/sso': - specifier: 1.5.0-beta.16 - version: 1.5.0-beta.16(e2684b66b852c11b7f6d95376fa7a8fe) + specifier: 1.5.4 + version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(e98fe91cbd4cf66623811f3957decc8e))(better-call@2.0.2(zod@4.3.6)) '@better-auth/utils': specifier: 0.3.1 version: 0.3.1 @@ -653,11 +659,11 @@ importers: specifier: 5.1.1 version: 5.1.1 better-auth: - specifier: 1.5.0-beta.16 - version: 1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)) + specifier: 1.5.4 + version: 1.5.4(e98fe91cbd4cf66623811f3957decc8e) better-call: - specifier: 1.3.2 - version: 1.3.2(zod@4.3.6) + specifier: 2.0.2 + version: 2.0.2(zod@4.3.6) bl: specifier: 6.0.11 version: 6.0.11 @@ -675,13 +681,13 @@ importers: version: 16.4.5 drizzle-dbml-generator: specifier: 0.10.0 - version: 0.10.0(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) + version: 0.10.0(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) drizzle-orm: specifier: 0.45.1 - version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + version: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) drizzle-zod: specifier: 0.5.1 - version: 0.5.1(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6) + version: 0.5.1(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6) lodash: specifier: 4.17.21 version: 4.17.21 @@ -756,8 +762,8 @@ importers: version: 4.3.6 devDependencies: '@better-auth/cli': - specifier: 1.5.0-beta.13 - version: 1.5.0-beta.13(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)) + specifier: 1.4.21 + version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)) '@types/adm-zip': specifier: ^0.5.7 version: 0.5.7 @@ -1069,117 +1075,96 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@better-auth/cli@1.5.0-beta.13': - resolution: {integrity: sha512-/Of+Pdsutq4R4R+bpWlaitc3epokj3R1M/6U2ZmZFyNz1YPMsjTQSR3zJBLJr1HnvbUzSeMBSNvOAJM4kyx84w==} + '@better-auth/api-key@1.5.4': + resolution: {integrity: sha512-McV2I4E8cR+jIYjIXy8hfhZg5EKOIPooAZSt78m5RvtLzYRcl4JKJbtKCh/au7zIh47yT+EcxM9pmL9/WigDcA==} + peerDependencies: + '@better-auth/core': 1.5.4 + '@better-auth/utils': 0.3.1 + better-auth: 1.5.4 + + '@better-auth/cli@1.4.21': + resolution: {integrity: sha512-bKEa8BupnZxNjLk9ZDntvgQGm5jogeE2wHdMbYifhet3GTyxgDi6pXoOK8+aqHYQGg1C3OALi9hVVWnrv7JJWQ==} hasBin: true - '@better-auth/core@1.5.0-beta.13': - resolution: {integrity: sha512-oOK1O3bwfzebK5W4iIYOdB+IBLk+RLWfOm/MU6dMq4l1HNWvi5iZ75lxxk7Sgx0MfaeEn1C+lXIHplDyeeZKhQ==} + '@better-auth/core@1.4.21': + resolution: {integrity: sha512-R4s7pwShkqB21fZ599QASbXxqFcoxanLyz7DHSX6SJPNYV748wBLsm3xM9VrjfvWMpS+cQUErOCt9yWT1hMn6w==} peerDependencies: - '@better-auth/utils': 0.3.1 + '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - better-call: 1.2.1 + better-call: 1.1.8 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/core@1.5.0-beta.16': - resolution: {integrity: sha512-eGM4Q2Btj6WNh/9jGTUIFsvFPt4kUsRa9/3tDSPfY/M3OkCUFkscANTeq/AVyJkBMO/IG2pQ8F48Hy1CBn7eyw==} + '@better-auth/core@1.5.4': + resolution: {integrity: sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ==} peerDependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' better-call: 1.3.2 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true - '@better-auth/drizzle-adapter@1.5.0-beta.13': - resolution: {integrity: sha512-PZmyTDun2AEhwQtO/xQb8Zi5eBMyJ/o69T3h4wj59CxHtBD11fvhY19csJd3hVk1YvFJobgMv3icWXmz02Egmw==} - peerDependencies: - '@better-auth/core': 1.5.0-beta.13 - '@better-auth/utils': ^0.3.0 - drizzle-orm: '>=0.41.0' - - '@better-auth/drizzle-adapter@1.5.0-beta.16': - resolution: {integrity: sha512-ArM+7rWepLN7kFXIDmkV9CFBblBhfwOVIzPJgv7ZaWnAsjGj5oLhSkBQXvAfw0W/2UK1yziSuD4/HqiqKuEl7Q==} + '@better-auth/drizzle-adapter@1.5.4': + resolution: {integrity: sha512-4M4nMAWrDd3TmpV6dONkJjybBVKRZghe5Oj0NNyDEoXubxastQdO7Sb5B54I1rTx5yoMgsqaB+kbJnu/9UgjQg==} peerDependencies: - '@better-auth/core': 1.5.0-beta.16 + '@better-auth/core': 1.5.4 '@better-auth/utils': ^0.3.0 drizzle-orm: '>=0.41.0' - '@better-auth/kysely-adapter@1.5.0-beta.13': - resolution: {integrity: sha512-1Ek0jV/FiFUEcTkoPkf4MWNR3k6b5t1mLBanJVjvSD+UhAXhe93H8onEKlRcSopNu2ls6z70BI75jYBxYSdf5Q==} + '@better-auth/kysely-adapter@1.5.4': + resolution: {integrity: sha512-DPww7rIfz6Ed7dZlJSW9xMQ42VKaJLB5Cs+pPqd+UHKRyighKjf3VgvMIcAdFPc4olQ0qRHo3+ZJhFlBCxRhxA==} peerDependencies: - '@better-auth/core': 1.5.0-beta.13 + '@better-auth/core': 1.5.4 '@better-auth/utils': ^0.3.0 kysely: ^0.27.0 || ^0.28.0 - '@better-auth/kysely-adapter@1.5.0-beta.16': - resolution: {integrity: sha512-I/JdEWaFFrQZSGwbr5NmG6CFSA2zqiQqOZODQN35jAKXcCRHl7ArMvBgEybxnposJfND857TsOGf6yGWlqDTJw==} - peerDependencies: - '@better-auth/core': 1.5.0-beta.16 - '@better-auth/utils': ^0.3.0 - kysely: ^0.27.0 || ^0.28.0 - - '@better-auth/memory-adapter@1.5.0-beta.13': - resolution: {integrity: sha512-7pCJXEiI4MPduxueVtUUnfUOxOQhu4EcFmQQ7zUodumtYmi5Xwar/bctl7v8GsabkG8bZwEQUEvO5uqVtnGwhA==} - peerDependencies: - '@better-auth/core': 1.5.0-beta.13 - '@better-auth/utils': ^0.3.0 - - '@better-auth/memory-adapter@1.5.0-beta.16': - resolution: {integrity: sha512-aldCjQS8c6MRTe7xzIYNUJyNHTUVXKnLjp/LkufZjzB2mgews0nTLhJNRN06qlGM4wZwt2xZArDYgZmmIBJp+Q==} + '@better-auth/memory-adapter@1.5.4': + resolution: {integrity: sha512-iiWYut9rbQqiAsgRBtj6+nxanwjapxRgpIJbiS2o81h7b9iclE0AiDA0Foes590gdFQvskNauZcCpuF8ytxthg==} peerDependencies: - '@better-auth/core': 1.5.0-beta.16 + '@better-auth/core': 1.5.4 '@better-auth/utils': ^0.3.0 - '@better-auth/mongo-adapter@1.5.0-beta.13': - resolution: {integrity: sha512-FDqbLWqreELN/wiIwGlItZmi+FShzGPNdNk2KaRo2hVLakBQy9hlCtnxeYV7OfptOPs+AkGc8hboKjBzFA1uUQ==} + '@better-auth/mongo-adapter@1.5.4': + resolution: {integrity: sha512-ArzJN5Obk6i6+vLK1HpPzLIcsjxZYXPPUvxVU8eyU5HyoUT2MlswWfPQ8UJAKPn0iq/T4PVp/wZcQMhWk1tuNA==} peerDependencies: - '@better-auth/core': 1.5.0-beta.13 + '@better-auth/core': 1.5.4 '@better-auth/utils': ^0.3.0 mongodb: ^6.0.0 || ^7.0.0 - '@better-auth/mongo-adapter@1.5.0-beta.16': - resolution: {integrity: sha512-9FxlAt0/q0Aozg201WsefMGupsXL2bDD1RaIHCJsuhviWybWqf8GIlNabir9q4h93Y8iv2pjS/wRa0dAbgZnjw==} + '@better-auth/prisma-adapter@1.5.4': + resolution: {integrity: sha512-ZQTbcBopw/ezjjbNFsfR3CRp0QciC4tJCarAnB5G9fZtUYbDjfY0vZOxIRmU4kI3x755CXQpGqTrkwmXaMRa3w==} peerDependencies: - '@better-auth/core': 1.5.0-beta.16 - '@better-auth/utils': ^0.3.0 - mongodb: ^6.0.0 || ^7.0.0 - - '@better-auth/prisma-adapter@1.5.0-beta.13': - resolution: {integrity: sha512-w2+KMfoWrPeYKAmG1tinwmsHe2Lc3HNyOASEO5jw6oJY7BrLDKcqeqCJuhavuwhd2fCAq3fEiqyJVCYDDLVKqA==} - peerDependencies: - '@better-auth/core': 1.5.0-beta.13 - '@better-auth/utils': ^0.3.0 - '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 - prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - - '@better-auth/prisma-adapter@1.5.0-beta.16': - resolution: {integrity: sha512-5xBsHd1LIhtIZGJ6D1m7GRKQ15pN1cX0N4yTY5BTvssmqGBUu2mKNmgcERESoENO5VAGpmd6Gky+nG/9hvH4vQ==} - peerDependencies: - '@better-auth/core': 1.5.0-beta.16 + '@better-auth/core': 1.5.4 '@better-auth/utils': ^0.3.0 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@better-auth/sso@1.5.0-beta.16': - resolution: {integrity: sha512-Ay9EFEBOiqe3mkfcfQIKgslnhcBYvHoRyskvpzSkwkZJ8nlowofs6mqddIeV7YUmDTmpc1FVdIzuPzIFr+T7zQ==} + '@better-auth/sso@1.5.4': + resolution: {integrity: sha512-XWrPIBp9d9Yx5bTe6lxQSkkRv14SQj0CFhblXM7cuCbK4ehk6xmBwiqcwJOMohGBrC6qxRItT0YkB9w1BCqP2A==} peerDependencies: - '@better-auth/core': 1.5.0-beta.16 + '@better-auth/core': 1.5.4 '@better-auth/utils': 0.3.1 - better-auth: 1.5.0-beta.16 + better-auth: 1.5.4 better-call: 1.3.2 - '@better-auth/telemetry@1.5.0-beta.13': - resolution: {integrity: sha512-Ju/29VMM+pfgFs/i7rX7scMO2QFgNbt/PRQGU3VJEQDC9M9NHhGgLe2kPkUEFQic5Z6sOYpXTGKFJyF/YSJRFw==} + '@better-auth/telemetry@1.4.21': + resolution: {integrity: sha512-LX+FGMZnhR2KQZ0idHH1+UwlXvkOl6P8w3Gne4TtjvUCt3QjG9FKIuP9JD3MAmEEkwGt0SoAPHPJEGTjUl3ydg==} peerDependencies: - '@better-auth/core': 1.5.0-beta.13 + '@better-auth/core': 1.4.21 - '@better-auth/telemetry@1.5.0-beta.16': - resolution: {integrity: sha512-EiNe7xSQkuypvR9i/C10M/DhoLyba+Ptdeczrj1z8Z+2/bP65utkwQ/Vq0j0zw7K8IgRp7CuMtrDa4j8Fp08Xw==} + '@better-auth/telemetry@1.5.4': + resolution: {integrity: sha512-mGXTY7Ecxo7uvlMr6TFCBUvlH0NUMOeE9LKgPhG4HyhBN6VfCEg/DD9PG0Z2IatmMWQbckkt7ox5A0eBpG9m5w==} peerDependencies: - '@better-auth/core': 1.5.0-beta.16 + '@better-auth/core': 1.5.4 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} '@better-auth/utils@0.3.1': resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} @@ -4374,8 +4359,8 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - better-auth@1.5.0-beta.13: - resolution: {integrity: sha512-J/48ye6NRNQH5Wj1FXrQdBR6/4l1ohOCVhHIqTB3u48jNCwmXwarhDcctTXM3/FGSffYmY8Hjc+kvbxd5IM5Ug==} + better-auth@1.4.21: + resolution: {integrity: sha512-qdrIZS7xnGF2HPBV5wYNPWTkPojhauOOjz1+MhLvwFy+zXpgLofQmWsI5I9DY+ef845NKt93XcgpyAc4RPPT9A==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -4436,8 +4421,8 @@ packages: vue: optional: true - better-auth@1.5.0-beta.16: - resolution: {integrity: sha512-hkpcNioqkgYs9MNRiK9fD/l8gaiFhODpf8U58v5efTZ2HrXbTfiWKfM2KxcOyFeF2SNdqal+cn762a0ym/3WGA==} + better-auth@1.5.4: + resolution: {integrity: sha512-ReykcEKx6Kp9560jG1wtlDBnftA7L7xb3ZZdDWm5yGXKKe2pUf+oBjH0fqekrkRII0m4XBVQbQ0mOrFv+3FdYg==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -4498,8 +4483,8 @@ packages: vue: optional: true - better-call@1.2.1: - resolution: {integrity: sha512-Ccgd5hj2Fmtu9Vjb9APXNYxutJWPRDhJanWAzFLwSzYAiOvkXN61OmYdvSirfrL2Z7REuK7TRklP8SIbZRlGwA==} + better-call@1.1.8: + resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -4514,6 +4499,18 @@ packages: zod: optional: true + better-call@2.0.2: + resolution: {integrity: sha512-QqSKtfJD/ZzQdlm7BTUxT9RCA0AxcrZEMyU/yl7/uoFDoR7YCTdc555xQXjReo75M6/xkskPawPdhbn3fge4Cg==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -4521,6 +4518,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -5396,6 +5396,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -5440,8 +5444,11 @@ packages: fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - fast-xml-parser@5.3.7: - resolution: {integrity: sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==} + fast-xml-builder@1.1.0: + resolution: {integrity: sha512-7mtITW/we2/wTUZqMyBOR2F8xP4CRxMiSEcQxPIqdRWdO2L/HZSOlzoNyghmyDwNB8BDxePooV1ZTJpkOUhdRg==} + + fast-xml-parser@5.5.1: + resolution: {integrity: sha512-JTpMz8P5mDoNYzXTmTT/xzWjFiCWi0U+UQTJtrFH9muXsr2RqtXZPbnCW5h2mKsOd4u3XcPWCvDSrnaBPlUcMQ==} hasBin: true fastq@1.20.1: @@ -5459,6 +5466,9 @@ packages: picomatch: optional: true + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5582,6 +5592,9 @@ packages: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6469,6 +6482,13 @@ packages: resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} engines: {node: ^20.0.0 || >=22.0.0} + nanostores@1.1.1: + resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==} + engines: {node: ^20.0.0 || >=22.0.0} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -6508,6 +6528,10 @@ packages: react: '>= 16.0.0' react-dom: '>= 16.0.0' + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -6700,6 +6724,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.1.2: + resolution: {integrity: sha512-LXWqJmcpp2BKOEmgt4CyuESFmBfPuhJlAHKJsFzuJU6CxErWk75BrO+Ni77M9OxHN6dCYKM4vj+21Z6cOL96YQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -6908,6 +6936,12 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} @@ -7030,6 +7064,10 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-confetti-explosion@2.1.2: resolution: {integrity: sha512-4UzDFBajAGXmF9TSJoRMO2QOBCIXc66idTxH8l7Mkul48HLGtk+tMzK9HYDYsy7Zmw5sEGchi2fbn4AJUuLrZw==} peerDependencies: @@ -7457,6 +7495,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -7577,6 +7621,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -7799,6 +7847,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -8530,24 +8581,39 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@better-auth/cli@1.5.0-beta.13(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.0)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))': + '@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(6416d86fb612cf38d909eeddb781eced))': + dependencies: + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + better-auth: 1.5.4(6416d86fb612cf38d909eeddb781eced) + zod: 4.3.6 + + '@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(e98fe91cbd4cf66623811f3957decc8e))': + dependencies: + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + better-auth: 1.5.4(e98fe91cbd4cf66623811f3957decc8e) + zod: 4.3.6 + + '@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0)(mysql2@3.15.3)(nanostores@1.1.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))': dependencies: '@babel/core': 7.29.0 '@babel/preset-react': 7.28.5(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/telemetry': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/telemetry': 1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/utils': 0.3.0 '@clack/prompts': 0.11.0 '@mrleebo/prisma-ast': 0.13.1 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) '@types/pg': 8.16.0 - better-auth: 1.5.0-beta.13(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)) + better-auth: 1.4.21(322d98a0971994b611c03db4abc7f931) + better-sqlite3: 12.6.2 c12: 3.3.3 chalk: 5.6.2 commander: 12.1.0 dotenv: 17.3.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) open: 10.2.0 pg: 8.18.0 prettier: 3.8.1 @@ -8576,7 +8642,6 @@ snapshots: - '@vercel/postgres' - '@xata.io/client' - better-call - - better-sqlite3 - bun-types - drizzle-kit - expo-sqlite @@ -8602,29 +8667,29 @@ snapshots: - vitest - vue - '@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.2.1(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': dependencies: - '@better-auth/utils': 0.3.1 + '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 - better-call: 1.2.1(zod@4.3.6) + better-call: 1.1.8(zod@4.3.6) jose: 6.1.3 kysely: 0.28.11 nanostores: 1.1.0 zod: 4.3.6 - '@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: - '@better-auth/utils': 0.3.1 + '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 - better-call: 1.3.2(zod@4.3.6) + better-call: 2.0.2(zod@4.3.6) jose: 6.1.3 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 - '@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -8632,105 +8697,88 @@ snapshots: better-call: 1.3.2(zod@4.3.6) jose: 6.1.3 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 - '@better-auth/drizzle-adapter@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))': - dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/utils': 0.3.1 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - - '@better-auth/drizzle-adapter@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/utils': 0.3.1 - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - - '@better-auth/kysely-adapter@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(kysely@0.28.11)': - dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/utils': 0.3.1 - kysely: 0.28.11 - - '@better-auth/kysely-adapter@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(kysely@0.28.11)': - dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 2.0.2(zod@4.3.6) + jose: 6.1.3 kysely: 0.28.11 + nanostores: 1.1.1 + zod: 4.3.6 - '@better-auth/memory-adapter@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)': + '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))': dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - '@better-auth/memory-adapter@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)': + '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 + kysely: 0.28.11 - '@better-auth/mongo-adapter@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - mongodb: 7.1.0 - '@better-auth/mongo-adapter@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))': + '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))': dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - '@better-auth/prisma-adapter@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))': + '@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(6416d86fb612cf38d909eeddb781eced))(better-call@2.0.2(zod@4.3.6))': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/utils': 0.3.1 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - - '@better-auth/sso@1.5.0-beta.16(e2684b66b852c11b7f6d95376fa7a8fe)': - dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 - better-auth: 1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)) - better-call: 1.3.2(zod@4.3.6) - fast-xml-parser: 5.3.7 + better-auth: 1.5.4(6416d86fb612cf38d909eeddb781eced) + better-call: 2.0.2(zod@4.3.6) + fast-xml-parser: 5.5.1 jose: 6.1.3 samlify: 2.10.2 zod: 4.3.6 - '@better-auth/sso@1.5.0-beta.16(ecc6d140ded1a4f90ca904fc42a7ea34)': + '@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(e98fe91cbd4cf66623811f3957decc8e))(better-call@2.0.2(zod@4.3.6))': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 - better-auth: 1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)) - better-call: 1.3.2(zod@4.3.6) - fast-xml-parser: 5.3.7 + better-auth: 1.5.4(e98fe91cbd4cf66623811f3957decc8e) + better-call: 2.0.2(zod@4.3.6) + fast-xml-parser: 5.5.1 jose: 6.1.3 samlify: 2.10.2 zod: 4.3.6 - '@better-auth/telemetry@1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 - '@better-auth/telemetry@1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 + '@better-auth/utils@0.3.0': {} + '@better-auth/utils@0.3.1': {} '@better-fetch/fetch@1.1.21': {} @@ -10314,9 +10362,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))': + '@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))': optionalDependencies: - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) '@prisma/config@7.4.1': dependencies: @@ -12385,47 +12433,43 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.5.0-beta.13(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)): + better-auth@1.4.21(322d98a0971994b611c03db4abc7f931): dependencies: - '@better-auth/core': 1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.2.1(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/drizzle-adapter': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) - '@better-auth/kysely-adapter': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - '@better-auth/telemetry': 1.5.0-beta.13(@better-auth/core@1.5.0-beta.13(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) - '@better-auth/utils': 0.3.1 + '@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.2.1(zod@4.3.6) + better-call: 1.1.8(zod@4.3.6) defu: 6.1.4 jose: 6.1.3 kysely: 0.28.11 nanostores: 1.1.0 zod: 4.3.6 optionalDependencies: - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + better-sqlite3: 12.6.2 drizzle-kit: 0.31.9 - drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) mongodb: 7.1.0 mysql2: 3.15.3 next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) pg: 8.18.0 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1) - better-auth@1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)): + better-auth@1.5.4(6416d86fb612cf38d909eeddb781eced): dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/drizzle-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) - '@better-auth/kysely-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - '@better-auth/telemetry': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -12434,30 +12478,33 @@ snapshots: defu: 6.1.4 jose: 6.1.3 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + better-sqlite3: 12.6.2 drizzle-kit: 0.31.9 - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) mongodb: 7.1.0 mysql2: 3.15.3 next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) pg: 8.18.0 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1) + transitivePeerDependencies: + - '@cloudflare/workers-types' - better-auth@1.5.0-beta.16(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(pg@8.18.0)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)): + better-auth@1.5.4(e98fe91cbd4cf66623811f3957decc8e): dependencies: - '@better-auth/core': 1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) - '@better-auth/drizzle-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) - '@better-auth/kysely-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) - '@better-auth/telemetry': 1.5.0-beta.16(@better-auth/core@1.5.0-beta.16(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -12466,22 +12513,25 @@ snapshots: defu: 6.1.4 jose: 6.1.3 kysely: 0.28.11 - nanostores: 1.1.0 + nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + better-sqlite3: 12.6.2 drizzle-kit: 0.31.9 - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) mongodb: 7.1.0 mysql2: 3.15.3 next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) pg: 8.18.0 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1) + transitivePeerDependencies: + - '@cloudflare/workers-types' - better-call@1.2.1(zod@4.3.6): + better-call@1.1.8(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -12499,10 +12549,28 @@ snapshots: optionalDependencies: zod: 4.3.6 + better-call@2.0.2(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.0.1 + optionalDependencies: + zod: 4.3.6 + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -13064,9 +13132,9 @@ snapshots: drange@1.1.1: {} - drizzle-dbml-generator@0.10.0(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))): + drizzle-dbml-generator@0.10.0(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))): dependencies: - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) drizzle-kit@0.31.9: dependencies: @@ -13077,50 +13145,53 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): + drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): optionalDependencies: '@electric-sql/pglite': 0.3.15 '@opentelemetry/api': 1.9.0 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) '@types/pg': 8.16.0 + better-sqlite3: 12.6.2 kysely: 0.28.11 mysql2: 3.15.3 pg: 8.18.0 postgres: 3.4.4 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): + drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): optionalDependencies: '@electric-sql/pglite': 0.3.15 '@opentelemetry/api': 1.9.0 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) '@types/pg': 8.16.0 + better-sqlite3: 12.6.2 kysely: 0.28.11 mysql2: 3.15.3 pg: 8.18.0 postgres: 3.4.4 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): + drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.7)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)): optionalDependencies: '@electric-sql/pglite': 0.3.15 '@opentelemetry/api': 1.9.0 - '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + '@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) '@types/pg': 8.16.0 + better-sqlite3: 12.6.2 kysely: 0.28.11 mysql2: 3.15.3 pg: 8.18.0 postgres: 3.4.7 - prisma: 7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) + prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3) - drizzle-zod@0.5.1(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6): + drizzle-zod@0.5.1(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) zod: 4.3.6 - drizzle-zod@0.8.3(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6): + drizzle-zod@0.8.3(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) + drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)) zod: 4.3.6 dunder-proto@1.0.1: @@ -13251,6 +13322,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expand-template@2.0.3: {} + expect-type@1.3.0: {} exsolve@1.0.8: {} @@ -13287,8 +13360,14 @@ snapshots: fast-sha256@1.3.0: {} - fast-xml-parser@5.3.7: + fast-xml-builder@1.1.0: + dependencies: + path-expression-matcher: 1.1.2 + + fast-xml-parser@5.5.1: dependencies: + fast-xml-builder: 1.1.0 + path-expression-matcher: 1.1.2 strnum: 2.1.2 fastq@1.20.1: @@ -13303,6 +13382,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -13435,6 +13516,8 @@ snapshots: nypm: 0.6.5 pathe: 2.0.3 + github-from-package@0.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -14494,6 +14577,10 @@ snapshots: nanostores@1.1.0: {} + nanostores@1.1.1: {} + + napi-build-utils@2.0.0: {} + neotraverse@0.6.18: {} next-themes@0.2.1(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -14535,6 +14622,10 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + node-abi@3.87.0: + dependencies: + semver: 7.7.3 + node-abort-controller@3.1.1: {} node-addon-api@5.1.0: {} @@ -14713,6 +14804,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.1.2: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -14906,9 +14999,24 @@ snapshots: postgres@3.4.7: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.0.1 + tunnel-agent: 0.6.0 + prettier@3.8.1: {} - prisma@7.4.1(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3): + prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3): dependencies: '@prisma/config': 7.4.1 '@prisma/dev': 0.20.0(typescript@5.9.3) @@ -14917,6 +15025,7 @@ snapshots: mysql2: 3.15.3 postgres: 3.4.7 optionalDependencies: + better-sqlite3: 12.6.2 typescript: 5.9.3 transitivePeerDependencies: - '@types/react' @@ -15035,6 +15144,13 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-confetti-explosion@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: lodash: 4.17.21 @@ -15549,6 +15665,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -15663,6 +15787,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} stripe@17.2.0: @@ -15962,6 +16088,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + tweetnacl@0.14.5: {} type-fest@0.20.2: {}