From c80a31e8c4868cd205df009f6bfd1e36befdca0e Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:16:29 -0600 Subject: [PATCH 1/5] refactor: improve code formatting and structure in ShowGeneralMongo component - Standardized indentation and formatting for better readability. - Enhanced tooltip integration within button elements for improved user experience. - Maintained functionality for deploying, reloading, starting, and stopping MongoDB instances while ensuring consistent code style. --- .../mongo/general/show-general-mongo.tsx | 464 +++++++++--------- 1 file changed, 237 insertions(+), 227 deletions(-) diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx index dfbf501e..fdc28adc 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx @@ -3,10 +3,10 @@ import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; @@ -16,236 +16,246 @@ import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; interface Props { - mongoId: string; + mongoId: string; } export const ShowGeneralMongo = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId } - ); + const { data, refetch } = api.mongo.one.useQuery( + { + mongoId, + }, + { enabled: !!mongoId }, + ); - const { mutateAsync: reload, isLoading: isReloading } = - api.mongo.reload.useMutation(); + const { mutateAsync: reload, isLoading: isReloading } = + api.mongo.reload.useMutation(); - const { mutateAsync: start, isLoading: isStarting } = - api.mongo.start.useMutation(); + const { mutateAsync: start, isLoading: isStarting } = + api.mongo.start.useMutation(); - const { mutateAsync: stop, isLoading: isStopping } = - api.mongo.stop.useMutation(); + const { mutateAsync: stop, isLoading: isStopping } = + api.mongo.stop.useMutation(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [filteredLogs, setFilteredLogs] = useState([]); - const [isDeploying, setIsDeploying] = useState(false); - api.mongo.deployWithLogs.useSubscription( - { - mongoId: mongoId, - }, - { - enabled: isDeploying, - onData(log) { - if (!isDrawerOpen) { - setIsDrawerOpen(true); - } + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.mongo.deployWithLogs.useSubscription( + { + mongoId: mongoId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } - if (log === "Deployment completed successfully!") { - setIsDeploying(false); - } + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } - const parsedLogs = parseLogs(log); - setFilteredLogs((prev) => [...prev, ...parsedLogs]); - }, - onError(error) { - console.error("Deployment logs error:", error); - setIsDeploying(false); - }, - } - ); - return ( - <> -
- - - Deploy Settings - - - - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - - - - -

Downloads and sets up the MongoDB database

-
-
-
-
- { - await reload({ - mongoId: mongoId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Mongo reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading Mongo"); - }); - }} - > - - - - - - -

Restart the MongoDB service without rebuilding

-
-
-
-
- {data?.applicationStatus === "idle" ? ( - { - await start({ - mongoId: mongoId, - }) - .then(() => { - toast.success("Mongo started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting Mongo"); - }); - }} - > - - - - - - -

- Start the MongoDB database (requires a previous - successful setup) -

-
-
-
-
- ) : ( - { - await stop({ - mongoId: mongoId, - }) - .then(() => { - toast.success("Mongo stopped successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error stopping Mongo"); - }); - }} - > - - - - - - -

Stop the currently running MongoDB database

-
-
-
-
- )} -
- - - - - - - -

Open a terminal to the MongoDB container

-
-
-
-
-
-
- { - setIsDrawerOpen(false); - setFilteredLogs([]); - setIsDeploying(false); - refetch(); - }} - filteredLogs={filteredLogs} - /> -
- - ); + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); + return ( + <> +
+ + + Deploy Settings + + + + { + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + { + await reload({ + mongoId: mongoId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Mongo reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Mongo"); + }); + }} + > + + + {data?.applicationStatus === "idle" ? ( + { + await start({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mongo"); + }); + }} + > + + + ) : ( + { + await stop({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mongo"); + }); + }} + > + + + )} + + + + + + + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + /> +
+ + ); }; From 0722182650fee325d670cecf36729263226694f5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:59:39 -0600 Subject: [PATCH 2/5] feat(auth): implement user creation validation and IP update logic - Added validation for user creation to check for existing admin presence and validate x-dokploy-token. - Integrated public IP retrieval for user updates when not in cloud environment. - Enhanced error handling with APIError for better feedback during user creation process. --- packages/server/src/lib/auth.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 9043f203..6695756c 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -8,6 +8,10 @@ import { db } from "../db"; import * as schema from "../db/schema"; import { sendEmail } from "../verification/send-verification-email"; import { IS_CLOUD } from "../constants"; +import { getPublicIpWithFallback } from "../wss/utils"; +import { updateUser } from "../services/user"; +import { getUserByToken } from "../services/admin"; +import { APIError } from "better-auth/api"; const { handler, api } = betterAuth({ database: drizzleAdapter(db, { @@ -88,11 +92,40 @@ const { handler, api } = betterAuth({ databaseHooks: { user: { create: { + before: async (_user, context) => { + if (!IS_CLOUD) { + const xDokployToken = + context?.request?.headers?.get("x-dokploy-token"); + if (xDokployToken) { + const user = await getUserByToken(xDokployToken); + if (!user) { + throw new APIError("BAD_REQUEST", { + message: "User not found", + }); + } + } else { + const isAdminPresent = await db.query.member.findFirst({ + where: eq(schema.member.role, "owner"), + }); + if (isAdminPresent) { + throw new APIError("BAD_REQUEST", { + message: "Admin is already created", + }); + } + } + } + }, after: async (user) => { const isAdminPresent = await db.query.member.findFirst({ where: eq(schema.member.role, "owner"), }); + if (!IS_CLOUD) { + await updateUser(user.id, { + serverIp: await getPublicIpWithFallback(), + }); + } + if (IS_CLOUD || !isAdminPresent) { await db.transaction(async (tx) => { const organization = await tx From 6a388fe3706c66ed89b8f2427545a529eac8149d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 18 Mar 2025 00:13:55 -0600 Subject: [PATCH 3/5] feat(domain): add validation for traefik.me domain IP address requirement - Implemented a check to ensure an IP address is set for traefik.me domains in the AddDomain and AddDomainCompose components. - Integrated a new API query to determine if traefik.me domains can be generated based on the server's IP address. - Added user feedback through alert messages when the IP address is not configured. --- .../application/domains/add-domain.tsx | 23 +++++++++++++++++++ .../dashboard/compose/domains/add-domain.tsx | 21 +++++++++++++++++ apps/dokploy/server/api/routers/domain.ts | 15 ++++++++++++ 3 files changed, 59 insertions(+) diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index f91218ce..8b1fa7e1 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -42,6 +42,7 @@ import { domain } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { Dices } from "lucide-react"; import type z from "zod"; +import Link from "next/link"; type Domain = z.infer; @@ -83,6 +84,13 @@ export const AddDomain = ({ const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } = api.domain.generateDomain.useMutation(); + const { data: canGenerateTraefikMeDomains } = + api.domain.canGenerateTraefikMeDomains.useQuery({ + serverId: application?.serverId || "", + }); + + console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains); + const form = useForm({ resolver: zodResolver(domain), defaultValues: { @@ -186,6 +194,21 @@ export const AddDomain = ({ name="host" render={({ field }) => ( + {!canGenerateTraefikMeDomains && + field.value.includes("traefik.me") && ( + + You need to set an IP address in your{" "} + + {application?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to make your traefik.me domain work. + + )} Host
diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 9b412c83..975ce1ff 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -42,6 +42,7 @@ import { domainCompose } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; import type z from "zod"; +import Link from "next/link"; type Domain = z.infer; @@ -102,6 +103,11 @@ export const AddDomainCompose = ({ ? api.domain.update.useMutation() : api.domain.create.useMutation(); + const { data: canGenerateTraefikMeDomains } = + api.domain.canGenerateTraefikMeDomains.useQuery({ + serverId: compose?.serverId || "", + }); + const form = useForm({ resolver: zodResolver(domainCompose), defaultValues: { @@ -313,6 +319,21 @@ export const AddDomainCompose = ({ name="host" render={({ field }) => ( + {!canGenerateTraefikMeDomains && + field.value.includes("traefik.me") && ( + + You need to set an IP address in your{" "} + + {compose?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to make your traefik.me domain work. + + )} Host
diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index aac2a016..9e81bee1 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -13,7 +13,9 @@ import { findDomainById, findDomainsByApplicationId, findDomainsByComposeId, + findOrganizationById, findPreviewDeploymentById, + findServerById, generateTraefikMeDomain, manageDomain, removeDomain, @@ -94,6 +96,19 @@ export const domainRouter = createTRPCRouter({ input.serverId, ); }), + canGenerateTraefikMeDomains: protectedProcedure + .input(z.object({ serverId: z.string() })) + .query(async ({ input, ctx }) => { + const organization = await findOrganizationById( + ctx.session.activeOrganizationId, + ); + + if (input.serverId) { + const server = await findServerById(input.serverId); + return server.ipAddress; + } + return organization?.owner.serverIp; + }), update: protectedProcedure .input(apiUpdateDomain) From 4fa5e10789cf7e3f73feb300b965dd9055f3a0d2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 18 Mar 2025 00:18:39 -0600 Subject: [PATCH 4/5] chore(package): bump version to v0.20.6 --- 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 84d24897..cf46d3ac 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.20.5", + "version": "v0.20.6", "private": true, "license": "Apache-2.0", "type": "module", From ea6cfc9d29ed270a113729890d7a6bb8d2d68bc5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 18 Mar 2025 00:47:50 -0600 Subject: [PATCH 5/5] feat(backup): enhance RestoreBackup component and API to include serverId - Added serverId prop to RestoreBackup component for better context during backup restoration. - Updated ShowBackups component to pass serverId from the Postgres object. - Modified backup API to handle serverId, allowing remote execution of backup commands when specified. - Improved file display in RestoreBackup for better user experience. --- .../database/backups/restore-backup.tsx | 12 ++++++++++-- .../dashboard/database/backups/show-backups.tsx | 12 ++++++++++-- apps/dokploy/server/api/routers/backup.ts | 17 +++++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index c761fc70..5dcd7732 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -48,6 +48,7 @@ import { toast } from "sonner"; interface Props { databaseId: string; databaseType: Exclude; + serverId: string | null; } const RestoreBackupSchema = z.object({ @@ -76,7 +77,11 @@ const RestoreBackupSchema = z.object({ type RestoreBackup = z.infer; -export const RestoreBackup = ({ databaseId, databaseType }: Props) => { +export const RestoreBackup = ({ + databaseId, + databaseType, + serverId, +}: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); @@ -101,6 +106,7 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => { { destinationId: destionationId, search, + serverId: serverId ?? "", }, { enabled: isOpen && !!destionationId, @@ -304,7 +310,9 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => { form.setValue("backupFile", file); }} > - {file} +
+ {file} +
{ {postgres && postgres?.backups?.length > 0 && (
- +
)} @@ -108,7 +112,11 @@ export const ShowBackups = ({ id, type }: Props) => { databaseType={type} refetch={refetch} /> - +
) : ( diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 8e585b7c..9ed8c6f9 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -31,7 +31,10 @@ import { import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { execAsync } from "@dokploy/server/utils/process/execAsync"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; import { findDestinationById } from "@dokploy/server/services/destination"; import { @@ -229,6 +232,7 @@ export const backupRouter = createTRPCRouter({ z.object({ destinationId: z.string(), search: z.string(), + serverId: z.string().optional(), }), ) .query(async ({ input }) => { @@ -250,7 +254,16 @@ export const backupRouter = createTRPCRouter({ const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`; - const { stdout } = await execAsync(listCommand); + let stdout = ""; + + if (input.serverId) { + const result = await execAsyncRemote(listCommand, input.serverId); + stdout = result.stdout; + } else { + const result = await execAsync(listCommand); + stdout = result.stdout; + } + const files = stdout.split("\n").filter(Boolean); const results = baseDir