Merge pull request #1637 from Dokploy/1601-duplicate-domain-bug

feat(settings): add HTTPS support and update user schema
This commit is contained in:
Mauricio Siu 2025-04-06 02:42:04 -06:00 committed by GitHub
commit 48ec0a74ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 5502 additions and 49 deletions

View File

@ -14,6 +14,7 @@ import {
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = { const baseAdmin: User = {
https: false,
enablePaidFeatures: false, enablePaidFeatures: false,
metricsConfig: { metricsConfig: {
containers: { containers: {
@ -73,7 +74,6 @@ beforeEach(() => {
test("Should read the configuration file", () => { test("Should read the configuration file", () => {
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe( expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
"dokploy-service-app", "dokploy-service-app",
); );
@ -83,6 +83,7 @@ test("Should apply redirect-to-https", () => {
updateServerTraefik( updateServerTraefik(
{ {
...baseAdmin, ...baseAdmin,
https: true,
certificateType: "letsencrypt", certificateType: "letsencrypt",
}, },
"example.com", "example.com",

View File

@ -9,6 +9,7 @@ import {
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -22,6 +23,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { GlobeIcon } from "lucide-react"; import { GlobeIcon } from "lucide-react";
@ -33,11 +35,19 @@ import { z } from "zod";
const addServerDomain = z const addServerDomain = z
.object({ .object({
domain: z.string().min(1, { message: "URL is required" }), domain: z.string(),
letsEncryptEmail: z.string(), letsEncryptEmail: z.string(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]), certificateType: z.enum(["letsencrypt", "none", "custom"]),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.https && !data.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@ -61,15 +71,18 @@ export const WebDomain = () => {
domain: "", domain: "",
certificateType: "none", certificateType: "none",
letsEncryptEmail: "", letsEncryptEmail: "",
https: false,
}, },
resolver: zodResolver(addServerDomain), resolver: zodResolver(addServerDomain),
}); });
const https = form.watch("https");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
domain: data?.user?.host || "", domain: data?.user?.host || "",
certificateType: data?.user?.certificateType, certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "", letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.user?.https || false,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@ -79,6 +92,7 @@ export const WebDomain = () => {
host: data.domain, host: data.domain,
letsEncryptEmail: data.letsEncryptEmail, letsEncryptEmail: data.letsEncryptEmail,
certificateType: data.certificateType, certificateType: data.certificateType,
https: data.https,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@ -155,44 +169,67 @@ export const WebDomain = () => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="certificateType" name="https"
render={({ field }) => { render={({ field }) => (
return ( <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm w-full col-span-2">
<FormItem className="md:col-span-2"> <div className="space-y-0.5">
<FormLabel> <FormLabel>HTTPS</FormLabel>
{t("settings.server.domain.form.certificate.label")} <FormDescription>
</FormLabel> Automatically provision SSL Certificate.
<Select </FormDescription>
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"letsencrypt"}>
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage /> <FormMessage />
</FormItem> </div>
); <FormControl>
}} <Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/> />
{https && (
<FormField
control={form.control}
name="certificateType"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>
{t("settings.server.domain.form.certificate.label")}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"letsencrypt"}>
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
)}
<div className="flex w-full justify-end col-span-2"> <div className="flex w-full justify-end col-span-2">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">

View File

@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "https" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -589,6 +589,13 @@
"when": 1743288371413, "when": 1743288371413,
"tag": "0083_parallel_stranger", "tag": "0083_parallel_stranger",
"breakpoints": true "breakpoints": true
},
{
"idx": 84,
"version": "7",
"when": 1743923992280,
"tag": "0084_thin_iron_lad",
"breakpoints": true
} }
] ]
} }

View File

@ -184,6 +184,7 @@ export const settingsRouter = createTRPCRouter({
letsEncryptEmail: input.letsEncryptEmail, letsEncryptEmail: input.letsEncryptEmail,
}), }),
certificateType: input.certificateType, certificateType: input.certificateType,
https: input.https,
}); });
if (!user) { if (!user) {

View File

@ -50,6 +50,7 @@ export const users_temp = pgTable("user_temp", {
// Admin // Admin
serverIp: text("serverIp"), serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"), certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"), host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"), letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"), sshPrivateKey: text("sshPrivateKey"),
@ -202,10 +203,12 @@ export const apiAssignDomain = createSchema
host: true, host: true,
certificateType: true, certificateType: true,
letsEncryptEmail: true, letsEncryptEmail: true,
https: true,
}) })
.required() .required()
.partial({ .partial({
letsEncryptEmail: true, letsEncryptEmail: true,
https: true,
}); });
export const apiUpdateDockerCleanup = createSchema export const apiUpdateDockerCleanup = createSchema

View File

@ -3,7 +3,11 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user"; import type { User } from "@dokploy/server/services/user";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { loadOrCreateConfig, writeTraefikConfig } from "./application"; import {
loadOrCreateConfig,
removeTraefikConfig,
writeTraefikConfig,
} from "./application";
import type { FileConfig } from "./file-types"; import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types"; import type { MainTraefikConfig } from "./types";
@ -11,32 +15,62 @@ export const updateServerTraefik = (
user: User | null, user: User | null,
newHost: string | null, newHost: string | null,
) => { ) => {
const { https, certificateType } = user || {};
const appName = "dokploy"; const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName); const config: FileConfig = loadOrCreateConfig(appName);
config.http = config.http || { routers: {}, services: {} }; config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {}; config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
const currentRouterConfig = config.http.routers[`${appName}-router-app`]; const currentRouterConfig = config.http.routers[`${appName}-router-app`] || {
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["web"],
};
config.http.routers[`${appName}-router-app`] = currentRouterConfig;
if (currentRouterConfig && newHost) { config.http.services = {
currentRouterConfig.rule = `Host(\`${newHost}\`)`; ...config.http.services,
[`${appName}-service-app`]: {
loadBalancer: {
servers: [
{
url: `http://dokploy:${process.env.PORT || 3000}`,
passHostHeader: true,
},
],
},
},
};
if (user?.certificateType === "letsencrypt") { if (https) {
currentRouterConfig.middlewares = ["redirect-to-https"];
if (certificateType === "letsencrypt") {
config.http.routers[`${appName}-router-app-secure`] = { config.http.routers[`${appName}-router-app-secure`] = {
...currentRouterConfig, rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"], entryPoints: ["websecure"],
tls: { certResolver: "letsencrypt" }, tls: { certResolver: "letsencrypt" },
}; };
currentRouterConfig.middlewares = ["redirect-to-https"];
} else { } else {
delete config.http.routers[`${appName}-router-app-secure`]; config.http.routers[`${appName}-router-app-secure`] = {
currentRouterConfig.middlewares = []; rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"],
};
} }
} else {
delete config.http.routers[`${appName}-router-app-secure`];
currentRouterConfig.middlewares = [];
} }
writeTraefikConfig(config, appName); if (newHost) {
writeTraefikConfig(config, appName);
} else {
removeTraefikConfig(appName);
}
}; };
export const updateLetsEncryptEmail = (newEmail: string | null) => { export const updateLetsEncryptEmail = (newEmail: string | null) => {