diff --git a/.gitignore b/.gitignore index 61e009710..4e788d919 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ yarn-error.log* *.lockb *.rdb +.idea diff --git a/README.md b/README.md index 58cb0961c..2d53242fb 100644 --- a/README.md +++ b/README.md @@ -55,5 +55,5 @@ Tested Systems: ## 📄 Documentation -For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com). +For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). diff --git a/components/dashboard/application/advanced/ports/add-port.tsx b/components/dashboard/application/advanced/ports/add-port.tsx index 52b303a57..76939d821 100644 --- a/components/dashboard/application/advanced/ports/add-port.tsx +++ b/components/dashboard/application/advanced/ports/add-port.tsx @@ -183,8 +183,7 @@ export const AddPort = ({ - - None + TCP UDP diff --git a/components/dashboard/application/environment/show.tsx b/components/dashboard/application/environment/show.tsx index 29b536cb2..72f25d2e7 100644 --- a/components/dashboard/application/environment/show.tsx +++ b/components/dashboard/application/environment/show.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Card, CardContent, @@ -19,8 +19,9 @@ import { } from "@/components/ui/form"; import { api } from "@/utils/api"; import { toast } from "sonner"; -import { Textarea } from "@/components/ui/textarea"; +import { Toggle } from "@/components/ui/toggle"; import { CodeEditor } from "@/components/shared/code-editor"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; const addEnvironmentSchema = z.object({ environment: z.string(), @@ -33,6 +34,7 @@ interface Props { } export const ShowEnvironment = ({ applicationId }: Props) => { + const [isEnvVisible, setIsEnvVisible] = useState(true); const { mutateAsync, isLoading } = api.application.saveEnvironment.useMutation(); @@ -72,15 +74,50 @@ export const ShowEnvironment = ({ applicationId }: Props) => { toast.error("Error to add environment"); }); }; + useEffect(() => { + if (isEnvVisible) { + if (data?.env) { + const maskedLines = data.env + .split("\n") + .map((line) => "*".repeat(line.length)) + .join("\n"); + form.reset({ + environment: maskedLines, + }); + } else { + form.reset({ + environment: "", + }); + } + } else { + form.reset({ + environment: data?.env || "", + }); + } + }, [form.reset, data, form, isEnvVisible]); return (
- - Environment Settings - - You can add environment variables to your resource. - + +
+ Environment Settings + + You can add environment variables to your resource. + +
+ + + {isEnvVisible ? ( + + ) : ( + + )} +
@@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
-
diff --git a/components/dashboard/application/general/deploy-application.tsx b/components/dashboard/application/general/deploy-application.tsx index 4ce1bb877..47fbb5c0e 100644 --- a/components/dashboard/application/general/deploy-application.tsx +++ b/components/dashboard/application/general/deploy-application.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; interface Props { @@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => { applicationId, }) .then(async () => { - toast.success("Application Deploying...."); + toast.success("Deploying Application...."); await refetch(); await deploy({ applicationId, - }) - .then(() => { - toast.success("Application Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Application"); - }); + }).catch(() => { + toast.error("Error to deploy Application"); + }); await refetch(); }) diff --git a/components/dashboard/compose/enviroment/show.tsx b/components/dashboard/compose/enviroment/show.tsx index 824e3ce86..c897ac6a0 100644 --- a/components/dashboard/compose/enviroment/show.tsx +++ b/components/dashboard/compose/enviroment/show.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Card, CardContent, @@ -20,6 +20,8 @@ import { import { api } from "@/utils/api"; import { toast } from "sonner"; import { CodeEditor } from "@/components/shared/code-editor"; +import { Toggle } from "@/components/ui/toggle"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; const addEnvironmentSchema = z.object({ environment: z.string(), @@ -32,6 +34,7 @@ interface Props { } export const ShowEnvironmentCompose = ({ composeId }: Props) => { + const [isEnvVisible, setIsEnvVisible] = useState(true); const { mutateAsync, isLoading } = api.compose.update.useMutation(); const { data, refetch } = api.compose.one.useQuery( @@ -71,14 +74,50 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => { }); }; + useEffect(() => { + if (isEnvVisible) { + if (data?.env) { + const maskedLines = data.env + .split("\n") + .map((line) => "*".repeat(line.length)) + .join("\n"); + form.reset({ + environment: maskedLines, + }); + } else { + form.reset({ + environment: "", + }); + } + } else { + form.reset({ + environment: data?.env || "", + }); + } + }, [form.reset, data, form, isEnvVisible]); + return (
- - Environment Settings - - You can add environment variables to your resource. - + +
+ Environment Settings + + You can add environment variables to your resource. + +
+ + + {isEnvVisible ? ( + + ) : ( + + )} +
@@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
-
diff --git a/components/dashboard/compose/general/deploy-compose.tsx b/components/dashboard/compose/general/deploy-compose.tsx index 3b5bb11b6..1617ee7bb 100644 --- a/components/dashboard/compose/general/deploy-compose.tsx +++ b/components/dashboard/compose/general/deploy-compose.tsx @@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => { composeStatus: "running", }) .then(async () => { - toast.success("Compose Deploying...."); + toast.success("Deploying Compose...."); await refetch(); await deploy({ composeId, - }) - .then(() => { - toast.success("Compose Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Compose"); - }); + }).catch(() => { + toast.error("Error to deploy Compose"); + }); await refetch(); }) diff --git a/components/dashboard/mariadb/general/deploy-mariadb.tsx b/components/dashboard/mariadb/general/deploy-mariadb.tsx index ac581d0d4..e3162f00f 100644 --- a/components/dashboard/mariadb/general/deploy-mariadb.tsx +++ b/components/dashboard/mariadb/general/deploy-mariadb.tsx @@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => { applicationStatus: "running", }) .then(async () => { - toast.success("Database Deploying...."); + toast.success("Deploying Database...."); await refetch(); await deploy({ mariadbId, - }) - .then(() => { - toast.success("Database Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Database"); - }); + }).catch(() => { + toast.error("Error to deploy Database"); + }); await refetch(); }) .catch((e) => { diff --git a/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx b/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx index 83f730fa7..2f19d78bd 100644 --- a/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx +++ b/components/dashboard/mariadb/general/show-external-mariadb-credentials.tsx @@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; const DockerProviderSchema = z.object({ externalPort: z.preprocess((a) => { @@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */} - +
)} diff --git a/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx b/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx index 7dc61772d..869409d59 100644 --- a/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx +++ b/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; interface Props { mariadbId: string; @@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
-
-
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
- diff --git a/components/dashboard/mongo/general/deploy-mongo.tsx b/components/dashboard/mongo/general/deploy-mongo.tsx index 41a92f64e..0ebda1ca6 100644 --- a/components/dashboard/mongo/general/deploy-mongo.tsx +++ b/components/dashboard/mongo/general/deploy-mongo.tsx @@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => { applicationStatus: "running", }) .then(async () => { - toast.success("Database Deploying...."); + toast.success("Deploying Database...."); await refetch(); await deploy({ mongoId, - }) - .then(() => { - toast.success("Database Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Database"); - }); + }).catch(() => { + toast.error("Error to deploy Database"); + }); await refetch(); }) .catch((e) => { diff --git a/components/dashboard/mongo/general/show-external-mongo-credentials.tsx b/components/dashboard/mongo/general/show-external-mongo-credentials.tsx index 772188578..c8a3fa230 100644 --- a/components/dashboard/mongo/general/show-external-mongo-credentials.tsx +++ b/components/dashboard/mongo/general/show-external-mongo-credentials.tsx @@ -1,3 +1,4 @@ +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; import { Card, @@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
- +
)} diff --git a/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx b/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx index a57933fe2..9fab4a8a8 100644 --- a/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx +++ b/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; interface Props { mongoId: string; @@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
-
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
- diff --git a/components/dashboard/mysql/general/deploy-mysql.tsx b/components/dashboard/mysql/general/deploy-mysql.tsx index 7074901f0..a773feffc 100644 --- a/components/dashboard/mysql/general/deploy-mysql.tsx +++ b/components/dashboard/mysql/general/deploy-mysql.tsx @@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => { applicationStatus: "running", }) .then(async () => { - toast.success("Database Deploying...."); + toast.success("Deploying Database...."); await refetch(); await deploy({ mysqlId, - }) - .then(() => { - toast.success("Database Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Database"); - }); + }).catch(() => { + toast.error("Error to deploy Database"); + }); await refetch(); }) .catch((e) => { diff --git a/components/dashboard/mysql/general/show-external-mysql-credentials.tsx b/components/dashboard/mysql/general/show-external-mysql-credentials.tsx index ab9388cda..ce184a983 100644 --- a/components/dashboard/mysql/general/show-external-mysql-credentials.tsx +++ b/components/dashboard/mysql/general/show-external-mysql-credentials.tsx @@ -1,3 +1,4 @@ +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; import { Card, @@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
- +
)} diff --git a/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx b/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx index 80a71bfef..c48fe95da 100644 --- a/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx +++ b/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; interface Props { mysqlId: string; @@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
-
-
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
- diff --git a/components/dashboard/postgres/general/deploy-postgres.tsx b/components/dashboard/postgres/general/deploy-postgres.tsx index 297df3530..e329d9f80 100644 --- a/components/dashboard/postgres/general/deploy-postgres.tsx +++ b/components/dashboard/postgres/general/deploy-postgres.tsx @@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => { applicationStatus: "running", }) .then(async () => { - toast.success("Database Deploying...."); + toast.success("Deploying Database...."); await refetch(); await deploy({ postgresId, - }) - .then(() => { - toast.success("Database Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Database"); - }); + }).catch(() => { + toast.error("Error to deploy Database"); + }); await refetch(); }) .catch((e) => { diff --git a/components/dashboard/postgres/general/show-external-postgres-credentials.tsx b/components/dashboard/postgres/general/show-external-postgres-credentials.tsx index 215356cee..edb128bf9 100644 --- a/components/dashboard/postgres/general/show-external-postgres-credentials.tsx +++ b/components/dashboard/postgres/general/show-external-postgres-credentials.tsx @@ -1,3 +1,4 @@ +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; import { Card, @@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
- +
)} diff --git a/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx b/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx index def7f9c0c..a8b5270d1 100644 --- a/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx +++ b/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; interface Props { postgresId: string; @@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
-
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
- diff --git a/components/dashboard/project/add-application.tsx b/components/dashboard/project/add-application.tsx index 145c601de..ecf2a1afb 100644 --- a/components/dashboard/project/add-application.tsx +++ b/components/dashboard/project/add-application.tsx @@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { Folder } from "lucide-react"; -import { useEffect } from "react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { Textarea } from "@/components/ui/textarea"; +import { slugify } from "@/lib/slug"; const AddTemplateSchema = z.object({ name: z.string().min(1, { message: "Name is required", }), + appName: z + .string() + .min(1, { + message: "App name is required", + }) + .regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, { + message: + "App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", + }), description: z.string().optional(), }); @@ -39,10 +49,13 @@ type AddTemplate = z.infer; interface Props { projectId: string; + projectName?: string; } -export const AddApplication = ({ projectId }: Props) => { +export const AddApplication = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); + const [visible, setVisible] = useState(false); + const slug = slugify(projectName); const { mutateAsync, isLoading, error, isError } = api.application.create.useMutation(); @@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => { const form = useForm({ defaultValues: { name: "", + appName: `${slug}-`, description: "", }, resolver: zodResolver(AddTemplateSchema), }); - useEffect(() => { - form.reset(); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - const onSubmit = async (data: AddTemplate) => { await mutateAsync({ name: data.name, + appName: data.appName, description: data.description, projectId, }) .then(async () => { toast.success("Service Created"); + form.reset(); + setVisible(false); await utils.project.one.invalidate({ projectId, }); }) - .catch(() => { + .catch((e) => { toast.error("Error to create the service"); }); }; return ( - + { {isError && {error?.message}} - -
- ( - - Name - - - - - - - )} - /> -
+ ( + + Name + + { + const val = e.target.value?.trim() || ""; + form.setValue("appName", `${slug}-${val}`); + field.onChange(val); + }} + /> + + + + )} + /> + ( + + AppName + + + + + + )} + /> { )} /> - - {/* ( - - Build Type - - - - - - - - Dockerfile - - - - - - - Nixpacks - - - - - - - Heroku Buildpacks - - - - - - - )} - /> */} diff --git a/components/dashboard/project/add-compose.tsx b/components/dashboard/project/add-compose.tsx index 97df55ef8..fd769c911 100644 --- a/components/dashboard/project/add-compose.tsx +++ b/components/dashboard/project/add-compose.tsx @@ -34,12 +34,22 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { slugify } from "@/lib/slug"; const AddComposeSchema = z.object({ composeType: z.enum(["docker-compose", "stack"]).optional(), name: z.string().min(1, { message: "Name is required", }), + appName: z + .string() + .min(1, { + message: "App name is required", + }) + .regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, { + message: + "App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", + }), description: z.string().optional(), }); @@ -47,11 +57,12 @@ type AddCompose = z.infer; interface Props { projectId: string; + projectName?: string; } -export const AddCompose = ({ projectId }: Props) => { +export const AddCompose = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); - + const slug = slugify(projectName); const { mutateAsync, isLoading, error, isError } = api.compose.create.useMutation(); @@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => { name: "", description: "", composeType: "docker-compose", + appName: `${slug}-`, }, resolver: zodResolver(AddComposeSchema), }); @@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => { description: data.description, projectId, composeType: data.composeType, + appName: data.appName, }) .then(async () => { toast.success("Compose Created"); @@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => { Name - + { + const val = e.target.value?.trim() || ""; + form.setValue("appName", `${slug}-${val}`); + field.onChange(val); + }} + /> - )} />
+ ( + + AppName + + + + + + )} + /> , + label: "PostgreSQL", + }, + mongo: { + icon: , + label: "MongoDB", + }, + mariadb: { + icon: , + label: "MariaDB", + }, + mysql: { + icon: , + label: "MySQL", + }, + redis: { + icon: , + label: "Redis", + }, +}; + type AddDatabase = z.infer; interface Props { projectId: string; + projectName?: string; } -export const AddDatabase = ({ projectId }: Props) => { +export const AddDatabase = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); - - const { mutateAsync: createPostgresql } = api.postgres.create.useMutation(); - - const { mutateAsync: createMongo } = api.mongo.create.useMutation(); - - const { mutateAsync: createRedis } = api.redis.create.useMutation(); - - const { mutateAsync: createMariadb } = api.mariadb.create.useMutation(); - - const { mutateAsync: createMysql } = api.mysql.create.useMutation(); + const [visible, setVisible] = useState(false); + const slug = slugify(projectName); + const postgresMutation = api.postgres.create.useMutation(); + const mongoMutation = api.mongo.create.useMutation(); + const redisMutation = api.redis.create.useMutation(); + const mariadbMutation = api.mariadb.create.useMutation(); + const mysqlMutation = api.mysql.create.useMutation(); const form = useForm({ defaultValues: { type: "postgres", dockerImage: "", name: "", + appName: `${slug}-`, databasePassword: "", description: "", databaseName: "", @@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => { resolver: zodResolver(mySchema), }); const type = form.watch("type"); - - useEffect(() => { - form.reset({ - type: "postgres", - dockerImage: "", - name: "", - databasePassword: "", - description: "", - databaseName: "", - databaseUser: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + const activeMutation = { + postgres: postgresMutation, + mongo: mongoMutation, + redis: redisMutation, + mariadb: mariadbMutation, + mysql: mysqlMutation, + }; const onSubmit = async (data: AddDatabase) => { const defaultDockerImage = data.dockerImage || dockerImageDefaultPlaceholder[data.type]; let promise: Promise | null = null; + const commonParams = { + name: data.name, + appName: data.appName, + dockerImage: defaultDockerImage, + projectId, + description: data.description, + }; + if (data.type === "postgres") { - promise = createPostgresql({ - name: data.name, - dockerImage: defaultDockerImage, + promise = postgresMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, - description: data.description, }); } else if (data.type === "mongo") { - promise = createMongo({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mongoMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, - description: data.description, }); } else if (data.type === "redis") { - promise = createRedis({ - name: data.name, - dockerImage: defaultDockerImage, + promise = redisMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, projectId, - description: data.description, }); } else if (data.type === "mariadb") { - promise = createMariadb({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mariadbMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, - projectId, databaseRootPassword: data.databaseRootPassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - description: data.description, }); } else if (data.type === "mysql") { - promise = createMysql({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mysqlMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, databaseRootPassword: data.databaseRootPassword, - description: data.description, }); } @@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => { await promise .then(async () => { toast.success("Database Created"); + form.reset({ + type: "postgres", + dockerImage: "", + name: "", + appName: `${projectName}-`, + databasePassword: "", + description: "", + databaseName: "", + databaseUser: "", + }); + setVisible(false); await utils.project.one.invalidate({ projectId, }); @@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => { } }; return ( - + { Database - + Databases - {/* {isError && ( -
- - - {error?.message} - -
- )} */}
{ defaultValue={field.value} className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" > - - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
+ {Object.entries(databasesMap).map(([key, value]) => ( + + +
+ + +
+
+
+ ))} + {activeMutation[field.value].isError && ( +
+ + + {activeMutation[field.value].error?.message} + +
+ )} )} /> @@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => { Name - + { + const val = e.target.value?.trim() || ""; + form.setValue("appName", `${slug}-${val}`); + field.onChange(val); + }} + /> )} /> + ( + + AppName + + + + + + )} + /> { applicationStatus: "running", }) .then(async () => { - toast.success("Database Deploying...."); + toast.success("Deploying Database..."); await refetch(); await deploy({ redisId, - }) - .then(() => { - toast.success("Database Deployed Succesfully"); - }) - .catch(() => { - toast.error("Error to deploy Database"); - }); + }).catch(() => { + toast.error("Error to deploy Database"); + }); await refetch(); }) .catch((e) => { diff --git a/components/dashboard/redis/general/show-external-redis-credentials.tsx b/components/dashboard/redis/general/show-external-redis-credentials.tsx index aaf4d0b7e..ea62d77e0 100644 --- a/components/dashboard/redis/general/show-external-redis-credentials.tsx +++ b/components/dashboard/redis/general/show-external-redis-credentials.tsx @@ -1,3 +1,4 @@ +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { Button } from "@/components/ui/button"; import { Card, @@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
- +
)} diff --git a/components/dashboard/redis/general/show-internal-redis-credentials.tsx b/components/dashboard/redis/general/show-internal-redis-credentials.tsx index 3f44b37a9..1f798144f 100644 --- a/components/dashboard/redis/general/show-internal-redis-credentials.tsx +++ b/components/dashboard/redis/general/show-internal-redis-credentials.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; interface Props { redisId: string; @@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
-
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
- diff --git a/components/shared/toggle-visibility-input.tsx b/components/shared/toggle-visibility-input.tsx new file mode 100644 index 000000000..d3a2b5e3e --- /dev/null +++ b/components/shared/toggle-visibility-input.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import { Input, type InputProps } from "../ui/input"; +import { Button } from "../ui/button"; + +export const ToggleVisibilityInput = ({ ...props }: InputProps) => { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const togglePasswordVisibility = () => { + setIsPasswordVisible((prevVisibility) => !prevVisibility); + }; + + const inputType = isPasswordVisible ? "text" : "password"; + return ( +
+ + +
+ ); +}; diff --git a/lib/slug.ts b/lib/slug.ts new file mode 100644 index 000000000..a4982a0e6 --- /dev/null +++ b/lib/slug.ts @@ -0,0 +1,15 @@ +import slug from "slugify"; + +export const slugify = (text: string | undefined) => { + if (!text) { + return ""; + } + + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); +}; diff --git a/package.json b/package.json index 547255814..36b8dab3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.2.0", + "version": "v0.2.2", "private": true, "license": "AGPL-3.0-only", "type": "module", @@ -31,11 +31,11 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { - "@codemirror/language":"^6.10.1", "@aws-sdk/client-s3": "3.515.0", - "@codemirror/legacy-modes":"6.4.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/language": "^6.10.1", + "@codemirror/legacy-modes": "6.4.0", "@faker-js/faker": "^8.4.1", "@hookform/resolvers": "^3.3.4", "@lucia-auth/adapter-drizzle": "1.0.7", @@ -76,6 +76,7 @@ "clsx": "^2.1.0", "cmdk": "^0.2.0", "copy-to-clipboard": "^3.3.3", + "copy-webpack-plugin": "^12.0.2", "date-fns": "3.6.0", "dockerode": "4.0.2", "dockerode-compose": "^1.4.0", @@ -105,6 +106,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.49.3", "recharts": "^2.12.3", + "slugify": "^1.6.6", "sonner": "^1.4.0", "superjson": "^2.2.1", "tailwind-merge": "^2.2.0", @@ -113,9 +115,7 @@ "use-resize-observer": "9.1.0", "ws": "8.16.0", "xterm-addon-fit": "^0.8.0", - "zod": "^3.23.4", - "copy-webpack-plugin": "^12.0.2" - + "zod": "^3.23.4" }, "devDependencies": { "@biomejs/biome": "1.7.1", diff --git a/pages/dashboard/project/[projectId].tsx b/pages/dashboard/project/[projectId].tsx index c4752b19c..acc4a1913 100644 --- a/pages/dashboard/project/[projectId].tsx +++ b/pages/dashboard/project/[projectId].tsx @@ -210,9 +210,12 @@ const Project = ( Actions - - - + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c44c7cff..f7af8ad9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,6 +230,9 @@ dependencies: recharts: specifier: ^2.12.3 version: 2.12.3(react-dom@18.2.0)(react@18.2.0) + slugify: + specifier: ^1.6.6 + version: 1.6.6 sonner: specifier: ^1.4.0 version: 1.4.3(react-dom@18.2.0)(react@18.2.0) @@ -8015,6 +8018,11 @@ packages: engines: {node: '>=14.16'} dev: false + /slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + dev: false + /sonner@1.4.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==} peerDependencies: diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index 8a3640ede..ea6dce7be 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -66,9 +66,10 @@ export const applicationRouter = createTRPCRouter({ if (ctx.user.rol === "user") { await addNewService(ctx.user.authId, newApplication.applicationId); } - - return newApplication; - } catch (error) { + } catch (error: unknown) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error to create the application", diff --git a/server/api/routers/compose.ts b/server/api/routers/compose.ts index 6da5fe860..f23550b40 100644 --- a/server/api/routers/compose.ts +++ b/server/api/routers/compose.ts @@ -34,13 +34,18 @@ import { nanoid } from "nanoid"; import { removeDeploymentsByComposeId } from "../services/deployment"; import { removeComposeDirectory } from "@/server/utils/filesystem/directory"; import { createCommand } from "@/server/utils/builders/compose"; -import { loadTemplateModule, readComposeFile } from "@/templates/utils"; +import { + generatePassword, + loadTemplateModule, + readComposeFile, +} from "@/templates/utils"; import { findAdmin } from "../services/admin"; import { TRPCError } from "@trpc/server"; -import { findProjectById, slugifyProjectName } from "../services/project"; +import { findProjectById } from "../services/project"; import { createMount } from "../services/mount"; import type { TemplatesKeys } from "@/templates/types/templates-data.type"; import { templates } from "@/templates/templates"; +import { slugify } from "@/lib/slug"; export const composeRouter = createTRPCRouter({ create: protectedProcedure @@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({ const project = await findProjectById(input.projectId); - const projectName = slugifyProjectName(`${project.name}-${input.id}`); + const projectName = slugify(`${project.name} ${input.id}`); const { envs, mounts } = generate({ serverIp: admin.serverIp, projectName: projectName, @@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({ env: envs.join("\n"), name: input.id, sourceType: "raw", + appName: `${projectName}-${generatePassword(6)}`, }); if (ctx.user.rol === "user") { diff --git a/server/api/routers/mariadb.ts b/server/api/routers/mariadb.ts index 59e577483..2ab8dd6a9 100644 --- a/server/api/routers/mariadb.ts +++ b/server/api/routers/mariadb.ts @@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mariadb database", diff --git a/server/api/routers/mongo.ts b/server/api/routers/mongo.ts index 705549b67..d9ddd2c27 100644 --- a/server/api/routers/mongo.ts +++ b/server/api/routers/mongo.ts @@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mongo database", diff --git a/server/api/routers/mysql.ts b/server/api/routers/mysql.ts index 02f683baa..f520064b3 100644 --- a/server/api/routers/mysql.ts +++ b/server/api/routers/mysql.ts @@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mysql database", diff --git a/server/api/routers/postgres.ts b/server/api/routers/postgres.ts index 45bd88a1a..4dc7ff5d7 100644 --- a/server/api/routers/postgres.ts +++ b/server/api/routers/postgres.ts @@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting postgresql database", diff --git a/server/api/routers/registry.ts b/server/api/routers/registry.ts index 63ffa213c..ce83c7963 100644 --- a/server/api/routers/registry.ts +++ b/server/api/routers/registry.ts @@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; import { TRPCError } from "@trpc/server"; import { manageRegistry } from "@/server/utils/traefik/registry"; import { initializeRegistry } from "@/server/setup/registry-setup"; -import { docker } from "@/server/constants"; +import { execAsync } from "@/server/utils/process/execAsync"; export const registryRouter = createTRPCRouter({ create: adminProcedure @@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({ .input(apiTestRegistry) .mutation(async ({ input }) => { try { - const result = await docker.checkAuth({ - username: input.username, - password: input.password, - serveraddress: input.registryUrl, - }); - + const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + await execAsync(loginCommand); return true; } catch (error) { - console.log(error); + console.log("Error Registry:", error); return false; } }), diff --git a/server/api/services/application.ts b/server/api/services/application.ts index a7e365334..3decd2818 100644 --- a/server/api/services/application.ts +++ b/server/api/services/application.ts @@ -15,11 +15,23 @@ import { findAdmin } from "./admin"; import { createTraefikConfig } from "@/server/utils/traefik/application"; import { docker } from "@/server/constants"; import { getAdvancedStats } from "@/server/monitoring/utilts"; +import { validUniqueServerAppName } from "./project"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( input: typeof apiCreateApplication._type, ) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Application with this 'AppName' already exists", + }); + } + } + return await db.transaction(async (tx) => { const newApplication = await tx .insert(applications) diff --git a/server/api/services/compose.ts b/server/api/services/compose.ts index 8f519e8a8..3a7739189 100644 --- a/server/api/services/compose.ts +++ b/server/api/services/compose.ts @@ -13,10 +13,21 @@ import { join } from "node:path"; import { COMPOSE_PATH } from "@/server/constants"; import { cloneGithubRepository } from "@/server/utils/providers/github"; import { cloneGitRepository } from "@/server/utils/providers/git"; +import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; export const createCompose = async (input: typeof apiCreateCompose._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } const newDestination = await db .insert(compose) .values({ @@ -39,6 +50,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => { export const createComposeByTemplate = async ( input: typeof compose.$inferInsert, ) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } const newDestination = await db .insert(compose) .values({ diff --git a/server/api/services/docker.ts b/server/api/services/docker.ts index 57f60c360..8c951e8d4 100644 --- a/server/api/services/docker.ts +++ b/server/api/services/docker.ts @@ -46,9 +46,7 @@ export const getContainers = async () => { .filter((container) => !container.name.includes("dokploy")); return containers; - } catch (error) { - console.error(`Execution error: ${error}`); - } + } catch (error) {} }; export const getConfig = async (containerId: string) => { @@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => { const config = JSON.parse(stdout); return config; - } catch (error) { - console.error(`Execution error: ${error}`); - } + } catch (error) {} }; export const getContainersByAppNameMatch = async (appName: string) => { @@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => { }); return containers || []; - } catch (error) { - console.error(`Execution error: ${error}`); - } + } catch (error) {} return []; }; @@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => { }); return containers || []; - } catch (error) { - console.error(`Execution error: ${error}`); - } + } catch (error) {} return []; }; diff --git a/server/api/services/mariadb.ts b/server/api/services/mariadb.ts index 7545087f4..1ebd3525d 100644 --- a/server/api/services/mariadb.ts +++ b/server/api/services/mariadb.ts @@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Mariadb = typeof mariadb.$inferSelect; export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMariadb = await db .insert(mariadb) .values({ diff --git a/server/api/services/mongo.ts b/server/api/services/mongo.ts index a7605ffec..e6114ef45 100644 --- a/server/api/services/mongo.ts +++ b/server/api/services/mongo.ts @@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Mongo = typeof mongo.$inferSelect; export const createMongo = async (input: typeof apiCreateMongo._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMongo = await db .insert(mongo) .values({ diff --git a/server/api/services/mysql.ts b/server/api/services/mysql.ts index b09aadaab..3482968dc 100644 --- a/server/api/services/mysql.ts +++ b/server/api/services/mysql.ts @@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; -import { nanoid } from "nanoid"; +import { validUniqueServerAppName } from "./project"; export type MySql = typeof mysql.$inferSelect; export const createMysql = async (input: typeof apiCreateMySql._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMysql = await db .insert(mysql) .values({ diff --git a/server/api/services/postgres.ts b/server/api/services/postgres.ts index 9575ac517..11ac1085b 100644 --- a/server/api/services/postgres.ts +++ b/server/api/services/postgres.ts @@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Postgres = typeof postgres.$inferSelect; export const createPostgres = async (input: typeof apiCreatePostgres._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newPostgres = await db .insert(postgres) .values({ diff --git a/server/api/services/project.ts b/server/api/services/project.ts index 482756071..56aecce5f 100644 --- a/server/api/services/project.ts +++ b/server/api/services/project.ts @@ -1,5 +1,14 @@ import { db } from "@/server/db"; -import { type apiCreateProject, projects } from "@/server/db/schema"; +import { + type apiCreateProject, + applications, + mariadb, + mongo, + mysql, + postgres, + projects, + redis, +} from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { findAdmin } from "./admin"; @@ -75,12 +84,40 @@ export const updateProjectById = async ( return result; }; -export const slugifyProjectName = (projectName: string): string => { - return projectName - .toLowerCase() - .replace(/[0-9]/g, "") - .replace(/[^a-z\s-]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .replace(/^-+|-+$/g, ""); +export const validUniqueServerAppName = async (appName: string) => { + const query = await db.query.projects.findMany({ + with: { + applications: { + where: eq(applications.appName, appName), + }, + mariadb: { + where: eq(mariadb.appName, appName), + }, + mongo: { + where: eq(mongo.appName, appName), + }, + mysql: { + where: eq(mysql.appName, appName), + }, + postgres: { + where: eq(postgres.appName, appName), + }, + redis: { + where: eq(redis.appName, appName), + }, + }, + }); + + // Filter out items with non-empty fields + const nonEmptyProjects = query.filter( + (project) => + project.applications.length > 0 || + project.mariadb.length > 0 || + project.mongo.length > 0 || + project.mysql.length > 0 || + project.postgres.length > 0 || + project.redis.length > 0, + ); + + return nonEmptyProjects.length === 0; }; diff --git a/server/api/services/redis.ts b/server/api/services/redis.ts index e04bf41bf..6137b922f 100644 --- a/server/api/services/redis.ts +++ b/server/api/services/redis.ts @@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Redis = typeof redis.$inferSelect; // https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 export const createRedis = async (input: typeof apiCreateRedis._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newRedis = await db .insert(redis) .values({ diff --git a/server/api/services/registry.ts b/server/api/services/registry.ts index 48077d055..29c421b5d 100644 --- a/server/api/services/registry.ts +++ b/server/api/services/registry.ts @@ -9,28 +9,35 @@ import { } from "@/server/utils/traefik/registry"; import { removeService } from "@/server/utils/docker/utils"; import { initializeRegistry } from "@/server/setup/registry-setup"; +import { execAsync } from "@/server/utils/process/execAsync"; export type Registry = typeof registry.$inferSelect; export const createRegistry = async (input: typeof apiCreateRegistry._type) => { const admin = await findAdmin(); - const newRegistry = await db - .insert(registry) - .values({ - ...input, - adminId: admin.adminId, - }) - .returning() - .then((value) => value[0]); + return await db.transaction(async (tx) => { + const newRegistry = await tx + .insert(registry) + .values({ + ...input, + adminId: admin.adminId, + }) + .returning() + .then((value) => value[0]); - if (!newRegistry) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting registry", - }); - } - return newRegistry; + if (!newRegistry) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting registry", + }); + } + + const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + await execAsync(loginCommand); + + return newRegistry; + }); }; export const removeRegistry = async (registryId: string) => { @@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => { await removeService("dokploy-registry"); } + await execAsync(`docker logout ${response.registryUrl}`); + return response; } catch (error) { throw new TRPCError({ diff --git a/server/db/schema/application.ts b/server/db/schema/application.ts index 45d346739..2b9f71961 100644 --- a/server/db/schema/application.ts +++ b/server/db/schema/application.ts @@ -20,6 +20,7 @@ import { } from "drizzle-orm/pg-core"; import { generateAppName } from "./utils"; import { registry } from "./registry"; +import { generatePassword } from "@/templates/utils"; export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]); @@ -307,11 +308,17 @@ const createSchema = createInsertSchema(applications, { networkSwarm: NetworkSwarmSchema.nullable(), }); -export const apiCreateApplication = createSchema.pick({ - name: true, - description: true, - projectId: true, -}); +export const apiCreateApplication = createSchema + .pick({ + name: true, + appName: true, + description: true, + projectId: true, + }) + .transform((data) => ({ + ...data, + appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"), + })); export const apiFindOneApplication = createSchema .pick({ diff --git a/server/db/schema/compose.ts b/server/db/schema/compose.ts index f94711e04..bc1e641f6 100644 --- a/server/db/schema/compose.ts +++ b/server/db/schema/compose.ts @@ -8,6 +8,7 @@ import { deployments } from "./deployment"; import { generateAppName } from "./utils"; import { applicationStatus } from "./shared"; import { mounts } from "./mount"; +import { generatePassword } from "@/templates/utils"; export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ "git", @@ -74,12 +75,19 @@ const createSchema = createInsertSchema(compose, { composeType: z.enum(["docker-compose", "stack"]).optional(), }); -export const apiCreateCompose = createSchema.pick({ - name: true, - description: true, - projectId: true, - composeType: true, -}); +export const apiCreateCompose = createSchema + .pick({ + name: true, + description: true, + projectId: true, + composeType: true, + appName: true, + }) + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("compose"), + })); export const apiCreateComposeByTemplate = createSchema .pick({ diff --git a/server/db/schema/mariadb.ts b/server/db/schema/mariadb.ts index 256dfbfbb..83ec2898c 100644 --- a/server/db/schema/mariadb.ts +++ b/server/db/schema/mariadb.ts @@ -8,6 +8,7 @@ import { projects } from "./project"; import { backups } from "./backups"; import { mounts } from "./mount"; import { generateAppName } from "./utils"; +import { generatePassword } from "@/templates/utils"; export const mariadb = pgTable("mariadb", { mariadbId: text("mariadbId") @@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, { export const apiCreateMariaDB = createSchema .pick({ name: true, + appName: true, dockerImage: true, databaseRootPassword: true, projectId: true, @@ -87,7 +89,12 @@ export const apiCreateMariaDB = createSchema databaseUser: true, databasePassword: true, }) - .required(); + .required() + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"), + })); export const apiFindOneMariaDB = createSchema .pick({ diff --git a/server/db/schema/mongo.ts b/server/db/schema/mongo.ts index 7406580e9..2dd1cbb7b 100644 --- a/server/db/schema/mongo.ts +++ b/server/db/schema/mongo.ts @@ -8,6 +8,7 @@ import { projects } from "./project"; import { backups } from "./backups"; import { mounts } from "./mount"; import { generateAppName } from "./utils"; +import { generatePassword } from "@/templates/utils"; export const mongo = pgTable("mongo", { mongoId: text("mongoId") @@ -73,13 +74,19 @@ const createSchema = createInsertSchema(mongo, { export const apiCreateMongo = createSchema .pick({ name: true, + appName: true, dockerImage: true, projectId: true, description: true, databaseUser: true, databasePassword: true, }) - .required(); + .required() + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("postgres"), + })); export const apiFindOneMongo = createSchema .pick({ diff --git a/server/db/schema/mysql.ts b/server/db/schema/mysql.ts index 9e0c8c77f..0efbf28a1 100644 --- a/server/db/schema/mysql.ts +++ b/server/db/schema/mysql.ts @@ -8,6 +8,7 @@ import { projects } from "./project"; import { backups } from "./backups"; import { mounts } from "./mount"; import { generateAppName } from "./utils"; +import { generatePassword } from "@/templates/utils"; export const mysql = pgTable("mysql", { mysqlId: text("mysqlId") @@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, { export const apiCreateMySql = createSchema .pick({ name: true, + appName: true, dockerImage: true, projectId: true, description: true, @@ -85,7 +87,12 @@ export const apiCreateMySql = createSchema databasePassword: true, databaseRootPassword: true, }) - .required(); + .required() + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("mysql"), + })); export const apiFindOneMySql = createSchema .pick({ diff --git a/server/db/schema/postgres.ts b/server/db/schema/postgres.ts index 7cf0f34dd..5e9077da1 100644 --- a/server/db/schema/postgres.ts +++ b/server/db/schema/postgres.ts @@ -8,6 +8,7 @@ import { projects } from "./project"; import { backups } from "./backups"; import { mounts } from "./mount"; import { generateAppName } from "./utils"; +import { generatePassword } from "@/templates/utils"; export const postgres = pgTable("postgres", { postgresId: text("postgresId") @@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, { export const apiCreatePostgres = createSchema .pick({ name: true, + appName: true, databaseName: true, databaseUser: true, databasePassword: true, @@ -81,7 +83,12 @@ export const apiCreatePostgres = createSchema projectId: true, description: true, }) - .required(); + .required() + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("postgres"), + })); export const apiFindOnePostgres = createSchema .pick({ diff --git a/server/db/schema/redis.ts b/server/db/schema/redis.ts index 842fe8093..eb9197640 100644 --- a/server/db/schema/redis.ts +++ b/server/db/schema/redis.ts @@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { projects } from "./project"; import { mounts } from "./mount"; import { generateAppName } from "./utils"; +import { generatePassword } from "@/templates/utils"; export const redis = pgTable("redis", { redisId: text("redisId") @@ -69,13 +70,18 @@ const createSchema = createInsertSchema(redis, { export const apiCreateRedis = createSchema .pick({ name: true, + appName: true, databasePassword: true, dockerImage: true, projectId: true, description: true, }) - - .required(); + .required() + .transform((data) => ({ + ...data, + appName: + `${data.appName}-${generatePassword(6)}` || generateAppName("redis"), + })); export const apiFindOneRedis = createSchema .pick({ diff --git a/server/setup/traefik-setup.ts b/server/setup/traefik-setup.ts index a6a8b7834..889988d66 100644 --- a/server/setup/traefik-setup.ts +++ b/server/setup/traefik-setup.ts @@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types"; import type { FileConfig } from "../utils/traefik/file-types"; import type { CreateServiceOptions } from "dockerode"; +const TRAEFIK_SSL_PORT = + Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443; +const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80; + export const initializeTraefik = async () => { const imageName = "traefik:v2.5"; const containerName = "dokploy-traefik"; @@ -47,12 +51,12 @@ export const initializeTraefik = async () => { Ports: [ { TargetPort: 443, - PublishedPort: 443, + PublishedPort: TRAEFIK_SSL_PORT, PublishMode: "host", }, { TargetPort: 80, - PublishedPort: 80, + PublishedPort: TRAEFIK_PORT, PublishMode: "host", }, { @@ -146,10 +150,10 @@ export const createDefaultTraefikConfig = () => { }, entryPoints: { web: { - address: ":80", + address: `:${TRAEFIK_PORT}`, }, websecure: { - address: ":443", + address: `:${TRAEFIK_SSL_PORT}`, ...(process.env.NODE_ENV === "production" && { http: { tls: { diff --git a/server/utils/builders/compose.ts b/server/utils/builders/compose.ts index 034a24037..d3c90e9ba 100644 --- a/server/utils/builders/compose.ts +++ b/server/utils/builders/compose.ts @@ -108,7 +108,12 @@ const createEnvFile = (compose: ComposeNested) => { join(COMPOSE_PATH, appName, "docker-compose.yml"); const envFilePath = join(dirname(composeFilePath), ".env"); - const envFileContent = prepareEnvironmentVariables(env).join("\n"); + let envContent = env || ""; + if (!envContent.includes("DOCKER_CONFIG")) { + envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; + } + + const envFileContent = prepareEnvironmentVariables(envContent).join("\n"); if (!existsSync(dirname(envFilePath))) { mkdirSync(dirname(envFilePath), { recursive: true }); diff --git a/server/utils/builders/index.ts b/server/utils/builders/index.ts index e67ad9be6..ce8cad39a 100644 --- a/server/utils/builders/index.ts +++ b/server/utils/builders/index.ts @@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async ( }, }); } catch (error) { - console.log(error); await docker.createService(settings); } }; diff --git a/server/utils/providers/docker.ts b/server/utils/providers/docker.ts index 997648d12..c77a6721b 100644 --- a/server/utils/providers/docker.ts +++ b/server/utils/providers/docker.ts @@ -3,47 +3,47 @@ import { type ApplicationNested, mechanizeDockerContainer } from "../builders"; import { pullImage } from "../docker/utils"; interface RegistryAuth { - username: string; - password: string; - serveraddress: string; + username: string; + password: string; + serveraddress: string; } export const buildDocker = async ( - application: ApplicationNested, - logPath: string, + application: ApplicationNested, + logPath: string, ): Promise => { - const { buildType, dockerImage, username, password } = application; - const authConfig: Partial = { - username: username || "", - password: password || "", - }; + const { buildType, dockerImage, username, password } = application; + const authConfig: Partial = { + username: username || "", + password: password || "", + }; - const writeStream = createWriteStream(logPath, { flags: "a" }); + const writeStream = createWriteStream(logPath, { flags: "a" }); - writeStream.write(`\nBuild ${buildType}\n`); + writeStream.write(`\nBuild ${buildType}\n`); - writeStream.write(`Pulling ${dockerImage}: ✅\n`); + writeStream.write(`Pulling ${dockerImage}: ✅\n`); - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - await pullImage( - dockerImage, - (data) => { - if (writeStream.writable) { - writeStream.write(`${data.status}\n`); - } - }, - authConfig, - ); - await mechanizeDockerContainer(application); - writeStream.write("\nDocker Deployed: ✅\n"); - } catch (error) { - writeStream.write(`ERROR: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } + await pullImage( + dockerImage, + (data) => { + if (writeStream.writable) { + writeStream.write(`${data.status}\n`); + } + }, + authConfig, + ); + await mechanizeDockerContainer(application); + writeStream.write("\nDocker Deployed: ✅\n"); + } catch (error) { + writeStream.write(`ERROR: ${error}: ❌`); + throw error; + } finally { + writeStream.end(); + } }; diff --git a/utils/api.ts b/utils/api.ts index 2fd1e4023..625f80087 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -35,17 +35,6 @@ export const api = createTRPCNext({ httpBatchLink({ url: `${getBaseUrl()}/api/trpc`, }), - // createWSClient({ - // url: `ws://localhost:3000`, - // }), - // loggerLink({ - // enabled: (opts) => - // process.env.NODE_ENV === "development" || - // (opts.direction === "down" && opts.result instanceof Error), - // }), - // httpBatchLink({ - // url: `${getBaseUrl()}/api/trpc`, - // }), ], }; },