feat(settings): add HTTPS support and update user schema

- Introduced a new boolean field 'https' in the user schema to manage HTTPS settings.
- Updated the web domain form to include an HTTPS toggle, allowing users to enable or disable HTTPS.
- Enhanced validation logic to ensure certificate type is required when HTTPS is enabled.
- Modified Traefik configuration to handle HTTPS routing based on user settings.
This commit is contained in:
Mauricio Siu 2025-04-06 01:41:47 -06:00
parent 14bc26e065
commit 1605aedd6e
7 changed files with 5500 additions and 48 deletions

View File

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

View File

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

View File

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