From 371c6317aa4371b879eae2acf44936c2f2c7e0b9 Mon Sep 17 00:00:00 2001 From: yni9ht Date: Mon, 17 Mar 2025 20:44:13 +0800 Subject: [PATCH 01/52] refactor(mount): streamline mount update logic and improve readability --- packages/server/src/services/mount.ts | 40 +++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts index 836feacec..1fa4db1e5 100644 --- a/packages/server/src/services/mount.ts +++ b/packages/server/src/services/mount.ts @@ -123,29 +123,27 @@ export const updateMount = async ( mountId: string, mountData: Partial, ) => { - return await db.transaction(async (tx) => { - const mount = await tx - .update(mounts) - .set({ - ...mountData, - }) - .where(eq(mounts.mountId, mountId)) - .returning() - .then((value) => value[0]); + const mount = await db + .update(mounts) + .set({ + ...mountData, + }) + .where(eq(mounts.mountId, mountId)) + .returning() + .then((value) => value[0]); - if (!mount) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Mount not found", - }); - } + if (!mount) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mount not found", + }); + } - if (mount.type === "file") { - await deleteFileMount(mountId); - await createFileMount(mountId); - } - return mount; - }); + if (mount.type === "file") { + await deleteFileMount(mountId); + await createFileMount(mountId); + } + return mount; }; export const findMountsByApplicationId = async ( From cc5a3e6873345bb6a1a87bfbeae3def1d4276d2e Mon Sep 17 00:00:00 2001 From: Yusoof Moh <18055365+yusoofsh@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:04:35 +0700 Subject: [PATCH 02/52] Add option to disable recurse submodules Add option to disable recurse submodules under "Provider Select the source of your code" form. * Add a checkbox to disable recurse submodules in `apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx`, `apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx`, and `apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx`. * Update the form schema in the above files to include the new option. * Conditionally include the `--recurse-submodules` flag in the `git clone` command in the above files. * Update the "Provider Select the source of your code" form in `apps/dokploy/components/dashboard/application/general/generic/show.tsx` to include the new option. * Conditionally include the `--recurse-submodules` flag in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`. * Add the `--depth` flag to optimize submodule cloning performance in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Dokploy/dokploy?shareId=XXXX-XXXX-XXXX-XXXX). --- .../general/generic/save-git-provider.tsx | 21 +++++++ .../general/generic/save-github-provider.tsx | 21 +++++++ .../general/generic/save-gitlab-provider.tsx | 21 +++++++ .../server/src/utils/providers/bitbucket.ts | 46 ++++++++------ packages/server/src/utils/providers/git.ts | 63 +++++++++++-------- packages/server/src/utils/providers/github.ts | 45 +++++++------ packages/server/src/utils/providers/gitlab.ts | 45 +++++++------ 7 files changed, 181 insertions(+), 81 deletions(-) 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 3d6f6a388..2613174b7 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 @@ -44,6 +44,7 @@ const GitProviderSchema = z.object({ branch: z.string().min(1, "Branch required"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), + recurseSubmodules: z.boolean().default(true), }); type GitProvider = z.infer; @@ -67,6 +68,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { repositoryURL: "", sshKey: undefined, watchPaths: [], + recurseSubmodules: true, }, resolver: zodResolver(GitProviderSchema), }); @@ -79,6 +81,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { buildPath: data.customGitBuildPath || "/", repositoryURL: data.customGitUrl || "", watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules ?? true, }); } }, [form.reset, data, form]); @@ -91,6 +94,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, applicationId, watchPaths: values.watchPaths || [], + recurseSubmodules: values.recurseSubmodules, }) .then(async () => { toast.success("Git Provider Saved"); @@ -294,6 +298,23 @@ export const SaveGitProvider = ({ applicationId }: Props) => { )} /> + ( + + + + + Recurse Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 202c7f880..30df1812c 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -57,6 +57,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), + recurseSubmodules: z.boolean().default(true), }); type GithubProvider = z.infer; @@ -81,6 +82,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { }, githubId: "", branch: "", + recurseSubmodules: true, }, resolver: zodResolver(GithubProviderSchema), }); @@ -124,6 +126,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath || "/", githubId: data.githubId || "", watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules ?? true, }); } }, [form.reset, data, form]); @@ -137,6 +140,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath, githubId: data.githubId, watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -458,6 +462,23 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { )} /> + ( + + + + + Recurse Submodules + + )} + />
From 8e8bc3e71e3c836dce085bc370be933b47bf570f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:58:25 -0600 Subject: [PATCH 28/52] Enhance PostgreSQL backup command in web server utility - Added error handling to check for the existence of the PostgreSQL container before executing the backup command. - Updated the backup command to use the retrieved container ID, ensuring the command runs correctly. --- packages/server/src/utils/backups/web-server.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index a7d48a2fa..ef2249d0a 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -23,7 +23,17 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { try { await execAsync(`mkdir -p ${tempDir}/filesystem`); - const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; + // First get the container ID + const { stdout: containerId } = await execAsync( + "docker ps --filter 'name=dokploy-postgres' -q", + ); + + if (!containerId) { + throw new Error("PostgreSQL container not found"); + } + + // Then run pg_dump with the container ID + const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; await execAsync(postgresCommand); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); From 33ab87f3db3dea19362ba1f2bc9e6fa1d8288c97 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:20:03 -0600 Subject: [PATCH 29/52] fix(gitlab): enhance group name matching logic to support multiple names - Updated the group name check to allow for a comma-separated list of names, improving flexibility in group name validation. --- packages/server/src/utils/providers/gitlab.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index b8a58fd82..d01cc4004 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -435,7 +435,9 @@ export const testGitlabConnection = async ( const { full_path, kind } = repo.namespace; if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + return groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); } return kind === "user"; }); From 43a17e7e75223f6f2c98b1bff3b4b077faadd746 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Fri, 18 Apr 2025 12:49:02 +1000 Subject: [PATCH 30/52] style: remove double space --- .../components/dashboard/application/general/generic/show.tsx | 2 +- apps/dokploy/components/dashboard/settings/ai-form.tsx | 2 +- .../dashboard/settings/certificates/show-certificates.tsx | 2 +- .../dashboard/settings/cluster/registry/show-registry.tsx | 2 +- .../dashboard/settings/destination/show-destinations.tsx | 2 +- .../dashboard/settings/notifications/show-notifications.tsx | 2 +- .../components/dashboard/settings/ssh-keys/show-ssh-keys.tsx | 2 +- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 3f8854888..9b9a0ba05 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { setSab(e as TabState); }} > -
+
{ key={config.aiId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{config.name} diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 6aaa25630..b80c7b549 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -70,7 +70,7 @@ export const ShowCertificates = () => { key={certificate.certificateId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
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 08cb03813..9ae595d6f 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -54,7 +54,7 @@ export const ShowRegistry = () => { key={registry.registryId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 0639b0f75..014596ce3 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -55,7 +55,7 @@ export const ShowDestinations = () => { key={destination.destinationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{index + 1}. {destination.name} diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 782b92413..26ac17932 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -61,7 +61,7 @@ export const ShowNotifications = () => { key={notification.notificationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{notification.notificationType === "slack" && (
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 5842457ba..00d685a8d 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 @@ -56,7 +56,7 @@ export const ShowDestinations = () => { key={sshKey.sshKeyId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index cff3a8db2..bf5ced4a9 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 12:54:42 +1000 Subject: [PATCH 31/52] fix: add overflow-x-scroll to tab list container --- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index bf5ced4a9..91af2cfab 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 13:01:43 +1000 Subject: [PATCH 32/52] fix: grid cols start from lg instead of md for compose --- .../project/[projectId]/services/compose/[composeId].tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 91da1623c..3bba9eb27 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -215,12 +215,12 @@ const Service = (
General From 08bbeceebae9d4e2d594a36cd228f5316a73ab02 Mon Sep 17 00:00:00 2001 From: "Max W." Date: Sat, 19 Apr 2025 16:10:35 +0200 Subject: [PATCH 33/52] Add Ctrl+S keyboard shortcut to save compose file https://github.com/Dokploy/dokploy/issues/1736 --- .../compose/general/compose-file-editor.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 bbcbfd833..e582d266d 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => { toast.error("Error updating the Compose config"); }); }; + + // Add keyboard shortcut for Ctrl+S/Cmd+S + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's' && !isLoading) { + e.preventDefault(); + form.handleSubmit(onSubmit)(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [form, onSubmit, isLoading]); + return ( <>
From bc17991580b195fa044e0c680f501577a319d02d Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 15:53:38 +0200 Subject: [PATCH 34/52] test: Add some template helpers test --- .../templates/helpers.template.test.ts | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 apps/dokploy/__test__/templates/helpers.template.test.ts diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts new file mode 100644 index 000000000..d6eb532ce --- /dev/null +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -0,0 +1,197 @@ +import type { Schema } from "@dokploy/server/templates"; +import { processValue } from "@dokploy/server/templates/processors"; +import { describe, expect, it } from "vitest"; + + +describe("helpers functions", () => { + // Mock schema for testing + const mockSchema: Schema = { + projectName: "test", + serverIp: "127.0.0.1", + }; + // some helpers to test jwt + type JWTParts = [string, string, string]; + const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + const jwtBase64Decode = (str: string) => { + const base64 = str.replace(/-/g, "+").replace(/_/g, "/"); + const padding = "=".repeat((4 - (base64.length % 4)) % 4); + const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8"); + return JSON.parse(decoded); + }; + const jwtCheckHeader = (jwtHeader: string) => { + const decodedHeader = jwtBase64Decode(jwtHeader); + expect(decodedHeader).toHaveProperty("alg"); + expect(decodedHeader).toHaveProperty("typ"); + expect(decodedHeader.alg).toEqual("HS256"); + expect(decodedHeader.typ).toEqual("JWT"); + }; + + describe("${domain}", () => { + it("should generate a random domain", () => { + const domain = processValue("${domain}", {}, mockSchema); + expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); + expect(domain.endsWith(`${mockSchema.serverIp.replaceAll(".","-")}.traefik.me`)).toBeTruthy(); + }); + }); + + describe("${base64}", () => { + it("should generate a base64 string", () => { + const base64 = processValue("${base64}", {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + }); + it.each([ + [4, 8], + [8, 12], + [16, 24], + [32, 44], + [64, 88], + [128, 172], + ])("should generate a base64 string from parameter %d bytes length", (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }); + }); + + describe("${password}", () => { + it("should generate a password string", () => { + const password = processValue("${password}", {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6,8,12,16,32])("should generate a password string respecting parameter %d length", (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }); + }); + + describe("${hash}", () => { + it("should generate a hash string", () => { + const hash = processValue("${hash}", {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6,8,12,16,32])("should generate a hash string respecting parameter %d length", (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }); + }); + + describe("${uuid}", () => { + it("should generate a UUID string", () => { + const uuid = processValue("${uuid}", {}, mockSchema); + expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + }); + }); + + describe("${timestamp}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestamp}", {}, mockSchema); + const nowLength = Math.floor(Date.now()).toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + }); + describe("${timestampms}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestampms}", {}, mockSchema); + const nowLength = Date.now().toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + it("should generate a timestamp string in milliseconds from parameter", () => { + const timestamp = processValue("${timestampms:2025-01-01}", {}, mockSchema); + expect(timestamp).toEqual('1735689600000'); + }); + }); + describe("${timestamps}", () => { + it("should generate a timestamp string in seconds", () => { + const timestamps = processValue("${timestamps}", {}, mockSchema); + const nowLength = Math.floor(Date.now() / 1000).toString().length; + expect(timestamps).toMatch(/^\d+$/); + expect(timestamps.length).toBe(nowLength); + }); + it("should generate a timestamp string in seconds from parameter", () => { + const timestamps = processValue("${timestamps:2025-01-01}", {}, mockSchema); + expect(timestamps).toEqual('1735689600'); + }); + }); + + describe("${randomPort}", () => { + it("should generate a random port string", () => { + const randomPort = processValue("${randomPort}", {}, mockSchema); + expect(randomPort).toMatch(/^\d+$/); + expect(Number(randomPort)).toBeLessThan(65536); + }); + }); + + describe("${username}", () => { + it("should generate a username string", () => { + const username = processValue("${username}", {}, mockSchema); + expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/); + }); + }); + + describe("${email}", () => { + it("should generate an email string", () => { + const email = processValue("${email}", {}, mockSchema); + expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/); + }); + }); + + describe("${jwt}", () => { + it("should generate a JWT string", () => { + const jwt = processValue("${jwt}", {}, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect (decodedPayload).toHaveProperty("iat"); + expect (decodedPayload).toHaveProperty("iss"); + expect (decodedPayload).toHaveProperty("exp"); + expect (decodedPayload.iss).toEqual("dokploy"); + }); + it.each([6,8,12,16,32])("should generate a random hex string from parameter %d byte length", (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }); + }); + describe("${jwt:secret}", () => { + it("should generate a JWT string respecting parameter secret from variable", () => { + const jwt = processValue("${jwt:secret}", {secret: "mysecret"}, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect (decodedPayload).toHaveProperty("iat"); + expect (decodedPayload).toHaveProperty("iss"); + expect (decodedPayload).toHaveProperty("exp"); + expect (decodedPayload.iss).toEqual("dokploy"); + }); + }); + describe("${jwt:secret:payload}", () => { + it("should generate a JWT string respecting parameters secret and payload from variables", () => { + const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); + const expiry = iat + 3600; + const jwt = processValue("${jwt:secret:payload}", { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + jwtCheckHeader(parts[0]); + const decodedPayload = jwtBase64Decode(parts[1]); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload.iat).toEqual(iat); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload.iss).toEqual("test-issuer"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.exp).toEqual(expiry); + expect(decodedPayload).toHaveProperty("customprop"); + expect(decodedPayload.customprop).toEqual("customvalue"); + expect(jwt).toEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI") + }); + }); +}); \ No newline at end of file From 11b9cee73dbafa70b2eacd80f857be0d9452d883 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 15:59:49 +0200 Subject: [PATCH 35/52] feat(template-helpers): Add more parameters to jwt helper - jwt without parameter now generate a real jwt - keep length parameter as is for backward compatibility - add secret and payload parameters - payload properties iss, iat, exp are automaticly set if not provided --- packages/server/src/templates/index.ts | 47 ++++++++++++++++++--- packages/server/src/templates/processors.ts | 32 +++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 6ae264185..0d0f87ceb 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes, createHmac } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -} +}; export type DomainSchema = Pick & { path?: string; @@ -22,6 +22,12 @@ export interface Template { content: string; }>; domains: DomainSchema[]; +}; + +export interface GenerateJWTOptions { + length?: number; + secret?: string; + payload?: Record | undefined; } export const generateRandomDomain = ({ @@ -59,10 +65,41 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -} +}; -export function generateJwt(length = 256): string { - return randomBytes(length).toString("hex"); +function safeBase64(str: string): string { + return str + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); +}; +function objToJWTBase64(obj: any): string { + return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); +}; + +export function generateJwt(options: GenerateJWTOptions = {}): string { + let { length, secret, payload = {} } = options; + if (length) { + return randomBytes(length).toString("hex"); + } + const encodedHeader = objToJWTBase64({ + alg: "HS256", + typ: "JWT", + }); + payload.iss || (payload.iss = "dokploy"); + payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); + payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + const encodedPayload = objToJWTBase64({ + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), + ...payload, + }); + secret || (secret = randomBytes(32).toString("hex")); + const signature = safeBase64(createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64")); + + return `${encodedHeader}.${encodedPayload}.${signature}`; } /** diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 31e7861ad..e59cddf5f 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -65,7 +65,7 @@ export interface Template { /** * Process a string value and replace variables */ -function processValue( +export function processValue( value: string, variables: Record, schema: Schema, @@ -84,11 +84,11 @@ function processValue( const length = Number.parseInt(varName.split(":")[1], 10) || 32; return generateBase64(length); } + if (varName.startsWith("password:")) { const length = Number.parseInt(varName.split(":")[1], 10) || 16; return generatePassword(length); } - if (varName === "password") { return generatePassword(16); } @@ -114,8 +114,30 @@ function processValue( } if (varName.startsWith("jwt:")) { - const length = Number.parseInt(varName.split(":")[1], 10) || 256; - return generateJwt(length); + const params:string[] = varName.split(":").slice(1); + if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { + return generateJwt({length: Number.parseInt(params[0], 10)}); + } + let [secret, payload] = params; + if (typeof payload === "string" && variables[payload]) { + payload = variables[payload]; + } + if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + try { + payload = JSON.parse(payload); + } catch (e) { + // If payload is not a valid JSON, invalid it + payload = undefined; + console.error("Invalid JWT payload", e); + } + } + if (typeof payload !== 'object') { + payload = undefined; + } + return generateJwt({ + secret: secret ? (variables[secret] || secret) : undefined, + payload: payload as any + }); } if (varName === "username") { @@ -147,7 +169,7 @@ export function processVariables( ): Record { const variables: Record = {}; - // First pass: Process variables that don't depend on other variables + // First pass: Process some variables that don't depend on other variables for (const [key, value] of Object.entries(template.variables)) { if (typeof value !== "string") continue; From 2b5af1897f1381b7f854d8b9f769a88be6bb383c Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:04:00 +0200 Subject: [PATCH 36/52] fix(template-helpers): hash not working without parameter --- packages/server/src/templates/processors.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index e59cddf5f..fb463b634 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -97,6 +97,10 @@ export function processValue( const length = Number.parseInt(varName.split(":")[1], 10) || 8; return generateHash(length); } + if (varName === "hash") { + return generateHash(); + } + if (varName === "uuid") { return crypto.randomUUID(); } @@ -183,6 +187,8 @@ export function processVariables( const match = value.match(/\${password:(\d+)}/); const length = match?.[1] ? Number.parseInt(match[1], 10) : 16; variables[key] = generatePassword(length); + } else if (value === "${hash}") { + variables[key] = generateHash(); } else if (value.startsWith("${hash:")) { const match = value.match(/\${hash:(\d+)}/); const length = match?.[1] ? Number.parseInt(match[1], 10) : 8; From d0dbc1837fff66088ae5d8181fd76252c1ca65c5 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:05:08 +0200 Subject: [PATCH 37/52] feat(template-helpers): Add timestamps and timestampms helpers --- packages/server/src/templates/processors.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index fb463b634..ff8fe277b 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -105,10 +105,21 @@ export function processValue( return crypto.randomUUID(); } - if (varName === "timestamp") { + if (varName === "timestamp" || varName === "timestampms") { return Date.now().toString(); } + if (varName === "timestamps") { + return Math.round(Date.now() / 1000).toString(); + } + + if (varName.startsWith("timestampms:")) { + return new Date(varName.slice(12)).getTime().toString(); + } + if (varName.startsWith("timestamps:")) { + return Math.round(new Date(varName.slice(11)).getTime() / 1000).toString(); + } + if (varName === "randomPort") { return Math.floor(Math.random() * 65535).toString(); } From e6d0b7b4eebd351599560032a6bd4980b24e99d7 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:12:34 +0200 Subject: [PATCH 38/52] test(templates): Add test for jwt generation --- .../templates/config.template.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index d6e87cb7a..6f5baaf10 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -51,6 +51,33 @@ describe("processTemplate", () => { expect(result.domains).toHaveLength(0); expect(result.mounts).toHaveLength(0); }); + + it("should allow creation of real jwt secret", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", + anon_payload: JSON.stringify({ + "role": "tester", + "iss": "dockploy", + "iat": "${timestamps:2025-01-01T00:00:00Z}", + "exp": "${timestamps:2030-01-01T00:00:00Z}", + }), + anon_key: "${jwt:jwt_secret:anon_payload}", + }, + config: { + domains: [], + env: { + ANON_KEY: "${anon_key}", + }, + }, + }; + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(1); + expect(result.envs).toContain("ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY"); + expect(result.mounts).toHaveLength(0); + expect(result.domains).toHaveLength(0); + }); }); describe("domains processing", () => { From dfda934726ab15719e79a2fc636be5e2f4bf4391 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:38:14 -0600 Subject: [PATCH 39/52] refactor(user-nav): remove settings dropdown for owner role --- apps/dokploy/components/layouts/user-nav.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 0aca5b00d..151bd3641 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -134,16 +134,7 @@ export const UserNav = () => { )} - {data?.role === "owner" && ( - { - router.push("/dashboard/settings"); - }} - > - Settings - - )} + )} From 4e5b5f219e73514a0cd8eb28c46d341e6cd01c92 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:41:03 -0600 Subject: [PATCH 40/52] fix(auth): update invite link host to use app.dokploy.com --- packages/server/src/lib/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 7ff53117d..dbe8842de 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -201,7 +201,7 @@ const { handler, api } = betterAuth({ const host = process.env.NODE_ENV === "development" ? "http://localhost:3000" - : "https://dokploy.com"; + : "https://app.dokploy.com"; const inviteLink = `${host}/invitation?token=${data.id}`; await sendEmail({ From d4c6e5b04859f445cfc30577802ef85c55dc028b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Sj=C3=B6sten?= Date: Fri, 25 Apr 2025 09:58:52 +0200 Subject: [PATCH 41/52] build: update nixpacks to 1.35.0 --- CONTRIBUTING.md | 7 +- Dockerfile | 2 +- packages/server/src/setup/server-setup.ts | 240 +++++++++++----------- 3 files changed, 123 insertions(+), 126 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52fd7f2f8..a69fa6861 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ feat: add new feature Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. -We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. +We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. ```bash git clone https://github.com/dokploy/dokploy.git @@ -147,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh ```bash # Install Buildpacks -curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack +curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack ``` - - ## Pull Request - The `main` branch is the source of truth and should always reflect the latest stable release. @@ -169,7 +167,6 @@ Thank you for your contribution! To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. - ### Recommendations - Use the same name of the folder as the id of the template. diff --git a/Dockerfile b/Dockerfile index ad2239b05..a9b5f9517 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash -ARG NIXPACKS_VERSION=1.29.1 +ARG NIXPACKS_VERSION=1.35.0 RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && chmod +x install.sh \ && ./install.sh \ diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 6fefabe9f..4fb9910a0 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,17 +1,17 @@ import path from "node:path"; import { paths } from "@dokploy/server/constants"; import { - createServerDeployment, - updateDeploymentStatus, + createServerDeployment, + updateDeploymentStatus, } from "@dokploy/server/services/deployment"; import { findServerById } from "@dokploy/server/services/server"; import { - TRAEFIK_HTTP3_PORT, - TRAEFIK_PORT, - TRAEFIK_SSL_PORT, - TRAEFIK_VERSION, - getDefaultMiddlewares, - getDefaultServerTraefikConfig, + TRAEFIK_HTTP3_PORT, + TRAEFIK_PORT, + TRAEFIK_SSL_PORT, + TRAEFIK_VERSION, + getDefaultMiddlewares, + getDefaultServerTraefikConfig, } from "@dokploy/server/setup/traefik-setup"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; @@ -19,55 +19,55 @@ import { recreateDirectory } from "../utils/filesystem/directory"; import slug from "slugify"; export const slugify = (text: string | undefined) => { - if (!text) { - return ""; - } + if (!text) { + return ""; + } - const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); - return slug(cleanedText, { - lower: true, - trim: true, - strict: true, - }); + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); }; export const serverSetup = async ( - serverId: string, - onData?: (data: any) => void, + serverId: string, + onData?: (data: any) => void ) => { - const server = await findServerById(serverId); - const { LOGS_PATH } = paths(); + const server = await findServerById(serverId); + const { LOGS_PATH } = paths(); - const slugifyName = slugify(`server ${server.name}`); + const slugifyName = slugify(`server ${server.name}`); - const fullPath = path.join(LOGS_PATH, slugifyName); + const fullPath = path.join(LOGS_PATH, slugifyName); - await recreateDirectory(fullPath); + await recreateDirectory(fullPath); - const deployment = await createServerDeployment({ - serverId: server.serverId, - title: "Setup Server", - description: "Setup Server", - }); + const deployment = await createServerDeployment({ + serverId: server.serverId, + title: "Setup Server", + description: "Setup Server", + }); - try { - onData?.("\nInstalling Server Dependencies: ✅\n"); - await installRequirements(serverId, onData); + try { + onData?.("\nInstalling Server Dependencies: ✅\n"); + await installRequirements(serverId, onData); - await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateDeploymentStatus(deployment.deploymentId, "done"); - onData?.("\nSetup Server: ✅\n"); - } catch (err) { - console.log(err); + onData?.("\nSetup Server: ✅\n"); + } catch (err) { + console.log(err); - await updateDeploymentStatus(deployment.deploymentId, "error"); - onData?.(`${err} ❌\n`); - } + await updateDeploymentStatus(deployment.deploymentId, "error"); + onData?.(`${err} ❌\n`); + } }; export const defaultCommand = () => { - const bashCommand = ` + const bashCommand = ` set -e; DOCKER_VERSION=27.0.3 OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') @@ -76,7 +76,7 @@ CURRENT_USER=$USER echo "Installing requirements for: OS: $OS_TYPE" if [ $EUID != 0 ]; then - echo "Please run this script as root or with sudo ❌" + echo "Please run this script as root or with sudo ❌" exit fi @@ -176,83 +176,83 @@ echo -e "13. Installing Railpack" ${installRailpack()} `; - return bashCommand; + return bashCommand; }; const installRequirements = async ( - serverId: string, - onData?: (data: any) => void, + serverId: string, + onData?: (data: any) => void ) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - onData?.("❌ No SSH Key found, please assign one to this server"); - throw new Error("No SSH Key found"); - } + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + onData?.("❌ No SSH Key found, please assign one to this server"); + throw new Error("No SSH Key found"); + } - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = server.command || defaultCommand(); - client.exec(command, (err, stream) => { - if (err) { - onData?.(err.message); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - onData?.(data.toString()); - }) - .stderr.on("data", (data) => { - onData?.(data.toString()); - }); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), - ); - } else { - onData?.(`SSH connection error: ${err.message} ${err.level}`); - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = server.command || defaultCommand(); + client.exec(command, (err, stream) => { + if (err) { + onData?.(err.message); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + onData?.(data.toString()); + }) + .stderr.on("data", (data) => { + onData?.(data.toString()); + }); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + onData?.( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` + ); + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` + ) + ); + } else { + onData?.(`SSH connection error: ${err.message} ${err.level}`); + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + }); + }); }; const setupDirectories = () => { - const { SSH_PATH } = paths(true); - const directories = Object.values(paths(true)); + const { SSH_PATH } = paths(true); + const directories = Object.values(paths(true)); - const createDirsCommand = directories - .map((dir) => `mkdir -p "${dir}"`) - .join(" && "); - const chmodCommand = `chmod 700 "${SSH_PATH}"`; + const createDirsCommand = directories + .map((dir) => `mkdir -p "${dir}"`) + .join(" && "); + const chmodCommand = `chmod 700 "${SSH_PATH}"`; - const command = ` + const command = ` ${createDirsCommand} ${chmodCommand} `; - return command; + return command; }; const setupMainDirectory = () => ` @@ -263,7 +263,7 @@ const setupMainDirectory = () => ` # Create the /etc/dokploy directory mkdir -p /etc/dokploy chmod 777 /etc/dokploy - + echo "Directory /etc/dokploy created ✅" fi `; @@ -276,16 +276,16 @@ export const setupSwarm = () => ` # Get IP address get_ip() { local ip="" - + # Try IPv4 with multiple services # First attempt: ifconfig.io ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) - + # Second attempt: icanhazip.com if [ -z "\$ip" ]; then ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) fi - + # Third attempt: ipecho.net if [ -z "\$ip" ]; then ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) @@ -295,12 +295,12 @@ export const setupSwarm = () => ` if [ -z "\$ip" ]; then # Try IPv6 with ifconfig.io ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) - + # Try IPv6 with icanhazip.com if [ -z "\$ip" ]; then ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) fi - + # Try IPv6 with ipecho.net if [ -z "\$ip" ]; then ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) @@ -502,9 +502,9 @@ fi `; const createTraefikConfig = () => { - const config = getDefaultServerTraefikConfig(); + const config = getDefaultServerTraefikConfig(); - const command = ` + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then chmod 600 "/etc/dokploy/traefik/dynamic/acme.json" fi @@ -515,19 +515,19 @@ const createTraefikConfig = () => { fi `; - return command; + return command; }; const createDefaultMiddlewares = () => { - const config = getDefaultMiddlewares(); - const command = ` + const config = getDefaultMiddlewares(); + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then echo "Middlewares config already exists ✅" else echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml fi `; - return command; + return command; }; export const installRClone = () => ` @@ -541,7 +541,7 @@ export const installRClone = () => ` `; export const createTraefikInstance = () => { - const command = ` + const command = ` # Check if dokpyloy-traefik exists if docker service inspect dokploy-traefik > /dev/null 2>&1; then echo "Migrating Traefik to Standalone..." @@ -549,7 +549,7 @@ export const createTraefikInstance = () => { sleep 8 echo "Traefik migrated to Standalone ✅" fi - + if docker inspect dokploy-traefik > /dev/null 2>&1; then echo "Traefik already exists ✅" else @@ -570,14 +570,14 @@ export const createTraefikInstance = () => { fi `; - return command; + return command; }; const installNixpacks = () => ` if command_exists nixpacks; then echo "Nixpacks already installed ✅" else - export NIXPACKS_VERSION=1.29.1 + export NIXPACKS_VERSION=1.35.0 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" echo "Nixpacks version $NIXPACKS_VERSION installed ✅" fi From 79d55d8d347f5f756eba8a33e37205b079aae529 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:17:18 +0000 Subject: [PATCH 42/52] [autofix.ci] apply automated fixes --- apps/dokploy/components/layouts/user-nav.tsx | 2 - packages/server/src/setup/server-setup.ts | 222 +++++++++---------- 2 files changed, 111 insertions(+), 113 deletions(-) diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 151bd3641..05c601f6e 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -133,8 +133,6 @@ export const UserNav = () => { Servers )} - - )} diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 4fb9910a0..bb0d34d8b 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,17 +1,17 @@ import path from "node:path"; import { paths } from "@dokploy/server/constants"; import { - createServerDeployment, - updateDeploymentStatus, + createServerDeployment, + updateDeploymentStatus, } from "@dokploy/server/services/deployment"; import { findServerById } from "@dokploy/server/services/server"; import { - TRAEFIK_HTTP3_PORT, - TRAEFIK_PORT, - TRAEFIK_SSL_PORT, - TRAEFIK_VERSION, - getDefaultMiddlewares, - getDefaultServerTraefikConfig, + TRAEFIK_HTTP3_PORT, + TRAEFIK_PORT, + TRAEFIK_SSL_PORT, + TRAEFIK_VERSION, + getDefaultMiddlewares, + getDefaultServerTraefikConfig, } from "@dokploy/server/setup/traefik-setup"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; @@ -19,55 +19,55 @@ import { recreateDirectory } from "../utils/filesystem/directory"; import slug from "slugify"; export const slugify = (text: string | undefined) => { - if (!text) { - return ""; - } + if (!text) { + return ""; + } - const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); - return slug(cleanedText, { - lower: true, - trim: true, - strict: true, - }); + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); }; export const serverSetup = async ( - serverId: string, - onData?: (data: any) => void + serverId: string, + onData?: (data: any) => void, ) => { - const server = await findServerById(serverId); - const { LOGS_PATH } = paths(); + const server = await findServerById(serverId); + const { LOGS_PATH } = paths(); - const slugifyName = slugify(`server ${server.name}`); + const slugifyName = slugify(`server ${server.name}`); - const fullPath = path.join(LOGS_PATH, slugifyName); + const fullPath = path.join(LOGS_PATH, slugifyName); - await recreateDirectory(fullPath); + await recreateDirectory(fullPath); - const deployment = await createServerDeployment({ - serverId: server.serverId, - title: "Setup Server", - description: "Setup Server", - }); + const deployment = await createServerDeployment({ + serverId: server.serverId, + title: "Setup Server", + description: "Setup Server", + }); - try { - onData?.("\nInstalling Server Dependencies: ✅\n"); - await installRequirements(serverId, onData); + try { + onData?.("\nInstalling Server Dependencies: ✅\n"); + await installRequirements(serverId, onData); - await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateDeploymentStatus(deployment.deploymentId, "done"); - onData?.("\nSetup Server: ✅\n"); - } catch (err) { - console.log(err); + onData?.("\nSetup Server: ✅\n"); + } catch (err) { + console.log(err); - await updateDeploymentStatus(deployment.deploymentId, "error"); - onData?.(`${err} ❌\n`); - } + await updateDeploymentStatus(deployment.deploymentId, "error"); + onData?.(`${err} ❌\n`); + } }; export const defaultCommand = () => { - const bashCommand = ` + const bashCommand = ` set -e; DOCKER_VERSION=27.0.3 OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') @@ -176,83 +176,83 @@ echo -e "13. Installing Railpack" ${installRailpack()} `; - return bashCommand; + return bashCommand; }; const installRequirements = async ( - serverId: string, - onData?: (data: any) => void + serverId: string, + onData?: (data: any) => void, ) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - onData?.("❌ No SSH Key found, please assign one to this server"); - throw new Error("No SSH Key found"); - } + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + onData?.("❌ No SSH Key found, please assign one to this server"); + throw new Error("No SSH Key found"); + } - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = server.command || defaultCommand(); - client.exec(command, (err, stream) => { - if (err) { - onData?.(err.message); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - onData?.(data.toString()); - }) - .stderr.on("data", (data) => { - onData?.(data.toString()); - }); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` - ); - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` - ) - ); - } else { - onData?.(`SSH connection error: ${err.message} ${err.level}`); - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = server.command || defaultCommand(); + client.exec(command, (err, stream) => { + if (err) { + onData?.(err.message); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + onData?.(data.toString()); + }) + .stderr.on("data", (data) => { + onData?.(data.toString()); + }); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + onData?.( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ); + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ), + ); + } else { + onData?.(`SSH connection error: ${err.message} ${err.level}`); + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + }); + }); }; const setupDirectories = () => { - const { SSH_PATH } = paths(true); - const directories = Object.values(paths(true)); + const { SSH_PATH } = paths(true); + const directories = Object.values(paths(true)); - const createDirsCommand = directories - .map((dir) => `mkdir -p "${dir}"`) - .join(" && "); - const chmodCommand = `chmod 700 "${SSH_PATH}"`; + const createDirsCommand = directories + .map((dir) => `mkdir -p "${dir}"`) + .join(" && "); + const chmodCommand = `chmod 700 "${SSH_PATH}"`; - const command = ` + const command = ` ${createDirsCommand} ${chmodCommand} `; - return command; + return command; }; const setupMainDirectory = () => ` @@ -502,9 +502,9 @@ fi `; const createTraefikConfig = () => { - const config = getDefaultServerTraefikConfig(); + const config = getDefaultServerTraefikConfig(); - const command = ` + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then chmod 600 "/etc/dokploy/traefik/dynamic/acme.json" fi @@ -515,19 +515,19 @@ const createTraefikConfig = () => { fi `; - return command; + return command; }; const createDefaultMiddlewares = () => { - const config = getDefaultMiddlewares(); - const command = ` + const config = getDefaultMiddlewares(); + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then echo "Middlewares config already exists ✅" else echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml fi `; - return command; + return command; }; export const installRClone = () => ` @@ -541,7 +541,7 @@ export const installRClone = () => ` `; export const createTraefikInstance = () => { - const command = ` + const command = ` # Check if dokpyloy-traefik exists if docker service inspect dokploy-traefik > /dev/null 2>&1; then echo "Migrating Traefik to Standalone..." @@ -570,7 +570,7 @@ export const createTraefikInstance = () => { fi `; - return command; + return command; }; const installNixpacks = () => ` From c3986d7a080628eb036b361999f3d2698437ee95 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 07:40:07 +0000 Subject: [PATCH 43/52] [autofix.ci] apply automated fixes --- .../templates/config.template.test.ts | 12 +- .../templates/helpers.template.test.ts | 121 +++++++++++------- packages/server/src/templates/index.ts | 32 +++-- packages/server/src/templates/processors.ts | 20 ++- 4 files changed, 116 insertions(+), 69 deletions(-) diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index 6f5baaf10..202abdf2d 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -58,10 +58,10 @@ describe("processTemplate", () => { variables: { jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", anon_payload: JSON.stringify({ - "role": "tester", - "iss": "dockploy", - "iat": "${timestamps:2025-01-01T00:00:00Z}", - "exp": "${timestamps:2030-01-01T00:00:00Z}", + role: "tester", + iss: "dockploy", + iat: "${timestamps:2025-01-01T00:00:00Z}", + exp: "${timestamps:2030-01-01T00:00:00Z}", }), anon_key: "${jwt:jwt_secret:anon_payload}", }, @@ -74,7 +74,9 @@ describe("processTemplate", () => { }; const result = processTemplate(template, mockSchema); expect(result.envs).toHaveLength(1); - expect(result.envs).toContain("ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY"); + expect(result.envs).toContain( + "ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY", + ); expect(result.mounts).toHaveLength(0); expect(result.domains).toHaveLength(0); }); diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts index d6eb532ce..1144b65fe 100644 --- a/apps/dokploy/__test__/templates/helpers.template.test.ts +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -2,7 +2,6 @@ import type { Schema } from "@dokploy/server/templates"; import { processValue } from "@dokploy/server/templates/processors"; import { describe, expect, it } from "vitest"; - describe("helpers functions", () => { // Mock schema for testing const mockSchema: Schema = { @@ -30,7 +29,11 @@ describe("helpers functions", () => { it("should generate a random domain", () => { const domain = processValue("${domain}", {}, mockSchema); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); - expect(domain.endsWith(`${mockSchema.serverIp.replaceAll(".","-")}.traefik.me`)).toBeTruthy(); + expect( + domain.endsWith( + `${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`, + ), + ).toBeTruthy(); }); }); @@ -46,11 +49,14 @@ describe("helpers functions", () => { [32, 44], [64, 88], [128, 172], - ])("should generate a base64 string from parameter %d bytes length", (length, finalLength) => { - const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); - expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); - expect(base64.length).toBe(finalLength); - }); + ])( + "should generate a base64 string from parameter %d bytes length", + (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }, + ); }); describe("${password}", () => { @@ -58,11 +64,14 @@ describe("helpers functions", () => { const password = processValue("${password}", {}, mockSchema); expect(password).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a password string respecting parameter %d length", (length) => { - const password = processValue(`\${password:${length}}`, {}, mockSchema); - expect(password).toMatch(/^[A-Za-z0-9]+$/); - expect(password.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a password string respecting parameter %d length", + (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }, + ); }); describe("${hash}", () => { @@ -70,17 +79,22 @@ describe("helpers functions", () => { const hash = processValue("${hash}", {}, mockSchema); expect(hash).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a hash string respecting parameter %d length", (length) => { - const hash = processValue(`\${hash:${length}}`, {}, mockSchema); - expect(hash).toMatch(/^[A-Za-z0-9]+$/); - expect(hash.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a hash string respecting parameter %d length", + (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }, + ); }); describe("${uuid}", () => { it("should generate a UUID string", () => { const uuid = processValue("${uuid}", {}, mockSchema); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); }); }); @@ -100,8 +114,12 @@ describe("helpers functions", () => { expect(timestamp.length).toBe(nowLength); }); it("should generate a timestamp string in milliseconds from parameter", () => { - const timestamp = processValue("${timestampms:2025-01-01}", {}, mockSchema); - expect(timestamp).toEqual('1735689600000'); + const timestamp = processValue( + "${timestampms:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamp).toEqual("1735689600000"); }); }); describe("${timestamps}", () => { @@ -112,8 +130,12 @@ describe("helpers functions", () => { expect(timestamps.length).toBe(nowLength); }); it("should generate a timestamp string in seconds from parameter", () => { - const timestamps = processValue("${timestamps:2025-01-01}", {}, mockSchema); - expect(timestamps).toEqual('1735689600'); + const timestamps = processValue( + "${timestamps:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamps).toEqual("1735689600"); }); }); @@ -146,39 +168,50 @@ describe("helpers functions", () => { const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); - }); - it.each([6,8,12,16,32])("should generate a random hex string from parameter %d byte length", (length) => { - const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); - expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); - expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length - expect(jwt.length).toBeLessThanOrEqual(length * 2); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); + it.each([6, 8, 12, 16, 32])( + "should generate a random hex string from parameter %d byte length", + (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }, + ); }); describe("${jwt:secret}", () => { it("should generate a JWT string respecting parameter secret from variable", () => { - const jwt = processValue("${jwt:secret}", {secret: "mysecret"}, mockSchema); + const jwt = processValue( + "${jwt:secret}", + { secret: "mysecret" }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); }); describe("${jwt:secret:payload}", () => { it("should generate a JWT string respecting parameters secret and payload from variables", () => { const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); const expiry = iat + 3600; - const jwt = processValue("${jwt:secret:payload}", { - secret: "mysecret", - payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, - }, mockSchema); + const jwt = processValue( + "${jwt:secret:payload}", + { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; jwtCheckHeader(parts[0]); @@ -191,7 +224,9 @@ describe("helpers functions", () => { expect(decodedPayload.exp).toEqual(expiry); expect(decodedPayload).toHaveProperty("customprop"); expect(decodedPayload.customprop).toEqual("customvalue"); - expect(jwt).toEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI") + expect(jwt).toEqual( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI", + ); }); }); -}); \ No newline at end of file +}); diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 0d0f87ceb..083b90bf4 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -}; +} export type DomainSchema = Pick & { path?: string; @@ -22,7 +22,7 @@ export interface Template { content: string; }>; domains: DomainSchema[]; -}; +} export interface GenerateJWTOptions { length?: number; @@ -65,17 +65,16 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -}; +} function safeBase64(str: string): string { - return str - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); -}; + return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); +} function objToJWTBase64(obj: any): string { - return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); -}; + return safeBase64( + Buffer.from(JSON.stringify(obj), "utf8").toString("base64"), + ); +} export function generateJwt(options: GenerateJWTOptions = {}): string { let { length, secret, payload = {} } = options; @@ -88,16 +87,21 @@ export function generateJwt(options: GenerateJWTOptions = {}): string { }); payload.iss || (payload.iss = "dokploy"); payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); - payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + payload.exp || + (payload.exp = Math.floor( + new Date("2030-01-01T00:00:00Z").getTime() / 1000, + )); const encodedPayload = objToJWTBase64({ iat: Math.floor(Date.now() / 1000), exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), ...payload, }); secret || (secret = randomBytes(32).toString("hex")); - const signature = safeBase64(createHmac("SHA256", secret) - .update(`${encodedHeader}.${encodedPayload}`) - .digest("base64")); + const signature = safeBase64( + createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64"), + ); return `${encodedHeader}.${encodedPayload}.${signature}`; } diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index ff8fe277b..5d9270aa1 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -117,7 +117,9 @@ export function processValue( return new Date(varName.slice(12)).getTime().toString(); } if (varName.startsWith("timestamps:")) { - return Math.round(new Date(varName.slice(11)).getTime() / 1000).toString(); + return Math.round( + new Date(varName.slice(11)).getTime() / 1000, + ).toString(); } if (varName === "randomPort") { @@ -129,15 +131,19 @@ export function processValue( } if (varName.startsWith("jwt:")) { - const params:string[] = varName.split(":").slice(1); + const params: string[] = varName.split(":").slice(1); if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { - return generateJwt({length: Number.parseInt(params[0], 10)}); + return generateJwt({ length: Number.parseInt(params[0], 10) }); } let [secret, payload] = params; if (typeof payload === "string" && variables[payload]) { payload = variables[payload]; } - if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + if ( + typeof payload === "string" && + payload.startsWith("{") && + payload.endsWith("}") + ) { try { payload = JSON.parse(payload); } catch (e) { @@ -146,12 +152,12 @@ export function processValue( console.error("Invalid JWT payload", e); } } - if (typeof payload !== 'object') { + if (typeof payload !== "object") { payload = undefined; } return generateJwt({ - secret: secret ? (variables[secret] || secret) : undefined, - payload: payload as any + secret: secret ? variables[secret] || secret : undefined, + payload: payload as any, }); } From f49a67f8dfc4b0c3050b0ee7e5eecb1e878332c2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:50:26 -0600 Subject: [PATCH 44/52] refactor(jwt generation): Simplify payload property assignments and secret initialization --- packages/server/src/templates/index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 083b90bf4..c42dd1b7a 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -85,18 +85,23 @@ export function generateJwt(options: GenerateJWTOptions = {}): string { alg: "HS256", typ: "JWT", }); - payload.iss || (payload.iss = "dokploy"); - payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); - payload.exp || - (payload.exp = Math.floor( - new Date("2030-01-01T00:00:00Z").getTime() / 1000, - )); + if (!payload.iss) { + payload.iss = "dokploy"; + } + if (!payload.iat) { + payload.iat = Math.floor(Date.now() / 1000); + } + if (!payload.exp) { + payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000); + } const encodedPayload = objToJWTBase64({ iat: Math.floor(Date.now() / 1000), exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), ...payload, }); - secret || (secret = randomBytes(32).toString("hex")); + if (!secret) { + secret = randomBytes(32).toString("hex"); + } const signature = safeBase64( createHmac("SHA256", secret) .update(`${encodedHeader}.${encodedPayload}`) From ceb16ae9f7b4bb5acf4cae771b785d6256409a90 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:35:02 -0600 Subject: [PATCH 45/52] Implement enableSubmodules feature across various Git provider components and update database schema. This change introduces a new boolean field `enableSubmodules` to control submodule behavior in Git operations, replacing the previous `recurseSubmodules` field. Updates include modifications to the UI components, API routers, and database schema to accommodate this new feature. --- .../general/generic/save-git-provider.tsx | 20 +- .../general/generic/save-github-provider.tsx | 20 +- .../general/generic/save-gitlab-provider.tsx | 19 +- .../save-bitbucket-provider-compose.tsx | 20 + .../generic/save-git-provider-compose.tsx | 20 + .../generic/save-gitea-provider-compose.tsx | 20 + .../generic/save-github-provider-compose.tsx | 20 + .../generic/save-gitlab-provider-compose.tsx | 20 + .../drizzle/0085_equal_captain_stacy.sql | 2 + .../drizzle/0086_rainy_gertrude_yorkes.sql | 2 + apps/dokploy/drizzle/meta/0085_snapshot.json | 5383 +++++++++++++++++ apps/dokploy/drizzle/meta/0086_snapshot.json | 5383 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 14 + .../dokploy/server/api/routers/application.ts | 5 + packages/server/src/db/schema/application.ts | 6 + packages/server/src/db/schema/compose.ts | 1 + packages/server/src/services/compose.ts | 1 + .../server/src/utils/providers/bitbucket.ts | 22 +- packages/server/src/utils/providers/git.ts | 28 +- packages/server/src/utils/providers/gitea.ts | 28 +- packages/server/src/utils/providers/github.ts | 42 +- packages/server/src/utils/providers/gitlab.ts | 21 +- 22 files changed, 11000 insertions(+), 97 deletions(-) create mode 100644 apps/dokploy/drizzle/0085_equal_captain_stacy.sql create mode 100644 apps/dokploy/drizzle/0086_rainy_gertrude_yorkes.sql create mode 100644 apps/dokploy/drizzle/meta/0085_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0086_snapshot.json 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 00d283956..ef2dd3da8 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 @@ -23,6 +23,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { KeyRoundIcon, LockIcon, X } from "lucide-react"; @@ -44,7 +45,7 @@ const GitProviderSchema = z.object({ branch: z.string().min(1, "Branch required"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GitProvider = z.infer; @@ -68,7 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { repositoryURL: "", sshKey: undefined, watchPaths: [], - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GitProviderSchema), }); @@ -81,7 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { buildPath: data.customGitBuildPath || "/", repositoryURL: data.customGitUrl || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -94,7 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, applicationId, watchPaths: values.watchPaths || [], - recurseSubmodules: values.recurseSubmodules, + enableSubmodules: values.enableSubmodules, }) .then(async () => { toast.success("Git Provider Saved"); @@ -298,20 +299,19 @@ export const SaveGitProvider = ({ applicationId }: Props) => { )} /> + ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 7637f596d..4befb215e 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -30,6 +30,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -57,7 +58,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GithubProvider = z.infer; @@ -82,7 +83,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { }, githubId: "", branch: "", - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GithubProviderSchema), }); @@ -126,7 +127,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath || "/", githubId: data.githubId || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -140,7 +141,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath, githubId: data.githubId, watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -462,20 +463,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { )} /> + ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index 996b1dca1..b4b55d3fa 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -60,7 +61,7 @@ const GitlabProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GitlabProvider = z.infer; @@ -87,7 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { }, gitlabId: "", branch: "", - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GitlabProviderSchema), }); @@ -137,7 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { buildPath: data.gitlabBuildPath || "/", gitlabId: data.gitlabId || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -153,7 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { gitlabProjectId: data.repository.id, gitlabPathNamespace: data.repository.gitlabPathNamespace, watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -489,18 +490,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { /> ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index ff329a0af..353ccc6ca 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type BitbucketProvider = z.infer; @@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { bitbucketId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(BitbucketProviderSchema), }); @@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { composePath: data.composePath, bitbucketId: data.bitbucketId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { sourceType: "bitbucket", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx index 201f9da2e..6f9b50dad 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -59,6 +60,7 @@ const GiteaProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type GiteaProvider = z.infer; @@ -83,6 +85,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { giteaId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(GiteaProviderSchema), }); @@ -136,6 +139,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { composePath: data.composePath || "./docker-compose.yml", giteaId: data.giteaId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -151,6 +155,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { sourceType: "gitea", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, } as any) .then(async () => { toast.success("Service Provider Saved"); @@ -469,6 +474,21 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 4f4c1d5ad..6e9b0a033 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -30,6 +30,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type GithubProvider = z.infer; @@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { githubId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(GithubProviderSchema), }); @@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { composePath: data.composePath, githubId: data.githubId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { sourceType: "github", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />