diff --git a/README.md b/README.md index 2f766e6ae..ade68e822 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Dokploy include multiples features to make your life easier. * **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing. * **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource. * **Docker Management**: Easily deploy and manage Docker containers. -* **CLI (Soon⌛)**: Manage your applications and databases using the command line. +* **CLI/API**: Manage your applications and databases using the command line or trought the API. * **Self-Hosted**: Self-host Dokploy on your VPS. diff --git a/components/dashboard/application/deployments/show-deployments.tsx b/components/dashboard/application/deployments/show-deployments.tsx index f70808a14..ff26dc849 100644 --- a/components/dashboard/application/deployments/show-deployments.tsx +++ b/components/dashboard/application/deployments/show-deployments.tsx @@ -86,6 +86,11 @@ export const ShowDeployments = ({ applicationId }: Props) => { {deployment.title} + {deployment.description && ( + + {deployment.description} + + )}
diff --git a/components/dashboard/application/domains/generate-domain.tsx b/components/dashboard/application/domains/generate-domain.tsx new file mode 100644 index 000000000..92ca19bd2 --- /dev/null +++ b/components/dashboard/application/domains/generate-domain.tsx @@ -0,0 +1,79 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { RefreshCcw } from "lucide-react"; +import { GenerateTraefikMe } from "./generate-traefikme"; +import { GenerateWildCard } from "./generate-wildcard"; +import Link from "next/link"; +import { api } from "@/utils/api"; + +interface Props { + applicationId: string; +} + +export const GenerateDomain = ({ applicationId }: Props) => { + return ( + + + + + + + Generate Domain + + Generate Domains for your applications + + + +
+
    +
  • +
    +
    + 1. Generate TraefikMe Domain +
    +
    + This option generates a free domain provided by{" "} + + TraefikMe + + . We recommend using this for quick domain testing or if you + don't have a domain yet. +
    +
    +
  • + {/*
  • +
    +
    + 2. Use Wildcard Domain +
    +
    + To use this option, you need to set up an 'A' record in your + domain provider. For example, create a record for + *.yourdomain.com. +
    +
    +
  • */} +
+
+ + {/* */} +
+
+
+
+ ); +}; diff --git a/components/dashboard/application/domains/generate-traefikme.tsx b/components/dashboard/application/domains/generate-traefikme.tsx new file mode 100644 index 000000000..264a626f1 --- /dev/null +++ b/components/dashboard/application/domains/generate-traefikme.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { RefreshCcw } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + applicationId: string; +} +export const GenerateTraefikMe = ({ applicationId }: Props) => { + const { mutateAsync, isLoading } = api.domain.generateDomain.useMutation(); + const utils = api.useUtils(); + return ( + + + + + + + + Are you sure to generate a new domain? + + + This will generate a new domain and will be used to access to the + application + + + + Cancel + { + await mutateAsync({ + applicationId, + }) + .then((data) => { + utils.domain.byApplicationId.invalidate({ + applicationId: applicationId, + }); + utils.application.readTraefikConfig.invalidate({ + applicationId: applicationId, + }); + toast.success("Generated Domain succesfully"); + }) + .catch(() => { + toast.error("Error to generate Domain"); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/application/domains/generate-wildcard.tsx b/components/dashboard/application/domains/generate-wildcard.tsx new file mode 100644 index 000000000..11babebdb --- /dev/null +++ b/components/dashboard/application/domains/generate-wildcard.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { SquareAsterisk } from "lucide-react"; +import { toast } from "sonner"; + +interface Props { + applicationId: string; +} +export const GenerateWildCard = ({ applicationId }: Props) => { + const { mutateAsync, isLoading } = api.domain.generateWildcard.useMutation(); + const utils = api.useUtils(); + return ( + + + + + + + + Are you sure to generate a new wildcard domain? + + + This will generate a new domain and will be used to access to the + application + + + + Cancel + { + await mutateAsync({ + applicationId, + }) + .then((data) => { + utils.domain.byApplicationId.invalidate({ + applicationId: applicationId, + }); + utils.application.readTraefikConfig.invalidate({ + applicationId: applicationId, + }); + toast.success("Generated Domain succesfully"); + }) + .catch((e) => { + toast.error(`Error to generate Domain: ${e.message}`); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/application/domains/show-domains.tsx b/components/dashboard/application/domains/show-domains.tsx index c12d91401..d4df0366a 100644 --- a/components/dashboard/application/domains/show-domains.tsx +++ b/components/dashboard/application/domains/show-domains.tsx @@ -6,7 +6,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { ExternalLink, GlobeIcon } from "lucide-react"; +import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; import { Input } from "@/components/ui/input"; @@ -14,6 +14,7 @@ import { DeleteDomain } from "./delete-domain"; import Link from "next/link"; import { AddDomain } from "./add-domain"; import { UpdateDomain } from "./update-domain"; +import { GenerateDomain } from "./generate-domain"; interface Props { applicationId: string; @@ -31,7 +32,7 @@ export const ShowDomains = ({ applicationId }: Props) => { return (
- +
Domains @@ -39,11 +40,16 @@ export const ShowDomains = ({ applicationId }: Props) => {
- {data && data?.length > 0 && ( - - Add Domain - - )} +
+ {data && data?.length > 0 && ( + + Add Domain + + )} + {data && data?.length > 0 && ( + + )} +
{data?.length === 0 ? ( @@ -53,9 +59,13 @@ export const ShowDomains = ({ applicationId }: Props) => { To access to the application is required to set at least 1 domain - - Add Domain - +
+ + Add Domain + + + +
) : (
diff --git a/components/dashboard/monitoring/docker/show.tsx b/components/dashboard/monitoring/docker/show.tsx index 0fae337ac..ea7556b58 100644 --- a/components/dashboard/monitoring/docker/show.tsx +++ b/components/dashboard/monitoring/docker/show.tsx @@ -141,7 +141,13 @@ export const DockerMonitoring = ({ network: data.network[data.network.length - 1] ?? currentData.network, disk: data.disk[data.disk.length - 1] ?? currentData.disk, }); - setAcummulativeData(data); + setAcummulativeData({ + block: data?.block || [], + cpu: data?.cpu || [], + disk: data?.disk || [], + memory: data?.memory || [], + network: data?.network || [], + }); }, [data]); useEffect(() => { diff --git a/components/dashboard/projects/show.tsx b/components/dashboard/projects/show.tsx index 124a447d1..8df235833 100644 --- a/components/dashboard/projects/show.tsx +++ b/components/dashboard/projects/show.tsx @@ -76,8 +76,8 @@ export const ShowProjects = () => { project?.compose.length; return (
- - + + - - - - -
- - - {project.name} - -
+ + + +
+ + + {project.name} + +
- - {project.description} + + {project.description} + -
-
- - - - - - - Actions - +
+ + + + + + + Actions + - +
e.stopPropagation()}> + +
- {(auth?.rol === "admin" || - user?.canDeleteProjects) && ( - - - e.preventDefault()} - > - - Delete - - - - - - Are you sure to delete this project? - - {!emptyServices ? ( -
- - - You have active services, please delete - them first - -
- ) : ( - - This action cannot be undone - - )} -
- - Cancel - { - await mutateAsync({ - projectId: project.projectId, - }) - .then(() => { - toast.success( - "Project delete succesfully", - ); - }) - .catch(() => { - toast.error( - "Error to delete this project", - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
+
e.stopPropagation()}> + {(auth?.rol === "admin" || + user?.canDeleteProjects) && ( + + + e.preventDefault()} + > + + Delete + + + + + + Are you sure to delete this project? + + {!emptyServices ? ( +
+ + + You have active services, please + delete them first + +
+ ) : ( + + This action cannot be undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: project.projectId, + }) + .then(() => { + toast.success( + "Project delete succesfully", + ); + }) + .catch(() => { + toast.error( + "Error to delete this project", + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 ? "service" : "services"} +
- - - -
- Created - - {totalServices}{" "} - {totalServices === 1 ? "service" : "services"} - -
-
- +
+ +
); })} diff --git a/drizzle/0017_minor_post.sql b/drizzle/0017_minor_post.sql new file mode 100644 index 000000000..80a71d095 --- /dev/null +++ b/drizzle/0017_minor_post.sql @@ -0,0 +1 @@ +ALTER TABLE "deployment" ADD COLUMN "description" text; \ No newline at end of file diff --git a/drizzle/0017_yummy_norrin_radd.sql b/drizzle/0017_yummy_norrin_radd.sql deleted file mode 100644 index 570a343e1..000000000 --- a/drizzle/0017_yummy_norrin_radd.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json index 83f744c5f..afeb4aa35 100644 --- a/drizzle/meta/0017_snapshot.json +++ b/drizzle/meta/0017_snapshot.json @@ -1,5 +1,5 @@ { - "id": "7610c85e-c3e4-4a32-8ce9-7f48b298f956", + "id": "ec852f38-886a-43b4-9295-73984ed8ef45", "prevId": "2d8d7670-b942-4573-9c44-6e81d2a2fa16", "version": "6", "dialect": "postgresql", @@ -465,7 +465,7 @@ "name": "token", "type": "text", "primaryKey": false, - "notNull": false + "notNull": true }, "isRegistered": { "name": "isRegistered", @@ -1585,6 +1585,12 @@ "primaryKey": false, "notNull": true }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, "status": { "name": "status", "type": "deploymentStatus", diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 79bef40a6..7a8a186f6 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -124,8 +124,8 @@ { "idx": 17, "version": "6", - "when": 1719109531147, - "tag": "0017_yummy_norrin_radd", + "when": 1719547174326, + "tag": "0017_minor_post", "breakpoints": true } ] diff --git a/package.json b/package.json index 8442d502b..6745af92b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.2.4", + "version": "v0.2.5", "private": true, "license": "AGPL-3.0-only", "type": "module", diff --git a/pages/api/[...trpc].ts b/pages/api/[...trpc].ts new file mode 100644 index 000000000..7f1d41d16 --- /dev/null +++ b/pages/api/[...trpc].ts @@ -0,0 +1,29 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { createOpenApiNextHandler } from "@dokploy/trpc-openapi"; +import { appRouter } from "@/server/api/root"; +import { createTRPCContext } from "@/server/api/trpc"; +import { validateBearerToken } from "@/server/auth/token"; +import { validateRequest } from "@/server/auth/auth"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + let { session, user } = await validateBearerToken(req); + + if (!session) { + const cookieResult = await validateRequest(req, res); + session = cookieResult.session; + user = cookieResult.user; + } + + if (!user || !session) { + res.status(401).json({ message: "Unauthorized" }); + return; + } + + // @ts-ignore + return createOpenApiNextHandler({ + router: appRouter, + createContext: createTRPCContext, + })(req, res); +}; + +export default handler; diff --git a/pages/api/deploy/[refreshToken].ts b/pages/api/deploy/[refreshToken].ts index 1e6326641..c2aa3f16f 100644 --- a/pages/api/deploy/[refreshToken].ts +++ b/pages/api/deploy/[refreshToken].ts @@ -33,6 +33,7 @@ export default async function handler( } const deploymentTitle = extractCommitMessage(req.headers, req.body); + const deploymentHash = extractHash(req.headers, req.body); const sourceType = application.sourceType; @@ -75,6 +76,7 @@ export default async function handler( const jobData: DeploymentJob = { applicationId: application.applicationId as string, titleLog: deploymentTitle, + descriptionLog: `Hash: ${deploymentHash}`, type: "deploy", applicationType: "application", }; @@ -166,6 +168,37 @@ export const extractCommitMessage = (headers: any, body: any) => { return "NEW CHANGES"; }; +export const extractHash = (headers: any, body: any) => { + // GitHub + if (headers["x-github-event"]) { + return body.head_commit ? body.head_commit.id : ""; + } + + // GitLab + if (headers["x-gitlab-event"]) { + return ( + body.checkout_sha || + (body.commits && body.commits.length > 0 + ? body.commits[0].id + : "NEW COMMIT") + ); + } + + // Bitbucket + if (headers["x-event-key"]?.includes("repo:push")) { + return body.push.changes && body.push.changes.length > 0 + ? body.push.changes[0].new.target.hash + : "NEW COMMIT"; + } + + // Gitea + if (headers["x-gitea-event"]) { + return body.after || "NEW COMMIT"; + } + + return ""; +}; + export const extractBranchName = (headers: any, body: any) => { if (headers["x-github-event"] || headers["x-gitea-event"]) { return body?.ref?.replace("refs/heads/", ""); diff --git a/pages/api/deploy/compose/[refreshToken].ts b/pages/api/deploy/compose/[refreshToken].ts index 4f9b121b2..1c5739818 100644 --- a/pages/api/deploy/compose/[refreshToken].ts +++ b/pages/api/deploy/compose/[refreshToken].ts @@ -4,7 +4,11 @@ import type { DeploymentJob } from "@/server/queues/deployments-queue"; import { myQueue } from "@/server/queues/queueSetup"; import { eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; -import { extractBranchName, extractCommitMessage } from "../[refreshToken]"; +import { + extractBranchName, + extractCommitMessage, + extractHash, +} from "../[refreshToken]"; import { updateCompose } from "@/server/api/services/compose"; export default async function handler( @@ -34,7 +38,7 @@ export default async function handler( } const deploymentTitle = extractCommitMessage(req.headers, req.body); - + const deploymentHash = extractHash(req.headers, req.body); const sourceType = composeResult.sourceType; if (sourceType === "github") { @@ -61,6 +65,7 @@ export default async function handler( titleLog: deploymentTitle, type: "deploy", applicationType: "compose", + descriptionLog: `Hash: ${deploymentHash}`, }; await myQueue.add( "deployments", diff --git a/pages/swagger.tsx b/pages/swagger.tsx index 3db4e0c3b..715781531 100644 --- a/pages/swagger.tsx +++ b/pages/swagger.tsx @@ -6,15 +6,32 @@ import type { GetServerSidePropsContext, NextPage } from "next"; import dynamic from "next/dynamic"; import "swagger-ui-react/swagger-ui.css"; import superjson from "superjson"; +import { useEffect, useState } from "react"; const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false }); const Home: NextPage = () => { const { data } = api.settings.getOpenApiDocument.useQuery(); + const [spec, setSpec] = useState({}); + + useEffect(() => { + // Esto solo se ejecutará en el cliente + if (data) { + const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`; + const newSpec = { + ...data, + servers: [{ url: protocolAndHost }], + externalDocs: { + url: `${protocolAndHost}/settings.getOpenApiDocument`, + }, + }; + setSpec(newSpec); + } + }, [data]); return (
- +
); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6f41788b..bd08da38b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ dependencies: version: 10.45.2(@trpc/server@10.45.2) '@trpc/next': specifier: ^10.43.6 - version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0) + version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': specifier: ^10.43.6 version: 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0) @@ -193,10 +193,10 @@ dependencies: version: 3.3.7 next: specifier: ^14.1.3 - version: 14.2.3(react-dom@18.2.0)(react@18.2.0) + version: 14.2.4(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0) node-os-utils: specifier: 1.3.7 version: 1.3.7 @@ -2059,12 +2059,12 @@ packages: dev: false optional: true - /@next/env@14.2.3: - resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} + /@next/env@14.2.4: + resolution: {integrity: sha512-3EtkY5VDkuV2+lNmKlbkibIJxcO4oIHEhBWne6PaAp+76J9KoSsGvNikp6ivzAT8dhhBMYrm6op2pS1ApG0Hzg==} dev: false - /@next/swc-darwin-arm64@14.2.3: - resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} + /@next/swc-darwin-arm64@14.2.4: + resolution: {integrity: sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -2072,8 +2072,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.2.3: - resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} + /@next/swc-darwin-x64@14.2.4: + resolution: {integrity: sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -2081,48 +2081,44 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.2.3: - resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} + /@next/swc-linux-arm64-gnu@14.2.4: + resolution: {integrity: sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] requiresBuild: true dev: false optional: true - /@next/swc-linux-arm64-musl@14.2.3: - resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} + /@next/swc-linux-arm64-musl@14.2.4: + resolution: {integrity: sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] requiresBuild: true dev: false optional: true - /@next/swc-linux-x64-gnu@14.2.3: - resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} + /@next/swc-linux-x64-gnu@14.2.4: + resolution: {integrity: sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] requiresBuild: true dev: false optional: true - /@next/swc-linux-x64-musl@14.2.3: - resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} + /@next/swc-linux-x64-musl@14.2.4: + resolution: {integrity: sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] requiresBuild: true dev: false optional: true - /@next/swc-win32-arm64-msvc@14.2.3: - resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} + /@next/swc-win32-arm64-msvc@14.2.4: + resolution: {integrity: sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -2130,8 +2126,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.2.3: - resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} + /@next/swc-win32-ia32-msvc@14.2.4: + resolution: {integrity: sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -2139,8 +2135,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.2.3: - resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} + /@next/swc-win32-x64-msvc@14.2.4: + resolution: {integrity: sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4952,7 +4948,7 @@ packages: '@trpc/server': 10.45.2 dev: false - /@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.3)(react-dom@18.2.0)(react@18.2.0): + /@trpc/next@10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/react-query@10.45.2)(@trpc/server@10.45.2)(next@14.2.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RSORmfC+/nXdmRY1pQ0AalsVgSzwNAFbZLYHiTvPM5QQ8wmMEHilseCYMXpu0se/TbPt9zVR6Ka2d7O6zxKkXg==} peerDependencies: '@tanstack/react-query': ^4.18.0 @@ -4967,7 +4963,7 @@ packages: '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1)(@trpc/client@10.45.2)(@trpc/server@10.45.2)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 10.45.2 - next: 14.2.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.4(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -8104,14 +8100,14 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false - /next-themes@0.2.1(next@14.2.3)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.2.4)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.2.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.4(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -8120,8 +8116,8 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true - /next@14.2.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} + /next@14.2.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-R8/V7vugY+822rsQGQCjoLhMuC9oFj9SOi4Cl4b2wjDrseD0LRZ10W7R6Czo4w9ZznVSshKjuIomsRjvm9EKJQ==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -8138,7 +8134,7 @@ packages: sass: optional: true dependencies: - '@next/env': 14.2.3 + '@next/env': 14.2.4 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001598 @@ -8148,15 +8144,15 @@ packages: react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.3 - '@next/swc-darwin-x64': 14.2.3 - '@next/swc-linux-arm64-gnu': 14.2.3 - '@next/swc-linux-arm64-musl': 14.2.3 - '@next/swc-linux-x64-gnu': 14.2.3 - '@next/swc-linux-x64-musl': 14.2.3 - '@next/swc-win32-arm64-msvc': 14.2.3 - '@next/swc-win32-ia32-msvc': 14.2.3 - '@next/swc-win32-x64-msvc': 14.2.3 + '@next/swc-darwin-arm64': 14.2.4 + '@next/swc-darwin-x64': 14.2.4 + '@next/swc-linux-arm64-gnu': 14.2.4 + '@next/swc-linux-arm64-musl': 14.2.4 + '@next/swc-linux-x64-gnu': 14.2.4 + '@next/swc-linux-x64-musl': 14.2.4 + '@next/swc-win32-arm64-msvc': 14.2.4 + '@next/swc-win32-ia32-msvc': 14.2.4 + '@next/swc-win32-x64-msvc': 14.2.4 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -8551,7 +8547,7 @@ packages: dependencies: nanoid: 3.3.7 picocolors: 1.0.0 - source-map-js: 1.0.2 + source-map-js: 1.2.0 dev: false /postcss@8.4.35: @@ -9404,7 +9400,6 @@ packages: /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} - dev: true /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index df5f2b507..77b8befe6 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -162,6 +162,7 @@ export const applicationRouter = createTRPCRouter({ const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: "Rebuild deployment", + descriptionLog: "", type: "redeploy", applicationType: "application", }; @@ -294,6 +295,7 @@ export const applicationRouter = createTRPCRouter({ const jobData: DeploymentJob = { applicationId: input.applicationId, titleLog: "Manual deployment", + descriptionLog: "", type: "deploy", applicationType: "application", }; diff --git a/server/api/routers/compose.ts b/server/api/routers/compose.ts index f23550b40..a722d17a9 100644 --- a/server/api/routers/compose.ts +++ b/server/api/routers/compose.ts @@ -138,6 +138,7 @@ export const composeRouter = createTRPCRouter({ titleLog: "Manual deployment", type: "deploy", applicationType: "compose", + descriptionLog: "", }; await myQueue.add( "deployments", @@ -156,6 +157,7 @@ export const composeRouter = createTRPCRouter({ titleLog: "Rebuild deployment", type: "redeploy", applicationType: "compose", + descriptionLog: "", }; await myQueue.add( "deployments", diff --git a/server/api/routers/domain.ts b/server/api/routers/domain.ts index ace938c01..ec7f13d4f 100644 --- a/server/api/routers/domain.ts +++ b/server/api/routers/domain.ts @@ -12,6 +12,8 @@ import { createDomain, findDomainById, findDomainsByApplicationId, + generateDomain, + generateWildcard, removeDomainById, updateDomainById, } from "../services/domain"; @@ -35,6 +37,16 @@ export const domainRouter = createTRPCRouter({ .query(async ({ input }) => { return await findDomainsByApplicationId(input.applicationId); }), + generateDomain: protectedProcedure + .input(apiFindDomainByApplication) + .mutation(async ({ input }) => { + return generateDomain(input); + }), + generateWildcard: protectedProcedure + .input(apiFindDomainByApplication) + .mutation(async ({ input }) => { + return generateWildcard(input); + }), update: protectedProcedure .input(apiUpdateDomain) .mutation(async ({ input }) => { diff --git a/server/api/routers/settings.ts b/server/api/routers/settings.ts index 7a222fe00..ac8eba3fb 100644 --- a/server/api/routers/settings.ts +++ b/server/api/routers/settings.ts @@ -248,7 +248,7 @@ export const settingsRouter = createTRPCRouter({ getOpenApiDocument: protectedProcedure.query( async ({ ctx }): Promise => { const protocol = ctx.req.headers["x-forwarded-proto"]; - const url = `${protocol}://${ctx.req.headers.host}/api/trpc`; + const url = `${protocol}://${ctx.req.headers.host}/api`; const openApiDocument = generateOpenApiDocument(appRouter, { title: "tRPC OpenAPI", version: "1.0.0", diff --git a/server/api/services/application.ts b/server/api/services/application.ts index e5719a9a5..b093f63fb 100644 --- a/server/api/services/application.ts +++ b/server/api/services/application.ts @@ -130,15 +130,18 @@ export const updateApplicationStatus = async ( export const deployApplication = async ({ applicationId, titleLog = "Manual deployment", + descriptionLog = "", }: { applicationId: string; titleLog: string; + descriptionLog: string; }) => { const application = await findApplicationById(applicationId); const admin = await findAdmin(); const deployment = await createDeployment({ applicationId: applicationId, title: titleLog, + description: descriptionLog, }); try { @@ -173,14 +176,17 @@ export const deployApplication = async ({ export const rebuildApplication = async ({ applicationId, titleLog = "Rebuild deployment", + descriptionLog = "", }: { applicationId: string; titleLog: string; + descriptionLog: string; }) => { const application = await findApplicationById(applicationId); const deployment = await createDeployment({ applicationId: applicationId, title: titleLog, + description: descriptionLog, }); try { diff --git a/server/api/services/compose.ts b/server/api/services/compose.ts index 63065e6ce..97d27e7ed 100644 --- a/server/api/services/compose.ts +++ b/server/api/services/compose.ts @@ -134,15 +134,18 @@ export const updateCompose = async ( export const deployCompose = async ({ composeId, titleLog = "Manual deployment", + descriptionLog = "", }: { composeId: string; titleLog: string; + descriptionLog: string; }) => { const compose = await findComposeById(composeId); const admin = await findAdmin(); const deployment = await createDeploymentCompose({ composeId: composeId, title: titleLog, + description: descriptionLog, }); try { @@ -170,14 +173,17 @@ export const deployCompose = async ({ export const rebuildCompose = async ({ composeId, titleLog = "Rebuild deployment", + descriptionLog = "", }: { composeId: string; titleLog: string; + descriptionLog: string; }) => { const compose = await findComposeById(composeId); const deployment = await createDeploymentCompose({ composeId: composeId, title: titleLog, + description: descriptionLog, }); try { diff --git a/server/api/services/deployment.ts b/server/api/services/deployment.ts index b946fa7ac..e3bbad79f 100644 --- a/server/api/services/deployment.ts +++ b/server/api/services/deployment.ts @@ -60,6 +60,7 @@ export const createDeployment = async ( title: deployment.title || "Deployment", status: "running", logPath: logFilePath, + description: deployment.description || "", }) .returning(); if (deploymentCreate.length === 0 || !deploymentCreate[0]) { @@ -100,6 +101,7 @@ export const createDeploymentCompose = async ( .values({ composeId: deployment.composeId, title: deployment.title || "Deployment", + description: deployment.description || "", status: "running", logPath: logFilePath, }) diff --git a/server/api/services/domain.ts b/server/api/services/domain.ts index 05a1cfa62..d7e4a271c 100644 --- a/server/api/services/domain.ts +++ b/server/api/services/domain.ts @@ -1,9 +1,15 @@ import { db } from "@/server/db"; -import { type apiCreateDomain, domains } from "@/server/db/schema"; +import { + type apiCreateDomain, + type apiFindDomainByApplication, + domains, +} from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { findApplicationById } from "./application"; import { manageDomain } from "@/server/utils/traefik/domain"; +import { findAdmin } from "./admin"; +import { generateRandomDomain } from "@/templates/utils"; export type Domain = typeof domains.$inferSelect; @@ -29,6 +35,58 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => { await manageDomain(application, domain); }); }; + +export const generateDomain = async ( + input: typeof apiFindDomainByApplication._type, +) => { + const application = await findApplicationById(input.applicationId); + const admin = await findAdmin(); + const domain = await createDomain({ + applicationId: application.applicationId, + host: generateRandomDomain({ + serverIp: admin.serverIp || "", + projectName: application.appName, + }), + port: 3000, + certificateType: "none", + https: false, + path: "/", + }); + + return domain; +}; + +export const generateWildcard = async ( + input: typeof apiFindDomainByApplication._type, +) => { + const application = await findApplicationById(input.applicationId); + const admin = await findAdmin(); + + if (!admin.host) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "We need a host to generate a wildcard domain", + }); + } + const domain = await createDomain({ + applicationId: application.applicationId, + host: generateWildcardDomain(application.appName, admin.host || ""), + port: 3000, + certificateType: "none", + https: false, + path: "/", + }); + + return domain; +}; + +export const generateWildcardDomain = ( + appName: string, + serverDomain: string, +) => { + return `${appName}-${serverDomain}`; +}; + export const findDomainById = async (domainId: string) => { const domain = await db.query.domains.findFirst({ where: eq(domains.domainId, domainId), diff --git a/server/db/schema/deployment.ts b/server/db/schema/deployment.ts index eaadc695d..58931d6de 100644 --- a/server/db/schema/deployment.ts +++ b/server/db/schema/deployment.ts @@ -18,6 +18,7 @@ export const deployments = pgTable("deployment", { .primaryKey() .$defaultFn(() => nanoid()), title: text("title").notNull(), + description: text("description"), status: deploymentStatus("status").default("running"), logPath: text("logPath").notNull(), applicationId: text("applicationId").references( @@ -49,6 +50,7 @@ const schema = createInsertSchema(deployments, { logPath: z.string().min(1), applicationId: z.string(), composeId: z.string(), + description: z.string().optional(), }); export const apiCreateDeployment = schema @@ -57,6 +59,7 @@ export const apiCreateDeployment = schema status: true, logPath: true, applicationId: true, + description: true, }) .extend({ applicationId: z.string().min(1), @@ -68,6 +71,7 @@ export const apiCreateDeploymentCompose = schema status: true, logPath: true, composeId: true, + description: true, }) .extend({ composeId: z.string().min(1), diff --git a/server/queues/deployments-queue.ts b/server/queues/deployments-queue.ts index ef909d8e8..54f23073b 100644 --- a/server/queues/deployments-queue.ts +++ b/server/queues/deployments-queue.ts @@ -10,12 +10,14 @@ type DeployJob = | { applicationId: string; titleLog: string; + descriptionLog: string; type: "deploy" | "redeploy"; applicationType: "application"; } | { composeId: string; titleLog: string; + descriptionLog: string; type: "deploy" | "redeploy"; applicationType: "compose"; }; @@ -31,11 +33,13 @@ export const deploymentWorker = new Worker( await rebuildApplication({ applicationId: job.data.applicationId, titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, }); } else if (job.data.type === "deploy") { await deployApplication({ applicationId: job.data.applicationId, titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, }); } } else if (job.data.applicationType === "compose") { @@ -43,11 +47,13 @@ export const deploymentWorker = new Worker( await deployCompose({ composeId: job.data.composeId, titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, }); } else if (job.data.type === "redeploy") { await rebuildCompose({ composeId: job.data.composeId, titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, }); } } diff --git a/server/utils/databases/mariadb.ts b/server/utils/databases/mariadb.ts index 34ea2c8c7..cd7c7e4ad 100644 --- a/server/utils/databases/mariadb.ts +++ b/server/utils/databases/mariadb.ts @@ -63,6 +63,9 @@ export const buildMariadb = async (mariadb: MariadbWithMounts) => { Resources: { ...resources, }, + Placement: { + Constraints: ["node.role==manager"], + }, }, Mode: { Replicated: { diff --git a/server/utils/databases/mongo.ts b/server/utils/databases/mongo.ts index c34824925..5c07bab4a 100644 --- a/server/utils/databases/mongo.ts +++ b/server/utils/databases/mongo.ts @@ -63,6 +63,9 @@ export const buildMongo = async (mongo: MongoWithMounts) => { Resources: { ...resources, }, + Placement: { + Constraints: ["node.role==manager"], + }, }, Mode: { Replicated: { diff --git a/server/utils/databases/mysql.ts b/server/utils/databases/mysql.ts index ec529241a..8c5bbce4c 100644 --- a/server/utils/databases/mysql.ts +++ b/server/utils/databases/mysql.ts @@ -69,6 +69,9 @@ export const buildMysql = async (mysql: MysqlWithMounts) => { Resources: { ...resources, }, + Placement: { + Constraints: ["node.role==manager"], + }, }, Mode: { Replicated: { diff --git a/server/utils/databases/postgres.ts b/server/utils/databases/postgres.ts index b2cbfb674..ef8857acb 100644 --- a/server/utils/databases/postgres.ts +++ b/server/utils/databases/postgres.ts @@ -63,6 +63,9 @@ export const buildPostgres = async (postgres: PostgresWithMounts) => { Resources: { ...resources, }, + Placement: { + Constraints: ["node.role==manager"], + }, }, Mode: { Replicated: { diff --git a/server/utils/databases/redis.ts b/server/utils/databases/redis.ts index a3e3d5933..76125ad7e 100644 --- a/server/utils/databases/redis.ts +++ b/server/utils/databases/redis.ts @@ -50,17 +50,19 @@ export const buildRedis = async (redis: RedisWithMounts) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(command - ? { - Command: ["/bin/sh"], - Args: ["-c", command], - } - : {}), + Command: ["/bin/sh"], + Args: [ + "-c", + command ? command : `redis-server --requirepass ${databasePassword}`, + ], }, Networks: [{ Target: "dokploy-network" }], Resources: { ...resources, }, + Placement: { + Constraints: ["node.role==manager"], + }, }, Mode: { Replicated: {