Files
dokploy/apps/dokploy/components/dashboard/settings/web-domain.tsx
Mauricio Siu d5dd35c8f8 fix(settings): allow clearing the server domain
The Server Domain form rejected an empty value ('Invalid domain name'),
making domain assignment a one-way operation. The backend already
removes the Traefik router and clears the host when it receives an
empty host, so only the client-side validation blocked removal.

Allow an empty domain to clear it, and skip the https/letsencrypt
requirements when the domain is being removed.

Fixes #4821
2026-07-14 11:39:30 -06:00

262 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
INVALID_HOSTNAME_MESSAGE,
VALID_HOSTNAME_REGEX,
} from "@dokploy/server/utils/hostname-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { GlobeIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
const addServerDomain = z
.object({
domain: z
.string()
.trim()
.toLowerCase()
// empty clears the server domain and reverts to IP-only access
.refine((val) => val === "" || VALID_HOSTNAME_REGEX.test(val), {
message: INVALID_HOSTNAME_MESSAGE,
}),
letsEncryptEmail: z.string(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
})
.superRefine((data, ctx) => {
if (data.domain && data.https && !data.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
if (
data.domain &&
data.https &&
data.certificateType === "letsencrypt" &&
!data.letsEncryptEmail
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"LetsEncrypt email is required when certificate type is letsencrypt",
path: ["letsEncryptEmail"],
});
}
});
type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isPending } =
api.settings.assignDomainServer.useMutation();
const form = useForm<AddServerDomain>({
defaultValues: {
domain: "",
certificateType: "none",
letsEncryptEmail: "",
https: false,
},
resolver: zodResolver(addServerDomain),
});
const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.host || "";
const hasChanged = domain !== host;
useEffect(() => {
if (data) {
form.reset({
domain: data?.host || "",
certificateType: data?.certificateType || "none",
letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.https || false,
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: AddServerDomain) => {
await mutateAsync({
host: data.domain,
letsEncryptEmail: data.letsEncryptEmail,
certificateType: data.certificateType,
https: data.https,
})
.then(async () => {
await refetch();
toast.success("Domain Assigned");
})
.catch(() => {
toast.error("Error assigning the domain");
});
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl flex flex-row gap-2">
<GlobeIcon className="size-6 text-muted-foreground self-center" />
Server Domain
</CardTitle>
<CardDescription>
Add a domain to your server application.
</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-2 py-6 border-t">
{/* Warning for GitHub webhook URL changes */}
{hasChanged && (
<AlertBlock type="warning">
<div className="space-y-2">
<p className="font-medium"> Important: URL Change Impact</p>
<p>
If you change the Dokploy Server URL make sure to update
your Github Apps to keep the auto-deploy working and preview
deployments working.
</p>
</div>
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 grid-cols-2"
>
<FormField
control={form.control}
name="domain"
render={({ field }) => {
return (
<FormItem className="col-span-2 md:col-span-1">
<FormLabel>Domain</FormLabel>
<FormControl>
<Input
className="w-full"
placeholder={"dokploy.com"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="letsEncryptEmail"
render={({ field }) => {
return (
<FormItem className="col-span-2 md:col-span-1">
<FormLabel>Let's Encrypt Email</FormLabel>
<FormControl>
<Input
className="w-full"
placeholder={"Dp4kz@example.com"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<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-xs 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"
render={({ field }) => {
return (
<FormItem className="col-span-2">
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>None</SelectItem>
<SelectItem value={"letsencrypt"}>
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
)}
<div className="flex w-full justify-end col-span-2">
<Button isLoading={isPending} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
);
};