Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const baseSettings: WebServerSettings = {
metaTitle: null,
footerText: null,
},
domainRestrictionConfig: {
enabled: false,
allowedWildcards: [],
},
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,23 @@ interface Props {
children: React.ReactNode;
}

const extractBaseDomain = (wildcardPattern: string): string => {
const normalized = wildcardPattern.toLowerCase().trim();
if (normalized.startsWith("**.")) {
return normalized.slice(3);
}
if (normalized.startsWith("*.")) {
return normalized.slice(2);
}
return normalized;
};

export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const [subdomain, setSubdomain] = useState("");
const [selectedBaseDomain, setSelectedBaseDomain] = useState("");

const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
Expand Down Expand Up @@ -183,6 +196,15 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
serverId: application?.serverId || "",
});

const { data: restrictionConfig } =
api.settings.getDomainRestrictionConfig.useQuery();

const isRestrictionEnabled =
restrictionConfig?.enabled &&
(restrictionConfig?.allowedWildcards?.length ?? 0) > 0;
const baseDomains =
restrictionConfig?.allowedWildcards?.map(extractBaseDomain) ?? [];

const {
data: services,
isFetching: isLoadingServices,
Expand Down Expand Up @@ -271,6 +293,25 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
}
}, [certificateType, form]);

// Initialize selected base domain when restriction config loads
useEffect(() => {
if (baseDomains.length > 0 && !selectedBaseDomain) {
setSelectedBaseDomain(baseDomains[0] ?? "");
}
}, [baseDomains, selectedBaseDomain]);

useEffect(() => {
if (isRestrictionEnabled && selectedBaseDomain) {
if (subdomain) {
form.setValue("host", `${subdomain}.${selectedBaseDomain}`);
form.clearErrors("host");
} else {
form.setValue("host", "");
form.setError("host", { message: "Subdomain is required" });
}
}
}, [subdomain, selectedBaseDomain, isRestrictionEnabled, form]);

const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId ? "Error updating the domain" : "Error creating the domain",
Expand Down Expand Up @@ -512,7 +553,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
name="host"
render={({ field }) => (
<FormItem>
{!canGenerateTraefikMeDomains &&
{!isRestrictionEnabled &&
!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
Expand All @@ -527,51 +569,140 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
to make your traefik.me domain work.
</AlertBlock>
)}
{isTraefikMeDomain && (
{!isRestrictionEnabled && isTraefikMeDomain && (
<AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP
service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect.
</AlertBlock>
)}
<FormLabel>Host</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: application?.appName || "",
serverId: application?.serverId || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>

{isRestrictionEnabled ? (
<>
<div className="space-y-4">
<div>
<FormLabel>Subdomain</FormLabel>
<div className="flex gap-2">
<Input
placeholder="my-app"
value={subdomain}
onChange={(e) =>
setSubdomain(
e.target.value.toLowerCase().trim(),
)
}
/>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
onClick={() => {
const randomSubdomain = `${application?.appName || "app"}-${Math.random().toString(36).substring(2, 8)}`;
setSubdomain(randomSubdomain);
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate random subdomain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>

{baseDomains.length > 1 ? (
<div>
<FormLabel>Domain</FormLabel>
<Select
value={selectedBaseDomain}
onValueChange={setSelectedBaseDomain}
>
<SelectTrigger>
<SelectValue placeholder="Select a domain" />
</SelectTrigger>
<SelectContent>
{baseDomains.map((domain) => (
<SelectItem key={domain} value={domain}>
{domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : (
<div>
<FormLabel>Domain</FormLabel>
<Input
value={selectedBaseDomain}
disabled
className="bg-muted"
/>
</div>
)}

{subdomain && selectedBaseDomain && (
<div className="p-3 bg-muted rounded-lg">
<span className="text-sm text-muted-foreground">
Preview:{" "}
</span>
<span className="font-mono">
{subdomain}.{selectedBaseDomain}
</span>
</div>
)}
</div>
<input type="hidden" {...field} />
</>
) : (
<>
<FormLabel>Host</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: application?.appName || "",
serverId: application?.serverId || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</>
)}

<FormMessage />
</FormItem>
Expand Down
Loading