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
90 changes: 90 additions & 0 deletions apps/dokploy/__test__/compose/domain/labels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("createDomainLabels", () => {
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
middlewares: null,
};

it("should create basic labels for web entrypoint", async () => {
Expand Down Expand Up @@ -240,4 +241,93 @@ describe("createDomainLabels", () => {
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});

it("should add single custom middleware to router", async () => {
const customMiddlewareDomain = {
...baseDomain,
middlewares: ["auth@file"],
};
const labels = await createDomainLabels(appName, customMiddlewareDomain, "web");

expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=auth@file",
);
});

it("should add multiple custom middlewares to router", async () => {
const customMiddlewareDomain = {
...baseDomain,
middlewares: ["auth@file", "rate-limit@file"],
};
const labels = await createDomainLabels(appName, customMiddlewareDomain, "web");

expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=auth@file,rate-limit@file",
);
});

it("should combine custom middlewares with HTTPS redirect in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
middlewares: ["auth@file"],
};
const labels = await createDomainLabels(appName, combinedDomain, "web");

// HTTPS redirect should come before custom middleware
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,auth@file",
);
});

it("should combine custom middlewares with stripPath middleware", async () => {
const combinedDomain = {
...baseDomain,
path: "/api",
stripPath: true,
middlewares: ["auth@file"],
};
const labels = await createDomainLabels(appName, combinedDomain, "web");

// stripprefix should come before custom middleware
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1,auth@file",
);
});

it("should combine all built-in middlewares with custom middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
middlewares: ["auth@file", "rate-limit@file"],
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");

// Order: redirect-to-https, stripprefix, addprefix, then custom middlewares
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1,auth@file,rate-limit@file",
);
});

it("should include custom middlewares on websecure entrypoint", async () => {
const customMiddlewareDomain = {
...baseDomain,
https: true,
middlewares: ["auth@file"],
};
const websecureLabels = await createDomainLabels(
appName,
customMiddlewareDomain,
"websecure",
);

// Websecure should have custom middleware but not redirect-to-https
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=auth@file",
);
expect(websecureLabels).not.toContain("redirect-to-https");
});
});
75 changes: 75 additions & 0 deletions apps/dokploy/__test__/traefik/traefik.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const baseDomain: Domain = {
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
middlewares: null,
};

const baseRedirect: Redirect = {
Expand Down Expand Up @@ -262,6 +263,80 @@ test("Websecure entrypoint on https domain with redirect", async () => {
expect(router.middlewares).toContain("redirect-test-1");
});

/** Custom Middlewares */

test("Web entrypoint with single custom middleware", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, middlewares: ["auth@file"] },
"web",
);

expect(router.middlewares).toContain("auth@file");
});

test("Web entrypoint with multiple custom middlewares", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, middlewares: ["auth@file", "rate-limit@file"] },
"web",
);

expect(router.middlewares).toContain("auth@file");
expect(router.middlewares).toContain("rate-limit@file");
});

test("Web entrypoint on https domain with custom middleware", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, https: true, middlewares: ["auth@file"] },
"web",
);

// Should only have HTTPS redirect - custom middleware applies on websecure
expect(router.middlewares).toContain("redirect-to-https");
expect(router.middlewares).not.toContain("auth@file");
});

test("Websecure entrypoint with custom middleware", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, https: true, middlewares: ["auth@file"] },
"websecure",
);

// Should have custom middleware but not HTTPS redirect
expect(router.middlewares).not.toContain("redirect-to-https");
expect(router.middlewares).toContain("auth@file");
});

test("Web entrypoint with redirect and custom middleware", async () => {
const router = await createRouterConfig(
{
...baseApp,
appName: "test",
redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }],
},
{ ...baseDomain, middlewares: ["auth@file"] },
"web",
);

// Should have both redirect middleware and custom middleware
expect(router.middlewares).toContain("redirect-test-1");
expect(router.middlewares).toContain("auth@file");
});

test("Web entrypoint with empty middlewares array", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, https: false, middlewares: [] },
"web",
);

// Should behave same as no middlewares - no redirect for http
expect(router.middlewares).not.toContain("redirect-to-https");
});

/** Certificates */

test("CertificateType on websecure entrypoint", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import { DatabaseZap, Dices, RefreshCw, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand Down Expand Up @@ -66,6 +67,7 @@ export const domain = z
customCertResolver: z.string().optional(),
serviceName: z.string().optional(),
domainType: z.enum(["application", "compose", "preview"]).optional(),
middlewares: z.array(z.string()).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
Expand Down Expand Up @@ -201,6 +203,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
customCertResolver: undefined,
serviceName: undefined,
domainType: type,
middlewares: [],
},
mode: "onChange",
});
Expand All @@ -224,6 +227,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
customCertResolver: data?.customCertResolver || undefined,
serviceName: data?.serviceName || undefined,
domainType: data?.domainType || type,
middlewares: data?.middlewares || [],
});
}

Expand All @@ -238,6 +242,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
certificateType: undefined,
customCertResolver: undefined,
domainType: type,
middlewares: [],
});
}
}, [form, data, isLoading, domainId]);
Expand Down Expand Up @@ -725,6 +730,88 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
)}
</>
)}
<FormField
control={form.control}
name="middlewares"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Middlewares</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
<p>
Add Traefik middleware references. Middlewares
must be defined in your Traefik configuration.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((name, index) => (
<Badge key={index} variant="secondary">
{name}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newMiddlewares = [...(field.value || [])];
newMiddlewares.splice(index, 1);
form.setValue("middlewares", newMiddlewares);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="e.g., rate-limit@file, auth@file"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value && !field.value?.includes(value)) {
form.setValue("middlewares", [
...(field.value || []),
value,
]);
input.value = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="e.g., rate-limit@file, auth@file"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value && !field.value?.includes(value)) {
form.setValue("middlewares", [
...(field.value || []),
value,
]);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,22 @@ export const ShowDomains = ({ id, type }: Props) => {
</TooltipProvider>
)}

{item.middlewares?.map((middleware, index) => (
<TooltipProvider key={`${middleware}-${index}`}>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="secondary">
<InfoIcon className="size-3 mr-1" />
Middleware: {middleware}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Traefik middleware reference</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}

<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/drizzle/0134_whole_dazzler.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "domain" ADD COLUMN "middlewares" text[];
Loading