mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 21:31:11 +00:00
ee802cd54c
Add an `enabled` flag (default true) so a domain can be switched off without deleting it, preserving its configuration. - schema: new `enabled` column + migration; added to apiUpdateDomain - traefik (applications): guard manageDomain so a disabled domain never exposes a router from any caller (create/update/forward-auth/toggle); applies instantly through the file provider - compose: skip disabled domains when generating docker labels, so the change applies on the next deployment - tRPC: new domain.toggleEnable procedure (permission-checked + audited), returns requiresRedeploy for compose domains - UI: switch on the domain card (host row, variant E) and a Status column in the table view; disabled cards are dimmed; reuses the compose redeploy hint for the toast Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
258 lines
7.6 KiB
TypeScript
258 lines
7.6 KiB
TypeScript
import {
|
|
createDomain,
|
|
findApplicationById,
|
|
findDomainById,
|
|
findDomainsByApplicationId,
|
|
findDomainsByComposeId,
|
|
findPreviewDeploymentById,
|
|
findServerById,
|
|
generateTraefikMeDomain,
|
|
getWebServerSettings,
|
|
manageDomain,
|
|
removeDomain,
|
|
removeDomainById,
|
|
updateDomainById,
|
|
validateDomain,
|
|
} from "@dokploy/server";
|
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
import {
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
withPermission,
|
|
} from "@/server/api/trpc";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
import {
|
|
apiCreateDomain,
|
|
apiFindCompose,
|
|
apiFindDomain,
|
|
apiFindOneApplication,
|
|
apiUpdateDomain,
|
|
} from "@/server/db/schema";
|
|
|
|
export const domainRouter = createTRPCRouter({
|
|
create: protectedProcedure
|
|
.input(apiCreateDomain)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
if (input.domainType === "compose" && input.composeId) {
|
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
|
domain: ["create"],
|
|
});
|
|
} else if (input.domainType === "application" && input.applicationId) {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
domain: ["create"],
|
|
});
|
|
}
|
|
const domain = await createDomain(input);
|
|
await audit(ctx, {
|
|
action: "create",
|
|
resourceType: "domain",
|
|
resourceId: domain.domainId,
|
|
resourceName: domain.host,
|
|
});
|
|
return domain;
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Error creating the domain",
|
|
cause: error,
|
|
});
|
|
}
|
|
}),
|
|
byApplicationId: protectedProcedure
|
|
.input(apiFindOneApplication)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
|
domain: ["read"],
|
|
});
|
|
return await findDomainsByApplicationId(input.applicationId);
|
|
}),
|
|
byComposeId: protectedProcedure
|
|
.input(apiFindCompose)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
|
domain: ["read"],
|
|
});
|
|
return await findDomainsByComposeId(input.composeId);
|
|
}),
|
|
generateDomain: withPermission("domain", "create")
|
|
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
return generateTraefikMeDomain(
|
|
input.appName,
|
|
ctx.user.ownerId,
|
|
input.serverId,
|
|
);
|
|
}),
|
|
canGenerateTraefikMeDomains: withPermission("domain", "read")
|
|
.input(z.object({ serverId: z.string() }))
|
|
.query(async ({ input }) => {
|
|
if (input.serverId) {
|
|
const server = await findServerById(input.serverId);
|
|
return server.ipAddress;
|
|
}
|
|
const settings = await getWebServerSettings();
|
|
return settings?.serverIp || "";
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(apiUpdateDomain)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const currentDomain = await findDomainById(input.domainId);
|
|
const serviceId = currentDomain.applicationId || currentDomain.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
domain: ["create"],
|
|
});
|
|
} else if (currentDomain.previewDeploymentId) {
|
|
const preview = await findPreviewDeploymentById(
|
|
currentDomain.previewDeploymentId,
|
|
);
|
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
|
domain: ["create"],
|
|
});
|
|
}
|
|
|
|
const result = await updateDomainById(input.domainId, input);
|
|
const domain = await findDomainById(input.domainId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "domain",
|
|
resourceId: domain.domainId,
|
|
resourceName: domain.host,
|
|
});
|
|
if (domain.applicationId) {
|
|
const application = await findApplicationById(domain.applicationId);
|
|
await manageDomain(application, domain);
|
|
} else if (domain.previewDeploymentId) {
|
|
const previewDeployment = await findPreviewDeploymentById(
|
|
domain.previewDeploymentId,
|
|
);
|
|
const application = await findApplicationById(
|
|
previewDeployment.applicationId,
|
|
);
|
|
application.appName = previewDeployment.appName;
|
|
await manageDomain(application, domain);
|
|
}
|
|
return result;
|
|
}),
|
|
toggleEnable: protectedProcedure
|
|
.input(apiFindDomain)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const currentDomain = await findDomainById(input.domainId);
|
|
const serviceId = currentDomain.applicationId || currentDomain.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
domain: ["create"],
|
|
});
|
|
} else if (currentDomain.previewDeploymentId) {
|
|
const preview = await findPreviewDeploymentById(
|
|
currentDomain.previewDeploymentId,
|
|
);
|
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
|
domain: ["create"],
|
|
});
|
|
}
|
|
|
|
const result = await updateDomainById(input.domainId, {
|
|
enabled: !currentDomain.enabled,
|
|
});
|
|
const domain = await findDomainById(input.domainId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "domain",
|
|
resourceId: domain.domainId,
|
|
resourceName: domain.host,
|
|
});
|
|
|
|
// Applications apply instantly through the traefik file provider:
|
|
// manageDomain creates the router when enabled and removes it when
|
|
// disabled. Compose domains are docker labels and only change on the
|
|
// next deployment, so we just persist the flag here.
|
|
if (domain.applicationId) {
|
|
const application = await findApplicationById(domain.applicationId);
|
|
await manageDomain(application, domain);
|
|
} else if (domain.previewDeploymentId) {
|
|
const previewDeployment = await findPreviewDeploymentById(
|
|
domain.previewDeploymentId,
|
|
);
|
|
const application = await findApplicationById(
|
|
previewDeployment.applicationId,
|
|
);
|
|
application.appName = previewDeployment.appName;
|
|
await manageDomain(application, domain);
|
|
}
|
|
|
|
return {
|
|
...result,
|
|
requiresRedeploy: domain.domainType === "compose",
|
|
};
|
|
}),
|
|
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
|
|
const domain = await findDomainById(input.domainId);
|
|
const serviceId = domain.applicationId || domain.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
domain: ["read"],
|
|
});
|
|
} else if (domain.previewDeploymentId) {
|
|
const preview = await findPreviewDeploymentById(
|
|
domain.previewDeploymentId,
|
|
);
|
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
|
domain: ["read"],
|
|
});
|
|
}
|
|
return domain;
|
|
}),
|
|
delete: protectedProcedure
|
|
.input(apiFindDomain)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const domain = await findDomainById(input.domainId);
|
|
const serviceId = domain.applicationId || domain.composeId;
|
|
if (serviceId) {
|
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
|
domain: ["delete"],
|
|
});
|
|
} else if (domain.previewDeploymentId) {
|
|
const preview = await findPreviewDeploymentById(
|
|
domain.previewDeploymentId,
|
|
);
|
|
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
|
|
domain: ["delete"],
|
|
});
|
|
}
|
|
|
|
const result = await removeDomainById(input.domainId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "domain",
|
|
resourceId: domain.domainId,
|
|
resourceName: domain.host,
|
|
});
|
|
|
|
if (domain.applicationId) {
|
|
const application = await findApplicationById(domain.applicationId);
|
|
await removeDomain(application, domain.uniqueConfigKey);
|
|
}
|
|
|
|
return result;
|
|
}),
|
|
|
|
validateDomain: withPermission("domain", "read")
|
|
.input(
|
|
z.object({
|
|
domain: z.string(),
|
|
serverIp: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input }) => {
|
|
return validateDomain(input.domain, input.serverIp);
|
|
}),
|
|
});
|