From 371c6317aa4371b879eae2acf44936c2f2c7e0b9 Mon Sep 17 00:00:00 2001 From: yni9ht Date: Mon, 17 Mar 2025 20:44:13 +0800 Subject: [PATCH 01/84] refactor(mount): streamline mount update logic and improve readability --- packages/server/src/services/mount.ts | 40 +++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts index 836feace..1fa4db1e 100644 --- a/packages/server/src/services/mount.ts +++ b/packages/server/src/services/mount.ts @@ -123,29 +123,27 @@ export const updateMount = async ( mountId: string, mountData: Partial, ) => { - return await db.transaction(async (tx) => { - const mount = await tx - .update(mounts) - .set({ - ...mountData, - }) - .where(eq(mounts.mountId, mountId)) - .returning() - .then((value) => value[0]); + const mount = await db + .update(mounts) + .set({ + ...mountData, + }) + .where(eq(mounts.mountId, mountId)) + .returning() + .then((value) => value[0]); - if (!mount) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Mount not found", - }); - } + if (!mount) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mount not found", + }); + } - if (mount.type === "file") { - await deleteFileMount(mountId); - await createFileMount(mountId); - } - return mount; - }); + if (mount.type === "file") { + await deleteFileMount(mountId); + await createFileMount(mountId); + } + return mount; }; export const findMountsByApplicationId = async ( From cc5a3e6873345bb6a1a87bfbeae3def1d4276d2e Mon Sep 17 00:00:00 2001 From: Yusoof Moh <18055365+yusoofsh@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:04:35 +0700 Subject: [PATCH 02/84] Add option to disable recurse submodules Add option to disable recurse submodules under "Provider Select the source of your code" form. * Add a checkbox to disable recurse submodules in `apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx`, `apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx`, and `apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx`. * Update the form schema in the above files to include the new option. * Conditionally include the `--recurse-submodules` flag in the `git clone` command in the above files. * Update the "Provider Select the source of your code" form in `apps/dokploy/components/dashboard/application/general/generic/show.tsx` to include the new option. * Conditionally include the `--recurse-submodules` flag in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`. * Add the `--depth` flag to optimize submodule cloning performance in the `git clone` command in `packages/server/src/utils/providers/bitbucket.ts`, `packages/server/src/utils/providers/git.ts`, `packages/server/src/utils/providers/github.ts`, and `packages/server/src/utils/providers/gitlab.ts`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Dokploy/dokploy?shareId=XXXX-XXXX-XXXX-XXXX). --- .../general/generic/save-git-provider.tsx | 21 +++++++ .../general/generic/save-github-provider.tsx | 21 +++++++ .../general/generic/save-gitlab-provider.tsx | 21 +++++++ .../server/src/utils/providers/bitbucket.ts | 46 ++++++++------ packages/server/src/utils/providers/git.ts | 63 +++++++++++-------- packages/server/src/utils/providers/github.ts | 45 +++++++------ packages/server/src/utils/providers/gitlab.ts | 45 +++++++------ 7 files changed, 181 insertions(+), 81 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index 3d6f6a38..2613174b 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -44,6 +44,7 @@ const GitProviderSchema = z.object({ branch: z.string().min(1, "Branch required"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), + recurseSubmodules: z.boolean().default(true), }); type GitProvider = z.infer; @@ -67,6 +68,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { repositoryURL: "", sshKey: undefined, watchPaths: [], + recurseSubmodules: true, }, resolver: zodResolver(GitProviderSchema), }); @@ -79,6 +81,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { buildPath: data.customGitBuildPath || "/", repositoryURL: data.customGitUrl || "", watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules ?? true, }); } }, [form.reset, data, form]); @@ -91,6 +94,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, applicationId, watchPaths: values.watchPaths || [], + recurseSubmodules: values.recurseSubmodules, }) .then(async () => { toast.success("Git Provider Saved"); @@ -294,6 +298,23 @@ export const SaveGitProvider = ({ applicationId }: Props) => { )} /> + ( + + + + + Recurse Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 202c7f88..30df1812 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -57,6 +57,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), + recurseSubmodules: z.boolean().default(true), }); type GithubProvider = z.infer; @@ -81,6 +82,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { }, githubId: "", branch: "", + recurseSubmodules: true, }, resolver: zodResolver(GithubProviderSchema), }); @@ -124,6 +126,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath || "/", githubId: data.githubId || "", watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules ?? true, }); } }, [form.reset, data, form]); @@ -137,6 +140,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath, githubId: data.githubId, watchPaths: data.watchPaths || [], + recurseSubmodules: data.recurseSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -458,6 +462,23 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { )} /> + ( + + + + + Recurse Submodules + + )} + />
)} -
+
{filteredProjects?.map((project) => { const emptyServices = project?.mariadb.length === 0 && From 5863e45c13916845c1b7581faae44c6c1a0b2a1c Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Fri, 4 Apr 2025 20:18:56 +0200 Subject: [PATCH 11/84] remove sensitive files on static build --- packages/server/src/utils/builders/static.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index c46bdf2e..f7fc87ca 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -25,6 +25,12 @@ export const buildStatic = async ( ].join("\n"), ); + createFile( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); + await buildCustomDocker( { ...application, From d9c34c4524c37c1b8f6e5436ca8db9ae95f704ff Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:09:30 +0800 Subject: [PATCH 12/84] Update Simplified Chinese --- .../public/locales/zh-Hans/common.json | 94 ++++++++++++++++++- .../public/locales/zh-Hans/settings.json | 40 ++++---- 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index 0967ef42..67797618 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -1 +1,93 @@ -{} +{ + "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 index c74fb21f..f7e8a38a 100644 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -1,17 +1,17 @@ { "settings.common.save": "保存", - "settings.common.enterTerminal": "进入终端", - "settings.server.domain.title": "域名设置", - "settings.server.domain.description": "添加域名到服务器", + "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.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.title": "Web 服务器", + "settings.server.webServer.description": "重载或清理 Web 服务器。", "settings.server.webServer.actions": "操作", "settings.server.webServer.reload": "重新加载", "settings.server.webServer.watchLogs": "查看日志", @@ -19,40 +19,40 @@ "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.managePorts": "额外端口映射", + "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", "settings.server.webServer.traefik.targetPort": "目标端口", - "settings.server.webServer.traefik.publishedPort": "对外端口", + "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.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.cleanDockerBuilder": "清理 Docker Builder 和系统", "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", "settings.server.webServer.storage.cleanAll": "清理所有内容", "settings.profile.title": "账户", - "settings.profile.description": "更改您的个人资料", + "settings.profile.description": "在此更改您的个人资料详情。", "settings.profile.email": "邮箱", "settings.profile.password": "密码", "settings.profile.avatar": "头像", "settings.appearance.title": "外观", - "settings.appearance.description": "自定义面板主题", + "settings.appearance.description": "自定义您的仪表盘主题。", "settings.appearance.theme": "主题", - "settings.appearance.themeDescription": "选择面板主题", + "settings.appearance.themeDescription": "为您的仪表盘选择主题", "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "黑暗", - "settings.appearance.themes.system": "系统主题", + "settings.appearance.themes.dark": "暗黑", + "settings.appearance.themes.system": "跟随系统", "settings.appearance.language": "语言", - "settings.appearance.languageDescription": "选择面板语言", + "settings.appearance.languageDescription": "为您的仪表盘选择语言", - "settings.terminal.connectionSettings": "终端设置", - "settings.terminal.ipAddress": "IP", + "settings.terminal.connectionSettings": "连接设置", + "settings.terminal.ipAddress": "IP 地址", "settings.terminal.port": "端口", "settings.terminal.username": "用户名" } From 7ac74813435ddcdf8e8bcc43cae7a921d4897489 Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:34:47 +0800 Subject: [PATCH 13/84] Update Simplified Chinese --- .../public/locales/zh-Hans/common.json | 17 +- .../public/locales/zh-Hans/settings.json | 123 ++++---- extract.js | 270 ++++++++++++++++++ merge-translations.js | 252 ++++++++++++++++ 4 files changed, 589 insertions(+), 73 deletions(-) create mode 100644 extract.js create mode 100644 merge-translations.js diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index 67797618..d8faad7d 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -13,7 +13,6 @@ "dashboard.images": "镜像", "dashboard.volumes": "卷", "dashboard.networks": "网络", - "button.create": "创建", "button.edit": "编辑", "button.delete": "删除", @@ -23,71 +22,57 @@ "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": "变量值" -} +} \ No newline at end of file diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json index f7e8a38a..06033238 100644 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -1,58 +1,67 @@ { - "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.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": "网络" +} \ No newline at end of file diff --git a/extract.js b/extract.js new file mode 100644 index 00000000..61d5d2d7 --- /dev/null +++ b/extract.js @@ -0,0 +1,270 @@ +console.log('Creating translation extractor script...'); + +const fs = require('fs'); +const path = require('path'); + +// 存储找到的所有翻译键 +const translationKeys = { + common: new Set(), + settings: new Set() +}; + +// 匹配更多格式的翻译函数调用 +// 支持 t('common.xxx')、t("common.xxx")、t(`common.xxx`) +const translationPatterns = [ + /t\(\s*['"]([a-zA-Z0-9._-]+)['"]?\s*[,)]/g, // t('key') 或 t("key") + /t\(\s*`([a-zA-Z0-9._-]+)`\s*[,)]/g, // t(`key`) + /useTranslation\(\s*["']([a-zA-Z0-9._-]+)["']\s*\)/g, // useTranslation('namespace') + /serverSideTranslations\([^)]*["']([a-zA-Z0-9._-]+)["']/g // serverSideTranslations(..., ['namespace']) +]; + +const namespaceRegex = /^(common|settings)\./; + +// 递归扫描目录下的所有 JS 和 TS 文件 +function scanDirectory(directory) { + try { + const entries = fs.readdirSync(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory() && !fullPath.includes('node_modules') && !fullPath.includes('.next')) { + scanDirectory(fullPath); + } else if (entry.isFile() && /\.(js|jsx|ts|tsx)$/.test(entry.name)) { + try { + const content = fs.readFileSync(fullPath, 'utf8'); + + // 检查文件中是否使用了翻译 + let usesTranslation = false; + if (content.includes('useTranslation') || content.includes('t(') || content.includes('serverSideTranslations')) { + usesTranslation = true; + } + + if (usesTranslation) { + // 使用所有模式匹配翻译键 + for (const pattern of translationPatterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const key = match[1]; + + // 检查是否有命名空间 + const namespaceMatch = key.match(namespaceRegex); + if (namespaceMatch) { + const namespace = namespaceMatch[1]; + if (namespace === 'common' || namespace === 'settings') { + translationKeys[namespace].add(key); + } + } + + // 如果文件中导入了特定命名空间,所有的 t(key) 都属于该命名空间 + if (content.includes(`useTranslation('common')`) || content.includes(`useTranslation("common")`)) { + if (!key.includes('.')) { + translationKeys.common.add(`common.${key}`); + } + } + + if (content.includes(`useTranslation('settings')`) || content.includes(`useTranslation("settings")`)) { + if (!key.includes('.')) { + translationKeys.settings.add(`settings.${key}`); + } + } + } + } + + // 控制台输出被处理的文件及其找到的翻译键 + if (usesTranslation) { + console.log(`检查文件: ${fullPath}`); + } + } + } catch (error) { + console.error(`Error reading file ${fullPath}:`, error); + } + } + } + } catch (error) { + console.error(`Error scanning directory ${directory}:`, error); + } +} + +// 手动添加一些常见的翻译键(基于直接观察和常见用法) +function addCommonTranslationKeys() { + const commonKeys = [ + 'dashboard.title', 'dashboard.overview', 'dashboard.projects', 'dashboard.servers', + 'dashboard.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', + + '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' + ]; + + commonKeys.forEach(key => { + translationKeys.common.add(`common.${key}`); + }); +} + +// 读取现有翻译文件 +function readTranslationFile(filePath) { + try { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } + } catch (error) { + console.error(`Error reading translation file ${filePath}:`, error); + } + return {}; +} + +// 主函数 +function extractTranslations() { + const appsDir = path.join(__dirname, 'apps', 'dokploy'); + + // 扫描代码库 + scanDirectory(appsDir); + + // 手动添加常见的翻译键 + addCommonTranslationKeys(); + + // 读取现有翻译文件 + const zhHansCommonPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'common.json'); + const zhHansSettingsPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'settings.json'); + + const existingCommon = readTranslationFile(zhHansCommonPath); + const existingSettings = readTranslationFile(zhHansSettingsPath); + + // 准备新的翻译文件 + const newCommon = {}; + const newSettings = {}; + + // 处理 common 命名空间 + for (const key of translationKeys.common) { + const shortKey = key.replace('common.', ''); + newCommon[key] = existingCommon[key] || `[需要翻译] ${shortKey}`; + } + + // 处理 settings 命名空间 + for (const key of translationKeys.settings) { + const shortKey = key.replace('settings.', ''); + newSettings[key] = existingSettings[key] || `[需要翻译] ${shortKey}`; + } + + // 输出结果 + console.log('=== 提取的 common 翻译键 ==='); + console.log(Array.from(translationKeys.common).sort().join('\n')); + console.log(`\n共找到 ${translationKeys.common.size} 个 common 翻译键`); + + console.log('\n=== 提取的 settings 翻译键 ==='); + console.log(Array.from(translationKeys.settings).sort().join('\n')); + console.log(`\n共找到 ${translationKeys.settings.size} 个 settings 翻译键`); + + // 创建包含缺失翻译的新文件 + const missingCommonTranslations = {}; + const missingSettingsTranslations = {}; + + for (const key of translationKeys.common) { + if (!existingCommon[key]) { + const shortKey = key.replace('common.', ''); + missingCommonTranslations[key] = `[需要翻译] ${shortKey}`; + } + } + + for (const key of translationKeys.settings) { + if (!existingSettings[key]) { + const shortKey = key.replace('settings.', ''); + missingSettingsTranslations[key] = `[需要翻译] ${shortKey}`; + } + } + + // 输出缺失的翻译 + console.log('\n=== 缺失的 common 翻译 ==='); + console.log(JSON.stringify(missingCommonTranslations, null, 2)); + console.log(`\n共缺失 ${Object.keys(missingCommonTranslations).length} 个 common 翻译`); + + console.log('\n=== 缺失的 settings 翻译 ==='); + console.log(JSON.stringify(missingSettingsTranslations, null, 2)); + console.log(`\n共缺失 ${Object.keys(missingSettingsTranslations).length} 个 settings 翻译`); + + // 输出可以直接复制到文件中的完整翻译对象 + console.log('\n=== 完整的 common.json 内容 ==='); + const fullCommon = { ...existingCommon }; + translationKeys.common.forEach(key => { + if (!fullCommon[key]) { + const shortKey = key.replace('common.', ''); + fullCommon[key] = `[翻译] ${shortKey}`; + } + }); + console.log(JSON.stringify(fullCommon, null, 2)); + + console.log('\n=== 完整的 settings.json 内容 ==='); + const fullSettings = { ...existingSettings }; + translationKeys.settings.forEach(key => { + if (!fullSettings[key]) { + const shortKey = key.replace('settings.', ''); + fullSettings[key] = `[翻译] ${shortKey}`; + } + }); + console.log(JSON.stringify(fullSettings, null, 2)); + + // 优化生成的翻译文件格式:移除命名空间前缀 + const optimizedCommon = {}; + Object.keys(fullCommon).forEach(key => { + const shortKey = key.replace('common.', ''); + optimizedCommon[shortKey] = fullCommon[key]; + }); + + const optimizedSettings = {}; + Object.keys(fullSettings).forEach(key => { + const shortKey = key.replace('settings.', ''); + optimizedSettings[shortKey] = fullSettings[key]; + }); + + // 写入文件 + fs.writeFileSync('missing-common-translations.json', JSON.stringify(missingCommonTranslations, null, 2), 'utf8'); + fs.writeFileSync('missing-settings-translations.json', JSON.stringify(missingSettingsTranslations, null, 2), 'utf8'); + fs.writeFileSync('full-common-translations.json', JSON.stringify(fullCommon, null, 2), 'utf8'); + fs.writeFileSync('full-settings-translations.json', JSON.stringify(fullSettings, null, 2), 'utf8'); + fs.writeFileSync('optimized-common-translations.json', JSON.stringify(optimizedCommon, null, 2), 'utf8'); + fs.writeFileSync('optimized-settings-translations.json', JSON.stringify(optimizedSettings, null, 2), 'utf8'); + + console.log('\n翻译提取完成!'); + console.log('文件已保存:'); + console.log('- missing-common-translations.json: 缺失的 common 翻译'); + console.log('- missing-settings-translations.json: 缺失的 settings 翻译'); + console.log('- full-common-translations.json: 完整的 common 翻译(包含命名空间)'); + console.log('- full-settings-translations.json: 完整的 settings 翻译(包含命名空间)'); + console.log('- optimized-common-translations.json: 优化格式的 common 翻译(不含命名空间)'); + console.log('- optimized-settings-translations.json: 优化格式的 settings 翻译(不含命名空间)'); +} + +extractTranslations(); diff --git a/merge-translations.js b/merge-translations.js new file mode 100644 index 00000000..441ebed4 --- /dev/null +++ b/merge-translations.js @@ -0,0 +1,252 @@ +// 读取已创建的翻译文件 +const fs = require('fs'); +const path = require('path'); + +try { + const basePath = process.cwd(); + const commonPart1Path = path.join(basePath, 'common-zh-Hans.json'); + const commonPart2Path = path.join(basePath, 'common-zh-Hans-buttons.json'); + const fullCommonPath = path.join(basePath, 'optimized-common-translations.json'); + const fullSettingsPath = path.join(basePath, 'optimized-settings-translations.json'); + + const commonPart1 = JSON.parse(fs.readFileSync(commonPart1Path, 'utf8')); + const commonPart2 = JSON.parse(fs.readFileSync(commonPart2Path, 'utf8')); + const fullCommon = JSON.parse(fs.readFileSync(fullCommonPath, 'utf8')); + const fullSettings = JSON.parse(fs.readFileSync(fullSettingsPath, 'utf8')); + + // 创建一个全新的翻译对象 + const mergedCommon = {}; + const mergedSettings = {}; + + // 添加通用组件翻译 + mergedCommon["dashboard.title"] = "仪表盘"; + mergedCommon["dashboard.overview"] = "概览"; + mergedCommon["dashboard.projects"] = "项目"; + mergedCommon["dashboard.servers"] = "服务器"; + mergedCommon["dashboard.docker"] = "Docker"; + mergedCommon["dashboard.monitoring"] = "监控"; + mergedCommon["dashboard.settings"] = "设置"; + mergedCommon["dashboard.logout"] = "退出登录"; + mergedCommon["dashboard.profile"] = "个人资料"; + mergedCommon["dashboard.terminal"] = "终端"; + mergedCommon["dashboard.containers"] = "容器"; + mergedCommon["dashboard.images"] = "镜像"; + mergedCommon["dashboard.volumes"] = "卷"; + mergedCommon["dashboard.networks"] = "网络"; + + // 按钮翻译 + mergedCommon["button.create"] = "创建"; + mergedCommon["button.edit"] = "编辑"; + mergedCommon["button.delete"] = "删除"; + mergedCommon["button.cancel"] = "取消"; + mergedCommon["button.save"] = "保存"; + mergedCommon["button.confirm"] = "确认"; + mergedCommon["button.back"] = "返回"; + mergedCommon["button.next"] = "下一步"; + mergedCommon["button.finish"] = "完成"; + + // 状态翻译 + mergedCommon["status.running"] = "运行中"; + mergedCommon["status.stopped"] = "已停止"; + mergedCommon["status.error"] = "错误"; + mergedCommon["status.pending"] = "等待中"; + mergedCommon["status.success"] = "成功"; + mergedCommon["status.failed"] = "失败"; + + // 表单翻译 + mergedCommon["form.required"] = "必填"; + mergedCommon["form.invalid"] = "无效"; + mergedCommon["form.submit"] = "提交"; + mergedCommon["form.reset"] = "重置"; + + // 通知翻译 + mergedCommon["notification.success"] = "操作成功"; + mergedCommon["notification.error"] = "操作失败"; + mergedCommon["notification.warning"] = "警告"; + mergedCommon["notification.info"] = "信息"; + + // 时间翻译 + mergedCommon["time.now"] = "刚刚"; + mergedCommon["time.minutes"] = "分钟前"; + mergedCommon["time.hours"] = "小时前"; + mergedCommon["time.days"] = "天前"; + + // 过滤翻译 + mergedCommon["filter.all"] = "全部"; + mergedCommon["filter.active"] = "活跃"; + mergedCommon["filter.inactive"] = "不活跃"; + + // 排序翻译 + mergedCommon["sort.asc"] = "升序"; + mergedCommon["sort.desc"] = "降序"; + + // 搜索翻译 + mergedCommon["search.placeholder"] = "搜索..."; + mergedCommon["search.noResults"] = "无结果"; + + // 分页翻译 + mergedCommon["pagination.prev"] = "上一页"; + mergedCommon["pagination.next"] = "下一页"; + mergedCommon["pagination.of"] = "共 {0} 页"; + + // 错误翻译 + mergedCommon["error.notFound"] = "未找到"; + mergedCommon["error.serverError"] = "服务器错误"; + mergedCommon["error.unauthorized"] = "未授权"; + mergedCommon["error.forbidden"] = "禁止访问"; + + // 通用状态翻译 + mergedCommon["loading"] = "加载中..."; + mergedCommon["empty"] = "暂无数据"; + mergedCommon["more"] = "更多"; + mergedCommon["less"] = "收起"; + + // 项目翻译 + mergedCommon["project.create"] = "创建项目"; + mergedCommon["project.edit"] = "编辑项目"; + mergedCommon["project.delete"] = "删除项目"; + mergedCommon["project.name"] = "项目名称"; + mergedCommon["project.description"] = "项目描述"; + + // 服务翻译 + mergedCommon["service.create"] = "创建服务"; + mergedCommon["service.edit"] = "编辑服务"; + mergedCommon["service.delete"] = "删除服务"; + mergedCommon["service.name"] = "服务名称"; + mergedCommon["service.type"] = "服务类型"; + + // 域名翻译 + mergedCommon["domain.add"] = "添加域名"; + mergedCommon["domain.remove"] = "移除域名"; + + // 环境变量翻译 + mergedCommon["environment.variables"] = "环境变量"; + mergedCommon["environment.add"] = "添加环境变量"; + mergedCommon["environment.edit"] = "编辑环境变量"; + mergedCommon["environment.name"] = "变量名"; + mergedCommon["environment.value"] = "变量值"; + + // 设置页面的通用翻译 + mergedSettings["common.save"] = "保存"; + mergedSettings["common.enterTerminal"] = "终端"; + + // 服务器域名设置 + mergedSettings["server.domain.title"] = "服务器域名"; + mergedSettings["server.domain.description"] = "为您的服务器应用添加域名。"; + mergedSettings["server.domain.form.domain"] = "域名"; + mergedSettings["server.domain.form.letsEncryptEmail"] = "Let's Encrypt 邮箱"; + mergedSettings["server.domain.form.certificate.label"] = "证书提供商"; + mergedSettings["server.domain.form.certificate.placeholder"] = "选择证书"; + mergedSettings["server.domain.form.certificateOptions.none"] = "无"; + mergedSettings["server.domain.form.certificateOptions.letsencrypt"] = "Let's Encrypt"; + + // Web服务器设置 + mergedSettings["server.webServer.title"] = "Web 服务器"; + mergedSettings["server.webServer.description"] = "重载或清理 Web 服务器。"; + mergedSettings["server.webServer.actions"] = "操作"; + mergedSettings["server.webServer.reload"] = "重新加载"; + mergedSettings["server.webServer.watchLogs"] = "查看日志"; + mergedSettings["server.webServer.updateServerIp"] = "更新服务器 IP"; + mergedSettings["server.webServer.server.label"] = "服务器"; + + // Traefik设置 + mergedSettings["server.webServer.traefik.label"] = "Traefik"; + mergedSettings["server.webServer.traefik.modifyEnv"] = "修改环境变量"; + mergedSettings["server.webServer.traefik.managePorts"] = "额外端口映射"; + mergedSettings["server.webServer.traefik.managePortsDescription"] = "为 Traefik 添加或删除额外端口"; + mergedSettings["server.webServer.traefik.targetPort"] = "目标端口"; + mergedSettings["server.webServer.traefik.publishedPort"] = "发布端口"; + mergedSettings["server.webServer.traefik.addPort"] = "添加端口"; + mergedSettings["server.webServer.traefik.portsUpdated"] = "端口更新成功"; + mergedSettings["server.webServer.traefik.portsUpdateError"] = "端口更新失败"; + mergedSettings["server.webServer.traefik.publishMode"] = "发布模式"; + + // 存储空间设置 + mergedSettings["server.webServer.storage.label"] = "存储空间"; + mergedSettings["server.webServer.storage.cleanUnusedImages"] = "清理未使用的镜像"; + mergedSettings["server.webServer.storage.cleanUnusedVolumes"] = "清理未使用的卷"; + mergedSettings["server.webServer.storage.cleanStoppedContainers"] = "清理已停止的容器"; + mergedSettings["server.webServer.storage.cleanDockerBuilder"] = "清理 Docker Builder 和系统"; + mergedSettings["server.webServer.storage.cleanMonitoring"] = "清理监控数据"; + mergedSettings["server.webServer.storage.cleanAll"] = "清理所有内容"; + + // 个人资料设置 + mergedSettings["profile.title"] = "账户"; + mergedSettings["profile.description"] = "在此更改您的个人资料详情。"; + mergedSettings["profile.email"] = "邮箱"; + mergedSettings["profile.password"] = "密码"; + mergedSettings["profile.avatar"] = "头像"; + + // 外观设置 + mergedSettings["appearance.title"] = "外观"; + mergedSettings["appearance.description"] = "自定义您的仪表盘主题。"; + mergedSettings["appearance.theme"] = "主题"; + mergedSettings["appearance.themeDescription"] = "为您的仪表盘选择主题"; + mergedSettings["appearance.themes.light"] = "明亮"; + mergedSettings["appearance.themes.dark"] = "暗黑"; + mergedSettings["appearance.themes.system"] = "跟随系统"; + mergedSettings["appearance.language"] = "语言"; + mergedSettings["appearance.languageDescription"] = "为您的仪表盘选择语言"; + + // 终端设置 + mergedSettings["terminal.connectionSettings"] = "连接设置"; + mergedSettings["terminal.ipAddress"] = "IP 地址"; + mergedSettings["terminal.port"] = "端口"; + mergedSettings["terminal.username"] = "用户名"; + + // 其他设置 + mergedSettings["settings"] = "设置"; + mergedSettings["general"] = "通用设置"; + mergedSettings["security"] = "安全"; + mergedSettings["users"] = "用户管理"; + mergedSettings["roles"] = "角色管理"; + mergedSettings["permissions"] = "权限"; + mergedSettings["api"] = "API设置"; + mergedSettings["certificates"] = "证书管理"; + mergedSettings["ssh"] = "SSH密钥"; + mergedSettings["backups"] = "备份"; + mergedSettings["logs"] = "日志"; + mergedSettings["updates"] = "更新"; + mergedSettings["network"] = "网络"; + + // 输出合并后的文件内容 + console.log('Common translations total:', Object.keys(mergedCommon).length); + console.log('Settings translations total:', Object.keys(mergedSettings).length); + + // 保存为最终的翻译文件 + fs.writeFileSync(path.join(basePath, 'final-zh-Hans-common.json'), JSON.stringify(mergedCommon, null, 2)); + fs.writeFileSync(path.join(basePath, 'final-zh-Hans-settings.json'), JSON.stringify(mergedSettings, null, 2)); + + // 输出翻译完成的统计 + const commonKeys = Object.keys(mergedCommon); + const settingsKeys = Object.keys(mergedSettings); + console.log('最终翻译文件已保存:'); + console.log(`- 通用翻译 (${commonKeys.length} 个词条)`); + console.log(`- 设置翻译 (${settingsKeys.length} 个词条)`); + + // 创建最终放入项目中的文件(按项目结构) + const projectCommonPath = path.join(basePath, 'apps', 'dokploy', 'public', 'locales', 'zh-Hans'); + + // 确保目录存在 + if (!fs.existsSync(projectCommonPath)) { + fs.mkdirSync(projectCommonPath, { recursive: true }); + console.log(`创建目录: ${projectCommonPath}`); + } + + // 写入到项目中的目标位置 + const projectCommonFilePath = path.join(projectCommonPath, 'common.json'); + const projectSettingsFilePath = path.join(projectCommonPath, 'settings.json'); + + console.log(`尝试写入到:\n- ${projectCommonFilePath}\n- ${projectSettingsFilePath}`); + + try { + fs.writeFileSync(projectCommonFilePath, JSON.stringify(mergedCommon, null, 2)); + fs.writeFileSync(projectSettingsFilePath, JSON.stringify(mergedSettings, null, 2)); + console.log('已成功写入到项目文件夹中!'); + } catch (error) { + console.error('写入到项目文件夹失败:', error.message); + console.log('请手动将文件复制到目标位置。'); + } +} catch (error) { + console.error('错误:', error); +} From 350bed217c5574ed829999a6a9996770dee36227 Mon Sep 17 00:00:00 2001 From: Hoofei Date: Sun, 6 Apr 2025 12:37:52 +0800 Subject: [PATCH 14/84] Delete the extract script --- extract.js | 270 ------------------------------------------ merge-translations.js | 252 --------------------------------------- 2 files changed, 522 deletions(-) delete mode 100644 extract.js delete mode 100644 merge-translations.js diff --git a/extract.js b/extract.js deleted file mode 100644 index 61d5d2d7..00000000 --- a/extract.js +++ /dev/null @@ -1,270 +0,0 @@ -console.log('Creating translation extractor script...'); - -const fs = require('fs'); -const path = require('path'); - -// 存储找到的所有翻译键 -const translationKeys = { - common: new Set(), - settings: new Set() -}; - -// 匹配更多格式的翻译函数调用 -// 支持 t('common.xxx')、t("common.xxx")、t(`common.xxx`) -const translationPatterns = [ - /t\(\s*['"]([a-zA-Z0-9._-]+)['"]?\s*[,)]/g, // t('key') 或 t("key") - /t\(\s*`([a-zA-Z0-9._-]+)`\s*[,)]/g, // t(`key`) - /useTranslation\(\s*["']([a-zA-Z0-9._-]+)["']\s*\)/g, // useTranslation('namespace') - /serverSideTranslations\([^)]*["']([a-zA-Z0-9._-]+)["']/g // serverSideTranslations(..., ['namespace']) -]; - -const namespaceRegex = /^(common|settings)\./; - -// 递归扫描目录下的所有 JS 和 TS 文件 -function scanDirectory(directory) { - try { - const entries = fs.readdirSync(directory, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(directory, entry.name); - - if (entry.isDirectory() && !fullPath.includes('node_modules') && !fullPath.includes('.next')) { - scanDirectory(fullPath); - } else if (entry.isFile() && /\.(js|jsx|ts|tsx)$/.test(entry.name)) { - try { - const content = fs.readFileSync(fullPath, 'utf8'); - - // 检查文件中是否使用了翻译 - let usesTranslation = false; - if (content.includes('useTranslation') || content.includes('t(') || content.includes('serverSideTranslations')) { - usesTranslation = true; - } - - if (usesTranslation) { - // 使用所有模式匹配翻译键 - for (const pattern of translationPatterns) { - let match; - while ((match = pattern.exec(content)) !== null) { - const key = match[1]; - - // 检查是否有命名空间 - const namespaceMatch = key.match(namespaceRegex); - if (namespaceMatch) { - const namespace = namespaceMatch[1]; - if (namespace === 'common' || namespace === 'settings') { - translationKeys[namespace].add(key); - } - } - - // 如果文件中导入了特定命名空间,所有的 t(key) 都属于该命名空间 - if (content.includes(`useTranslation('common')`) || content.includes(`useTranslation("common")`)) { - if (!key.includes('.')) { - translationKeys.common.add(`common.${key}`); - } - } - - if (content.includes(`useTranslation('settings')`) || content.includes(`useTranslation("settings")`)) { - if (!key.includes('.')) { - translationKeys.settings.add(`settings.${key}`); - } - } - } - } - - // 控制台输出被处理的文件及其找到的翻译键 - if (usesTranslation) { - console.log(`检查文件: ${fullPath}`); - } - } - } catch (error) { - console.error(`Error reading file ${fullPath}:`, error); - } - } - } - } catch (error) { - console.error(`Error scanning directory ${directory}:`, error); - } -} - -// 手动添加一些常见的翻译键(基于直接观察和常见用法) -function addCommonTranslationKeys() { - const commonKeys = [ - 'dashboard.title', 'dashboard.overview', 'dashboard.projects', 'dashboard.servers', - 'dashboard.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', - - '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' - ]; - - commonKeys.forEach(key => { - translationKeys.common.add(`common.${key}`); - }); -} - -// 读取现有翻译文件 -function readTranslationFile(filePath) { - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } - } catch (error) { - console.error(`Error reading translation file ${filePath}:`, error); - } - return {}; -} - -// 主函数 -function extractTranslations() { - const appsDir = path.join(__dirname, 'apps', 'dokploy'); - - // 扫描代码库 - scanDirectory(appsDir); - - // 手动添加常见的翻译键 - addCommonTranslationKeys(); - - // 读取现有翻译文件 - const zhHansCommonPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'common.json'); - const zhHansSettingsPath = path.join(appsDir, 'public', 'locales', 'zh-Hans', 'settings.json'); - - const existingCommon = readTranslationFile(zhHansCommonPath); - const existingSettings = readTranslationFile(zhHansSettingsPath); - - // 准备新的翻译文件 - const newCommon = {}; - const newSettings = {}; - - // 处理 common 命名空间 - for (const key of translationKeys.common) { - const shortKey = key.replace('common.', ''); - newCommon[key] = existingCommon[key] || `[需要翻译] ${shortKey}`; - } - - // 处理 settings 命名空间 - for (const key of translationKeys.settings) { - const shortKey = key.replace('settings.', ''); - newSettings[key] = existingSettings[key] || `[需要翻译] ${shortKey}`; - } - - // 输出结果 - console.log('=== 提取的 common 翻译键 ==='); - console.log(Array.from(translationKeys.common).sort().join('\n')); - console.log(`\n共找到 ${translationKeys.common.size} 个 common 翻译键`); - - console.log('\n=== 提取的 settings 翻译键 ==='); - console.log(Array.from(translationKeys.settings).sort().join('\n')); - console.log(`\n共找到 ${translationKeys.settings.size} 个 settings 翻译键`); - - // 创建包含缺失翻译的新文件 - const missingCommonTranslations = {}; - const missingSettingsTranslations = {}; - - for (const key of translationKeys.common) { - if (!existingCommon[key]) { - const shortKey = key.replace('common.', ''); - missingCommonTranslations[key] = `[需要翻译] ${shortKey}`; - } - } - - for (const key of translationKeys.settings) { - if (!existingSettings[key]) { - const shortKey = key.replace('settings.', ''); - missingSettingsTranslations[key] = `[需要翻译] ${shortKey}`; - } - } - - // 输出缺失的翻译 - console.log('\n=== 缺失的 common 翻译 ==='); - console.log(JSON.stringify(missingCommonTranslations, null, 2)); - console.log(`\n共缺失 ${Object.keys(missingCommonTranslations).length} 个 common 翻译`); - - console.log('\n=== 缺失的 settings 翻译 ==='); - console.log(JSON.stringify(missingSettingsTranslations, null, 2)); - console.log(`\n共缺失 ${Object.keys(missingSettingsTranslations).length} 个 settings 翻译`); - - // 输出可以直接复制到文件中的完整翻译对象 - console.log('\n=== 完整的 common.json 内容 ==='); - const fullCommon = { ...existingCommon }; - translationKeys.common.forEach(key => { - if (!fullCommon[key]) { - const shortKey = key.replace('common.', ''); - fullCommon[key] = `[翻译] ${shortKey}`; - } - }); - console.log(JSON.stringify(fullCommon, null, 2)); - - console.log('\n=== 完整的 settings.json 内容 ==='); - const fullSettings = { ...existingSettings }; - translationKeys.settings.forEach(key => { - if (!fullSettings[key]) { - const shortKey = key.replace('settings.', ''); - fullSettings[key] = `[翻译] ${shortKey}`; - } - }); - console.log(JSON.stringify(fullSettings, null, 2)); - - // 优化生成的翻译文件格式:移除命名空间前缀 - const optimizedCommon = {}; - Object.keys(fullCommon).forEach(key => { - const shortKey = key.replace('common.', ''); - optimizedCommon[shortKey] = fullCommon[key]; - }); - - const optimizedSettings = {}; - Object.keys(fullSettings).forEach(key => { - const shortKey = key.replace('settings.', ''); - optimizedSettings[shortKey] = fullSettings[key]; - }); - - // 写入文件 - fs.writeFileSync('missing-common-translations.json', JSON.stringify(missingCommonTranslations, null, 2), 'utf8'); - fs.writeFileSync('missing-settings-translations.json', JSON.stringify(missingSettingsTranslations, null, 2), 'utf8'); - fs.writeFileSync('full-common-translations.json', JSON.stringify(fullCommon, null, 2), 'utf8'); - fs.writeFileSync('full-settings-translations.json', JSON.stringify(fullSettings, null, 2), 'utf8'); - fs.writeFileSync('optimized-common-translations.json', JSON.stringify(optimizedCommon, null, 2), 'utf8'); - fs.writeFileSync('optimized-settings-translations.json', JSON.stringify(optimizedSettings, null, 2), 'utf8'); - - console.log('\n翻译提取完成!'); - console.log('文件已保存:'); - console.log('- missing-common-translations.json: 缺失的 common 翻译'); - console.log('- missing-settings-translations.json: 缺失的 settings 翻译'); - console.log('- full-common-translations.json: 完整的 common 翻译(包含命名空间)'); - console.log('- full-settings-translations.json: 完整的 settings 翻译(包含命名空间)'); - console.log('- optimized-common-translations.json: 优化格式的 common 翻译(不含命名空间)'); - console.log('- optimized-settings-translations.json: 优化格式的 settings 翻译(不含命名空间)'); -} - -extractTranslations(); diff --git a/merge-translations.js b/merge-translations.js deleted file mode 100644 index 441ebed4..00000000 --- a/merge-translations.js +++ /dev/null @@ -1,252 +0,0 @@ -// 读取已创建的翻译文件 -const fs = require('fs'); -const path = require('path'); - -try { - const basePath = process.cwd(); - const commonPart1Path = path.join(basePath, 'common-zh-Hans.json'); - const commonPart2Path = path.join(basePath, 'common-zh-Hans-buttons.json'); - const fullCommonPath = path.join(basePath, 'optimized-common-translations.json'); - const fullSettingsPath = path.join(basePath, 'optimized-settings-translations.json'); - - const commonPart1 = JSON.parse(fs.readFileSync(commonPart1Path, 'utf8')); - const commonPart2 = JSON.parse(fs.readFileSync(commonPart2Path, 'utf8')); - const fullCommon = JSON.parse(fs.readFileSync(fullCommonPath, 'utf8')); - const fullSettings = JSON.parse(fs.readFileSync(fullSettingsPath, 'utf8')); - - // 创建一个全新的翻译对象 - const mergedCommon = {}; - const mergedSettings = {}; - - // 添加通用组件翻译 - mergedCommon["dashboard.title"] = "仪表盘"; - mergedCommon["dashboard.overview"] = "概览"; - mergedCommon["dashboard.projects"] = "项目"; - mergedCommon["dashboard.servers"] = "服务器"; - mergedCommon["dashboard.docker"] = "Docker"; - mergedCommon["dashboard.monitoring"] = "监控"; - mergedCommon["dashboard.settings"] = "设置"; - mergedCommon["dashboard.logout"] = "退出登录"; - mergedCommon["dashboard.profile"] = "个人资料"; - mergedCommon["dashboard.terminal"] = "终端"; - mergedCommon["dashboard.containers"] = "容器"; - mergedCommon["dashboard.images"] = "镜像"; - mergedCommon["dashboard.volumes"] = "卷"; - mergedCommon["dashboard.networks"] = "网络"; - - // 按钮翻译 - mergedCommon["button.create"] = "创建"; - mergedCommon["button.edit"] = "编辑"; - mergedCommon["button.delete"] = "删除"; - mergedCommon["button.cancel"] = "取消"; - mergedCommon["button.save"] = "保存"; - mergedCommon["button.confirm"] = "确认"; - mergedCommon["button.back"] = "返回"; - mergedCommon["button.next"] = "下一步"; - mergedCommon["button.finish"] = "完成"; - - // 状态翻译 - mergedCommon["status.running"] = "运行中"; - mergedCommon["status.stopped"] = "已停止"; - mergedCommon["status.error"] = "错误"; - mergedCommon["status.pending"] = "等待中"; - mergedCommon["status.success"] = "成功"; - mergedCommon["status.failed"] = "失败"; - - // 表单翻译 - mergedCommon["form.required"] = "必填"; - mergedCommon["form.invalid"] = "无效"; - mergedCommon["form.submit"] = "提交"; - mergedCommon["form.reset"] = "重置"; - - // 通知翻译 - mergedCommon["notification.success"] = "操作成功"; - mergedCommon["notification.error"] = "操作失败"; - mergedCommon["notification.warning"] = "警告"; - mergedCommon["notification.info"] = "信息"; - - // 时间翻译 - mergedCommon["time.now"] = "刚刚"; - mergedCommon["time.minutes"] = "分钟前"; - mergedCommon["time.hours"] = "小时前"; - mergedCommon["time.days"] = "天前"; - - // 过滤翻译 - mergedCommon["filter.all"] = "全部"; - mergedCommon["filter.active"] = "活跃"; - mergedCommon["filter.inactive"] = "不活跃"; - - // 排序翻译 - mergedCommon["sort.asc"] = "升序"; - mergedCommon["sort.desc"] = "降序"; - - // 搜索翻译 - mergedCommon["search.placeholder"] = "搜索..."; - mergedCommon["search.noResults"] = "无结果"; - - // 分页翻译 - mergedCommon["pagination.prev"] = "上一页"; - mergedCommon["pagination.next"] = "下一页"; - mergedCommon["pagination.of"] = "共 {0} 页"; - - // 错误翻译 - mergedCommon["error.notFound"] = "未找到"; - mergedCommon["error.serverError"] = "服务器错误"; - mergedCommon["error.unauthorized"] = "未授权"; - mergedCommon["error.forbidden"] = "禁止访问"; - - // 通用状态翻译 - mergedCommon["loading"] = "加载中..."; - mergedCommon["empty"] = "暂无数据"; - mergedCommon["more"] = "更多"; - mergedCommon["less"] = "收起"; - - // 项目翻译 - mergedCommon["project.create"] = "创建项目"; - mergedCommon["project.edit"] = "编辑项目"; - mergedCommon["project.delete"] = "删除项目"; - mergedCommon["project.name"] = "项目名称"; - mergedCommon["project.description"] = "项目描述"; - - // 服务翻译 - mergedCommon["service.create"] = "创建服务"; - mergedCommon["service.edit"] = "编辑服务"; - mergedCommon["service.delete"] = "删除服务"; - mergedCommon["service.name"] = "服务名称"; - mergedCommon["service.type"] = "服务类型"; - - // 域名翻译 - mergedCommon["domain.add"] = "添加域名"; - mergedCommon["domain.remove"] = "移除域名"; - - // 环境变量翻译 - mergedCommon["environment.variables"] = "环境变量"; - mergedCommon["environment.add"] = "添加环境变量"; - mergedCommon["environment.edit"] = "编辑环境变量"; - mergedCommon["environment.name"] = "变量名"; - mergedCommon["environment.value"] = "变量值"; - - // 设置页面的通用翻译 - mergedSettings["common.save"] = "保存"; - mergedSettings["common.enterTerminal"] = "终端"; - - // 服务器域名设置 - mergedSettings["server.domain.title"] = "服务器域名"; - mergedSettings["server.domain.description"] = "为您的服务器应用添加域名。"; - mergedSettings["server.domain.form.domain"] = "域名"; - mergedSettings["server.domain.form.letsEncryptEmail"] = "Let's Encrypt 邮箱"; - mergedSettings["server.domain.form.certificate.label"] = "证书提供商"; - mergedSettings["server.domain.form.certificate.placeholder"] = "选择证书"; - mergedSettings["server.domain.form.certificateOptions.none"] = "无"; - mergedSettings["server.domain.form.certificateOptions.letsencrypt"] = "Let's Encrypt"; - - // Web服务器设置 - mergedSettings["server.webServer.title"] = "Web 服务器"; - mergedSettings["server.webServer.description"] = "重载或清理 Web 服务器。"; - mergedSettings["server.webServer.actions"] = "操作"; - mergedSettings["server.webServer.reload"] = "重新加载"; - mergedSettings["server.webServer.watchLogs"] = "查看日志"; - mergedSettings["server.webServer.updateServerIp"] = "更新服务器 IP"; - mergedSettings["server.webServer.server.label"] = "服务器"; - - // Traefik设置 - mergedSettings["server.webServer.traefik.label"] = "Traefik"; - mergedSettings["server.webServer.traefik.modifyEnv"] = "修改环境变量"; - mergedSettings["server.webServer.traefik.managePorts"] = "额外端口映射"; - mergedSettings["server.webServer.traefik.managePortsDescription"] = "为 Traefik 添加或删除额外端口"; - mergedSettings["server.webServer.traefik.targetPort"] = "目标端口"; - mergedSettings["server.webServer.traefik.publishedPort"] = "发布端口"; - mergedSettings["server.webServer.traefik.addPort"] = "添加端口"; - mergedSettings["server.webServer.traefik.portsUpdated"] = "端口更新成功"; - mergedSettings["server.webServer.traefik.portsUpdateError"] = "端口更新失败"; - mergedSettings["server.webServer.traefik.publishMode"] = "发布模式"; - - // 存储空间设置 - mergedSettings["server.webServer.storage.label"] = "存储空间"; - mergedSettings["server.webServer.storage.cleanUnusedImages"] = "清理未使用的镜像"; - mergedSettings["server.webServer.storage.cleanUnusedVolumes"] = "清理未使用的卷"; - mergedSettings["server.webServer.storage.cleanStoppedContainers"] = "清理已停止的容器"; - mergedSettings["server.webServer.storage.cleanDockerBuilder"] = "清理 Docker Builder 和系统"; - mergedSettings["server.webServer.storage.cleanMonitoring"] = "清理监控数据"; - mergedSettings["server.webServer.storage.cleanAll"] = "清理所有内容"; - - // 个人资料设置 - mergedSettings["profile.title"] = "账户"; - mergedSettings["profile.description"] = "在此更改您的个人资料详情。"; - mergedSettings["profile.email"] = "邮箱"; - mergedSettings["profile.password"] = "密码"; - mergedSettings["profile.avatar"] = "头像"; - - // 外观设置 - mergedSettings["appearance.title"] = "外观"; - mergedSettings["appearance.description"] = "自定义您的仪表盘主题。"; - mergedSettings["appearance.theme"] = "主题"; - mergedSettings["appearance.themeDescription"] = "为您的仪表盘选择主题"; - mergedSettings["appearance.themes.light"] = "明亮"; - mergedSettings["appearance.themes.dark"] = "暗黑"; - mergedSettings["appearance.themes.system"] = "跟随系统"; - mergedSettings["appearance.language"] = "语言"; - mergedSettings["appearance.languageDescription"] = "为您的仪表盘选择语言"; - - // 终端设置 - mergedSettings["terminal.connectionSettings"] = "连接设置"; - mergedSettings["terminal.ipAddress"] = "IP 地址"; - mergedSettings["terminal.port"] = "端口"; - mergedSettings["terminal.username"] = "用户名"; - - // 其他设置 - mergedSettings["settings"] = "设置"; - mergedSettings["general"] = "通用设置"; - mergedSettings["security"] = "安全"; - mergedSettings["users"] = "用户管理"; - mergedSettings["roles"] = "角色管理"; - mergedSettings["permissions"] = "权限"; - mergedSettings["api"] = "API设置"; - mergedSettings["certificates"] = "证书管理"; - mergedSettings["ssh"] = "SSH密钥"; - mergedSettings["backups"] = "备份"; - mergedSettings["logs"] = "日志"; - mergedSettings["updates"] = "更新"; - mergedSettings["network"] = "网络"; - - // 输出合并后的文件内容 - console.log('Common translations total:', Object.keys(mergedCommon).length); - console.log('Settings translations total:', Object.keys(mergedSettings).length); - - // 保存为最终的翻译文件 - fs.writeFileSync(path.join(basePath, 'final-zh-Hans-common.json'), JSON.stringify(mergedCommon, null, 2)); - fs.writeFileSync(path.join(basePath, 'final-zh-Hans-settings.json'), JSON.stringify(mergedSettings, null, 2)); - - // 输出翻译完成的统计 - const commonKeys = Object.keys(mergedCommon); - const settingsKeys = Object.keys(mergedSettings); - console.log('最终翻译文件已保存:'); - console.log(`- 通用翻译 (${commonKeys.length} 个词条)`); - console.log(`- 设置翻译 (${settingsKeys.length} 个词条)`); - - // 创建最终放入项目中的文件(按项目结构) - const projectCommonPath = path.join(basePath, 'apps', 'dokploy', 'public', 'locales', 'zh-Hans'); - - // 确保目录存在 - if (!fs.existsSync(projectCommonPath)) { - fs.mkdirSync(projectCommonPath, { recursive: true }); - console.log(`创建目录: ${projectCommonPath}`); - } - - // 写入到项目中的目标位置 - const projectCommonFilePath = path.join(projectCommonPath, 'common.json'); - const projectSettingsFilePath = path.join(projectCommonPath, 'settings.json'); - - console.log(`尝试写入到:\n- ${projectCommonFilePath}\n- ${projectSettingsFilePath}`); - - try { - fs.writeFileSync(projectCommonFilePath, JSON.stringify(mergedCommon, null, 2)); - fs.writeFileSync(projectSettingsFilePath, JSON.stringify(mergedSettings, null, 2)); - console.log('已成功写入到项目文件夹中!'); - } catch (error) { - console.error('写入到项目文件夹失败:', error.message); - console.log('请手动将文件复制到目标位置。'); - } -} catch (error) { - console.error('错误:', error); -} From cb20950dd9222dba19e3f48404416e4414f7eabb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:03:57 -0600 Subject: [PATCH 15/84] feat(registry): refactor Docker login command execution to use execFileAsync for improved input handling --- apps/dokploy/server/api/routers/registry.ts | 19 +++++++-- .../server/src/utils/process/execAsync.ts | 41 ++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index a9a6be89..5486f37c 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -10,8 +10,8 @@ import { import { IS_CLOUD, createRegistry, - execAsync, execAsyncRemote, + execFileAsync, findRegistryById, removeRegistry, updateRegistry, @@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({ .input(apiTestRegistry) .mutation(async ({ input }) => { try { - const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + const args = [ + "login", + input.registryUrl, + "--username", + input.username, + "--password-stdin", + ]; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({ } if (input.serverId && input.serverId !== "none") { - await execAsyncRemote(input.serverId, loginCommand); + await execAsyncRemote( + input.serverId, + `echo ${input.password} | docker ${args.join(" ")}`, + ); } else { - await execAsync(loginCommand); + await execFileAsync("docker", args, { + input: Buffer.from(input.password).toString(), + }); } return true; diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index aee1e821..c3e40907 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,9 +1,48 @@ -import { exec } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; + export const execAsync = util.promisify(exec); +export const execFileAsync = async ( + command: string, + args: string[], + options: { input?: string } = {}, +): Promise<{ stdout: string; stderr: string }> => { + const child = execFile(command, args); + + if (options.input && child.stdin) { + child.stdin.write(options.input); + child.stdin.end(); + } + + return new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject( + new Error(`Command failed with code ${code}. Stderr: ${stderr}`), + ); + } + }); + + child.on("error", reject); + }); +}; + export const execAsyncRemote = async ( serverId: string | null, command: string, From 14bc26e065dc572b839c6cbd44b1a6ed26d80613 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:07:41 -0600 Subject: [PATCH 16/84] feat(websocket): enhance WebSocket server with request validation and client instantiation - Added request validation to ensure user authentication before establishing WebSocket connections. - Refactored WebSocket client instantiation to simplify connection management. --- apps/dokploy/server/wss/drawer-logs.ts | 14 +++++++++---- apps/dokploy/utils/api.ts | 27 +++++++------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index dcdeaad7..0202ae52 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -3,6 +3,7 @@ import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; import { createTRPCContext } from "../api/trpc"; +import { validateRequest } from "@dokploy/server/lib/auth"; export const setupDrawerLogsWebSocketServer = ( server: http.Server, @@ -32,8 +33,13 @@ export const setupDrawerLogsWebSocketServer = ( } }); - // Return cleanup function - return () => { - wssTerm.close(); - }; + wssTerm.on("connection", async (ws, req) => { + const _url = new URL(req.url || "", `http://${req.headers.host}`); + const { user, session } = await validateRequest(req); + + if (!user || !session) { + ws.close(); + return; + } + }); }; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index 7c003f48..56197528 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -27,28 +27,15 @@ const getWsUrl = () => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const host = window.location.host; - // Use the base URL for all tRPC WebSocket connections return `${protocol}${host}/drawer-logs`; }; -// Singleton WebSocket client instance -let wsClientInstance: ReturnType | null = null; - -const getWsClient = () => { - if (typeof window === "undefined") return null; - - if (!wsClientInstance) { - wsClientInstance = createWSClient({ - url: getWsUrl() || "", - onClose: () => { - // Reset the instance when connection closes so it can be recreated - wsClientInstance = null; - }, - }); - } - - return wsClientInstance; -}; +const wsClient = + typeof window !== "undefined" + ? createWSClient({ + url: getWsUrl() || "", + }) + : null; /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ @@ -70,7 +57,7 @@ export const api = createTRPCNext({ splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ - client: getWsClient()!, + client: wsClient!, }), false: splitLink({ condition: (op) => op.input instanceof FormData, From 1605aedd6e55d6efe299b725e31317e2cbc7a916 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 01:41:47 -0600 Subject: [PATCH 17/84] feat(settings): add HTTPS support and update user schema - Introduced a new boolean field 'https' in the user schema to manage HTTPS settings. - Updated the web domain form to include an HTTPS toggle, allowing users to enable or disable HTTPS. - Enhanced validation logic to ensure certificate type is required when HTTPS is enabled. - Modified Traefik configuration to handle HTTPS routing based on user settings. --- .../dashboard/settings/web-domain.tsx | 111 +- apps/dokploy/drizzle/0084_thin_iron_lad.sql | 1 + apps/dokploy/drizzle/meta/0084_snapshot.json | 5369 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/server/api/routers/settings.ts | 1 + packages/server/src/db/schema/user.ts | 3 + .../server/src/utils/traefik/web-server.ts | 56 +- 7 files changed, 5500 insertions(+), 48 deletions(-) create mode 100644 apps/dokploy/drizzle/0084_thin_iron_lad.sql create mode 100644 apps/dokploy/drizzle/meta/0084_snapshot.json diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index a579df39..d35dae35 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -22,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { GlobeIcon } from "lucide-react"; @@ -33,11 +35,19 @@ import { z } from "zod"; const addServerDomain = z .object({ - domain: z.string().min(1, { message: "URL is required" }), + domain: z.string(), letsEncryptEmail: z.string(), + https: z.boolean().optional(), certificateType: z.enum(["letsencrypt", "none", "custom"]), }) .superRefine((data, ctx) => { + if (data.https && !data.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -61,15 +71,18 @@ export const WebDomain = () => { domain: "", certificateType: "none", letsEncryptEmail: "", + https: false, }, resolver: zodResolver(addServerDomain), }); + const https = form.watch("https"); useEffect(() => { if (data) { form.reset({ domain: data?.user?.host || "", certificateType: data?.user?.certificateType, letsEncryptEmail: data?.user?.letsEncryptEmail || "", + https: data?.user?.https || false, }); } }, [form, form.reset, data]); @@ -79,6 +92,7 @@ export const WebDomain = () => { host: data.domain, letsEncryptEmail: data.letsEncryptEmail, certificateType: data.certificateType, + https: data.https, }) .then(async () => { await refetch(); @@ -155,44 +169,67 @@ export const WebDomain = () => { /> { - return ( - - - {t("settings.server.domain.form.certificate.label")} - - + name="https" + render={({ field }) => ( + +
+ HTTPS + + Automatically provision SSL Certificate. + - - ); - }} +
+ + + +
+ )} /> + {https && ( + { + return ( + + + {t("settings.server.domain.form.certificate.label")} + + + + + ); + }} + /> + )}
- - - - -
- -
- ); -}; - -export default Page; - -Page.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }>, -) { - const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - if (user.role === "member") { - return { - redirect: { - permanent: true, - destination: "/dashboard/settings/profile", - }, - }; - } - - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session as any, - user: user as any, - }, - transformer: superjson, - }); - await helpers.user.get.prefetch(); - - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; -} From f5cffca37cabb800330e2791578ed86d9f5d2a20 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:56:32 +0000 Subject: [PATCH 25/84] [autofix.ci] apply automated fixes (attempt 2/3) --- apps/dokploy/pages/api/deploy/github.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index ace72148..4757bbab 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -188,11 +188,9 @@ export default async function handler( return; } - res - .status(200) - .json({ - message: `Deployed ${totalApps} apps based on tag ${tagName}`, - }); + res.status(200).json({ + message: `Deployed ${totalApps} apps based on tag ${tagName}`, + }); return; } catch (error) { console.error("Error deploying applications on tag:", error); From 8f0697b0e974048baade3f1d114b0d27490d4406 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:29:13 -0600 Subject: [PATCH 26/84] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 34ca86fa..be1143be 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.3", + "version": "v0.21.4", "private": true, "license": "Apache-2.0", "type": "module", From 05f43ad06b4c7ccdb95b241f2dabe3636335e6b8 Mon Sep 17 00:00:00 2001 From: Axodouble Date: Mon, 7 Apr 2025 10:36:37 +0200 Subject: [PATCH 27/84] FEAT: Add Dutch / NL language translations --- apps/dokploy/public/locales/nl/common.json | 1 + apps/dokploy/public/locales/nl/settings.json | 58 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 apps/dokploy/public/locales/nl/common.json create mode 100644 apps/dokploy/public/locales/nl/settings.json diff --git a/apps/dokploy/public/locales/nl/common.json b/apps/dokploy/public/locales/nl/common.json new file mode 100644 index 00000000..69a88e3b --- /dev/null +++ b/apps/dokploy/public/locales/nl/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/nl/settings.json b/apps/dokploy/public/locales/nl/settings.json new file mode 100644 index 00000000..34c492ec --- /dev/null +++ b/apps/dokploy/public/locales/nl/settings.json @@ -0,0 +1,58 @@ +{ + "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" +} From 0e1f0b42eeb07a8aa9dafff9b4af16ed27de0287 Mon Sep 17 00:00:00 2001 From: Lorenzo Migliorero Date: Mon, 7 Apr 2025 21:43:56 +0200 Subject: [PATCH 28/84] fix(gitlab): update group name label and enhance group name handling - Updated the label for the group name input field to indicate it accepts a comma-separated list. - Modified the logic for checking group name inclusion to support multiple names separated by commas. --- .../dashboard/settings/git/gitlab/add-gitlab-provider.tsx | 4 +++- .../dashboard/settings/git/gitlab/edit-gitlab-provider.tsx | 4 +++- packages/server/src/utils/providers/gitlab.ts | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx index 4dd7da93..023e46ed 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx @@ -248,7 +248,9 @@ export const AddGitlabProvider = () => { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { const groupName = gitlabProvider.groupName?.toLowerCase(); if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + const isIncluded = groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); + + return isIncluded && kind === "group"; } return kind === "user"; }); From 1279fac1375b043c2812c36748b1883661f81da0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:46:14 +0000 Subject: [PATCH 29/84] [autofix.ci] apply automated fixes --- apps/dokploy/pages/dashboard/project/[projectId].tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index 6c4ac4bc..728d83d1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -365,7 +365,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.start.mutateAsync({ applicationId: serviceId }); + await applicationActions.start.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.start.mutateAsync({ composeId: serviceId }); @@ -410,7 +412,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + await applicationActions.stop.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.stop.mutateAsync({ composeId: serviceId }); From fa698d173ef0f49d54fa812f75659d7016aa7e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20T=C3=B8n=20L=C3=B8vhaug?= Date: Tue, 8 Apr 2025 22:24:19 +0200 Subject: [PATCH 30/84] Move passHostHeader to correct position --- packages/server/src/utils/traefik/web-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts index 2a997147..1534e2f1 100644 --- a/packages/server/src/utils/traefik/web-server.ts +++ b/packages/server/src/utils/traefik/web-server.ts @@ -37,9 +37,9 @@ export const updateServerTraefik = ( servers: [ { url: `http://dokploy:${process.env.PORT || 3000}`, - passHostHeader: true, }, ], + passHostHeader: true, }, }, }; From 3ede89fe8adc42ebd0a74fd1d8bc539438102a59 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:27:50 +0000 Subject: [PATCH 31/84] [autofix.ci] apply automated fixes --- apps/dokploy/pages/dashboard/project/[projectId].tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index 6c4ac4bc..728d83d1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -365,7 +365,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.start.mutateAsync({ applicationId: serviceId }); + await applicationActions.start.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.start.mutateAsync({ composeId: serviceId }); @@ -410,7 +412,9 @@ const Project = ( switch (service.type) { case "application": - await applicationActions.stop.mutateAsync({ applicationId: serviceId }); + await applicationActions.stop.mutateAsync({ + applicationId: serviceId, + }); break; case "compose": await composeActions.stop.mutateAsync({ composeId: serviceId }); From ee6ad07c0a8efe58afa7fab5f57de167bc3b3b59 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:44:17 -0600 Subject: [PATCH 32/84] Update package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index be1143be..df9f6d53 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.21.4", + "version": "v0.21.5", "private": true, "license": "Apache-2.0", "type": "module", From 7a5a3de43dd6f6f437406b2c65aa8cc54748a4f3 Mon Sep 17 00:00:00 2001 From: vytenisstaugaitis <30520456+vytenisstaugaitis@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:47:34 +0300 Subject: [PATCH 33/84] fix: correct message on preview deployments disabling --- .../application/preview-deployments/show-preview-settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index bfc6ad2e..7bf2063b 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -298,7 +298,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { }) .then(() => { refetch(); - toast.success("Preview deployments enabled"); + toast.success(checked ? "Preview deployments enabled" : "Preview deployments disabled"); }) .catch((error) => { toast.error(error.message); From d335a9515d0c09b8ccc71208472cf82c347d6e04 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:53:37 +0000 Subject: [PATCH 34/84] [autofix.ci] apply automated fixes --- .../preview-deployments/show-preview-settings.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 7bf2063b..4c5068ee 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -298,7 +298,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { }) .then(() => { refetch(); - toast.success(checked ? "Preview deployments enabled" : "Preview deployments disabled"); + toast.success( + checked + ? "Preview deployments enabled" + : "Preview deployments disabled", + ); }) .catch((error) => { toast.error(error.message); From 37f9e073f084161a15b58289f37d0e43de6c749d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:16:39 -0600 Subject: [PATCH 35/84] fix(railpack): update environment variable handling to include quotes for consistency --- packages/server/src/utils/builders/railpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 612e02cf..55fd4049 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -84,7 +84,7 @@ export const buildRailpack = async ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); env[key] = value; } } @@ -132,7 +132,7 @@ export const getRailpackCommand = ( ]; for (const env of envVariables) { - prepareArgs.push("--env", env); + prepareArgs.push("--env", `'${env}'`); } // Calculate secrets hash for layer invalidation @@ -164,7 +164,7 @@ export const getRailpackCommand = ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); exportEnvs.push(`export ${key}=${value}`); } } From 773a610be17e96b4446a2ff8b4fc8cd3242c8627 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 02:27:43 -0600 Subject: [PATCH 36/84] fix(profile-form): disable refetch on window focus for user query --- .../settings/profile/profile-form.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 32179378..7a59469f 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -56,6 +56,8 @@ const randomImages = [ export const ProfileForm = () => { const _utils = api.useUtils(); const { data, refetch, isLoading } = api.user.get.useQuery(); + + console.log(data); const { mutateAsync, isLoading: isUpdating, @@ -84,12 +86,17 @@ export const ProfileForm = () => { useEffect(() => { if (data) { - form.reset({ - email: data?.user?.email || "", - password: "", - image: data?.user?.image || "", - currentPassword: "", - }); + form.reset( + { + email: data?.user?.email || "", + password: form.getValues("password") || "", + image: data?.user?.image || "", + currentPassword: form.getValues("currentPassword") || "", + }, + { + keepValues: true, + }, + ); if (data.user.email) { generateSHA256Hash(data.user.email).then((hash) => { @@ -97,8 +104,7 @@ export const ProfileForm = () => { }); } } - form.reset(); - }, [form, form.reset, data]); + }, [form, data]); const onSubmit = async (values: Profile) => { await mutateAsync({ @@ -110,7 +116,12 @@ export const ProfileForm = () => { .then(async () => { await refetch(); toast.success("Profile Updated"); - form.reset(); + form.reset({ + email: values.email, + password: "", + image: values.image, + currentPassword: "", + }); }) .catch(() => { toast.error("Error updating the profile"); From efee79888039298a84dba39e3fcb03f868bb4c4b Mon Sep 17 00:00:00 2001 From: Ron_Tayler Date: Sat, 12 Apr 2025 22:02:27 +0300 Subject: [PATCH 37/84] Fixed network search in Traefik Labels for the service --- packages/server/src/utils/docker/domain.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 5a68146a..4f008397 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -249,6 +249,11 @@ export const addDomainToCompose = async ( labels.unshift("traefik.enable=true"); } labels.unshift(...httpLabels); + if (!compose.isolatedDeployment) { + if (!labels.includes("traefik.docker.network=dokploy-network")) { + labels.unshift("traefik.docker.network=dokploy-network"); + } + } } if (!compose.isolatedDeployment) { From 9b5cd0f5fe043dabb8d36558702c0b630711d1e6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:11:21 -0600 Subject: [PATCH 38/84] chore: update dependencies and enhance 2FA form - Updated `better-auth` to version 1.2.6 in multiple package.json files. - Updated `@better-auth/utils` to version 0.2.4 in server package.json. - Added optional `issuer` field to the 2FA form for enhanced user experience. - Removed unnecessary console log from the profile form component. --- .../dashboard/settings/profile/enable-2fa.tsx | 22 +++++ .../settings/profile/profile-form.tsx | 1 - apps/dokploy/package.json | 2 +- packages/server/package.json | 4 +- pnpm-lock.yaml | 88 ++++++++----------- 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 6cf2c6a5..1cfa7574 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -36,6 +36,7 @@ const PasswordSchema = z.object({ password: z.string().min(8, { message: "Password is required", }), + issuer: z.string().optional(), }); const PinSchema = z.object({ @@ -66,6 +67,7 @@ export const Enable2FA = () => { try { const { data: enableData, error } = await authClient.twoFactor.enable({ password: formData.password, + issuer: formData.issuer, }); if (!enableData) { @@ -217,6 +219,26 @@ export const Enable2FA = () => { )} /> + ( + + Issuer + + + + + Enter your password to enable 2FA + + + + )} + /> From 8e8bc3e71e3c836dce085bc370be933b47bf570f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:58:25 -0600 Subject: [PATCH 48/84] Enhance PostgreSQL backup command in web server utility - Added error handling to check for the existence of the PostgreSQL container before executing the backup command. - Updated the backup command to use the retrieved container ID, ensuring the command runs correctly. --- packages/server/src/utils/backups/web-server.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index a7d48a2f..ef2249d0 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -23,7 +23,17 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { try { await execAsync(`mkdir -p ${tempDir}/filesystem`); - const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; + // First get the container ID + const { stdout: containerId } = await execAsync( + "docker ps --filter 'name=dokploy-postgres' -q", + ); + + if (!containerId) { + throw new Error("PostgreSQL container not found"); + } + + // Then run pg_dump with the container ID + const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; await execAsync(postgresCommand); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); From 33ab87f3db3dea19362ba1f2bc9e6fa1d8288c97 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:20:03 -0600 Subject: [PATCH 49/84] fix(gitlab): enhance group name matching logic to support multiple names - Updated the group name check to allow for a comma-separated list of names, improving flexibility in group name validation. --- packages/server/src/utils/providers/gitlab.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index b8a58fd8..d01cc400 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -435,7 +435,9 @@ export const testGitlabConnection = async ( const { full_path, kind } = repo.namespace; if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + return groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); } return kind === "user"; }); From 43a17e7e75223f6f2c98b1bff3b4b077faadd746 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Fri, 18 Apr 2025 12:49:02 +1000 Subject: [PATCH 50/84] style: remove double space --- .../components/dashboard/application/general/generic/show.tsx | 2 +- apps/dokploy/components/dashboard/settings/ai-form.tsx | 2 +- .../dashboard/settings/certificates/show-certificates.tsx | 2 +- .../dashboard/settings/cluster/registry/show-registry.tsx | 2 +- .../dashboard/settings/destination/show-destinations.tsx | 2 +- .../dashboard/settings/notifications/show-notifications.tsx | 2 +- .../components/dashboard/settings/ssh-keys/show-ssh-keys.tsx | 2 +- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 3f885488..9b9a0ba0 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { setSab(e as TabState); }} > -
+
{ key={config.aiId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{config.name} diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 6aaa2563..b80c7b54 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -70,7 +70,7 @@ export const ShowCertificates = () => { key={certificate.certificateId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx index 08cb0381..9ae595d6 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -54,7 +54,7 @@ export const ShowRegistry = () => { key={registry.registryId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 0639b0f7..014596ce 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -55,7 +55,7 @@ export const ShowDestinations = () => { key={destination.destinationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{index + 1}. {destination.name} diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 782b9241..26ac1793 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -61,7 +61,7 @@ export const ShowNotifications = () => { key={notification.notificationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{notification.notificationType === "slack" && (
diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx index 5842457b..00d685a8 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx @@ -56,7 +56,7 @@ export const ShowDestinations = () => { key={sshKey.sshKeyId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index cff3a8db..bf5ced4a 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 12:54:42 +1000 Subject: [PATCH 51/84] fix: add overflow-x-scroll to tab list container --- .../[projectId]/services/application/[applicationId].tsx | 2 +- .../project/[projectId]/services/compose/[composeId].tsx | 2 +- .../project/[projectId]/services/mariadb/[mariadbId].tsx | 2 +- .../dashboard/project/[projectId]/services/mongo/[mongoId].tsx | 2 +- .../dashboard/project/[projectId]/services/mysql/[mysqlId].tsx | 2 +- .../project/[projectId]/services/postgres/[postgresId].tsx | 2 +- .../dashboard/project/[projectId]/services/redis/[redisId].tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index bf5ced4a..91af2cfa 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -215,7 +215,7 @@ const Service = ( router.push(newPath); }} > -
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Fri, 18 Apr 2025 13:01:43 +1000 Subject: [PATCH 52/84] fix: grid cols start from lg instead of md for compose --- .../project/[projectId]/services/compose/[composeId].tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 91da1623..3bba9eb2 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -215,12 +215,12 @@ const Service = (
General From 08bbeceebae9d4e2d594a36cd228f5316a73ab02 Mon Sep 17 00:00:00 2001 From: "Max W." Date: Sat, 19 Apr 2025 16:10:35 +0200 Subject: [PATCH 53/84] Add Ctrl+S keyboard shortcut to save compose file https://github.com/Dokploy/dokploy/issues/1736 --- .../compose/general/compose-file-editor.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index bbcbfd83..e582d266 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => { toast.error("Error updating the Compose config"); }); }; + + // Add keyboard shortcut for Ctrl+S/Cmd+S + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's' && !isLoading) { + e.preventDefault(); + form.handleSubmit(onSubmit)(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [form, onSubmit, isLoading]); + return ( <>
From 8d28a50a17ae3f54cad33c8368e71ea68079e9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20DEM=C4=B0RC=C4=B0?= Date: Sun, 20 Apr 2025 12:14:41 +0000 Subject: [PATCH 54/84] fix(backup): handle multiple container IDs in backup command Ensure only one container ID is used when running `docker exec` for pg_dump to avoid errors caused by multiple matching containers. Fixes INTERNAL_SERVER_ERROR from backup.manualBackupWebServer path. Co-authored-by: Merloss 54235902+Merloss@users.noreply.github.com --- packages/server/src/utils/backups/web-server.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index ef2249d0..1870a08c 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -24,6 +24,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await execAsync(`mkdir -p ${tempDir}/filesystem`); // First get the container ID + // Returns: ID\nID\nID... const { stdout: containerId } = await execAsync( "docker ps --filter 'name=dokploy-postgres' -q", ); @@ -32,14 +33,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { throw new Error("PostgreSQL container not found"); } + // ID\nID\nID... => [ "ID", "ID", ... ] + const containers = containerId.trim().split("\n").filter(Boolean); + // Then run pg_dump with the container ID - const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; - await execAsync(postgresCommand); + for (const containerId of containers) { + // Maybe we can find a better identification for this part vvv + const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database-${containerId}.sql'`; + await execAsync(postgresCommand); + } await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); - await execAsync( - `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`, + await execAsync( // Zip all .sql files since we created more than one + `cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`, ); const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; From 459c94929a893d44cd6170b019e6119bd98dc62d Mon Sep 17 00:00:00 2001 From: Theo D Date: Mon, 21 Apr 2025 02:25:41 +0200 Subject: [PATCH 55/84] fix GitHub event handling for tag deployments. --- ...igger_type.sql => 0085_add_trigger_type.sql} | 0 apps/dokploy/pages/api/deploy/github.ts | 17 ++++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) rename apps/dokploy/drizzle/{0084_add_trigger_type.sql => 0085_add_trigger_type.sql} (100%) diff --git a/apps/dokploy/drizzle/0084_add_trigger_type.sql b/apps/dokploy/drizzle/0085_add_trigger_type.sql similarity index 100% rename from apps/dokploy/drizzle/0084_add_trigger_type.sql rename to apps/dokploy/drizzle/0085_add_trigger_type.sql diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index 4757bbab..15547d72 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -62,12 +62,11 @@ export default async function handler( if ( req.headers["x-github-event"] !== "push" && - req.headers["x-github-event"] !== "pull_request" && - req.headers["x-github-event"] !== "create" + req.headers["x-github-event"] !== "pull_request" ) { res .status(400) - .json({ message: "We only accept push, pull_request, or create events" }); + .json({ message: "We only accept push events or pull_request events" }); return; } @@ -92,17 +91,17 @@ export default async function handler( // Handle tag creation event if ( - req.headers["x-github-event"] === "create" && - githubBody?.ref_type === "tag" + req.headers["x-github-event"] === "push" && + githubBody?.ref?.startsWith("refs/tags/") ) { try { - const tagName = githubBody?.ref; + const tagName = githubBody?.ref.replace("refs/tags/", ""); const repository = githubBody?.repository?.name; const owner = githubBody?.repository?.owner?.name || githubBody?.repository?.owner?.login; const deploymentTitle = `Tag created: ${tagName}`; - const deploymentHash = githubBody?.master_branch || ""; + const deploymentHash = extractHash(req.headers, githubBody); // Find applications configured to deploy on tag const apps = await db.query.applications.findMany({ @@ -120,7 +119,7 @@ export default async function handler( const jobData: DeploymentJob = { applicationId: app.applicationId as string, titleLog: deploymentTitle, - descriptionLog: `Tag: ${tagName}`, + descriptionLog: `Hash: ${deploymentHash}`, type: "deploy", applicationType: "application", server: !!app.serverId, @@ -159,7 +158,7 @@ export default async function handler( titleLog: deploymentTitle, type: "deploy", applicationType: "compose", - descriptionLog: `Tag: ${tagName}`, + descriptionLog: `Hash: ${deploymentHash}`, server: !!composeApp.serverId, }; From bc17991580b195fa044e0c680f501577a319d02d Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 15:53:38 +0200 Subject: [PATCH 56/84] test: Add some template helpers test --- .../templates/helpers.template.test.ts | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 apps/dokploy/__test__/templates/helpers.template.test.ts diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts new file mode 100644 index 00000000..d6eb532c --- /dev/null +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -0,0 +1,197 @@ +import type { Schema } from "@dokploy/server/templates"; +import { processValue } from "@dokploy/server/templates/processors"; +import { describe, expect, it } from "vitest"; + + +describe("helpers functions", () => { + // Mock schema for testing + const mockSchema: Schema = { + projectName: "test", + serverIp: "127.0.0.1", + }; + // some helpers to test jwt + type JWTParts = [string, string, string]; + const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + const jwtBase64Decode = (str: string) => { + const base64 = str.replace(/-/g, "+").replace(/_/g, "/"); + const padding = "=".repeat((4 - (base64.length % 4)) % 4); + const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8"); + return JSON.parse(decoded); + }; + const jwtCheckHeader = (jwtHeader: string) => { + const decodedHeader = jwtBase64Decode(jwtHeader); + expect(decodedHeader).toHaveProperty("alg"); + expect(decodedHeader).toHaveProperty("typ"); + expect(decodedHeader.alg).toEqual("HS256"); + expect(decodedHeader.typ).toEqual("JWT"); + }; + + describe("${domain}", () => { + it("should generate a random domain", () => { + const domain = processValue("${domain}", {}, mockSchema); + expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); + expect(domain.endsWith(`${mockSchema.serverIp.replaceAll(".","-")}.traefik.me`)).toBeTruthy(); + }); + }); + + describe("${base64}", () => { + it("should generate a base64 string", () => { + const base64 = processValue("${base64}", {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + }); + it.each([ + [4, 8], + [8, 12], + [16, 24], + [32, 44], + [64, 88], + [128, 172], + ])("should generate a base64 string from parameter %d bytes length", (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }); + }); + + describe("${password}", () => { + it("should generate a password string", () => { + const password = processValue("${password}", {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6,8,12,16,32])("should generate a password string respecting parameter %d length", (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }); + }); + + describe("${hash}", () => { + it("should generate a hash string", () => { + const hash = processValue("${hash}", {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + }); + it.each([6,8,12,16,32])("should generate a hash string respecting parameter %d length", (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }); + }); + + describe("${uuid}", () => { + it("should generate a UUID string", () => { + const uuid = processValue("${uuid}", {}, mockSchema); + expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + }); + }); + + describe("${timestamp}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestamp}", {}, mockSchema); + const nowLength = Math.floor(Date.now()).toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + }); + describe("${timestampms}", () => { + it("should generate a timestamp string in milliseconds", () => { + const timestamp = processValue("${timestampms}", {}, mockSchema); + const nowLength = Date.now().toString().length; + expect(timestamp).toMatch(/^\d+$/); + expect(timestamp.length).toBe(nowLength); + }); + it("should generate a timestamp string in milliseconds from parameter", () => { + const timestamp = processValue("${timestampms:2025-01-01}", {}, mockSchema); + expect(timestamp).toEqual('1735689600000'); + }); + }); + describe("${timestamps}", () => { + it("should generate a timestamp string in seconds", () => { + const timestamps = processValue("${timestamps}", {}, mockSchema); + const nowLength = Math.floor(Date.now() / 1000).toString().length; + expect(timestamps).toMatch(/^\d+$/); + expect(timestamps.length).toBe(nowLength); + }); + it("should generate a timestamp string in seconds from parameter", () => { + const timestamps = processValue("${timestamps:2025-01-01}", {}, mockSchema); + expect(timestamps).toEqual('1735689600'); + }); + }); + + describe("${randomPort}", () => { + it("should generate a random port string", () => { + const randomPort = processValue("${randomPort}", {}, mockSchema); + expect(randomPort).toMatch(/^\d+$/); + expect(Number(randomPort)).toBeLessThan(65536); + }); + }); + + describe("${username}", () => { + it("should generate a username string", () => { + const username = processValue("${username}", {}, mockSchema); + expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/); + }); + }); + + describe("${email}", () => { + it("should generate an email string", () => { + const email = processValue("${email}", {}, mockSchema); + expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/); + }); + }); + + describe("${jwt}", () => { + it("should generate a JWT string", () => { + const jwt = processValue("${jwt}", {}, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect (decodedPayload).toHaveProperty("iat"); + expect (decodedPayload).toHaveProperty("iss"); + expect (decodedPayload).toHaveProperty("exp"); + expect (decodedPayload.iss).toEqual("dokploy"); + }); + it.each([6,8,12,16,32])("should generate a random hex string from parameter %d byte length", (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }); + }); + describe("${jwt:secret}", () => { + it("should generate a JWT string respecting parameter secret from variable", () => { + const jwt = processValue("${jwt:secret}", {secret: "mysecret"}, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + const decodedPayload = jwtBase64Decode(parts[1]); + jwtCheckHeader(parts[0]); + expect (decodedPayload).toHaveProperty("iat"); + expect (decodedPayload).toHaveProperty("iss"); + expect (decodedPayload).toHaveProperty("exp"); + expect (decodedPayload.iss).toEqual("dokploy"); + }); + }); + describe("${jwt:secret:payload}", () => { + it("should generate a JWT string respecting parameters secret and payload from variables", () => { + const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); + const expiry = iat + 3600; + const jwt = processValue("${jwt:secret:payload}", { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, mockSchema); + expect(jwt).toMatch(jwtMatchExp); + const parts = jwt.split(".") as JWTParts; + jwtCheckHeader(parts[0]); + const decodedPayload = jwtBase64Decode(parts[1]); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload.iat).toEqual(iat); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload.iss).toEqual("test-issuer"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.exp).toEqual(expiry); + expect(decodedPayload).toHaveProperty("customprop"); + expect(decodedPayload.customprop).toEqual("customvalue"); + expect(jwt).toEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI") + }); + }); +}); \ No newline at end of file From 11b9cee73dbafa70b2eacd80f857be0d9452d883 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 15:59:49 +0200 Subject: [PATCH 57/84] feat(template-helpers): Add more parameters to jwt helper - jwt without parameter now generate a real jwt - keep length parameter as is for backward compatibility - add secret and payload parameters - payload properties iss, iat, exp are automaticly set if not provided --- packages/server/src/templates/index.ts | 47 ++++++++++++++++++--- packages/server/src/templates/processors.ts | 32 +++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 6ae26418..0d0f87ce 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes, createHmac } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -} +}; export type DomainSchema = Pick & { path?: string; @@ -22,6 +22,12 @@ export interface Template { content: string; }>; domains: DomainSchema[]; +}; + +export interface GenerateJWTOptions { + length?: number; + secret?: string; + payload?: Record | undefined; } export const generateRandomDomain = ({ @@ -59,10 +65,41 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -} +}; -export function generateJwt(length = 256): string { - return randomBytes(length).toString("hex"); +function safeBase64(str: string): string { + return str + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); +}; +function objToJWTBase64(obj: any): string { + return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); +}; + +export function generateJwt(options: GenerateJWTOptions = {}): string { + let { length, secret, payload = {} } = options; + if (length) { + return randomBytes(length).toString("hex"); + } + const encodedHeader = objToJWTBase64({ + alg: "HS256", + typ: "JWT", + }); + payload.iss || (payload.iss = "dokploy"); + payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); + payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + const encodedPayload = objToJWTBase64({ + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), + ...payload, + }); + secret || (secret = randomBytes(32).toString("hex")); + const signature = safeBase64(createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64")); + + return `${encodedHeader}.${encodedPayload}.${signature}`; } /** diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 31e7861a..e59cddf5 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -65,7 +65,7 @@ export interface Template { /** * Process a string value and replace variables */ -function processValue( +export function processValue( value: string, variables: Record, schema: Schema, @@ -84,11 +84,11 @@ function processValue( const length = Number.parseInt(varName.split(":")[1], 10) || 32; return generateBase64(length); } + if (varName.startsWith("password:")) { const length = Number.parseInt(varName.split(":")[1], 10) || 16; return generatePassword(length); } - if (varName === "password") { return generatePassword(16); } @@ -114,8 +114,30 @@ function processValue( } if (varName.startsWith("jwt:")) { - const length = Number.parseInt(varName.split(":")[1], 10) || 256; - return generateJwt(length); + const params:string[] = varName.split(":").slice(1); + if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { + return generateJwt({length: Number.parseInt(params[0], 10)}); + } + let [secret, payload] = params; + if (typeof payload === "string" && variables[payload]) { + payload = variables[payload]; + } + if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + try { + payload = JSON.parse(payload); + } catch (e) { + // If payload is not a valid JSON, invalid it + payload = undefined; + console.error("Invalid JWT payload", e); + } + } + if (typeof payload !== 'object') { + payload = undefined; + } + return generateJwt({ + secret: secret ? (variables[secret] || secret) : undefined, + payload: payload as any + }); } if (varName === "username") { @@ -147,7 +169,7 @@ export function processVariables( ): Record { const variables: Record = {}; - // First pass: Process variables that don't depend on other variables + // First pass: Process some variables that don't depend on other variables for (const [key, value] of Object.entries(template.variables)) { if (typeof value !== "string") continue; From 2b5af1897f1381b7f854d8b9f769a88be6bb383c Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:04:00 +0200 Subject: [PATCH 58/84] fix(template-helpers): hash not working without parameter --- packages/server/src/templates/processors.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index e59cddf5..fb463b63 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -97,6 +97,10 @@ export function processValue( const length = Number.parseInt(varName.split(":")[1], 10) || 8; return generateHash(length); } + if (varName === "hash") { + return generateHash(); + } + if (varName === "uuid") { return crypto.randomUUID(); } @@ -183,6 +187,8 @@ export function processVariables( const match = value.match(/\${password:(\d+)}/); const length = match?.[1] ? Number.parseInt(match[1], 10) : 16; variables[key] = generatePassword(length); + } else if (value === "${hash}") { + variables[key] = generateHash(); } else if (value.startsWith("${hash:")) { const match = value.match(/\${hash:(\d+)}/); const length = match?.[1] ? Number.parseInt(match[1], 10) : 8; From d0dbc1837fff66088ae5d8181fd76252c1ca65c5 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:05:08 +0200 Subject: [PATCH 59/84] feat(template-helpers): Add timestamps and timestampms helpers --- packages/server/src/templates/processors.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index fb463b63..ff8fe277 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -105,10 +105,21 @@ export function processValue( return crypto.randomUUID(); } - if (varName === "timestamp") { + if (varName === "timestamp" || varName === "timestampms") { return Date.now().toString(); } + if (varName === "timestamps") { + return Math.round(Date.now() / 1000).toString(); + } + + if (varName.startsWith("timestampms:")) { + return new Date(varName.slice(12)).getTime().toString(); + } + if (varName.startsWith("timestamps:")) { + return Math.round(new Date(varName.slice(11)).getTime() / 1000).toString(); + } + if (varName === "randomPort") { return Math.floor(Math.random() * 65535).toString(); } From e6d0b7b4eebd351599560032a6bd4980b24e99d7 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 16:12:34 +0200 Subject: [PATCH 60/84] test(templates): Add test for jwt generation --- .../templates/config.template.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index d6e87cb7..6f5baaf1 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -51,6 +51,33 @@ describe("processTemplate", () => { expect(result.domains).toHaveLength(0); expect(result.mounts).toHaveLength(0); }); + + it("should allow creation of real jwt secret", () => { + const template: CompleteTemplate = { + metadata: {} as any, + variables: { + jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", + anon_payload: JSON.stringify({ + "role": "tester", + "iss": "dockploy", + "iat": "${timestamps:2025-01-01T00:00:00Z}", + "exp": "${timestamps:2030-01-01T00:00:00Z}", + }), + anon_key: "${jwt:jwt_secret:anon_payload}", + }, + config: { + domains: [], + env: { + ANON_KEY: "${anon_key}", + }, + }, + }; + const result = processTemplate(template, mockSchema); + expect(result.envs).toHaveLength(1); + expect(result.envs).toContain("ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY"); + expect(result.mounts).toHaveLength(0); + expect(result.domains).toHaveLength(0); + }); }); describe("domains processing", () => { From dfda934726ab15719e79a2fc636be5e2f4bf4391 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:38:14 -0600 Subject: [PATCH 61/84] refactor(user-nav): remove settings dropdown for owner role --- apps/dokploy/components/layouts/user-nav.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 0aca5b00..151bd364 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -134,16 +134,7 @@ export const UserNav = () => { )} - {data?.role === "owner" && ( - { - router.push("/dashboard/settings"); - }} - > - Settings - - )} + )} From 4e5b5f219e73514a0cd8eb28c46d341e6cd01c92 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:41:03 -0600 Subject: [PATCH 62/84] fix(auth): update invite link host to use app.dokploy.com --- packages/server/src/lib/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 7ff53117..dbe8842d 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -201,7 +201,7 @@ const { handler, api } = betterAuth({ const host = process.env.NODE_ENV === "development" ? "http://localhost:3000" - : "https://dokploy.com"; + : "https://app.dokploy.com"; const inviteLink = `${host}/invitation?token=${data.id}`; await sendEmail({ From d4c6e5b04859f445cfc30577802ef85c55dc028b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Sj=C3=B6sten?= Date: Fri, 25 Apr 2025 09:58:52 +0200 Subject: [PATCH 63/84] build: update nixpacks to 1.35.0 --- CONTRIBUTING.md | 7 +- Dockerfile | 2 +- packages/server/src/setup/server-setup.ts | 240 +++++++++++----------- 3 files changed, 123 insertions(+), 126 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52fd7f2f..a69fa686 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ feat: add new feature Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. -We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. +We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. ```bash git clone https://github.com/dokploy/dokploy.git @@ -147,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh ```bash # Install Buildpacks -curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack +curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack ``` - - ## Pull Request - The `main` branch is the source of truth and should always reflect the latest stable release. @@ -169,7 +167,6 @@ Thank you for your contribution! To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. - ### Recommendations - Use the same name of the folder as the id of the template. diff --git a/Dockerfile b/Dockerfile index ad2239b0..a9b5f951 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash -ARG NIXPACKS_VERSION=1.29.1 +ARG NIXPACKS_VERSION=1.35.0 RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && chmod +x install.sh \ && ./install.sh \ diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 6fefabe9..4fb9910a 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,17 +1,17 @@ import path from "node:path"; import { paths } from "@dokploy/server/constants"; import { - createServerDeployment, - updateDeploymentStatus, + createServerDeployment, + updateDeploymentStatus, } from "@dokploy/server/services/deployment"; import { findServerById } from "@dokploy/server/services/server"; import { - TRAEFIK_HTTP3_PORT, - TRAEFIK_PORT, - TRAEFIK_SSL_PORT, - TRAEFIK_VERSION, - getDefaultMiddlewares, - getDefaultServerTraefikConfig, + TRAEFIK_HTTP3_PORT, + TRAEFIK_PORT, + TRAEFIK_SSL_PORT, + TRAEFIK_VERSION, + getDefaultMiddlewares, + getDefaultServerTraefikConfig, } from "@dokploy/server/setup/traefik-setup"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; @@ -19,55 +19,55 @@ import { recreateDirectory } from "../utils/filesystem/directory"; import slug from "slugify"; export const slugify = (text: string | undefined) => { - if (!text) { - return ""; - } + if (!text) { + return ""; + } - const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); - return slug(cleanedText, { - lower: true, - trim: true, - strict: true, - }); + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); }; export const serverSetup = async ( - serverId: string, - onData?: (data: any) => void, + serverId: string, + onData?: (data: any) => void ) => { - const server = await findServerById(serverId); - const { LOGS_PATH } = paths(); + const server = await findServerById(serverId); + const { LOGS_PATH } = paths(); - const slugifyName = slugify(`server ${server.name}`); + const slugifyName = slugify(`server ${server.name}`); - const fullPath = path.join(LOGS_PATH, slugifyName); + const fullPath = path.join(LOGS_PATH, slugifyName); - await recreateDirectory(fullPath); + await recreateDirectory(fullPath); - const deployment = await createServerDeployment({ - serverId: server.serverId, - title: "Setup Server", - description: "Setup Server", - }); + const deployment = await createServerDeployment({ + serverId: server.serverId, + title: "Setup Server", + description: "Setup Server", + }); - try { - onData?.("\nInstalling Server Dependencies: ✅\n"); - await installRequirements(serverId, onData); + try { + onData?.("\nInstalling Server Dependencies: ✅\n"); + await installRequirements(serverId, onData); - await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateDeploymentStatus(deployment.deploymentId, "done"); - onData?.("\nSetup Server: ✅\n"); - } catch (err) { - console.log(err); + onData?.("\nSetup Server: ✅\n"); + } catch (err) { + console.log(err); - await updateDeploymentStatus(deployment.deploymentId, "error"); - onData?.(`${err} ❌\n`); - } + await updateDeploymentStatus(deployment.deploymentId, "error"); + onData?.(`${err} ❌\n`); + } }; export const defaultCommand = () => { - const bashCommand = ` + const bashCommand = ` set -e; DOCKER_VERSION=27.0.3 OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') @@ -76,7 +76,7 @@ CURRENT_USER=$USER echo "Installing requirements for: OS: $OS_TYPE" if [ $EUID != 0 ]; then - echo "Please run this script as root or with sudo ❌" + echo "Please run this script as root or with sudo ❌" exit fi @@ -176,83 +176,83 @@ echo -e "13. Installing Railpack" ${installRailpack()} `; - return bashCommand; + return bashCommand; }; const installRequirements = async ( - serverId: string, - onData?: (data: any) => void, + serverId: string, + onData?: (data: any) => void ) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - onData?.("❌ No SSH Key found, please assign one to this server"); - throw new Error("No SSH Key found"); - } + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + onData?.("❌ No SSH Key found, please assign one to this server"); + throw new Error("No SSH Key found"); + } - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = server.command || defaultCommand(); - client.exec(command, (err, stream) => { - if (err) { - onData?.(err.message); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - onData?.(data.toString()); - }) - .stderr.on("data", (data) => { - onData?.(data.toString()); - }); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), - ); - } else { - onData?.(`SSH connection error: ${err.message} ${err.level}`); - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = server.command || defaultCommand(); + client.exec(command, (err, stream) => { + if (err) { + onData?.(err.message); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + onData?.(data.toString()); + }) + .stderr.on("data", (data) => { + onData?.(data.toString()); + }); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + onData?.( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` + ); + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` + ) + ); + } else { + onData?.(`SSH connection error: ${err.message} ${err.level}`); + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + }); + }); }; const setupDirectories = () => { - const { SSH_PATH } = paths(true); - const directories = Object.values(paths(true)); + const { SSH_PATH } = paths(true); + const directories = Object.values(paths(true)); - const createDirsCommand = directories - .map((dir) => `mkdir -p "${dir}"`) - .join(" && "); - const chmodCommand = `chmod 700 "${SSH_PATH}"`; + const createDirsCommand = directories + .map((dir) => `mkdir -p "${dir}"`) + .join(" && "); + const chmodCommand = `chmod 700 "${SSH_PATH}"`; - const command = ` + const command = ` ${createDirsCommand} ${chmodCommand} `; - return command; + return command; }; const setupMainDirectory = () => ` @@ -263,7 +263,7 @@ const setupMainDirectory = () => ` # Create the /etc/dokploy directory mkdir -p /etc/dokploy chmod 777 /etc/dokploy - + echo "Directory /etc/dokploy created ✅" fi `; @@ -276,16 +276,16 @@ export const setupSwarm = () => ` # Get IP address get_ip() { local ip="" - + # Try IPv4 with multiple services # First attempt: ifconfig.io ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) - + # Second attempt: icanhazip.com if [ -z "\$ip" ]; then ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) fi - + # Third attempt: ipecho.net if [ -z "\$ip" ]; then ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) @@ -295,12 +295,12 @@ export const setupSwarm = () => ` if [ -z "\$ip" ]; then # Try IPv6 with ifconfig.io ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) - + # Try IPv6 with icanhazip.com if [ -z "\$ip" ]; then ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) fi - + # Try IPv6 with ipecho.net if [ -z "\$ip" ]; then ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) @@ -502,9 +502,9 @@ fi `; const createTraefikConfig = () => { - const config = getDefaultServerTraefikConfig(); + const config = getDefaultServerTraefikConfig(); - const command = ` + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then chmod 600 "/etc/dokploy/traefik/dynamic/acme.json" fi @@ -515,19 +515,19 @@ const createTraefikConfig = () => { fi `; - return command; + return command; }; const createDefaultMiddlewares = () => { - const config = getDefaultMiddlewares(); - const command = ` + const config = getDefaultMiddlewares(); + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then echo "Middlewares config already exists ✅" else echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml fi `; - return command; + return command; }; export const installRClone = () => ` @@ -541,7 +541,7 @@ export const installRClone = () => ` `; export const createTraefikInstance = () => { - const command = ` + const command = ` # Check if dokpyloy-traefik exists if docker service inspect dokploy-traefik > /dev/null 2>&1; then echo "Migrating Traefik to Standalone..." @@ -549,7 +549,7 @@ export const createTraefikInstance = () => { sleep 8 echo "Traefik migrated to Standalone ✅" fi - + if docker inspect dokploy-traefik > /dev/null 2>&1; then echo "Traefik already exists ✅" else @@ -570,14 +570,14 @@ export const createTraefikInstance = () => { fi `; - return command; + return command; }; const installNixpacks = () => ` if command_exists nixpacks; then echo "Nixpacks already installed ✅" else - export NIXPACKS_VERSION=1.29.1 + export NIXPACKS_VERSION=1.35.0 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" echo "Nixpacks version $NIXPACKS_VERSION installed ✅" fi From 79d55d8d347f5f756eba8a33e37205b079aae529 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:17:18 +0000 Subject: [PATCH 64/84] [autofix.ci] apply automated fixes --- apps/dokploy/components/layouts/user-nav.tsx | 2 - packages/server/src/setup/server-setup.ts | 222 +++++++++---------- 2 files changed, 111 insertions(+), 113 deletions(-) diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 151bd364..05c601f6 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -133,8 +133,6 @@ export const UserNav = () => { Servers )} - - )} diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 4fb9910a..bb0d34d8 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -1,17 +1,17 @@ import path from "node:path"; import { paths } from "@dokploy/server/constants"; import { - createServerDeployment, - updateDeploymentStatus, + createServerDeployment, + updateDeploymentStatus, } from "@dokploy/server/services/deployment"; import { findServerById } from "@dokploy/server/services/server"; import { - TRAEFIK_HTTP3_PORT, - TRAEFIK_PORT, - TRAEFIK_SSL_PORT, - TRAEFIK_VERSION, - getDefaultMiddlewares, - getDefaultServerTraefikConfig, + TRAEFIK_HTTP3_PORT, + TRAEFIK_PORT, + TRAEFIK_SSL_PORT, + TRAEFIK_VERSION, + getDefaultMiddlewares, + getDefaultServerTraefikConfig, } from "@dokploy/server/setup/traefik-setup"; import { Client } from "ssh2"; import { recreateDirectory } from "../utils/filesystem/directory"; @@ -19,55 +19,55 @@ import { recreateDirectory } from "../utils/filesystem/directory"; import slug from "slugify"; export const slugify = (text: string | undefined) => { - if (!text) { - return ""; - } + if (!text) { + return ""; + } - const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); - return slug(cleanedText, { - lower: true, - trim: true, - strict: true, - }); + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); }; export const serverSetup = async ( - serverId: string, - onData?: (data: any) => void + serverId: string, + onData?: (data: any) => void, ) => { - const server = await findServerById(serverId); - const { LOGS_PATH } = paths(); + const server = await findServerById(serverId); + const { LOGS_PATH } = paths(); - const slugifyName = slugify(`server ${server.name}`); + const slugifyName = slugify(`server ${server.name}`); - const fullPath = path.join(LOGS_PATH, slugifyName); + const fullPath = path.join(LOGS_PATH, slugifyName); - await recreateDirectory(fullPath); + await recreateDirectory(fullPath); - const deployment = await createServerDeployment({ - serverId: server.serverId, - title: "Setup Server", - description: "Setup Server", - }); + const deployment = await createServerDeployment({ + serverId: server.serverId, + title: "Setup Server", + description: "Setup Server", + }); - try { - onData?.("\nInstalling Server Dependencies: ✅\n"); - await installRequirements(serverId, onData); + try { + onData?.("\nInstalling Server Dependencies: ✅\n"); + await installRequirements(serverId, onData); - await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateDeploymentStatus(deployment.deploymentId, "done"); - onData?.("\nSetup Server: ✅\n"); - } catch (err) { - console.log(err); + onData?.("\nSetup Server: ✅\n"); + } catch (err) { + console.log(err); - await updateDeploymentStatus(deployment.deploymentId, "error"); - onData?.(`${err} ❌\n`); - } + await updateDeploymentStatus(deployment.deploymentId, "error"); + onData?.(`${err} ❌\n`); + } }; export const defaultCommand = () => { - const bashCommand = ` + const bashCommand = ` set -e; DOCKER_VERSION=27.0.3 OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') @@ -176,83 +176,83 @@ echo -e "13. Installing Railpack" ${installRailpack()} `; - return bashCommand; + return bashCommand; }; const installRequirements = async ( - serverId: string, - onData?: (data: any) => void + serverId: string, + onData?: (data: any) => void, ) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - onData?.("❌ No SSH Key found, please assign one to this server"); - throw new Error("No SSH Key found"); - } + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + onData?.("❌ No SSH Key found, please assign one to this server"); + throw new Error("No SSH Key found"); + } - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = server.command || defaultCommand(); - client.exec(command, (err, stream) => { - if (err) { - onData?.(err.message); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - onData?.(data.toString()); - }) - .stderr.on("data", (data) => { - onData?.(data.toString()); - }); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` - ); - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}` - ) - ); - } else { - onData?.(`SSH connection error: ${err.message} ${err.level}`); - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = server.command || defaultCommand(); + client.exec(command, (err, stream) => { + if (err) { + onData?.(err.message); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + onData?.(data.toString()); + }) + .stderr.on("data", (data) => { + onData?.(data.toString()); + }); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + onData?.( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ); + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ), + ); + } else { + onData?.(`SSH connection error: ${err.message} ${err.level}`); + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + }); + }); }; const setupDirectories = () => { - const { SSH_PATH } = paths(true); - const directories = Object.values(paths(true)); + const { SSH_PATH } = paths(true); + const directories = Object.values(paths(true)); - const createDirsCommand = directories - .map((dir) => `mkdir -p "${dir}"`) - .join(" && "); - const chmodCommand = `chmod 700 "${SSH_PATH}"`; + const createDirsCommand = directories + .map((dir) => `mkdir -p "${dir}"`) + .join(" && "); + const chmodCommand = `chmod 700 "${SSH_PATH}"`; - const command = ` + const command = ` ${createDirsCommand} ${chmodCommand} `; - return command; + return command; }; const setupMainDirectory = () => ` @@ -502,9 +502,9 @@ fi `; const createTraefikConfig = () => { - const config = getDefaultServerTraefikConfig(); + const config = getDefaultServerTraefikConfig(); - const command = ` + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then chmod 600 "/etc/dokploy/traefik/dynamic/acme.json" fi @@ -515,19 +515,19 @@ const createTraefikConfig = () => { fi `; - return command; + return command; }; const createDefaultMiddlewares = () => { - const config = getDefaultMiddlewares(); - const command = ` + const config = getDefaultMiddlewares(); + const command = ` if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then echo "Middlewares config already exists ✅" else echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml fi `; - return command; + return command; }; export const installRClone = () => ` @@ -541,7 +541,7 @@ export const installRClone = () => ` `; export const createTraefikInstance = () => { - const command = ` + const command = ` # Check if dokpyloy-traefik exists if docker service inspect dokploy-traefik > /dev/null 2>&1; then echo "Migrating Traefik to Standalone..." @@ -570,7 +570,7 @@ export const createTraefikInstance = () => { fi `; - return command; + return command; }; const installNixpacks = () => ` From c3986d7a080628eb036b361999f3d2698437ee95 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 07:40:07 +0000 Subject: [PATCH 65/84] [autofix.ci] apply automated fixes --- .../templates/config.template.test.ts | 12 +- .../templates/helpers.template.test.ts | 121 +++++++++++------- packages/server/src/templates/index.ts | 32 +++-- packages/server/src/templates/processors.ts | 20 ++- 4 files changed, 116 insertions(+), 69 deletions(-) diff --git a/apps/dokploy/__test__/templates/config.template.test.ts b/apps/dokploy/__test__/templates/config.template.test.ts index 6f5baaf1..202abdf2 100644 --- a/apps/dokploy/__test__/templates/config.template.test.ts +++ b/apps/dokploy/__test__/templates/config.template.test.ts @@ -58,10 +58,10 @@ describe("processTemplate", () => { variables: { jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ", anon_payload: JSON.stringify({ - "role": "tester", - "iss": "dockploy", - "iat": "${timestamps:2025-01-01T00:00:00Z}", - "exp": "${timestamps:2030-01-01T00:00:00Z}", + role: "tester", + iss: "dockploy", + iat: "${timestamps:2025-01-01T00:00:00Z}", + exp: "${timestamps:2030-01-01T00:00:00Z}", }), anon_key: "${jwt:jwt_secret:anon_payload}", }, @@ -74,7 +74,9 @@ describe("processTemplate", () => { }; const result = processTemplate(template, mockSchema); expect(result.envs).toHaveLength(1); - expect(result.envs).toContain("ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY"); + expect(result.envs).toContain( + "ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY", + ); expect(result.mounts).toHaveLength(0); expect(result.domains).toHaveLength(0); }); diff --git a/apps/dokploy/__test__/templates/helpers.template.test.ts b/apps/dokploy/__test__/templates/helpers.template.test.ts index d6eb532c..1144b65f 100644 --- a/apps/dokploy/__test__/templates/helpers.template.test.ts +++ b/apps/dokploy/__test__/templates/helpers.template.test.ts @@ -2,7 +2,6 @@ import type { Schema } from "@dokploy/server/templates"; import { processValue } from "@dokploy/server/templates/processors"; import { describe, expect, it } from "vitest"; - describe("helpers functions", () => { // Mock schema for testing const mockSchema: Schema = { @@ -30,7 +29,11 @@ describe("helpers functions", () => { it("should generate a random domain", () => { const domain = processValue("${domain}", {}, mockSchema); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); - expect(domain.endsWith(`${mockSchema.serverIp.replaceAll(".","-")}.traefik.me`)).toBeTruthy(); + expect( + domain.endsWith( + `${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`, + ), + ).toBeTruthy(); }); }); @@ -46,11 +49,14 @@ describe("helpers functions", () => { [32, 44], [64, 88], [128, 172], - ])("should generate a base64 string from parameter %d bytes length", (length, finalLength) => { - const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); - expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); - expect(base64.length).toBe(finalLength); - }); + ])( + "should generate a base64 string from parameter %d bytes length", + (length, finalLength) => { + const base64 = processValue(`\${base64:${length}}`, {}, mockSchema); + expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/); + expect(base64.length).toBe(finalLength); + }, + ); }); describe("${password}", () => { @@ -58,11 +64,14 @@ describe("helpers functions", () => { const password = processValue("${password}", {}, mockSchema); expect(password).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a password string respecting parameter %d length", (length) => { - const password = processValue(`\${password:${length}}`, {}, mockSchema); - expect(password).toMatch(/^[A-Za-z0-9]+$/); - expect(password.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a password string respecting parameter %d length", + (length) => { + const password = processValue(`\${password:${length}}`, {}, mockSchema); + expect(password).toMatch(/^[A-Za-z0-9]+$/); + expect(password.length).toBe(length); + }, + ); }); describe("${hash}", () => { @@ -70,17 +79,22 @@ describe("helpers functions", () => { const hash = processValue("${hash}", {}, mockSchema); expect(hash).toMatch(/^[A-Za-z0-9]+$/); }); - it.each([6,8,12,16,32])("should generate a hash string respecting parameter %d length", (length) => { - const hash = processValue(`\${hash:${length}}`, {}, mockSchema); - expect(hash).toMatch(/^[A-Za-z0-9]+$/); - expect(hash.length).toBe(length); - }); + it.each([6, 8, 12, 16, 32])( + "should generate a hash string respecting parameter %d length", + (length) => { + const hash = processValue(`\${hash:${length}}`, {}, mockSchema); + expect(hash).toMatch(/^[A-Za-z0-9]+$/); + expect(hash.length).toBe(length); + }, + ); }); describe("${uuid}", () => { it("should generate a UUID string", () => { const uuid = processValue("${uuid}", {}, mockSchema); - expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); }); }); @@ -100,8 +114,12 @@ describe("helpers functions", () => { expect(timestamp.length).toBe(nowLength); }); it("should generate a timestamp string in milliseconds from parameter", () => { - const timestamp = processValue("${timestampms:2025-01-01}", {}, mockSchema); - expect(timestamp).toEqual('1735689600000'); + const timestamp = processValue( + "${timestampms:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamp).toEqual("1735689600000"); }); }); describe("${timestamps}", () => { @@ -112,8 +130,12 @@ describe("helpers functions", () => { expect(timestamps.length).toBe(nowLength); }); it("should generate a timestamp string in seconds from parameter", () => { - const timestamps = processValue("${timestamps:2025-01-01}", {}, mockSchema); - expect(timestamps).toEqual('1735689600'); + const timestamps = processValue( + "${timestamps:2025-01-01}", + {}, + mockSchema, + ); + expect(timestamps).toEqual("1735689600"); }); }); @@ -146,39 +168,50 @@ describe("helpers functions", () => { const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); - }); - it.each([6,8,12,16,32])("should generate a random hex string from parameter %d byte length", (length) => { - const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); - expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); - expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length - expect(jwt.length).toBeLessThanOrEqual(length * 2); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); + it.each([6, 8, 12, 16, 32])( + "should generate a random hex string from parameter %d byte length", + (length) => { + const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema); + expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/); + expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length + expect(jwt.length).toBeLessThanOrEqual(length * 2); + }, + ); }); describe("${jwt:secret}", () => { it("should generate a JWT string respecting parameter secret from variable", () => { - const jwt = processValue("${jwt:secret}", {secret: "mysecret"}, mockSchema); + const jwt = processValue( + "${jwt:secret}", + { secret: "mysecret" }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; const decodedPayload = jwtBase64Decode(parts[1]); jwtCheckHeader(parts[0]); - expect (decodedPayload).toHaveProperty("iat"); - expect (decodedPayload).toHaveProperty("iss"); - expect (decodedPayload).toHaveProperty("exp"); - expect (decodedPayload.iss).toEqual("dokploy"); + expect(decodedPayload).toHaveProperty("iat"); + expect(decodedPayload).toHaveProperty("iss"); + expect(decodedPayload).toHaveProperty("exp"); + expect(decodedPayload.iss).toEqual("dokploy"); }); }); describe("${jwt:secret:payload}", () => { it("should generate a JWT string respecting parameters secret and payload from variables", () => { const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000); const expiry = iat + 3600; - const jwt = processValue("${jwt:secret:payload}", { - secret: "mysecret", - payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, - }, mockSchema); + const jwt = processValue( + "${jwt:secret:payload}", + { + secret: "mysecret", + payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`, + }, + mockSchema, + ); expect(jwt).toMatch(jwtMatchExp); const parts = jwt.split(".") as JWTParts; jwtCheckHeader(parts[0]); @@ -191,7 +224,9 @@ describe("helpers functions", () => { expect(decodedPayload.exp).toEqual(expiry); expect(decodedPayload).toHaveProperty("customprop"); expect(decodedPayload.customprop).toEqual("customvalue"); - expect(jwt).toEqual("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI") + expect(jwt).toEqual( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI", + ); }); }); -}); \ No newline at end of file +}); diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 0d0f87ce..083b90bf 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -}; +} export type DomainSchema = Pick & { path?: string; @@ -22,7 +22,7 @@ export interface Template { content: string; }>; domains: DomainSchema[]; -}; +} export interface GenerateJWTOptions { length?: number; @@ -65,17 +65,16 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -}; +} function safeBase64(str: string): string { - return str - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); -}; + return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); +} function objToJWTBase64(obj: any): string { - return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); -}; + return safeBase64( + Buffer.from(JSON.stringify(obj), "utf8").toString("base64"), + ); +} export function generateJwt(options: GenerateJWTOptions = {}): string { let { length, secret, payload = {} } = options; @@ -88,16 +87,21 @@ export function generateJwt(options: GenerateJWTOptions = {}): string { }); payload.iss || (payload.iss = "dokploy"); payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); - payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + payload.exp || + (payload.exp = Math.floor( + new Date("2030-01-01T00:00:00Z").getTime() / 1000, + )); const encodedPayload = objToJWTBase64({ iat: Math.floor(Date.now() / 1000), exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), ...payload, }); secret || (secret = randomBytes(32).toString("hex")); - const signature = safeBase64(createHmac("SHA256", secret) - .update(`${encodedHeader}.${encodedPayload}`) - .digest("base64")); + const signature = safeBase64( + createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64"), + ); return `${encodedHeader}.${encodedPayload}.${signature}`; } diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index ff8fe277..5d9270aa 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -117,7 +117,9 @@ export function processValue( return new Date(varName.slice(12)).getTime().toString(); } if (varName.startsWith("timestamps:")) { - return Math.round(new Date(varName.slice(11)).getTime() / 1000).toString(); + return Math.round( + new Date(varName.slice(11)).getTime() / 1000, + ).toString(); } if (varName === "randomPort") { @@ -129,15 +131,19 @@ export function processValue( } if (varName.startsWith("jwt:")) { - const params:string[] = varName.split(":").slice(1); + const params: string[] = varName.split(":").slice(1); if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { - return generateJwt({length: Number.parseInt(params[0], 10)}); + return generateJwt({ length: Number.parseInt(params[0], 10) }); } let [secret, payload] = params; if (typeof payload === "string" && variables[payload]) { payload = variables[payload]; } - if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + if ( + typeof payload === "string" && + payload.startsWith("{") && + payload.endsWith("}") + ) { try { payload = JSON.parse(payload); } catch (e) { @@ -146,12 +152,12 @@ export function processValue( console.error("Invalid JWT payload", e); } } - if (typeof payload !== 'object') { + if (typeof payload !== "object") { payload = undefined; } return generateJwt({ - secret: secret ? (variables[secret] || secret) : undefined, - payload: payload as any + secret: secret ? variables[secret] || secret : undefined, + payload: payload as any, }); } From f49a67f8dfc4b0c3050b0ee7e5eecb1e878332c2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:50:26 -0600 Subject: [PATCH 66/84] refactor(jwt generation): Simplify payload property assignments and secret initialization --- packages/server/src/templates/index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 083b90bf..c42dd1b7 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -85,18 +85,23 @@ export function generateJwt(options: GenerateJWTOptions = {}): string { alg: "HS256", typ: "JWT", }); - payload.iss || (payload.iss = "dokploy"); - payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); - payload.exp || - (payload.exp = Math.floor( - new Date("2030-01-01T00:00:00Z").getTime() / 1000, - )); + if (!payload.iss) { + payload.iss = "dokploy"; + } + if (!payload.iat) { + payload.iat = Math.floor(Date.now() / 1000); + } + if (!payload.exp) { + payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000); + } const encodedPayload = objToJWTBase64({ iat: Math.floor(Date.now() / 1000), exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), ...payload, }); - secret || (secret = randomBytes(32).toString("hex")); + if (!secret) { + secret = randomBytes(32).toString("hex"); + } const signature = safeBase64( createHmac("SHA256", secret) .update(`${encodedHeader}.${encodedPayload}`) From 461d7c530ac40757a5ace7aa2b554e3f5613faa8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:07:50 -0600 Subject: [PATCH 67/84] fix(restore): streamline container ID retrieval for database operations Refactor the database restore process to consistently use a single container ID for the PostgreSQL container. This change enhances reliability by ensuring that commands are executed against the correct container, preventing potential errors from multiple matches. Co-authored-by: Merloss 54235902+Merloss@users.noreply.github.com --- .../server/src/utils/backups/web-server.ts | 17 +++++-------- .../server/src/utils/restore/web-server.ts | 24 +++++++++++++------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 1870a08c..2dea3d81 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -24,28 +24,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await execAsync(`mkdir -p ${tempDir}/filesystem`); // First get the container ID - // Returns: ID\nID\nID... const { stdout: containerId } = await execAsync( - "docker ps --filter 'name=dokploy-postgres' -q", + `docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`, ); if (!containerId) { throw new Error("PostgreSQL container not found"); } - // ID\nID\nID... => [ "ID", "ID", ... ] - const containers = containerId.trim().split("\n").filter(Boolean); + const postgresContainerId = containerId.trim(); - // Then run pg_dump with the container ID - for (const containerId of containers) { - // Maybe we can find a better identification for this part vvv - const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database-${containerId}.sql'`; - await execAsync(postgresCommand); - } + const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; + await execAsync(postgresCommand); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); - await execAsync( // Zip all .sql files since we created more than one + await execAsync( + // Zip all .sql files since we created more than one `cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`, ); diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 46aa9239..8397dcf2 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -83,44 +83,54 @@ export const restoreWebServerBackup = async ( throw new Error("Database file not found after extraction"); } + const { stdout: postgresContainer } = await execAsync( + `docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`, + ); + + if (!postgresContainer) { + throw new Error("Dokploy Postgres container not found"); + } + + const postgresContainerId = postgresContainer.trim(); + // Drop and recreate database emit("Disconnecting all users from database..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`, + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`, ); emit("Dropping existing database..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`, + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`, ); emit("Creating fresh database..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`, + `docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`, ); // Copy the backup file into the container emit("Copying backup file into container..."); await execAsync( - `docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`, + `docker cp ${tempDir}/database.sql ${postgresContainerId}:/tmp/database.sql`, ); // Verify file in container emit("Verifying file in container..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`, + `docker exec ${postgresContainerId} ls -l /tmp/database.sql`, ); // Restore from the copied file emit("Running database restore..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /tmp/database.sql`, + `docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`, ); // Cleanup the temporary file in the container emit("Cleaning up container temp file..."); await execAsync( - `docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`, + `docker exec ${postgresContainerId} rm /tmp/database.sql`, ); emit("Restore completed successfully!"); From ceb16ae9f7b4bb5acf4cae771b785d6256409a90 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:35:02 -0600 Subject: [PATCH 68/84] Implement enableSubmodules feature across various Git provider components and update database schema. This change introduces a new boolean field `enableSubmodules` to control submodule behavior in Git operations, replacing the previous `recurseSubmodules` field. Updates include modifications to the UI components, API routers, and database schema to accommodate this new feature. --- .../general/generic/save-git-provider.tsx | 20 +- .../general/generic/save-github-provider.tsx | 20 +- .../general/generic/save-gitlab-provider.tsx | 19 +- .../save-bitbucket-provider-compose.tsx | 20 + .../generic/save-git-provider-compose.tsx | 20 + .../generic/save-gitea-provider-compose.tsx | 20 + .../generic/save-github-provider-compose.tsx | 20 + .../generic/save-gitlab-provider-compose.tsx | 20 + .../drizzle/0085_equal_captain_stacy.sql | 2 + .../drizzle/0086_rainy_gertrude_yorkes.sql | 2 + apps/dokploy/drizzle/meta/0085_snapshot.json | 5383 +++++++++++++++++ apps/dokploy/drizzle/meta/0086_snapshot.json | 5383 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 14 + .../dokploy/server/api/routers/application.ts | 5 + packages/server/src/db/schema/application.ts | 6 + packages/server/src/db/schema/compose.ts | 1 + packages/server/src/services/compose.ts | 1 + .../server/src/utils/providers/bitbucket.ts | 22 +- packages/server/src/utils/providers/git.ts | 28 +- packages/server/src/utils/providers/gitea.ts | 28 +- packages/server/src/utils/providers/github.ts | 42 +- packages/server/src/utils/providers/gitlab.ts | 21 +- 22 files changed, 11000 insertions(+), 97 deletions(-) create mode 100644 apps/dokploy/drizzle/0085_equal_captain_stacy.sql create mode 100644 apps/dokploy/drizzle/0086_rainy_gertrude_yorkes.sql create mode 100644 apps/dokploy/drizzle/meta/0085_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0086_snapshot.json diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index 00d28395..ef2dd3da 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -23,6 +23,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { KeyRoundIcon, LockIcon, X } from "lucide-react"; @@ -44,7 +45,7 @@ const GitProviderSchema = z.object({ branch: z.string().min(1, "Branch required"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GitProvider = z.infer; @@ -68,7 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { repositoryURL: "", sshKey: undefined, watchPaths: [], - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GitProviderSchema), }); @@ -81,7 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { buildPath: data.customGitBuildPath || "/", repositoryURL: data.customGitUrl || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -94,7 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, applicationId, watchPaths: values.watchPaths || [], - recurseSubmodules: values.recurseSubmodules, + enableSubmodules: values.enableSubmodules, }) .then(async () => { toast.success("Git Provider Saved"); @@ -298,20 +299,19 @@ export const SaveGitProvider = ({ applicationId }: Props) => { )} /> + ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 7637f596..4befb215 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -30,6 +30,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -57,7 +58,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GithubProvider = z.infer; @@ -82,7 +83,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { }, githubId: "", branch: "", - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GithubProviderSchema), }); @@ -126,7 +127,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath || "/", githubId: data.githubId || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -140,7 +141,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { buildPath: data.buildPath, githubId: data.githubId, watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -462,20 +463,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { )} /> + ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index 996b1dca..b4b55d3f 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -60,7 +61,7 @@ const GitlabProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), - recurseSubmodules: z.boolean().default(true), + enableSubmodules: z.boolean().default(false), }); type GitlabProvider = z.infer; @@ -87,7 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { }, gitlabId: "", branch: "", - recurseSubmodules: true, + enableSubmodules: false, }, resolver: zodResolver(GitlabProviderSchema), }); @@ -137,7 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { buildPath: data.gitlabBuildPath || "/", gitlabId: data.gitlabId || "", watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules ?? true, + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -153,7 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { gitlabProjectId: data.repository.id, gitlabPathNamespace: data.repository.gitlabPathNamespace, watchPaths: data.watchPaths || [], - recurseSubmodules: data.recurseSubmodules, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -489,18 +490,16 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { /> ( - - Recurse Submodules + Enable Submodules )} /> diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index ff329a0a..353ccc6c 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type BitbucketProvider = z.infer; @@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { bitbucketId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(BitbucketProviderSchema), }); @@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { composePath: data.composePath, bitbucketId: data.bitbucketId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { sourceType: "bitbucket", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx index 201f9da2..6f9b50da 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -31,6 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -59,6 +60,7 @@ const GiteaProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type GiteaProvider = z.infer; @@ -83,6 +85,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { giteaId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(GiteaProviderSchema), }); @@ -136,6 +139,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { composePath: data.composePath || "./docker-compose.yml", giteaId: data.giteaId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -151,6 +155,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { sourceType: "gitea", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, } as any) .then(async () => { toast.success("Service Provider Saved"); @@ -469,6 +474,21 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 4f4c1d5a..6e9b0a03 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -30,6 +30,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, @@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({ branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), + enableSubmodules: z.boolean().default(false), }); type GithubProvider = z.infer; @@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { githubId: "", branch: "", watchPaths: [], + enableSubmodules: false, }, resolver: zodResolver(GithubProviderSchema), }); @@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { composePath: data.composePath, githubId: data.githubId || "", watchPaths: data.watchPaths || [], + enableSubmodules: data.enableSubmodules ?? false, }); } }, [form.reset, data, form]); @@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { sourceType: "github", composeStatus: "idle", watchPaths: data.watchPaths, + enableSubmodules: data.enableSubmodules, }) .then(async () => { toast.success("Service Provided Saved"); @@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { )} /> + ( + + + + + Enable Submodules + + )} + />
-
- - - )} - /> + > + + +
+ + + )} + /> + )} { const repository = form.watch("repository"); const githubId = form.watch("githubId"); - + const triggerType = form.watch("triggerType"); const { data: repositories, isLoading: isLoadingRepositories } = api.github.getGithubRepositories.useQuery( { @@ -431,84 +431,90 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { )} /> - ( - -
- Watch Paths - - - -
- ? -
-
- -

- Add paths to watch for changes. When files in these - paths change, a new deployment will be triggered. -

-
-
-
-
-
- {field.value?.map((path, index) => ( - - {path} - { - const newPaths = [...(field.value || [])]; - newPaths.splice(index, 1); - form.setValue("watchPaths", newPaths); + {triggerType === "push" && ( + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in + these paths change, a new deployment will be + triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [ + ...(field.value || []), + value, + ]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } }} /> - - ))} -
- -
- { - if (e.key === "Enter") { - e.preventDefault(); - const input = e.currentTarget; + -
-
- -
- )} - /> + }} + > + Add + +
+ + +
+ )} + /> + )} Date: Sat, 26 Apr 2025 23:23:51 -0600 Subject: [PATCH 81/84] Enhance backup restoration UI and API by adding file size formatting, improving search debounce timing, and updating file listing to include additional metadata. Refactor file handling to ensure proper path resolution and error handling during JSON parsing. --- .../database/backups/restore-backup.tsx | 69 ++++++++++++++----- apps/dokploy/server/api/routers/backup.ts | 41 +++++++++-- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 797e1ca8..69d37935 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -77,6 +77,14 @@ const RestoreBackupSchema = z.object({ type RestoreBackup = z.infer; +const formatBytes = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; +}; + export const RestoreBackup = ({ databaseId, databaseType, @@ -101,7 +109,7 @@ export const RestoreBackup = ({ const debouncedSetSearch = debounce((value: string) => { setDebouncedSearchTerm(value); - }, 150); + }, 350); const handleSearchChange = (value: string) => { setSearch(value); @@ -271,7 +279,7 @@ export const RestoreBackup = ({ )} - +