From 05a75edbecf92dd5209f698724a46ed184c88d2b Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Tue, 21 Jan 2025 19:03:35 +1100 Subject: [PATCH 01/17] feat(template): added apache superset (unofficial) --- apps/dokploy/public/templates/superset.svg | 9 +++ .../templates/superset/docker-compose.yml | 79 +++++++++++++++++++ apps/dokploy/templates/superset/index.ts | 38 +++++++++ apps/dokploy/templates/templates.ts | 14 ++++ 4 files changed, 140 insertions(+) create mode 100644 apps/dokploy/public/templates/superset.svg create mode 100644 apps/dokploy/templates/superset/docker-compose.yml create mode 100644 apps/dokploy/templates/superset/index.ts diff --git a/apps/dokploy/public/templates/superset.svg b/apps/dokploy/public/templates/superset.svg new file mode 100644 index 00000000..522c3b28 --- /dev/null +++ b/apps/dokploy/public/templates/superset.svg @@ -0,0 +1,9 @@ + + + Superset + + + + + + diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml new file mode 100644 index 00000000..c95dec6e --- /dev/null +++ b/apps/dokploy/templates/superset/docker-compose.yml @@ -0,0 +1,79 @@ +# Note: this is an UNOFFICIAL production docker image build for Superset: +# - https://github.com/amancevice/docker-superset +# +# Before deploying, you must mount your `superset_config.py` file to +# the superset container. An example config is: +# +# ```python +# import os +# +# SECRET_KEY = os.getenv("SECRET_KEY") +# MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "") +# +# CACHE_CONFIG = { +# "CACHE_TYPE": "RedisCache", +# "CACHE_DEFAULT_TIMEOUT": 300, +# "CACHE_KEY_PREFIX": "superset_", +# "CACHE_REDIS_HOST": "redis", +# "CACHE_REDIS_PORT": 6379, +# "CACHE_REDIS_DB": 1, +# "CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1", +# } +# +# FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"} +# EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"} +# +# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}" +# SQLALCHEMY_TRACK_MODIFICATIONS = True +# ``` +# +# After deploying this image, you will need to run one of the two +# commands below in a terminal within the superset container: +# $ superset-demo # Initialise database + load demo charts/datasets +# $ superset-init # Initialise database only +# +# You will be prompted to enter the credentials for the admin user. + +services: + superset: + image: amancevice/superset + restart: always + depends_on: + - db + - redis + environment: + SECRET_KEY: ${SECRET_KEY} + MAPBOX_API_KEY: ${MAPBOX_API_KEY} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + # NOTE: ensure `/opt/superset/superset_config.py` exists on your + # host machine (or change the path as appropriate) + - /opt/superset/superset_config.py:/etc/superset/superset_config.py + + db: + image: postgres + restart: always + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres:/var/lib/postgresql/data + networks: + - dokploy-network + + redis: + image: redis + restart: always + volumes: + - redis:/data + command: redis-server --requirepass ${REDIS_PASSWORD} + networks: + - dokploy-network + +volumes: + postgres: + redis: diff --git a/apps/dokploy/templates/superset/index.ts b/apps/dokploy/templates/superset/index.ts new file mode 100644 index 00000000..84e6dd0c --- /dev/null +++ b/apps/dokploy/templates/superset/index.ts @@ -0,0 +1,38 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mapboxApiKey = ""; + const secretKey = generatePassword(30); + const postgresDb = "superset"; + const postgresUser = "superset"; + const postgresPassword = generatePassword(30); + const redisPassword = generatePassword(30); + + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 8088, + serviceName: "superset", + }, + ]; + + const envs = [ + `SECRET_KEY=${secretKey}`, + `MAPBOX_API_KEY=${mapboxApiKey}`, + `POSTGRES_DB=${postgresDb}`, + `POSTGRES_USER=${postgresUser}`, + `POSTGRES_PASSWORD=${postgresPassword}`, + `REDIS_PASSWORD=${redisPassword}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 9531eb7a..a885f906 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1298,4 +1298,18 @@ export const templates: TemplateData[] = [ tags: ["developer", "tools"], load: () => import("./it-tools/index").then((m) => m.generate), }, + { + id: "superset", + name: "Superset (Unofficial)", + version: "latest", + description: "Data visualization and data exploration platform.", + logo: "superset.svg", + links: { + github: "https://github.com/amancevice/docker-superset", + website: "https://superset.apache.org", + docs: "https://superset.apache.org/docs/intro", + }, + tags: ["developer", "tools"], + load: () => import("./superset/index").then((m) => m.generate), + }, ]; From 444121f8d8b2798ec4d199ddb504042de0ce3aa8 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Tue, 21 Jan 2025 20:40:46 +1100 Subject: [PATCH 02/17] fix(template): more appropriate tags for superset --- apps/dokploy/templates/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index a885f906..9ea88a8d 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1309,7 +1309,7 @@ export const templates: TemplateData[] = [ website: "https://superset.apache.org", docs: "https://superset.apache.org/docs/intro", }, - tags: ["developer", "tools"], + tags: ["bi", "dashboard", "database", "sql"], load: () => import("./superset/index").then((m) => m.generate), }, ]; From 1a44a0ea5a73ec0eaed6d13712ef4cffa4df8ca2 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Tue, 21 Jan 2025 23:14:28 +1100 Subject: [PATCH 03/17] refactor(template): use dokploy mount volume for superset_config.py --- .../templates/superset/docker-compose.yml | 31 ++----------------- apps/dokploy/templates/superset/index.ts | 29 +++++++++++++++++ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml index c95dec6e..e0f17913 100644 --- a/apps/dokploy/templates/superset/docker-compose.yml +++ b/apps/dokploy/templates/superset/docker-compose.yml @@ -1,32 +1,6 @@ # Note: this is an UNOFFICIAL production docker image build for Superset: # - https://github.com/amancevice/docker-superset # -# Before deploying, you must mount your `superset_config.py` file to -# the superset container. An example config is: -# -# ```python -# import os -# -# SECRET_KEY = os.getenv("SECRET_KEY") -# MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "") -# -# CACHE_CONFIG = { -# "CACHE_TYPE": "RedisCache", -# "CACHE_DEFAULT_TIMEOUT": 300, -# "CACHE_KEY_PREFIX": "superset_", -# "CACHE_REDIS_HOST": "redis", -# "CACHE_REDIS_PORT": 6379, -# "CACHE_REDIS_DB": 1, -# "CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1", -# } -# -# FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"} -# EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"} -# -# SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}" -# SQLALCHEMY_TRACK_MODIFICATIONS = True -# ``` -# # After deploying this image, you will need to run one of the two # commands below in a terminal within the superset container: # $ superset-demo # Initialise database + load demo charts/datasets @@ -49,9 +23,8 @@ services: POSTGRES_DB: ${POSTGRES_DB} REDIS_PASSWORD: ${REDIS_PASSWORD} volumes: - # NOTE: ensure `/opt/superset/superset_config.py` exists on your - # host machine (or change the path as appropriate) - - /opt/superset/superset_config.py:/etc/superset/superset_config.py + # Note: superset_config.py can be edited in Dokploy's UI Volume Mount + - ../files/superset/superset_config.py:/etc/superset/superset_config.py db: image: postgres diff --git a/apps/dokploy/templates/superset/index.ts b/apps/dokploy/templates/superset/index.ts index 84e6dd0c..6132f978 100644 --- a/apps/dokploy/templates/superset/index.ts +++ b/apps/dokploy/templates/superset/index.ts @@ -31,8 +31,37 @@ export function generate(schema: Schema): Template { `REDIS_PASSWORD=${redisPassword}`, ]; + const mounts: Template["mounts"] = [ + { + filePath: "./superset/superset_config.py", + content: ` +import os + +SECRET_KEY = os.getenv("SECRET_KEY") +MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "") + +CACHE_CONFIG = { + "CACHE_TYPE": "RedisCache", + "CACHE_DEFAULT_TIMEOUT": 300, + "CACHE_KEY_PREFIX": "superset_", + "CACHE_REDIS_HOST": "redis", + "CACHE_REDIS_PORT": 6379, + "CACHE_REDIS_DB": 1, + "CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1", +} + +FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"} +EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"} + +SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}" +SQLALCHEMY_TRACK_MODIFICATIONS = True + `.trim(), + }, + ]; + return { envs, domains, + mounts, }; } From ce06cd42b335ad12d4f013520d22392c06079a94 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Tue, 21 Jan 2025 23:41:05 +1100 Subject: [PATCH 04/17] fix(ui): show filePath instead of mountPath for file mounts --- .../advanced/volumes/show-volumes.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 9575c59c..c84ed594 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -98,12 +98,20 @@ export const ShowVolumes = ({ id, type }: Props) => { )} {mount.type === "file" && ( -
- Content - - {mount.content} - -
+ <> +
+ Content + + {mount.content} + +
+
+ File Path + + {mount.filePath} + +
+ )} {mount.type === "bind" && (
@@ -113,12 +121,14 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} -
- Mount Path - - {mount.mountPath} - -
+ {mount.type !== "file" && ( +
+ Mount Path + + {mount.mountPath} + +
+ )}
Date: Tue, 21 Jan 2025 23:45:54 +1100 Subject: [PATCH 05/17] refactor(ui): clearer ui display condition for volume mount display --- .../advanced/volumes/show-volumes.tsx | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index c84ed594..bbfe4fa6 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -98,20 +98,12 @@ export const ShowVolumes = ({ id, type }: Props) => { )} {mount.type === "file" && ( - <> -
- Content - - {mount.content} - -
-
- File Path - - {mount.filePath} - -
- +
+ Content + + {mount.content} + +
)} {mount.type === "bind" && (
@@ -121,7 +113,14 @@ export const ShowVolumes = ({ id, type }: Props) => {
)} - {mount.type !== "file" && ( + {mount.type === "file" ? ( +
+ File Path + + {mount.filePath} + +
+ ) : (
Mount Path From 1d86f1a0fcc212fe8fb1b77bc364f5ef2b9b0075 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Wed, 22 Jan 2025 00:05:55 +1100 Subject: [PATCH 06/17] fix(template): added analytics tag to superset --- apps/dokploy/templates/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 9ea88a8d..4cd167a5 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1309,7 +1309,7 @@ export const templates: TemplateData[] = [ website: "https://superset.apache.org", docs: "https://superset.apache.org/docs/intro", }, - tags: ["bi", "dashboard", "database", "sql"], + tags: ["analytics", "bi", "dashboard", "database", "sql"], load: () => import("./superset/index").then((m) => m.generate), }, ]; From 6d90e268f78f5f6faa78791360073eeb1de54bec Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Wed, 22 Jan 2025 01:09:44 +1100 Subject: [PATCH 07/17] fix(ui): volume file mount content, line clamp and preserve whitespace --- .../dashboard/application/advanced/volumes/show-volumes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx index 9575c59c..9bbc3944 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/show-volumes.tsx @@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => { {mount.type === "file" && (
Content - + {mount.content}
From 692f8830649cd4286d66ac2ccd6169504b9828b8 Mon Sep 17 00:00:00 2001 From: Vladyslav G Date: Tue, 21 Jan 2025 16:21:29 +0100 Subject: [PATCH 08/17] style(i18n) add ukrainian language --- apps/dokploy/lib/languages.ts | 1 + apps/dokploy/public/locales/uk/common.json | 1 + apps/dokploy/public/locales/uk/settings.json | 58 ++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 apps/dokploy/public/locales/uk/common.json create mode 100644 apps/dokploy/public/locales/uk/settings.json diff --git a/apps/dokploy/lib/languages.ts b/apps/dokploy/lib/languages.ts index f83b3de6..883570bd 100644 --- a/apps/dokploy/lib/languages.ts +++ b/apps/dokploy/lib/languages.ts @@ -1,6 +1,7 @@ export const Languages = { english: { code: "en", name: "English" }, polish: { code: "pl", name: "Polski" }, + ukrainian: {code: 'uk', name: "Українська"}, russian: { code: "ru", name: "Русский" }, french: { code: "fr", name: "Français" }, german: { code: "de", name: "Deutsch" }, diff --git a/apps/dokploy/public/locales/uk/common.json b/apps/dokploy/public/locales/uk/common.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/dokploy/public/locales/uk/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/uk/settings.json b/apps/dokploy/public/locales/uk/settings.json new file mode 100644 index 00000000..d917f326 --- /dev/null +++ b/apps/dokploy/public/locales/uk/settings.json @@ -0,0 +1,58 @@ +{ + "settings.common.save": "Зберегти", + "settings.common.enterTerminal": "Увійти в термінал", + "settings.server.domain.title": "Домен сервера", + "settings.server.domain.description": "Додайте домен до вашого серверного застосунку.", + "settings.server.domain.form.domain": "Домен", + "settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt", + "settings.server.domain.form.certificate.label": "Постачальник сертифікатів", + "settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат", + "settings.server.domain.form.certificateOptions.none": "Відсутній", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", + + "settings.server.webServer.title": "Веб-сервер", + "settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.", + "settings.server.webServer.actions": "Дії", + "settings.server.webServer.reload": "Перезавантажити", + "settings.server.webServer.watchLogs": "Перегляд логів", + "settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера", + "settings.server.webServer.server.label": "Сервер", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "Змінити середовище", + "settings.server.webServer.traefik.managePorts": "Додаткові порти", + "settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik", + "settings.server.webServer.traefik.targetPort": "Цільовий порт", + "settings.server.webServer.traefik.publishedPort": "Опублікований порт", + "settings.server.webServer.traefik.addPort": "Додати порт", + "settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено", + "settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти", + "settings.server.webServer.traefik.publishMode": "Режим публікації", + "settings.server.webServer.storage.label": "Дисковий простір", + "settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи", + "settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи", + "settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери", + "settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему", + "settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг", + "settings.server.webServer.storage.cleanAll": "Очистити все", + + "settings.profile.title": "Обліковий запис", + "settings.profile.description": "Змініть дані вашого профілю.", + "settings.profile.email": "Електронна пошта", + "settings.profile.password": "Пароль", + "settings.profile.avatar": "Аватар", + + "settings.appearance.title": "Зовнішній вигляд", + "settings.appearance.description": "Налаштуйте тему вашої панелі керування.", + "settings.appearance.theme": "Тема", + "settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування", + "settings.appearance.themes.light": "Світла", + "settings.appearance.themes.dark": "Темна", + "settings.appearance.themes.system": "Системна", + "settings.appearance.language": "Мова", + "settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування", + + "settings.terminal.connectionSettings": "Налаштування з'єднання", + "settings.terminal.ipAddress": "IP-адреса", + "settings.terminal.port": "Порт", + "settings.terminal.username": "Ім'я користувача" +} \ No newline at end of file From 52dbc0d8f1a8a4ab69b86e868308d7a1ddf0d78b Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 22 Jan 2025 09:14:21 +1100 Subject: [PATCH 09/17] fix(template): superset healthchecks --- apps/dokploy/templates/superset/docker-compose.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/dokploy/templates/superset/docker-compose.yml b/apps/dokploy/templates/superset/docker-compose.yml index e0f17913..1766b86b 100644 --- a/apps/dokploy/templates/superset/docker-compose.yml +++ b/apps/dokploy/templates/superset/docker-compose.yml @@ -35,6 +35,11 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 networks: - dokploy-network @@ -44,6 +49,11 @@ services: volumes: - redis:/data command: redis-server --requirepass ${REDIS_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 networks: - dokploy-network From 026e1bece679305f9ea0f06e47cb1df4268ecb29 Mon Sep 17 00:00:00 2001 From: Rahadi Jalu Date: Wed, 22 Jan 2025 11:22:30 +0700 Subject: [PATCH 10/17] fix: filter navigation items based on user's permissions and role --- apps/dokploy/components/layouts/side.tsx | 770 +++++++++++++---------- 1 file changed, 442 insertions(+), 328 deletions(-) diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index b2f87e41..c711b862 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -1,7 +1,6 @@ "use client"; import { Activity, - AudioWaveform, BarChartHorizontalBigIcon, Bell, BlocksIcon, @@ -9,7 +8,6 @@ import { Boxes, ChevronRight, CircleHelp, - Command, CreditCard, Database, Folder, @@ -27,8 +25,8 @@ import { Users, } from "lucide-react"; import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; import type * as React from "react"; +import { useEffect, useState } from "react"; import { Breadcrumb, @@ -65,243 +63,290 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; +import type { AppRouter } from "@/server/api/root"; import { api } from "@/utils/api"; +import type { inferRouterOutputs } from "@trpc/server"; import Link from "next/link"; import { useRouter } from "next/router"; import { Logo } from "../shared/logo"; import { UpdateServerButton } from "./update-server"; import { UserNav } from "./user-nav"; -// This is sample data. -interface NavItem { + +// The types of the queries we are going to use +type AuthQueryOutput = inferRouterOutputs["auth"]["get"]; +type UserQueryOutput = inferRouterOutputs["user"]["byAuthId"]; + +type SingleNavItem = { + isSingle?: true; title: string; url: string; - icon: LucideIcon; - isSingle: boolean; - isActive: boolean; - items?: { - title: string; - url: string; - icon?: LucideIcon; - }[]; -} + icon?: LucideIcon; + isEnabled?: (opts: { + auth?: AuthQueryOutput; + user?: UserQueryOutput; + isCloud: boolean; + }) => boolean; +}; -interface ExternalLink { +// NavItem type +// Consists of a single item or a group of items +// If `isSingle` is true or undefined, the item is a single item +// If `isSingle` is false, the item is a group of items +type NavItem = + | SingleNavItem + | { + isSingle: false; + title: string; + icon: LucideIcon; + items: SingleNavItem[]; + isEnabled?: (opts: { + auth?: AuthQueryOutput; + user?: UserQueryOutput; + isCloud: boolean; + }) => boolean; + }; + +// ExternalLink type +// Represents an external link item (used for the help section) +type ExternalLink = { name: string; url: string; icon: React.ComponentType<{ className?: string }>; -} + isEnabled?: (opts: { + auth?: AuthQueryOutput; + user?: UserQueryOutput; + isCloud: boolean; + }) => boolean; +}; -const data = { - user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", - }, - teams: [ - { - name: "Dokploy", - logo: Logo, - plan: "Enterprise", - }, - { - name: "Acme Corp.", - logo: AudioWaveform, - plan: "Startup", - }, - { - name: "Evil Corp.", - logo: Command, - plan: "Free", - }, - ], +// Menu type +// Consists of home, settings, and help items +type Menu = { + home: NavItem[]; + settings: NavItem[]; + help: ExternalLink[]; +}; + +// Menu items +// Consists of unfiltered home, settings, and help items +// The items are filtered based on the user's role and permissions +// The `isEnabled` function is called to determine if the item should be displayed +const MENU: Menu = { home: [ { + isSingle: true, title: "Projects", url: "/dashboard/projects", icon: Folder, - isSingle: true, - isActive: false, }, { + isSingle: true, title: "Monitoring", url: "/dashboard/monitoring", icon: BarChartHorizontalBigIcon, - isSingle: true, - isActive: false, + // Only enabled in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => !isCloud, }, { + isSingle: true, title: "Traefik File System", url: "/dashboard/traefik", icon: GalleryVerticalEnd, - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to Traefik files in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!( + (auth?.rol === "admin" || user?.canAccessToTraefikFiles) && + !isCloud + ), }, { + isSingle: true, title: "Docker", url: "/dashboard/docker", icon: BlocksIcon, - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud), }, { + isSingle: true, title: "Swarm", url: "/dashboard/swarm", icon: PieChart, - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud), }, { + isSingle: true, title: "Requests", url: "/dashboard/requests", icon: Forward, - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud), }, + // Legacy unused menu, adjusted to the new structure // { + // isSingle: true, // title: "Projects", // url: "/dashboard/projects", // icon: Folder, - // isSingle: true, // }, // { + // isSingle: true, // title: "Monitoring", // icon: BarChartHorizontalBigIcon, // url: "/dashboard/settings/monitoring", - // isSingle: true, // }, - // { - // title: "Settings", - // url: "#", - // icon: Settings2, - // isActive: true, - // items: [ - // { - // title: "Profile", - // url: "/dashboard/settings/profile", - // }, - // { - // title: "Users", - // url: "/dashboard/settings/users", - // }, - // { - // title: "SSH Key", - // url: "/dashboard/settings/ssh-keys", - // }, - // { - // title: "Git", - // url: "/dashboard/settings/git-providers", - // }, - // ], + // isSingle: false, + // title: "Settings", + // icon: Settings2, + // items: [ + // { + // title: "Profile", + // url: "/dashboard/settings/profile", + // }, + // { + // title: "Users", + // url: "/dashboard/settings/users", + // }, + // { + // title: "SSH Key", + // url: "/dashboard/settings/ssh-keys", + // }, + // { + // title: "Git", + // url: "/dashboard/settings/git-providers", + // }, + // ], // }, - // { - // title: "Integrations", - // icon: BlocksIcon, - // items: [ - // { - // title: "S3 Destinations", - // url: "/dashboard/settings/destinations", - // }, - // { - // title: "Registry", - // url: "/dashboard/settings/registry", - // }, - // { - // title: "Notifications", - // url: "/dashboard/settings/notifications", - // }, - // ], - ] as NavItem[], + // isSingle: false, + // title: "Integrations", + // icon: BlocksIcon, + // items: [ + // { + // title: "S3 Destinations", + // url: "/dashboard/settings/destinations", + // }, + // { + // title: "Registry", + // url: "/dashboard/settings/registry", + // }, + // { + // title: "Notifications", + // url: "/dashboard/settings/notifications", + // }, + // ], + // }, + ], + settings: [ { + isSingle: true, title: "Server", url: "/dashboard/settings/server", icon: Activity, - isSingle: true, - isActive: false, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!(auth?.rol === "admin" && !isCloud), }, { + isSingle: true, title: "Profile", url: "/dashboard/settings/profile", icon: User, - isSingle: true, - isActive: false, }, { + isSingle: true, title: "Servers", url: "/dashboard/settings/servers", icon: Server, - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "Users", icon: Users, url: "/dashboard/settings/users", - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "SSH Keys", icon: KeyRound, url: "/dashboard/settings/ssh-keys", - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to SSH keys + isEnabled: ({ auth, user }) => + !!(auth?.rol === "admin" || user?.canAccessToSSHKeys), }, - { + isSingle: true, title: "Git", url: "/dashboard/settings/git-providers", icon: GitBranch, - isSingle: true, - isActive: false, + // Only enabled for admins and users with access to Git providers + isEnabled: ({ auth, user }) => + !!(auth?.rol === "admin" || user?.canAccessToGitProviders), }, { + isSingle: true, title: "Registry", url: "/dashboard/settings/registry", icon: Package, - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "S3 Destinations", url: "/dashboard/settings/destinations", icon: Database, - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "Certificates", url: "/dashboard/settings/certificates", icon: ShieldCheck, - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "Cluster", url: "/dashboard/settings/cluster", icon: Boxes, - isSingle: true, - isActive: false, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!(auth?.rol === "admin" && !isCloud), }, { + isSingle: true, title: "Notifications", url: "/dashboard/settings/notifications", icon: Bell, - isSingle: true, - isActive: false, + // Only enabled for admins + isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"), }, { + isSingle: true, title: "Billing", url: "/dashboard/settings/billing", icon: CreditCard, - isSingle: true, - isActive: false, + // Only enabled for admins in cloud environments + isEnabled: ({ auth, user, isCloud }) => + !!(auth?.rol === "admin" && isCloud), }, - ] as NavItem[], + ], + help: [ { name: "Documentation", @@ -325,8 +370,108 @@ const data = { /> ), }, - ] as ExternalLink[], -}; + ], +} as const; + +/** + * Creates a menu based on the current user's role and permissions + * @returns a menu object with the home, settings, and help items + */ +function createMenuForAuthUser(opts: { + auth?: AuthQueryOutput; + user?: UserQueryOutput; + isCloud: boolean; +}): Menu { + return { + // Filter the home items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + home: MENU.home.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + user: opts.user, + isCloud: opts.isCloud, + }), + ), + // Filter the settings items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + settings: MENU.settings.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + user: opts.user, + isCloud: opts.isCloud, + }), + ), + // Filter the help items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + help: MENU.help.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + user: opts.user, + isCloud: opts.isCloud, + }), + ), + }; +} + +/** + * Determines if an item url is active based on the current pathname + * @returns true if the item url is active, false otherwise + */ +function isActiveRoute(opts: { + /** The url of the item. Usually obtained from `item.url` */ + itemUrl: string; + /** The current pathname. Usually obtained from `usePathname()` */ + pathname: string; +}): boolean { + const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); + const normalizedPathname = opts.pathname?.replace("/projects", "/project"); + + if (!normalizedPathname) return false; + + if (normalizedPathname === normalizedItemUrl) return true; + + if (normalizedPathname.startsWith(normalizedItemUrl)) { + const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); + return nextChar === "/"; + } + + return false; +} + +/** + * Finds the active nav item based on the current pathname + * @returns the active nav item with `SingleNavItem` type or undefined if none is active + */ +function findActiveNavItem( + navItems: NavItem[], + pathname: string, +): SingleNavItem | undefined { + const found = navItems.find((item) => + item.isSingle !== false + ? // The current item is single, so check if the item url is active + isActiveRoute({ itemUrl: item.url, pathname }) + : // The current item is not single, so check if any of the sub items are active + item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ), + ); + + if (found?.isSingle !== false) { + // The found item is single, so return it + return found; + } + + // The found item is not single, so find the active sub item + return found?.items.find((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); +} interface Props { children: React.ReactNode; @@ -398,64 +543,21 @@ export default function Page({ children }: Props) { const includesProjects = pathname?.includes("/dashboard/project"); const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); - const isActiveRoute = (itemUrl: string) => { - const normalizedItemUrl = itemUrl?.replace("/projects", "/project"); - const normalizedPathname = pathname?.replace("/projects", "/project"); - if (!normalizedPathname) return false; + const { + home: filteredHome, + settings: filteredSettings, + help, + } = createMenuForAuthUser({ auth, user, isCloud: !!isCloud }); - if (normalizedPathname === normalizedItemUrl) return true; + const activeItem = findActiveNavItem( + [...filteredHome, ...filteredSettings], + pathname, + ); - if (normalizedPathname.startsWith(normalizedItemUrl)) { - const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); - return nextChar === "/"; - } - - return false; - }; - - let filteredHome = isCloud - ? data.home.filter( - (item) => - ![ - "/dashboard/monitoring", - "/dashboard/traefik", - "/dashboard/docker", - "/dashboard/swarm", - "/dashboard/requests", - ].includes(item.url), - ) - : data.home; - - let filteredSettings = isCloud - ? data.settings.filter( - (item) => - ![ - "/dashboard/settings/server", - "/dashboard/settings/cluster", - ].includes(item.url), - ) - : data.settings.filter( - (item) => !["/dashboard/settings/billing"].includes(item.url), - ); - - filteredHome = filteredHome.map((item) => ({ - ...item, - isActive: isActiveRoute(item.url), - })); - - filteredSettings = filteredSettings.map((item) => ({ - ...item, - isActive: isActiveRoute(item.url), - })); - - const activeItem = - filteredHome.find((item) => item.isActive) || - filteredSettings.find((item) => item.isActive); - - const showProjectsButton = - currentPath === "/dashboard/projects" && - (auth?.rol === "admin" || user?.canCreateProjects); + // const showProjectsButton = + // currentPath === "/dashboard/projects" && + // (auth?.rol === "admin" || user?.canCreateProjects); return ( Home - {filteredHome.map((item) => ( - - - {item.isSingle ? ( - - - - {item.title} - - - ) : ( - <> - - - {item.icon && } + {filteredHome.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); - {item.title} - {item.items?.length && ( - + return ( + + + {isSingle ? ( + + + {item.icon && ( + )} - - - - - {item.items?.map((subItem) => ( - - - {item.title}
+ + + ) : ( + <> + + + {item.icon && } + + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ))} + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} Settings - {filteredSettings.map((item) => ( - - - {item.isSingle ? ( - - - - {item.title} - - - ) : ( - <> - - - {item.icon && } + {filteredSettings.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); - {item.title} - {item.items?.length && ( - + return ( + + + {isSingle ? ( + + + {item.icon && ( + )} - - - - - {item.items?.map((subItem) => ( - - - {item.title} + + + ) : ( + <> + + + {item.icon && } + + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ))} + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} Extra - {data.help.map((item: ExternalLink) => ( + {help.map((item: ExternalLink) => ( Date: Wed, 22 Jan 2025 11:23:18 +0700 Subject: [PATCH 11/17] fix: add condition to show create project button --- apps/dokploy/components/dashboard/projects/show.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index c4b2f672..afeabf30 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -87,9 +87,12 @@ export const ShowProjects = () => { Create and manage your projects -
- -
+ + {(auth?.rol === "admin" || user?.canCreateProjects) && ( +
+ +
+ )}
From 9e6e68558aeb56908e10c50be94198d14293f1a0 Mon Sep 17 00:00:00 2001 From: Rahadi Jalu Date: Wed, 22 Jan 2025 11:23:58 +0700 Subject: [PATCH 12/17] fix: adjust dialog title based on add/update condition --- apps/dokploy/components/dashboard/projects/handle-project.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx index cf38c57c..08e3e0a8 100644 --- a/apps/dokploy/components/dashboard/projects/handle-project.tsx +++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx @@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => { - Add a project + {projectId ? "Update" : "Add a"} project The home of something big! {isError && {error?.message}} From 53df7d969e9192ebc05a66f948dd0af9a04ca095 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:13:22 -0600 Subject: [PATCH 13/17] refactor: make protected instead of admin --- apps/dokploy/server/api/routers/settings.ts | 1450 +++++++++---------- 1 file changed, 725 insertions(+), 725 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 449a2233..0224e343 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -1,59 +1,59 @@ import { db } from "@/server/db"; import { - apiAssignDomain, - apiEnableDashboard, - apiModifyTraefikConfig, - apiReadStatsLogs, - apiReadTraefikConfig, - apiSaveSSHKey, - apiServerSchema, - apiTraefikConfig, - apiUpdateDockerCleanup, + apiAssignDomain, + apiEnableDashboard, + apiModifyTraefikConfig, + apiReadStatsLogs, + apiReadTraefikConfig, + apiSaveSSHKey, + apiServerSchema, + apiTraefikConfig, + apiUpdateDockerCleanup, } from "@/server/db/schema"; import { removeJob, schedule } from "@/server/utils/backup"; import { - DEFAULT_UPDATE_DATA, - IS_CLOUD, - canAccessToTraefikFiles, - cleanStoppedContainers, - cleanUpDockerBuilder, - cleanUpSystemPrune, - cleanUpUnusedImages, - cleanUpUnusedVolumes, - execAsync, - execAsyncRemote, - findAdmin, - findAdminById, - findServerById, - getDokployImage, - getDokployImageTag, - getUpdateData, - initializeTraefik, - logRotationManager, - parseRawConfig, - paths, - prepareEnvironmentVariables, - processLogs, - pullLatestRelease, - readConfig, - readConfigInPath, - readDirectory, - readMainConfig, - readMonitoringConfig, - recreateDirectory, - sendDockerCleanupNotifications, - spawnAsync, - startService, - startServiceRemote, - stopService, - stopServiceRemote, - updateAdmin, - updateLetsEncryptEmail, - updateServerById, - updateServerTraefik, - writeConfig, - writeMainConfig, - writeTraefikConfigInPath, + DEFAULT_UPDATE_DATA, + IS_CLOUD, + canAccessToTraefikFiles, + cleanStoppedContainers, + cleanUpDockerBuilder, + cleanUpSystemPrune, + cleanUpUnusedImages, + cleanUpUnusedVolumes, + execAsync, + execAsyncRemote, + findAdmin, + findAdminById, + findServerById, + getDokployImage, + getDokployImageTag, + getUpdateData, + initializeTraefik, + logRotationManager, + parseRawConfig, + paths, + prepareEnvironmentVariables, + processLogs, + pullLatestRelease, + readConfig, + readConfigInPath, + readDirectory, + readMainConfig, + readMonitoringConfig, + recreateDirectory, + sendDockerCleanupNotifications, + spawnAsync, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateAdmin, + updateLetsEncryptEmail, + updateServerById, + updateServerTraefik, + writeConfig, + writeMainConfig, + writeTraefikConfigInPath, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; @@ -65,736 +65,736 @@ import { z } from "zod"; import packageInfo from "../../../package.json"; import { appRouter } from "../root"; import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, } from "../trpc"; export const settingsRouter = createTRPCRouter({ - reloadServer: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } - const { stdout } = await execAsync( - "docker service inspect dokploy --format '{{.ID}}'", - ); - await execAsync(`docker service update --force ${stdout.trim()}`); - return true; - }), - reloadTraefik: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - try { - if (input?.serverId) { - await stopServiceRemote(input.serverId, "dokploy-traefik"); - await startServiceRemote(input.serverId, "dokploy-traefik"); - } else if (!IS_CLOUD) { - await stopService("dokploy-traefik"); - await startService("dokploy-traefik"); - } - } catch (err) { - console.error(err); - } + reloadServer: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } + const { stdout } = await execAsync( + "docker service inspect dokploy --format '{{.ID}}'" + ); + await execAsync(`docker service update --force ${stdout.trim()}`); + return true; + }), + reloadTraefik: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + try { + if (input?.serverId) { + await stopServiceRemote(input.serverId, "dokploy-traefik"); + await startServiceRemote(input.serverId, "dokploy-traefik"); + } else if (!IS_CLOUD) { + await stopService("dokploy-traefik"); + await startService("dokploy-traefik"); + } + } catch (err) { + console.error(err); + } - return true; - }), - toggleDashboard: adminProcedure - .input(apiEnableDashboard) - .mutation(async ({ input }) => { - await initializeTraefik({ - enableDashboard: input.enableDashboard, - serverId: input.serverId, - }); - return true; - }), + return true; + }), + toggleDashboard: adminProcedure + .input(apiEnableDashboard) + .mutation(async ({ input }) => { + await initializeTraefik({ + enableDashboard: input.enableDashboard, + serverId: input.serverId, + }); + return true; + }), - cleanUnusedImages: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedImages(input?.serverId); - return true; - }), - cleanUnusedVolumes: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedVolumes(input?.serverId); - return true; - }), - cleanStoppedContainers: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanStoppedContainers(input?.serverId); - return true; - }), - cleanDockerBuilder: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpDockerBuilder(input?.serverId); - }), - cleanDockerPrune: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpSystemPrune(input?.serverId); - await cleanUpDockerBuilder(input?.serverId); + cleanUnusedImages: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedImages(input?.serverId); + return true; + }), + cleanUnusedVolumes: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedVolumes(input?.serverId); + return true; + }), + cleanStoppedContainers: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanStoppedContainers(input?.serverId); + return true; + }), + cleanDockerBuilder: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpDockerBuilder(input?.serverId); + }), + cleanDockerPrune: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpSystemPrune(input?.serverId); + await cleanUpDockerBuilder(input?.serverId); - return true; - }), - cleanAll: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedImages(input?.serverId); - await cleanStoppedContainers(input?.serverId); - await cleanUpDockerBuilder(input?.serverId); - await cleanUpSystemPrune(input?.serverId); + return true; + }), + cleanAll: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedImages(input?.serverId); + await cleanStoppedContainers(input?.serverId); + await cleanUpDockerBuilder(input?.serverId); + await cleanUpSystemPrune(input?.serverId); - return true; - }), - cleanMonitoring: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } - const { MONITORING_PATH } = paths(); - await recreateDirectory(MONITORING_PATH); - return true; - }), - saveSSHPrivateKey: adminProcedure - .input(apiSaveSSHKey) - .mutation(async ({ input, ctx }) => { - if (IS_CLOUD) { - return true; - } - await updateAdmin(ctx.user.authId, { - sshPrivateKey: input.sshPrivateKey, - }); + return true; + }), + cleanMonitoring: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } + const { MONITORING_PATH } = paths(); + await recreateDirectory(MONITORING_PATH); + return true; + }), + saveSSHPrivateKey: adminProcedure + .input(apiSaveSSHKey) + .mutation(async ({ input, ctx }) => { + if (IS_CLOUD) { + return true; + } + await updateAdmin(ctx.user.authId, { + sshPrivateKey: input.sshPrivateKey, + }); - return true; - }), - assignDomainServer: adminProcedure - .input(apiAssignDomain) - .mutation(async ({ ctx, input }) => { - if (IS_CLOUD) { - return true; - } - const admin = await updateAdmin(ctx.user.authId, { - host: input.host, - ...(input.letsEncryptEmail && { - letsEncryptEmail: input.letsEncryptEmail, - }), - certificateType: input.certificateType, - }); + return true; + }), + assignDomainServer: adminProcedure + .input(apiAssignDomain) + .mutation(async ({ ctx, input }) => { + if (IS_CLOUD) { + return true; + } + const admin = await updateAdmin(ctx.user.authId, { + host: input.host, + ...(input.letsEncryptEmail && { + letsEncryptEmail: input.letsEncryptEmail, + }), + certificateType: input.certificateType, + }); - if (!admin) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Admin not found", - }); - } + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } - updateServerTraefik(admin, input.host); - if (input.letsEncryptEmail) { - updateLetsEncryptEmail(input.letsEncryptEmail); - } + updateServerTraefik(admin, input.host); + if (input.letsEncryptEmail) { + updateLetsEncryptEmail(input.letsEncryptEmail); + } - return admin; - }), - cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { - if (IS_CLOUD) { - return true; - } - await updateAdmin(ctx.user.authId, { - sshPrivateKey: null, - }); - return true; - }), - updateDockerCleanup: adminProcedure - .input(apiUpdateDockerCleanup) - .mutation(async ({ input, ctx }) => { - if (input.serverId) { - await updateServerById(input.serverId, { - enableDockerCleanup: input.enableDockerCleanup, - }); + return admin; + }), + cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { + if (IS_CLOUD) { + return true; + } + await updateAdmin(ctx.user.authId, { + sshPrivateKey: null, + }); + return true; + }), + updateDockerCleanup: adminProcedure + .input(apiUpdateDockerCleanup) + .mutation(async ({ input, ctx }) => { + if (input.serverId) { + await updateServerById(input.serverId, { + enableDockerCleanup: input.enableDockerCleanup, + }); - const server = await findServerById(input.serverId); + const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this server", - }); - } + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } - if (server.enableDockerCleanup) { - const server = await findServerById(input.serverId); - if (server.serverStatus === "inactive") { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server is inactive", - }); - } - if (IS_CLOUD) { - await schedule({ - cronSchedule: "0 0 * * *", - serverId: input.serverId, - type: "server", - }); - } else { - scheduleJob(server.serverId, "0 0 * * *", async () => { - console.log( - `Docker Cleanup ${new Date().toLocaleString()}] Running...`, - ); - await cleanUpUnusedImages(server.serverId); - await cleanUpDockerBuilder(server.serverId); - await cleanUpSystemPrune(server.serverId); - await sendDockerCleanupNotifications(server.adminId); - }); - } - } else { - if (IS_CLOUD) { - await removeJob({ - cronSchedule: "0 0 * * *", - serverId: input.serverId, - type: "server", - }); - } else { - const currentJob = scheduledJobs[server.serverId]; - currentJob?.cancel(); - } - } - } else if (!IS_CLOUD) { - const admin = await findAdminById(ctx.user.adminId); + if (server.enableDockerCleanup) { + const server = await findServerById(input.serverId); + if (server.serverStatus === "inactive") { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server is inactive", + }); + } + if (IS_CLOUD) { + await schedule({ + cronSchedule: "0 0 * * *", + serverId: input.serverId, + type: "server", + }); + } else { + scheduleJob(server.serverId, "0 0 * * *", async () => { + console.log( + `Docker Cleanup ${new Date().toLocaleString()}] Running...` + ); + await cleanUpUnusedImages(server.serverId); + await cleanUpDockerBuilder(server.serverId); + await cleanUpSystemPrune(server.serverId); + await sendDockerCleanupNotifications(server.adminId); + }); + } + } else { + if (IS_CLOUD) { + await removeJob({ + cronSchedule: "0 0 * * *", + serverId: input.serverId, + type: "server", + }); + } else { + const currentJob = scheduledJobs[server.serverId]; + currentJob?.cancel(); + } + } + } else if (!IS_CLOUD) { + const admin = await findAdminById(ctx.user.adminId); - if (admin.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this admin", - }); - } - const adminUpdated = await updateAdmin(ctx.user.authId, { - enableDockerCleanup: input.enableDockerCleanup, - }); + if (admin.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this admin", + }); + } + const adminUpdated = await updateAdmin(ctx.user.authId, { + enableDockerCleanup: input.enableDockerCleanup, + }); - if (adminUpdated?.enableDockerCleanup) { - scheduleJob("docker-cleanup", "0 0 * * *", async () => { - console.log( - `Docker Cleanup ${new Date().toLocaleString()}] Running...`, - ); - await cleanUpUnusedImages(); - await cleanUpDockerBuilder(); - await cleanUpSystemPrune(); - await sendDockerCleanupNotifications(admin.adminId); - }); - } else { - const currentJob = scheduledJobs["docker-cleanup"]; - currentJob?.cancel(); - } - } + if (adminUpdated?.enableDockerCleanup) { + scheduleJob("docker-cleanup", "0 0 * * *", async () => { + console.log( + `Docker Cleanup ${new Date().toLocaleString()}] Running...` + ); + await cleanUpUnusedImages(); + await cleanUpDockerBuilder(); + await cleanUpSystemPrune(); + await sendDockerCleanupNotifications(admin.adminId); + }); + } else { + const currentJob = scheduledJobs["docker-cleanup"]; + currentJob?.cancel(); + } + } - return true; - }), + return true; + }), - readTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readMainConfig(); - return traefikConfig; - }), + readTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readMainConfig(); + return traefikConfig; + }), - updateTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeMainConfig(input.traefikConfig); - return true; - }), + updateTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeMainConfig(input.traefikConfig); + return true; + }), - readWebServerTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readConfig("dokploy"); - return traefikConfig; - }), - updateWebServerTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeConfig("dokploy", input.traefikConfig); - return true; - }), + readWebServerTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readConfig("dokploy"); + return traefikConfig; + }), + updateWebServerTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeConfig("dokploy", input.traefikConfig); + return true; + }), - readMiddlewareTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readConfig("middlewares"); - return traefikConfig; - }), + readMiddlewareTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readConfig("middlewares"); + return traefikConfig; + }), - updateMiddlewareTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeConfig("middlewares", input.traefikConfig); - return true; - }), - getUpdateData: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return DEFAULT_UPDATE_DATA; - } + updateMiddlewareTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeConfig("middlewares", input.traefikConfig); + return true; + }), + getUpdateData: protectedProcedure.mutation(async () => { + if (IS_CLOUD) { + return DEFAULT_UPDATE_DATA; + } - return await getUpdateData(); - }), - updateServer: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } + return await getUpdateData(); + }), + updateServer: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } - await pullLatestRelease(); + await pullLatestRelease(); - // This causes restart of dokploy, thus it will not finish executing properly, so don't await it - // Status after restart is checked via frontend /api/health endpoint - void spawnAsync("docker", [ - "service", - "update", - "--force", - "--image", - getDokployImage(), - "dokploy", - ]); + // This causes restart of dokploy, thus it will not finish executing properly, so don't await it + // Status after restart is checked via frontend /api/health endpoint + void spawnAsync("docker", [ + "service", + "update", + "--force", + "--image", + getDokployImage(), + "dokploy", + ]); - return true; - }), + return true; + }), - getDokployVersion: adminProcedure.query(() => { - return packageInfo.version; - }), - getReleaseTag: adminProcedure.query(() => { - return getDokployImageTag(); - }), - readDirectories: protectedProcedure - .input(apiServerSchema) - .query(async ({ ctx, input }) => { - try { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + getDokployVersion: protectedProcedure.query(() => { + return packageInfo.version; + }), + getReleaseTag: protectedProcedure.query(() => { + return getDokployImageTag(); + }), + readDirectories: protectedProcedure + .input(apiServerSchema) + .query(async ({ ctx, input }) => { + try { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); - const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); - return result || []; - } catch (error) { - throw error; - } - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); + const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); + return result || []; + } catch (error) { + throw error; + } + }), - updateTraefikFile: protectedProcedure - .input(apiModifyTraefikConfig) - .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + updateTraefikFile: protectedProcedure + .input(apiModifyTraefikConfig) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - await writeTraefikConfigInPath( - input.path, - input.traefikConfig, - input?.serverId, - ); - return true; - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + await writeTraefikConfigInPath( + input.path, + input.traefikConfig, + input?.serverId + ); + return true; + }), - readTraefikFile: protectedProcedure - .input(apiReadTraefikConfig) - .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + readTraefikFile: protectedProcedure + .input(apiReadTraefikConfig) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - return readConfigInPath(input.path, input.serverId); - }), - getIp: protectedProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - const admin = await findAdmin(); - return admin.serverIp; - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + return readConfigInPath(input.path, input.serverId); + }), + getIp: protectedProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + const admin = await findAdmin(); + return admin.serverIp; + }), - getOpenApiDocument: protectedProcedure.query( - async ({ ctx }): Promise => { - const protocol = ctx.req.headers["x-forwarded-proto"]; - const url = `${protocol}://${ctx.req.headers.host}/api`; - const openApiDocument = generateOpenApiDocument(appRouter, { - title: "tRPC OpenAPI", - version: "1.0.0", - baseUrl: url, - docsUrl: `${url}/settings.getOpenApiDocument`, - tags: [ - "admin", - "docker", - "compose", - "registry", - "cluster", - "user", - "domain", - "destination", - "backup", - "deployment", - "mounts", - "certificates", - "settings", - "security", - "redirects", - "port", - "project", - "application", - "mysql", - "postgres", - "redis", - "mongo", - "mariadb", - "sshRouter", - "gitProvider", - "bitbucket", - "github", - "gitlab", - ], - }); + getOpenApiDocument: protectedProcedure.query( + async ({ ctx }): Promise => { + const protocol = ctx.req.headers["x-forwarded-proto"]; + const url = `${protocol}://${ctx.req.headers.host}/api`; + const openApiDocument = generateOpenApiDocument(appRouter, { + title: "tRPC OpenAPI", + version: "1.0.0", + baseUrl: url, + docsUrl: `${url}/settings.getOpenApiDocument`, + tags: [ + "admin", + "docker", + "compose", + "registry", + "cluster", + "user", + "domain", + "destination", + "backup", + "deployment", + "mounts", + "certificates", + "settings", + "security", + "redirects", + "port", + "project", + "application", + "mysql", + "postgres", + "redis", + "mongo", + "mariadb", + "sshRouter", + "gitProvider", + "bitbucket", + "github", + "gitlab", + ], + }); - openApiDocument.info = { - title: "Dokploy API", - description: "Endpoints for dokploy", - // TODO: get version from package.json - version: "1.0.0", - }; + openApiDocument.info = { + title: "Dokploy API", + description: "Endpoints for dokploy", + // TODO: get version from package.json + version: "1.0.0", + }; - return openApiDocument; - }, - ), - readTraefikEnv: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = - "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik"; + return openApiDocument; + } + ), + readTraefikEnv: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = + "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik"; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - return result.stdout.trim(); - } - if (!IS_CLOUD) { - const result = await execAsync(command); - return result.stdout.trim(); - } - }), + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + return result.stdout.trim(); + } + if (!IS_CLOUD) { + const result = await execAsync(command); + return result.stdout.trim(); + } + }), - writeTraefikEnv: adminProcedure - .input(z.object({ env: z.string(), serverId: z.string().optional() })) - .mutation(async ({ input }) => { - const envs = prepareEnvironmentVariables(input.env); - await initializeTraefik({ - env: envs, - serverId: input.serverId, - }); + writeTraefikEnv: adminProcedure + .input(z.object({ env: z.string(), serverId: z.string().optional() })) + .mutation(async ({ input }) => { + const envs = prepareEnvironmentVariables(input.env); + await initializeTraefik({ + env: envs, + serverId: input.serverId, + }); - return true; - }), - haveTraefikDashboardPortEnabled: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + return true; + }), + haveTraefikDashboardPortEnabled: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; - let stdout = ""; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - stdout = result.stdout; - } else if (!IS_CLOUD) { - const result = await execAsync( - "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik", - ); - stdout = result.stdout; - } + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync( + "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik" + ); + stdout = result.stdout; + } - const parsed: any[] = JSON.parse(stdout.trim()); - for (const port of parsed) { - if (port.PublishedPort === 8080) { - return true; - } - } + const parsed: any[] = JSON.parse(stdout.trim()); + for (const port of parsed) { + if (port.PublishedPort === 8080) { + return true; + } + } - return false; - }), + return false; + }), - readStatsLogs: adminProcedure - .meta({ - openapi: { - path: "/read-stats-logs", - method: "POST", - override: true, - enabled: false, - }, - }) - .input(apiReadStatsLogs) - .query(({ input }) => { - if (IS_CLOUD) { - return { - data: [], - totalCount: 0, - }; - } - const rawConfig = readMonitoringConfig(); - const parsedConfig = parseRawConfig( - rawConfig as string, - input.page, - input.sort, - input.search, - input.status, - ); + readStatsLogs: adminProcedure + .meta({ + openapi: { + path: "/read-stats-logs", + method: "POST", + override: true, + enabled: false, + }, + }) + .input(apiReadStatsLogs) + .query(({ input }) => { + if (IS_CLOUD) { + return { + data: [], + totalCount: 0, + }; + } + const rawConfig = readMonitoringConfig(); + const parsedConfig = parseRawConfig( + rawConfig as string, + input.page, + input.sort, + input.search, + input.status + ); - return parsedConfig; - }), - readStats: adminProcedure.query(() => { - if (IS_CLOUD) { - return []; - } - const rawConfig = readMonitoringConfig(); - const processedLogs = processLogs(rawConfig as string); - return processedLogs || []; - }), - getLogRotateStatus: adminProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - return await logRotationManager.getStatus(); - }), - toggleLogRotate: adminProcedure - .input( - z.object({ - enable: z.boolean(), - }), - ) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - if (input.enable) { - await logRotationManager.activate(); - } else { - await logRotationManager.deactivate(); - } + return parsedConfig; + }), + readStats: adminProcedure.query(() => { + if (IS_CLOUD) { + return []; + } + const rawConfig = readMonitoringConfig(); + const processedLogs = processLogs(rawConfig as string); + return processedLogs || []; + }), + getLogRotateStatus: adminProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + return await logRotationManager.getStatus(); + }), + toggleLogRotate: adminProcedure + .input( + z.object({ + enable: z.boolean(), + }) + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + if (input.enable) { + await logRotationManager.activate(); + } else { + await logRotationManager.deactivate(); + } - return true; - }), - haveActivateRequests: adminProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - const config = readMainConfig(); + return true; + }), + haveActivateRequests: adminProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + const config = readMainConfig(); - if (!config) return false; - const parsedConfig = load(config) as { - accessLog?: { - filePath: string; - }; - }; + if (!config) return false; + const parsedConfig = load(config) as { + accessLog?: { + filePath: string; + }; + }; - return !!parsedConfig?.accessLog?.filePath; - }), - toggleRequests: adminProcedure - .input( - z.object({ - enable: z.boolean(), - }), - ) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - const mainConfig = readMainConfig(); - if (!mainConfig) return false; + return !!parsedConfig?.accessLog?.filePath; + }), + toggleRequests: adminProcedure + .input( + z.object({ + enable: z.boolean(), + }) + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + const mainConfig = readMainConfig(); + if (!mainConfig) return false; - const currentConfig = load(mainConfig) as { - accessLog?: { - filePath: string; - }; - }; + const currentConfig = load(mainConfig) as { + accessLog?: { + filePath: string; + }; + }; - if (input.enable) { - const config = { - accessLog: { - filePath: "/etc/dokploy/traefik/dynamic/access.log", - format: "json", - bufferingSize: 100, - filters: { - retryAttempts: true, - minDuration: "10ms", - }, - }, - }; - currentConfig.accessLog = config.accessLog; - } else { - currentConfig.accessLog = undefined; - } + if (input.enable) { + const config = { + accessLog: { + filePath: "/etc/dokploy/traefik/dynamic/access.log", + format: "json", + bufferingSize: 100, + filters: { + retryAttempts: true, + minDuration: "10ms", + }, + }, + }; + currentConfig.accessLog = config.accessLog; + } else { + currentConfig.accessLog = undefined; + } - writeMainConfig(dump(currentConfig)); + writeMainConfig(dump(currentConfig)); - return true; - }), - isCloud: protectedProcedure.query(async () => { - return IS_CLOUD; - }), - health: publicProcedure.query(async () => { - if (IS_CLOUD) { - try { - await db.execute(sql`SELECT 1`); - return { status: "ok" }; - } catch (error) { - console.error("Database connection error:", error); - throw error; - } - } - return { status: "not_cloud" }; - }), - setupGPU: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - }), - ) - .mutation(async ({ input }) => { - if (IS_CLOUD && !input.serverId) { - throw new Error("Select a server to enable the GPU Setup"); - } + return true; + }), + isCloud: protectedProcedure.query(async () => { + return IS_CLOUD; + }), + health: publicProcedure.query(async () => { + if (IS_CLOUD) { + try { + await db.execute(sql`SELECT 1`); + return { status: "ok" }; + } catch (error) { + console.error("Database connection error:", error); + throw error; + } + } + return { status: "not_cloud" }; + }), + setupGPU: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + }) + ) + .mutation(async ({ input }) => { + if (IS_CLOUD && !input.serverId) { + throw new Error("Select a server to enable the GPU Setup"); + } - try { - await setupGPUSupport(input.serverId); - return { success: true }; - } catch (error) { - console.error("GPU Setup Error:", error); - throw error; - } - }), - checkGPUStatus: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - }), - ) - .query(async ({ input }) => { - if (IS_CLOUD && !input.serverId) { - return { - driverInstalled: false, - driverVersion: undefined, - gpuModel: undefined, - runtimeInstalled: false, - runtimeConfigured: false, - cudaSupport: undefined, - cudaVersion: undefined, - memoryInfo: undefined, - availableGPUs: 0, - swarmEnabled: false, - gpuResources: 0, - }; - } + try { + await setupGPUSupport(input.serverId); + return { success: true }; + } catch (error) { + console.error("GPU Setup Error:", error); + throw error; + } + }), + checkGPUStatus: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + }) + ) + .query(async ({ input }) => { + if (IS_CLOUD && !input.serverId) { + return { + driverInstalled: false, + driverVersion: undefined, + gpuModel: undefined, + runtimeInstalled: false, + runtimeConfigured: false, + cudaSupport: undefined, + cudaVersion: undefined, + memoryInfo: undefined, + availableGPUs: 0, + swarmEnabled: false, + gpuResources: 0, + }; + } - try { - return await checkGPUStatus(input.serverId || ""); - } catch (error) { - throw new Error("Failed to check GPU status"); - } - }), - updateTraefikPorts: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - additionalPorts: z.array( - z.object({ - targetPort: z.number(), - publishedPort: z.number(), - publishMode: z.enum(["ingress", "host"]).default("host"), - }), - ), - }), - ) - .mutation(async ({ input }) => { - try { - if (IS_CLOUD && !input.serverId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Please set a serverId to update Traefik ports", - }); - } - await initializeTraefik({ - serverId: input.serverId, - additionalPorts: input.additionalPorts, - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - error instanceof Error - ? error.message - : "Error updating Traefik ports", - cause: error, - }); - } - }), - getTraefikPorts: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + try { + return await checkGPUStatus(input.serverId || ""); + } catch (error) { + throw new Error("Failed to check GPU status"); + } + }), + updateTraefikPorts: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + additionalPorts: z.array( + z.object({ + targetPort: z.number(), + publishedPort: z.number(), + publishMode: z.enum(["ingress", "host"]).default("host"), + }) + ), + }) + ) + .mutation(async ({ input }) => { + try { + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please set a serverId to update Traefik ports", + }); + } + await initializeTraefik({ + serverId: input.serverId, + additionalPorts: input.additionalPorts, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error updating Traefik ports", + cause: error, + }); + } + }), + getTraefikPorts: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; - try { - let stdout = ""; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - stdout = result.stdout; - } else if (!IS_CLOUD) { - const result = await execAsync(command); - stdout = result.stdout; - } + try { + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync(command); + stdout = result.stdout; + } - const ports: { - Protocol: string; - TargetPort: number; - PublishedPort: number; - PublishMode: string; - }[] = JSON.parse(stdout.trim()); + const ports: { + Protocol: string; + TargetPort: number; + PublishedPort: number; + PublishMode: string; + }[] = JSON.parse(stdout.trim()); - // Filter out the default ports (80, 443, and optionally 8080) - const additionalPorts = ports - .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) - .map((port) => ({ - targetPort: port.TargetPort, - publishedPort: port.PublishedPort, - publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", - })); + // Filter out the default ports (80, 443, and optionally 8080) + const additionalPorts = ports + .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) + .map((port) => ({ + targetPort: port.TargetPort, + publishedPort: port.PublishedPort, + publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", + })); - return additionalPorts; - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to get Traefik ports", - cause: error, - }); - } - }), + return additionalPorts; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get Traefik ports", + cause: error, + }); + } + }), }); // { // "Parallelism": 1, From 02ff507094dcb5e5d7ae819278b74fbc681a75b5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:14:30 -0600 Subject: [PATCH 14/17] refactor: update lint --- apps/dokploy/server/api/routers/settings.ts | 1450 +++++++++---------- 1 file changed, 725 insertions(+), 725 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 0224e343..cb0e32d9 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -1,59 +1,59 @@ import { db } from "@/server/db"; import { - apiAssignDomain, - apiEnableDashboard, - apiModifyTraefikConfig, - apiReadStatsLogs, - apiReadTraefikConfig, - apiSaveSSHKey, - apiServerSchema, - apiTraefikConfig, - apiUpdateDockerCleanup, + apiAssignDomain, + apiEnableDashboard, + apiModifyTraefikConfig, + apiReadStatsLogs, + apiReadTraefikConfig, + apiSaveSSHKey, + apiServerSchema, + apiTraefikConfig, + apiUpdateDockerCleanup, } from "@/server/db/schema"; import { removeJob, schedule } from "@/server/utils/backup"; import { - DEFAULT_UPDATE_DATA, - IS_CLOUD, - canAccessToTraefikFiles, - cleanStoppedContainers, - cleanUpDockerBuilder, - cleanUpSystemPrune, - cleanUpUnusedImages, - cleanUpUnusedVolumes, - execAsync, - execAsyncRemote, - findAdmin, - findAdminById, - findServerById, - getDokployImage, - getDokployImageTag, - getUpdateData, - initializeTraefik, - logRotationManager, - parseRawConfig, - paths, - prepareEnvironmentVariables, - processLogs, - pullLatestRelease, - readConfig, - readConfigInPath, - readDirectory, - readMainConfig, - readMonitoringConfig, - recreateDirectory, - sendDockerCleanupNotifications, - spawnAsync, - startService, - startServiceRemote, - stopService, - stopServiceRemote, - updateAdmin, - updateLetsEncryptEmail, - updateServerById, - updateServerTraefik, - writeConfig, - writeMainConfig, - writeTraefikConfigInPath, + DEFAULT_UPDATE_DATA, + IS_CLOUD, + canAccessToTraefikFiles, + cleanStoppedContainers, + cleanUpDockerBuilder, + cleanUpSystemPrune, + cleanUpUnusedImages, + cleanUpUnusedVolumes, + execAsync, + execAsyncRemote, + findAdmin, + findAdminById, + findServerById, + getDokployImage, + getDokployImageTag, + getUpdateData, + initializeTraefik, + logRotationManager, + parseRawConfig, + paths, + prepareEnvironmentVariables, + processLogs, + pullLatestRelease, + readConfig, + readConfigInPath, + readDirectory, + readMainConfig, + readMonitoringConfig, + recreateDirectory, + sendDockerCleanupNotifications, + spawnAsync, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + updateAdmin, + updateLetsEncryptEmail, + updateServerById, + updateServerTraefik, + writeConfig, + writeMainConfig, + writeTraefikConfigInPath, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; @@ -65,736 +65,736 @@ import { z } from "zod"; import packageInfo from "../../../package.json"; import { appRouter } from "../root"; import { - adminProcedure, - createTRPCRouter, - protectedProcedure, - publicProcedure, + adminProcedure, + createTRPCRouter, + protectedProcedure, + publicProcedure, } from "../trpc"; export const settingsRouter = createTRPCRouter({ - reloadServer: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } - const { stdout } = await execAsync( - "docker service inspect dokploy --format '{{.ID}}'" - ); - await execAsync(`docker service update --force ${stdout.trim()}`); - return true; - }), - reloadTraefik: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - try { - if (input?.serverId) { - await stopServiceRemote(input.serverId, "dokploy-traefik"); - await startServiceRemote(input.serverId, "dokploy-traefik"); - } else if (!IS_CLOUD) { - await stopService("dokploy-traefik"); - await startService("dokploy-traefik"); - } - } catch (err) { - console.error(err); - } + reloadServer: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } + const { stdout } = await execAsync( + "docker service inspect dokploy --format '{{.ID}}'", + ); + await execAsync(`docker service update --force ${stdout.trim()}`); + return true; + }), + reloadTraefik: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + try { + if (input?.serverId) { + await stopServiceRemote(input.serverId, "dokploy-traefik"); + await startServiceRemote(input.serverId, "dokploy-traefik"); + } else if (!IS_CLOUD) { + await stopService("dokploy-traefik"); + await startService("dokploy-traefik"); + } + } catch (err) { + console.error(err); + } - return true; - }), - toggleDashboard: adminProcedure - .input(apiEnableDashboard) - .mutation(async ({ input }) => { - await initializeTraefik({ - enableDashboard: input.enableDashboard, - serverId: input.serverId, - }); - return true; - }), + return true; + }), + toggleDashboard: adminProcedure + .input(apiEnableDashboard) + .mutation(async ({ input }) => { + await initializeTraefik({ + enableDashboard: input.enableDashboard, + serverId: input.serverId, + }); + return true; + }), - cleanUnusedImages: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedImages(input?.serverId); - return true; - }), - cleanUnusedVolumes: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedVolumes(input?.serverId); - return true; - }), - cleanStoppedContainers: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanStoppedContainers(input?.serverId); - return true; - }), - cleanDockerBuilder: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpDockerBuilder(input?.serverId); - }), - cleanDockerPrune: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpSystemPrune(input?.serverId); - await cleanUpDockerBuilder(input?.serverId); + cleanUnusedImages: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedImages(input?.serverId); + return true; + }), + cleanUnusedVolumes: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedVolumes(input?.serverId); + return true; + }), + cleanStoppedContainers: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanStoppedContainers(input?.serverId); + return true; + }), + cleanDockerBuilder: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpDockerBuilder(input?.serverId); + }), + cleanDockerPrune: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpSystemPrune(input?.serverId); + await cleanUpDockerBuilder(input?.serverId); - return true; - }), - cleanAll: adminProcedure - .input(apiServerSchema) - .mutation(async ({ input }) => { - await cleanUpUnusedImages(input?.serverId); - await cleanStoppedContainers(input?.serverId); - await cleanUpDockerBuilder(input?.serverId); - await cleanUpSystemPrune(input?.serverId); + return true; + }), + cleanAll: adminProcedure + .input(apiServerSchema) + .mutation(async ({ input }) => { + await cleanUpUnusedImages(input?.serverId); + await cleanStoppedContainers(input?.serverId); + await cleanUpDockerBuilder(input?.serverId); + await cleanUpSystemPrune(input?.serverId); - return true; - }), - cleanMonitoring: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } - const { MONITORING_PATH } = paths(); - await recreateDirectory(MONITORING_PATH); - return true; - }), - saveSSHPrivateKey: adminProcedure - .input(apiSaveSSHKey) - .mutation(async ({ input, ctx }) => { - if (IS_CLOUD) { - return true; - } - await updateAdmin(ctx.user.authId, { - sshPrivateKey: input.sshPrivateKey, - }); + return true; + }), + cleanMonitoring: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } + const { MONITORING_PATH } = paths(); + await recreateDirectory(MONITORING_PATH); + return true; + }), + saveSSHPrivateKey: adminProcedure + .input(apiSaveSSHKey) + .mutation(async ({ input, ctx }) => { + if (IS_CLOUD) { + return true; + } + await updateAdmin(ctx.user.authId, { + sshPrivateKey: input.sshPrivateKey, + }); - return true; - }), - assignDomainServer: adminProcedure - .input(apiAssignDomain) - .mutation(async ({ ctx, input }) => { - if (IS_CLOUD) { - return true; - } - const admin = await updateAdmin(ctx.user.authId, { - host: input.host, - ...(input.letsEncryptEmail && { - letsEncryptEmail: input.letsEncryptEmail, - }), - certificateType: input.certificateType, - }); + return true; + }), + assignDomainServer: adminProcedure + .input(apiAssignDomain) + .mutation(async ({ ctx, input }) => { + if (IS_CLOUD) { + return true; + } + const admin = await updateAdmin(ctx.user.authId, { + host: input.host, + ...(input.letsEncryptEmail && { + letsEncryptEmail: input.letsEncryptEmail, + }), + certificateType: input.certificateType, + }); - if (!admin) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Admin not found", - }); - } + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } - updateServerTraefik(admin, input.host); - if (input.letsEncryptEmail) { - updateLetsEncryptEmail(input.letsEncryptEmail); - } + updateServerTraefik(admin, input.host); + if (input.letsEncryptEmail) { + updateLetsEncryptEmail(input.letsEncryptEmail); + } - return admin; - }), - cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { - if (IS_CLOUD) { - return true; - } - await updateAdmin(ctx.user.authId, { - sshPrivateKey: null, - }); - return true; - }), - updateDockerCleanup: adminProcedure - .input(apiUpdateDockerCleanup) - .mutation(async ({ input, ctx }) => { - if (input.serverId) { - await updateServerById(input.serverId, { - enableDockerCleanup: input.enableDockerCleanup, - }); + return admin; + }), + cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { + if (IS_CLOUD) { + return true; + } + await updateAdmin(ctx.user.authId, { + sshPrivateKey: null, + }); + return true; + }), + updateDockerCleanup: adminProcedure + .input(apiUpdateDockerCleanup) + .mutation(async ({ input, ctx }) => { + if (input.serverId) { + await updateServerById(input.serverId, { + enableDockerCleanup: input.enableDockerCleanup, + }); - const server = await findServerById(input.serverId); + const server = await findServerById(input.serverId); - if (server.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this server", - }); - } + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } - if (server.enableDockerCleanup) { - const server = await findServerById(input.serverId); - if (server.serverStatus === "inactive") { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server is inactive", - }); - } - if (IS_CLOUD) { - await schedule({ - cronSchedule: "0 0 * * *", - serverId: input.serverId, - type: "server", - }); - } else { - scheduleJob(server.serverId, "0 0 * * *", async () => { - console.log( - `Docker Cleanup ${new Date().toLocaleString()}] Running...` - ); - await cleanUpUnusedImages(server.serverId); - await cleanUpDockerBuilder(server.serverId); - await cleanUpSystemPrune(server.serverId); - await sendDockerCleanupNotifications(server.adminId); - }); - } - } else { - if (IS_CLOUD) { - await removeJob({ - cronSchedule: "0 0 * * *", - serverId: input.serverId, - type: "server", - }); - } else { - const currentJob = scheduledJobs[server.serverId]; - currentJob?.cancel(); - } - } - } else if (!IS_CLOUD) { - const admin = await findAdminById(ctx.user.adminId); + if (server.enableDockerCleanup) { + const server = await findServerById(input.serverId); + if (server.serverStatus === "inactive") { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server is inactive", + }); + } + if (IS_CLOUD) { + await schedule({ + cronSchedule: "0 0 * * *", + serverId: input.serverId, + type: "server", + }); + } else { + scheduleJob(server.serverId, "0 0 * * *", async () => { + console.log( + `Docker Cleanup ${new Date().toLocaleString()}] Running...`, + ); + await cleanUpUnusedImages(server.serverId); + await cleanUpDockerBuilder(server.serverId); + await cleanUpSystemPrune(server.serverId); + await sendDockerCleanupNotifications(server.adminId); + }); + } + } else { + if (IS_CLOUD) { + await removeJob({ + cronSchedule: "0 0 * * *", + serverId: input.serverId, + type: "server", + }); + } else { + const currentJob = scheduledJobs[server.serverId]; + currentJob?.cancel(); + } + } + } else if (!IS_CLOUD) { + const admin = await findAdminById(ctx.user.adminId); - if (admin.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this admin", - }); - } - const adminUpdated = await updateAdmin(ctx.user.authId, { - enableDockerCleanup: input.enableDockerCleanup, - }); + if (admin.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this admin", + }); + } + const adminUpdated = await updateAdmin(ctx.user.authId, { + enableDockerCleanup: input.enableDockerCleanup, + }); - if (adminUpdated?.enableDockerCleanup) { - scheduleJob("docker-cleanup", "0 0 * * *", async () => { - console.log( - `Docker Cleanup ${new Date().toLocaleString()}] Running...` - ); - await cleanUpUnusedImages(); - await cleanUpDockerBuilder(); - await cleanUpSystemPrune(); - await sendDockerCleanupNotifications(admin.adminId); - }); - } else { - const currentJob = scheduledJobs["docker-cleanup"]; - currentJob?.cancel(); - } - } + if (adminUpdated?.enableDockerCleanup) { + scheduleJob("docker-cleanup", "0 0 * * *", async () => { + console.log( + `Docker Cleanup ${new Date().toLocaleString()}] Running...`, + ); + await cleanUpUnusedImages(); + await cleanUpDockerBuilder(); + await cleanUpSystemPrune(); + await sendDockerCleanupNotifications(admin.adminId); + }); + } else { + const currentJob = scheduledJobs["docker-cleanup"]; + currentJob?.cancel(); + } + } - return true; - }), + return true; + }), - readTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readMainConfig(); - return traefikConfig; - }), + readTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readMainConfig(); + return traefikConfig; + }), - updateTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeMainConfig(input.traefikConfig); - return true; - }), + updateTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeMainConfig(input.traefikConfig); + return true; + }), - readWebServerTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readConfig("dokploy"); - return traefikConfig; - }), - updateWebServerTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeConfig("dokploy", input.traefikConfig); - return true; - }), + readWebServerTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readConfig("dokploy"); + return traefikConfig; + }), + updateWebServerTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeConfig("dokploy", input.traefikConfig); + return true; + }), - readMiddlewareTraefikConfig: adminProcedure.query(() => { - if (IS_CLOUD) { - return true; - } - const traefikConfig = readConfig("middlewares"); - return traefikConfig; - }), + readMiddlewareTraefikConfig: adminProcedure.query(() => { + if (IS_CLOUD) { + return true; + } + const traefikConfig = readConfig("middlewares"); + return traefikConfig; + }), - updateMiddlewareTraefikConfig: adminProcedure - .input(apiTraefikConfig) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - writeConfig("middlewares", input.traefikConfig); - return true; - }), - getUpdateData: protectedProcedure.mutation(async () => { - if (IS_CLOUD) { - return DEFAULT_UPDATE_DATA; - } + updateMiddlewareTraefikConfig: adminProcedure + .input(apiTraefikConfig) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + writeConfig("middlewares", input.traefikConfig); + return true; + }), + getUpdateData: protectedProcedure.mutation(async () => { + if (IS_CLOUD) { + return DEFAULT_UPDATE_DATA; + } - return await getUpdateData(); - }), - updateServer: adminProcedure.mutation(async () => { - if (IS_CLOUD) { - return true; - } + return await getUpdateData(); + }), + updateServer: adminProcedure.mutation(async () => { + if (IS_CLOUD) { + return true; + } - await pullLatestRelease(); + await pullLatestRelease(); - // This causes restart of dokploy, thus it will not finish executing properly, so don't await it - // Status after restart is checked via frontend /api/health endpoint - void spawnAsync("docker", [ - "service", - "update", - "--force", - "--image", - getDokployImage(), - "dokploy", - ]); + // This causes restart of dokploy, thus it will not finish executing properly, so don't await it + // Status after restart is checked via frontend /api/health endpoint + void spawnAsync("docker", [ + "service", + "update", + "--force", + "--image", + getDokployImage(), + "dokploy", + ]); - return true; - }), + return true; + }), - getDokployVersion: protectedProcedure.query(() => { - return packageInfo.version; - }), - getReleaseTag: protectedProcedure.query(() => { - return getDokployImageTag(); - }), - readDirectories: protectedProcedure - .input(apiServerSchema) - .query(async ({ ctx, input }) => { - try { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + getDokployVersion: protectedProcedure.query(() => { + return packageInfo.version; + }), + getReleaseTag: protectedProcedure.query(() => { + return getDokployImageTag(); + }), + readDirectories: protectedProcedure + .input(apiServerSchema) + .query(async ({ ctx, input }) => { + try { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); - const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); - return result || []; - } catch (error) { - throw error; - } - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); + const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); + return result || []; + } catch (error) { + throw error; + } + }), - updateTraefikFile: protectedProcedure - .input(apiModifyTraefikConfig) - .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + updateTraefikFile: protectedProcedure + .input(apiModifyTraefikConfig) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - await writeTraefikConfigInPath( - input.path, - input.traefikConfig, - input?.serverId - ); - return true; - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + await writeTraefikConfigInPath( + input.path, + input.traefikConfig, + input?.serverId, + ); + return true; + }), - readTraefikFile: protectedProcedure - .input(apiReadTraefikConfig) - .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - const canAccess = await canAccessToTraefikFiles(ctx.user.authId); + readTraefikFile: protectedProcedure + .input(apiReadTraefikConfig) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + const canAccess = await canAccessToTraefikFiles(ctx.user.authId); - if (!canAccess) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - return readConfigInPath(input.path, input.serverId); - }), - getIp: protectedProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - const admin = await findAdmin(); - return admin.serverIp; - }), + if (!canAccess) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + } + return readConfigInPath(input.path, input.serverId); + }), + getIp: protectedProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + const admin = await findAdmin(); + return admin.serverIp; + }), - getOpenApiDocument: protectedProcedure.query( - async ({ ctx }): Promise => { - const protocol = ctx.req.headers["x-forwarded-proto"]; - const url = `${protocol}://${ctx.req.headers.host}/api`; - const openApiDocument = generateOpenApiDocument(appRouter, { - title: "tRPC OpenAPI", - version: "1.0.0", - baseUrl: url, - docsUrl: `${url}/settings.getOpenApiDocument`, - tags: [ - "admin", - "docker", - "compose", - "registry", - "cluster", - "user", - "domain", - "destination", - "backup", - "deployment", - "mounts", - "certificates", - "settings", - "security", - "redirects", - "port", - "project", - "application", - "mysql", - "postgres", - "redis", - "mongo", - "mariadb", - "sshRouter", - "gitProvider", - "bitbucket", - "github", - "gitlab", - ], - }); + getOpenApiDocument: protectedProcedure.query( + async ({ ctx }): Promise => { + const protocol = ctx.req.headers["x-forwarded-proto"]; + const url = `${protocol}://${ctx.req.headers.host}/api`; + const openApiDocument = generateOpenApiDocument(appRouter, { + title: "tRPC OpenAPI", + version: "1.0.0", + baseUrl: url, + docsUrl: `${url}/settings.getOpenApiDocument`, + tags: [ + "admin", + "docker", + "compose", + "registry", + "cluster", + "user", + "domain", + "destination", + "backup", + "deployment", + "mounts", + "certificates", + "settings", + "security", + "redirects", + "port", + "project", + "application", + "mysql", + "postgres", + "redis", + "mongo", + "mariadb", + "sshRouter", + "gitProvider", + "bitbucket", + "github", + "gitlab", + ], + }); - openApiDocument.info = { - title: "Dokploy API", - description: "Endpoints for dokploy", - // TODO: get version from package.json - version: "1.0.0", - }; + openApiDocument.info = { + title: "Dokploy API", + description: "Endpoints for dokploy", + // TODO: get version from package.json + version: "1.0.0", + }; - return openApiDocument; - } - ), - readTraefikEnv: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = - "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik"; + return openApiDocument; + }, + ), + readTraefikEnv: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = + "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik"; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - return result.stdout.trim(); - } - if (!IS_CLOUD) { - const result = await execAsync(command); - return result.stdout.trim(); - } - }), + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + return result.stdout.trim(); + } + if (!IS_CLOUD) { + const result = await execAsync(command); + return result.stdout.trim(); + } + }), - writeTraefikEnv: adminProcedure - .input(z.object({ env: z.string(), serverId: z.string().optional() })) - .mutation(async ({ input }) => { - const envs = prepareEnvironmentVariables(input.env); - await initializeTraefik({ - env: envs, - serverId: input.serverId, - }); + writeTraefikEnv: adminProcedure + .input(z.object({ env: z.string(), serverId: z.string().optional() })) + .mutation(async ({ input }) => { + const envs = prepareEnvironmentVariables(input.env); + await initializeTraefik({ + env: envs, + serverId: input.serverId, + }); - return true; - }), - haveTraefikDashboardPortEnabled: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + return true; + }), + haveTraefikDashboardPortEnabled: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; - let stdout = ""; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - stdout = result.stdout; - } else if (!IS_CLOUD) { - const result = await execAsync( - "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik" - ); - stdout = result.stdout; - } + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync( + "docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik", + ); + stdout = result.stdout; + } - const parsed: any[] = JSON.parse(stdout.trim()); - for (const port of parsed) { - if (port.PublishedPort === 8080) { - return true; - } - } + const parsed: any[] = JSON.parse(stdout.trim()); + for (const port of parsed) { + if (port.PublishedPort === 8080) { + return true; + } + } - return false; - }), + return false; + }), - readStatsLogs: adminProcedure - .meta({ - openapi: { - path: "/read-stats-logs", - method: "POST", - override: true, - enabled: false, - }, - }) - .input(apiReadStatsLogs) - .query(({ input }) => { - if (IS_CLOUD) { - return { - data: [], - totalCount: 0, - }; - } - const rawConfig = readMonitoringConfig(); - const parsedConfig = parseRawConfig( - rawConfig as string, - input.page, - input.sort, - input.search, - input.status - ); + readStatsLogs: adminProcedure + .meta({ + openapi: { + path: "/read-stats-logs", + method: "POST", + override: true, + enabled: false, + }, + }) + .input(apiReadStatsLogs) + .query(({ input }) => { + if (IS_CLOUD) { + return { + data: [], + totalCount: 0, + }; + } + const rawConfig = readMonitoringConfig(); + const parsedConfig = parseRawConfig( + rawConfig as string, + input.page, + input.sort, + input.search, + input.status, + ); - return parsedConfig; - }), - readStats: adminProcedure.query(() => { - if (IS_CLOUD) { - return []; - } - const rawConfig = readMonitoringConfig(); - const processedLogs = processLogs(rawConfig as string); - return processedLogs || []; - }), - getLogRotateStatus: adminProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - return await logRotationManager.getStatus(); - }), - toggleLogRotate: adminProcedure - .input( - z.object({ - enable: z.boolean(), - }) - ) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - if (input.enable) { - await logRotationManager.activate(); - } else { - await logRotationManager.deactivate(); - } + return parsedConfig; + }), + readStats: adminProcedure.query(() => { + if (IS_CLOUD) { + return []; + } + const rawConfig = readMonitoringConfig(); + const processedLogs = processLogs(rawConfig as string); + return processedLogs || []; + }), + getLogRotateStatus: adminProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + return await logRotationManager.getStatus(); + }), + toggleLogRotate: adminProcedure + .input( + z.object({ + enable: z.boolean(), + }), + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + if (input.enable) { + await logRotationManager.activate(); + } else { + await logRotationManager.deactivate(); + } - return true; - }), - haveActivateRequests: adminProcedure.query(async () => { - if (IS_CLOUD) { - return true; - } - const config = readMainConfig(); + return true; + }), + haveActivateRequests: adminProcedure.query(async () => { + if (IS_CLOUD) { + return true; + } + const config = readMainConfig(); - if (!config) return false; - const parsedConfig = load(config) as { - accessLog?: { - filePath: string; - }; - }; + if (!config) return false; + const parsedConfig = load(config) as { + accessLog?: { + filePath: string; + }; + }; - return !!parsedConfig?.accessLog?.filePath; - }), - toggleRequests: adminProcedure - .input( - z.object({ - enable: z.boolean(), - }) - ) - .mutation(async ({ input }) => { - if (IS_CLOUD) { - return true; - } - const mainConfig = readMainConfig(); - if (!mainConfig) return false; + return !!parsedConfig?.accessLog?.filePath; + }), + toggleRequests: adminProcedure + .input( + z.object({ + enable: z.boolean(), + }), + ) + .mutation(async ({ input }) => { + if (IS_CLOUD) { + return true; + } + const mainConfig = readMainConfig(); + if (!mainConfig) return false; - const currentConfig = load(mainConfig) as { - accessLog?: { - filePath: string; - }; - }; + const currentConfig = load(mainConfig) as { + accessLog?: { + filePath: string; + }; + }; - if (input.enable) { - const config = { - accessLog: { - filePath: "/etc/dokploy/traefik/dynamic/access.log", - format: "json", - bufferingSize: 100, - filters: { - retryAttempts: true, - minDuration: "10ms", - }, - }, - }; - currentConfig.accessLog = config.accessLog; - } else { - currentConfig.accessLog = undefined; - } + if (input.enable) { + const config = { + accessLog: { + filePath: "/etc/dokploy/traefik/dynamic/access.log", + format: "json", + bufferingSize: 100, + filters: { + retryAttempts: true, + minDuration: "10ms", + }, + }, + }; + currentConfig.accessLog = config.accessLog; + } else { + currentConfig.accessLog = undefined; + } - writeMainConfig(dump(currentConfig)); + writeMainConfig(dump(currentConfig)); - return true; - }), - isCloud: protectedProcedure.query(async () => { - return IS_CLOUD; - }), - health: publicProcedure.query(async () => { - if (IS_CLOUD) { - try { - await db.execute(sql`SELECT 1`); - return { status: "ok" }; - } catch (error) { - console.error("Database connection error:", error); - throw error; - } - } - return { status: "not_cloud" }; - }), - setupGPU: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - if (IS_CLOUD && !input.serverId) { - throw new Error("Select a server to enable the GPU Setup"); - } + return true; + }), + isCloud: protectedProcedure.query(async () => { + return IS_CLOUD; + }), + health: publicProcedure.query(async () => { + if (IS_CLOUD) { + try { + await db.execute(sql`SELECT 1`); + return { status: "ok" }; + } catch (error) { + console.error("Database connection error:", error); + throw error; + } + } + return { status: "not_cloud" }; + }), + setupGPU: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + if (IS_CLOUD && !input.serverId) { + throw new Error("Select a server to enable the GPU Setup"); + } - try { - await setupGPUSupport(input.serverId); - return { success: true }; - } catch (error) { - console.error("GPU Setup Error:", error); - throw error; - } - }), - checkGPUStatus: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - }) - ) - .query(async ({ input }) => { - if (IS_CLOUD && !input.serverId) { - return { - driverInstalled: false, - driverVersion: undefined, - gpuModel: undefined, - runtimeInstalled: false, - runtimeConfigured: false, - cudaSupport: undefined, - cudaVersion: undefined, - memoryInfo: undefined, - availableGPUs: 0, - swarmEnabled: false, - gpuResources: 0, - }; - } + try { + await setupGPUSupport(input.serverId); + return { success: true }; + } catch (error) { + console.error("GPU Setup Error:", error); + throw error; + } + }), + checkGPUStatus: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + if (IS_CLOUD && !input.serverId) { + return { + driverInstalled: false, + driverVersion: undefined, + gpuModel: undefined, + runtimeInstalled: false, + runtimeConfigured: false, + cudaSupport: undefined, + cudaVersion: undefined, + memoryInfo: undefined, + availableGPUs: 0, + swarmEnabled: false, + gpuResources: 0, + }; + } - try { - return await checkGPUStatus(input.serverId || ""); - } catch (error) { - throw new Error("Failed to check GPU status"); - } - }), - updateTraefikPorts: adminProcedure - .input( - z.object({ - serverId: z.string().optional(), - additionalPorts: z.array( - z.object({ - targetPort: z.number(), - publishedPort: z.number(), - publishMode: z.enum(["ingress", "host"]).default("host"), - }) - ), - }) - ) - .mutation(async ({ input }) => { - try { - if (IS_CLOUD && !input.serverId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Please set a serverId to update Traefik ports", - }); - } - await initializeTraefik({ - serverId: input.serverId, - additionalPorts: input.additionalPorts, - }); - return true; - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - error instanceof Error - ? error.message - : "Error updating Traefik ports", - cause: error, - }); - } - }), - getTraefikPorts: adminProcedure - .input(apiServerSchema) - .query(async ({ input }) => { - const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + try { + return await checkGPUStatus(input.serverId || ""); + } catch (error) { + throw new Error("Failed to check GPU status"); + } + }), + updateTraefikPorts: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + additionalPorts: z.array( + z.object({ + targetPort: z.number(), + publishedPort: z.number(), + publishMode: z.enum(["ingress", "host"]).default("host"), + }), + ), + }), + ) + .mutation(async ({ input }) => { + try { + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please set a serverId to update Traefik ports", + }); + } + await initializeTraefik({ + serverId: input.serverId, + additionalPorts: input.additionalPorts, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error updating Traefik ports", + cause: error, + }); + } + }), + getTraefikPorts: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; - try { - let stdout = ""; - if (input?.serverId) { - const result = await execAsyncRemote(input.serverId, command); - stdout = result.stdout; - } else if (!IS_CLOUD) { - const result = await execAsync(command); - stdout = result.stdout; - } + try { + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync(command); + stdout = result.stdout; + } - const ports: { - Protocol: string; - TargetPort: number; - PublishedPort: number; - PublishMode: string; - }[] = JSON.parse(stdout.trim()); + const ports: { + Protocol: string; + TargetPort: number; + PublishedPort: number; + PublishMode: string; + }[] = JSON.parse(stdout.trim()); - // Filter out the default ports (80, 443, and optionally 8080) - const additionalPorts = ports - .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) - .map((port) => ({ - targetPort: port.TargetPort, - publishedPort: port.PublishedPort, - publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", - })); + // Filter out the default ports (80, 443, and optionally 8080) + const additionalPorts = ports + .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) + .map((port) => ({ + targetPort: port.TargetPort, + publishedPort: port.PublishedPort, + publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", + })); - return additionalPorts; - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to get Traefik ports", - cause: error, - }); - } - }), + return additionalPorts; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get Traefik ports", + cause: error, + }); + } + }), }); // { // "Parallelism": 1, From adaf12a9a4221661a2ed49b231917f2f88bd890d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:16:00 -0600 Subject: [PATCH 15/17] refactor: update --- apps/dokploy/lib/languages.ts | 2 +- apps/dokploy/public/locales/uk/settings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/lib/languages.ts b/apps/dokploy/lib/languages.ts index 883570bd..de08a9fb 100644 --- a/apps/dokploy/lib/languages.ts +++ b/apps/dokploy/lib/languages.ts @@ -1,7 +1,7 @@ export const Languages = { english: { code: "en", name: "English" }, polish: { code: "pl", name: "Polski" }, - ukrainian: {code: 'uk', name: "Українська"}, + ukrainian: { code: "uk", name: "Українська" }, russian: { code: "ru", name: "Русский" }, french: { code: "fr", name: "Français" }, german: { code: "de", name: "Deutsch" }, diff --git a/apps/dokploy/public/locales/uk/settings.json b/apps/dokploy/public/locales/uk/settings.json index d917f326..766a1bff 100644 --- a/apps/dokploy/public/locales/uk/settings.json +++ b/apps/dokploy/public/locales/uk/settings.json @@ -55,4 +55,4 @@ "settings.terminal.ipAddress": "IP-адреса", "settings.terminal.port": "Порт", "settings.terminal.username": "Ім'я користувача" -} \ No newline at end of file +} From c7d5900e111dd0f52918de357675d97fdeb8b161 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:25:25 -0600 Subject: [PATCH 16/17] chore: bump version --- 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 094823af..f1123794 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.17.5", + "version": "v0.17.6", "private": true, "license": "Apache-2.0", "type": "module", From 81248ed03f0501d8f126665341bb74f40481e8a0 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:39:28 -0600 Subject: [PATCH 17/17] fix: add continue to process all applications --- apps/dokploy/pages/api/deploy/github.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index 2ee17394..ff7a9221 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -121,7 +121,7 @@ export default async function handler( if (IS_CLOUD && app.serverId) { jobData.serverId = app.serverId; await deploy(jobData); - return true; + continue; } await myQueue.add( "deployments", @@ -156,7 +156,7 @@ export default async function handler( if (IS_CLOUD && composeApp.serverId) { jobData.serverId = composeApp.serverId; await deploy(jobData); - return true; + continue; } await myQueue.add(