mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
8018027330
* feat: add self-hosted enterprise restrictions (remote-servers-only, enforce-sso) - Add `remoteServersOnly` field to webServerSettings: prevents creating services on the local Dokploy VM, forcing all deployments to remote servers. Validated in all 8 service routers (application, compose, postgres, mysql, mongo, redis, mariadb, libsql). - Add `enforceSSO` field to webServerSettings: hides the email/password login form and shows only the SSO button on the login page. - Both settings are enterprise-only (enterpriseProcedure) and self-hosted-only (blocked at the API level when IS_CLOUD=true). - UI toggles added to the SSO settings page under a new "Self-hosted Restrictions" card (hidden in cloud). Login page reads enforceSSO from getServerSideProps to avoid client-side flash. - Migrations: 0167_fresh_goliath.sql, 0168_long_justice.sql * fix: add missing final newlines to migration files * refactor: improve code formatting for better readability in multiple components - Adjusted formatting in `add-application.tsx`, `add-compose.tsx`, and `add-database.tsx` to enhance readability by adding line breaks and consistent indentation. - Updated `toggle-enforce-sso.tsx` to simplify the Switch component's props. - Reformatted imports in `index.tsx` and `sso.tsx` for consistency. - Cleaned up conditional statements in various router files for improved clarity. * fix: add enforceSSO to test mock
1120 lines
27 KiB
TypeScript
1120 lines
27 KiB
TypeScript
import {
|
|
CLEANUP_CRON_JOB,
|
|
checkGPUStatus,
|
|
checkPortInUse,
|
|
checkPostgresHealth,
|
|
checkRedisHealth,
|
|
checkTraefikHealth,
|
|
cleanupAll,
|
|
cleanupAllBackground,
|
|
cleanupBuilders,
|
|
cleanupContainers,
|
|
cleanupImages,
|
|
cleanupSystem,
|
|
cleanupVolumes,
|
|
DEFAULT_UPDATE_DATA,
|
|
execAsync,
|
|
findServerById,
|
|
getDockerDiskUsage,
|
|
getDokployImageTag,
|
|
getLogCleanupStatus,
|
|
getUpdateData,
|
|
getWebServerSettings,
|
|
IS_CLOUD,
|
|
parseRawConfig,
|
|
paths,
|
|
prepareEnvironmentVariables,
|
|
processLogs,
|
|
readConfig,
|
|
readConfigInPath,
|
|
readDirectory,
|
|
readEnvironmentVariables,
|
|
readMainConfig,
|
|
readMonitoringConfig,
|
|
readPorts,
|
|
recreateDirectory,
|
|
reloadDockerResource,
|
|
sendDockerCleanupNotifications,
|
|
setupGPUSupport,
|
|
spawnAsync,
|
|
startLogCleanup,
|
|
stopLogCleanup,
|
|
updateLetsEncryptEmail,
|
|
updateServerById,
|
|
updateServerTraefik,
|
|
updateWebServerSettings,
|
|
writeConfig,
|
|
writeMainConfig,
|
|
writeTraefikConfigInPath,
|
|
writeTraefikSetup,
|
|
} from "@dokploy/server";
|
|
import { db } from "@dokploy/server/db";
|
|
import { checkPermission } from "@dokploy/server/services/permission";
|
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { eq, sql } from "drizzle-orm";
|
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
|
import { parse, stringify } from "yaml";
|
|
import { z } from "zod";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
import {
|
|
apiAssignDomain,
|
|
apiEnableDashboard,
|
|
apiModifyTraefikConfig,
|
|
apiReadStatsLogs,
|
|
apiReadTraefikConfig,
|
|
apiSaveSSHKey,
|
|
apiServerSchema,
|
|
apiTraefikConfig,
|
|
apiUpdateDockerCleanup,
|
|
projects,
|
|
server,
|
|
} from "@/server/db/schema";
|
|
import { cleanAllDeploymentQueue } from "@/server/queues/queueSetup";
|
|
import { removeJob, schedule } from "@/server/utils/backup";
|
|
import packageInfo from "../../../package.json";
|
|
import { appRouter } from "../root";
|
|
import {
|
|
adminProcedure,
|
|
createTRPCRouter,
|
|
enterpriseProcedure,
|
|
protectedProcedure,
|
|
publicProcedure,
|
|
} from "../trpc";
|
|
|
|
export const settingsRouter = createTRPCRouter({
|
|
getWebServerSettings: protectedProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return null;
|
|
}
|
|
const settings = await getWebServerSettings();
|
|
return settings;
|
|
}),
|
|
reloadServer: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
await reloadDockerResource("dokploy", undefined, packageInfo.version);
|
|
await audit(ctx, {
|
|
action: "reload",
|
|
resourceType: "settings",
|
|
resourceName: "dokploy",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanRedis: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
|
|
const { stdout: containerId } = await execAsync(
|
|
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
|
|
);
|
|
|
|
if (!containerId) {
|
|
throw new Error("Redis container not found");
|
|
}
|
|
|
|
const redisContainerId = containerId.trim();
|
|
|
|
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "clean-redis",
|
|
});
|
|
return true;
|
|
}),
|
|
reloadRedis: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
await reloadDockerResource("dokploy-redis");
|
|
await audit(ctx, {
|
|
action: "reload",
|
|
resourceType: "settings",
|
|
resourceName: "dokploy-redis",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const result = cleanAllDeploymentQueue();
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "clean-deployment-queue",
|
|
});
|
|
return result;
|
|
}),
|
|
reloadTraefik: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
// Run in background so the request returns immediately; avoids proxy timeouts.
|
|
void reloadDockerResource("dokploy-traefik", input?.serverId).catch(
|
|
(err) => {
|
|
console.error("reloadTraefik background:", err);
|
|
},
|
|
);
|
|
await audit(ctx, {
|
|
action: "reload",
|
|
resourceType: "settings",
|
|
resourceName: "dokploy-traefik",
|
|
});
|
|
return true;
|
|
}),
|
|
toggleDashboard: adminProcedure
|
|
.input(apiEnableDashboard)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const ports = await readPorts("dokploy-traefik", input.serverId);
|
|
const env = await readEnvironmentVariables(
|
|
"dokploy-traefik",
|
|
input.serverId,
|
|
);
|
|
const preparedEnv = prepareEnvironmentVariables(env);
|
|
let newPorts = ports;
|
|
// If receive true, add 8080 to ports
|
|
if (input.enableDashboard) {
|
|
// Check if port 8080 is already in use before enabling dashboard
|
|
const portCheck = await checkPortInUse(8080, input.serverId);
|
|
if (portCheck.isInUse) {
|
|
const conflictInfo = portCheck.conflictingContainer
|
|
? ` by ${portCheck.conflictingContainer}`
|
|
: "";
|
|
throw new TRPCError({
|
|
code: "CONFLICT",
|
|
message: `Port 8080 is already in use${conflictInfo}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
|
});
|
|
}
|
|
newPorts.push({
|
|
targetPort: 8080,
|
|
publishedPort: 8080,
|
|
protocol: "tcp",
|
|
});
|
|
} else {
|
|
newPorts = ports.filter((port) => port.targetPort !== 8080);
|
|
}
|
|
|
|
// Run in background so the request returns immediately; client polls /api/health.
|
|
// Avoids proxy timeouts (520) while Traefik is recreated.
|
|
void writeTraefikSetup({
|
|
env: preparedEnv,
|
|
additionalPorts: newPorts,
|
|
serverId: input.serverId,
|
|
}).catch((err) => {
|
|
console.error("toggleDashboard background writeTraefikSetup:", err);
|
|
});
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "toggle-dashboard",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanUnusedImages: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await cleanupImages(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-unused-images",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanUnusedVolumes: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await cleanupVolumes(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-unused-volumes",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanStoppedContainers: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await cleanupContainers(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-stopped-containers",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanDockerBuilder: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await cleanupBuilders(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-docker-builder",
|
|
});
|
|
}),
|
|
cleanDockerPrune: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await cleanupSystem(input?.serverId);
|
|
await cleanupBuilders(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-docker-prune",
|
|
});
|
|
return true;
|
|
}),
|
|
cleanAll: adminProcedure
|
|
.input(apiServerSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
// Execute cleanup in background and return immediately to avoid gateway timeouts
|
|
const result = await cleanupAllBackground(input?.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-all",
|
|
});
|
|
return result;
|
|
}),
|
|
cleanMonitoring: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const { MONITORING_PATH } = paths();
|
|
await recreateDirectory(MONITORING_PATH);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "clean-monitoring",
|
|
});
|
|
return true;
|
|
}),
|
|
getDockerDiskUsage: adminProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return [];
|
|
}
|
|
return getDockerDiskUsage();
|
|
}),
|
|
saveSSHPrivateKey: adminProcedure
|
|
.input(apiSaveSSHKey)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
await updateWebServerSettings({
|
|
sshPrivateKey: input.sshPrivateKey,
|
|
});
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "ssh-private-key",
|
|
});
|
|
return true;
|
|
}),
|
|
assignDomainServer: adminProcedure
|
|
.input(apiAssignDomain)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const settings = await updateWebServerSettings({
|
|
host: input.host,
|
|
letsEncryptEmail: input.letsEncryptEmail,
|
|
certificateType: input.certificateType,
|
|
https: input.https,
|
|
});
|
|
|
|
if (!settings) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Web server settings not found",
|
|
});
|
|
}
|
|
|
|
updateServerTraefik(settings, input.host);
|
|
if (input.letsEncryptEmail) {
|
|
updateLetsEncryptEmail(input.letsEncryptEmail);
|
|
}
|
|
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "assign-domain-server",
|
|
});
|
|
return settings;
|
|
}),
|
|
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
await updateWebServerSettings({
|
|
sshPrivateKey: null,
|
|
});
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "settings",
|
|
resourceName: "ssh-private-key",
|
|
});
|
|
return true;
|
|
}),
|
|
updateDockerCleanup: adminProcedure
|
|
.input(apiUpdateDockerCleanup)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (input.serverId) {
|
|
await updateServerById(input.serverId, {
|
|
enableDockerCleanup: input.enableDockerCleanup,
|
|
});
|
|
|
|
const server = await findServerById(input.serverId);
|
|
|
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to access this server",
|
|
});
|
|
}
|
|
|
|
if (server.enableDockerCleanup) {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.serverStatus === "inactive") {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Server is inactive",
|
|
});
|
|
}
|
|
if (IS_CLOUD) {
|
|
await schedule({
|
|
cronSchedule: CLEANUP_CRON_JOB,
|
|
serverId: input.serverId,
|
|
type: "server",
|
|
});
|
|
} else {
|
|
scheduleJob(server.serverId, CLEANUP_CRON_JOB, async () => {
|
|
console.log(
|
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
|
);
|
|
|
|
await cleanupAll(server.serverId);
|
|
|
|
await sendDockerCleanupNotifications(server.organizationId);
|
|
});
|
|
}
|
|
} else {
|
|
if (IS_CLOUD) {
|
|
await removeJob({
|
|
cronSchedule: CLEANUP_CRON_JOB,
|
|
serverId: input.serverId,
|
|
type: "server",
|
|
});
|
|
} else {
|
|
const currentJob = scheduledJobs[server.serverId];
|
|
currentJob?.cancel();
|
|
}
|
|
}
|
|
} else if (!IS_CLOUD) {
|
|
const settingsUpdated = await updateWebServerSettings({
|
|
enableDockerCleanup: input.enableDockerCleanup,
|
|
});
|
|
|
|
if (settingsUpdated?.enableDockerCleanup) {
|
|
scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
|
|
console.log(
|
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
|
);
|
|
|
|
await cleanupAll();
|
|
|
|
await sendDockerCleanupNotifications(
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
});
|
|
} else {
|
|
const currentJob = scheduledJobs["docker-cleanup"];
|
|
currentJob?.cancel();
|
|
}
|
|
}
|
|
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "docker-cleanup",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
updateRemoteServersOnly: enterpriseProcedure
|
|
.input(z.object({ remoteServersOnly: z.boolean() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "This feature is only available for self-hosted instances",
|
|
});
|
|
}
|
|
|
|
await updateWebServerSettings({
|
|
remoteServersOnly: input.remoteServersOnly,
|
|
});
|
|
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "remote-servers-only",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
updateEnforceSSO: enterpriseProcedure
|
|
.input(z.object({ enforceSSO: z.boolean() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "This feature is only available for self-hosted instances",
|
|
});
|
|
}
|
|
|
|
await updateWebServerSettings({
|
|
enforceSSO: input.enforceSSO,
|
|
});
|
|
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "enforce-sso",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
readTraefikConfig: adminProcedure.query(() => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const traefikConfig = readMainConfig();
|
|
return traefikConfig;
|
|
}),
|
|
|
|
updateTraefikConfig: adminProcedure
|
|
.input(apiTraefikConfig)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
writeMainConfig(input.traefikConfig);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "traefik-config",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
readWebServerTraefikConfig: adminProcedure.query(() => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const traefikConfig = readConfig("dokploy");
|
|
return traefikConfig;
|
|
}),
|
|
updateWebServerTraefikConfig: adminProcedure
|
|
.input(apiTraefikConfig)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
writeConfig("dokploy", input.traefikConfig);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "web-server-traefik-config",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
readMiddlewareTraefikConfig: adminProcedure.query(() => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const traefikConfig = readConfig("middlewares");
|
|
return traefikConfig;
|
|
}),
|
|
|
|
updateMiddlewareTraefikConfig: adminProcedure
|
|
.input(apiTraefikConfig)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
writeConfig("middlewares", input.traefikConfig);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "middleware-traefik-config",
|
|
});
|
|
return true;
|
|
}),
|
|
getUpdateData: protectedProcedure.mutation(async () => {
|
|
if (IS_CLOUD) {
|
|
return DEFAULT_UPDATE_DATA;
|
|
}
|
|
|
|
return await getUpdateData(packageInfo.version);
|
|
}),
|
|
updateServer: adminProcedure.mutation(async ({ ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
|
|
const data = await getUpdateData(packageInfo.version);
|
|
if (data.updateAvailable) {
|
|
void spawnAsync("docker", [
|
|
"service",
|
|
"update",
|
|
"--force",
|
|
"--image",
|
|
`dokploy/dokploy:${data.latestVersion}`,
|
|
"dokploy",
|
|
]);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "dokploy-version",
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}),
|
|
|
|
getDokployVersion: protectedProcedure.query(() => {
|
|
return packageInfo.version;
|
|
}),
|
|
getReleaseTag: protectedProcedure.query(() => {
|
|
return getDokployImageTag();
|
|
}),
|
|
readDirectories: protectedProcedure
|
|
.input(apiServerSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
try {
|
|
await checkPermission(ctx, { traefikFiles: ["read"] });
|
|
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
|
|
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
|
|
return result || [];
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
|
|
updateTraefikFile: protectedProcedure
|
|
.input(apiModifyTraefikConfig)
|
|
.mutation(async ({ input, ctx }) => {
|
|
await checkPermission(ctx, { traefikFiles: ["write"] });
|
|
await writeTraefikConfigInPath(
|
|
input.path,
|
|
input.traefikConfig,
|
|
input?.serverId,
|
|
);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "traefik-file",
|
|
});
|
|
return true;
|
|
}),
|
|
|
|
readTraefikFile: protectedProcedure
|
|
.input(apiReadTraefikConfig)
|
|
.query(async ({ input, ctx }) => {
|
|
await checkPermission(ctx, { traefikFiles: ["read"] });
|
|
|
|
if (input.serverId) {
|
|
const server = await findServerById(input.serverId);
|
|
|
|
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
}
|
|
}
|
|
|
|
return readConfigInPath(input.path, input.serverId);
|
|
}),
|
|
getIp: protectedProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return "";
|
|
}
|
|
const settings = await getWebServerSettings();
|
|
return settings?.serverIp || "";
|
|
}),
|
|
updateServerIp: adminProcedure
|
|
.input(
|
|
z.object({
|
|
serverIp: z.string(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const settings = await updateWebServerSettings({
|
|
serverIp: input.serverIp,
|
|
});
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "server-ip",
|
|
});
|
|
return settings;
|
|
}),
|
|
|
|
getOpenApiDocument: protectedProcedure.query(
|
|
async ({ ctx }): Promise<unknown> => {
|
|
const protocol = ctx.req.headers["x-forwarded-proto"];
|
|
const url = `${protocol}://${ctx.req.headers.host}/api`;
|
|
const openApiDocument = generateOpenApiDocument(appRouter, {
|
|
title: "tRPC OpenAPI",
|
|
version: packageInfo.version,
|
|
baseUrl: url,
|
|
docsUrl: `${url}/settings.getOpenApiDocument`,
|
|
tags: [
|
|
"admin",
|
|
"docker",
|
|
"compose",
|
|
"registry",
|
|
"cluster",
|
|
"user",
|
|
"domain",
|
|
"destination",
|
|
"backup",
|
|
"deployment",
|
|
"mounts",
|
|
"certificates",
|
|
"settings",
|
|
"security",
|
|
"redirects",
|
|
"port",
|
|
"project",
|
|
"application",
|
|
"mysql",
|
|
"postgres",
|
|
"redis",
|
|
"mongo",
|
|
"libsql",
|
|
"mariadb",
|
|
"sshRouter",
|
|
"gitProvider",
|
|
"bitbucket",
|
|
"ai",
|
|
"github",
|
|
"gitlab",
|
|
"gitea",
|
|
"tag",
|
|
"patch",
|
|
"server",
|
|
"volumeBackups",
|
|
"environment",
|
|
"auditLog",
|
|
"customRole",
|
|
"whitelabeling",
|
|
"sso",
|
|
"licenseKey",
|
|
"organization",
|
|
"previewDeployment",
|
|
],
|
|
});
|
|
|
|
openApiDocument.info = {
|
|
title: "Dokploy API",
|
|
description: "Endpoints for dokploy",
|
|
version: packageInfo.version,
|
|
};
|
|
|
|
// Add security schemes configuration
|
|
openApiDocument.components = {
|
|
...openApiDocument.components,
|
|
securitySchemes: {
|
|
apiKey: {
|
|
type: "apiKey",
|
|
in: "header",
|
|
name: "x-api-key",
|
|
description: "API key authentication",
|
|
},
|
|
},
|
|
};
|
|
|
|
// Apply security globally to all endpoints
|
|
openApiDocument.security = [
|
|
{
|
|
apiKey: [],
|
|
},
|
|
];
|
|
return openApiDocument;
|
|
},
|
|
),
|
|
readTraefikEnv: adminProcedure
|
|
.input(apiServerSchema)
|
|
.query(async ({ input }) => {
|
|
const envVars = await readEnvironmentVariables(
|
|
"dokploy-traefik",
|
|
input?.serverId,
|
|
);
|
|
return envVars;
|
|
}),
|
|
|
|
writeTraefikEnv: adminProcedure
|
|
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const envs = prepareEnvironmentVariables(input.env);
|
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
|
|
|
// Run in background so the request returns immediately; client polls /api/health.
|
|
void writeTraefikSetup({
|
|
env: envs,
|
|
additionalPorts: ports,
|
|
serverId: input.serverId,
|
|
}).catch((err) => {
|
|
console.error("writeTraefikEnv background writeTraefikSetup:", err);
|
|
});
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "traefik-env",
|
|
});
|
|
return true;
|
|
}),
|
|
haveTraefikDashboardPortEnabled: adminProcedure
|
|
.input(apiServerSchema)
|
|
.query(async ({ input }) => {
|
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
|
return ports.some((port) => port.targetPort === 8080);
|
|
}),
|
|
|
|
readStatsLogs: protectedProcedure
|
|
.meta({
|
|
openapi: {
|
|
path: "/read-stats-logs",
|
|
method: "POST",
|
|
override: true,
|
|
enabled: false,
|
|
},
|
|
})
|
|
.input(apiReadStatsLogs)
|
|
.query(async ({ input }) => {
|
|
if (IS_CLOUD) {
|
|
return {
|
|
data: [],
|
|
totalCount: 0,
|
|
};
|
|
}
|
|
const rawConfig = await readMonitoringConfig(
|
|
!!input.dateRange?.start && !!input.dateRange?.end,
|
|
);
|
|
|
|
const parsedConfig = parseRawConfig(
|
|
rawConfig as string,
|
|
input.page,
|
|
input.sort,
|
|
input.search,
|
|
input.status,
|
|
input.dateRange,
|
|
);
|
|
|
|
return parsedConfig;
|
|
}),
|
|
readStats: adminProcedure
|
|
.meta({
|
|
openapi: {
|
|
path: "/read-stats",
|
|
method: "POST",
|
|
override: true,
|
|
enabled: false,
|
|
},
|
|
})
|
|
.input(
|
|
z
|
|
.object({
|
|
dateRange: z
|
|
.object({
|
|
start: z.string().optional(),
|
|
end: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
)
|
|
.query(async ({ input }) => {
|
|
if (IS_CLOUD) {
|
|
return [];
|
|
}
|
|
const rawConfig = await readMonitoringConfig(
|
|
!!input?.dateRange?.start || !!input?.dateRange?.end,
|
|
);
|
|
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
|
|
return processedLogs || [];
|
|
}),
|
|
haveActivateRequests: protectedProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const config = readMainConfig();
|
|
|
|
if (!config) return false;
|
|
const parsedConfig = parse(config) as {
|
|
accessLog?: {
|
|
filePath: string;
|
|
};
|
|
};
|
|
|
|
return !!parsedConfig?.accessLog?.filePath;
|
|
}),
|
|
toggleRequests: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
enable: z.boolean(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
const mainConfig = readMainConfig();
|
|
if (!mainConfig) return false;
|
|
|
|
const currentConfig = parse(mainConfig) as {
|
|
accessLog?: {
|
|
filePath: string;
|
|
};
|
|
};
|
|
|
|
if (input.enable) {
|
|
const config = {
|
|
accessLog: {
|
|
filePath: "/etc/dokploy/traefik/dynamic/access.log",
|
|
format: "json",
|
|
bufferingSize: 100,
|
|
},
|
|
};
|
|
currentConfig.accessLog = config.accessLog;
|
|
} else {
|
|
currentConfig.accessLog = undefined;
|
|
}
|
|
|
|
writeMainConfig(stringify(currentConfig));
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "toggle-requests",
|
|
});
|
|
return true;
|
|
}),
|
|
isCloud: publicProcedure.query(async () => {
|
|
return IS_CLOUD;
|
|
}),
|
|
isUserSubscribed: protectedProcedure.query(async ({ ctx }) => {
|
|
const haveServers = await db.query.server.findMany({
|
|
where: eq(server.organizationId, ctx.session?.activeOrganizationId || ""),
|
|
});
|
|
const haveProjects = await db.query.projects.findMany({
|
|
where: eq(
|
|
projects.organizationId,
|
|
ctx.session?.activeOrganizationId || "",
|
|
),
|
|
});
|
|
return haveServers.length > 0 || haveProjects.length > 0;
|
|
}),
|
|
health: publicProcedure.query(async () => {
|
|
try {
|
|
await db.execute(sql`SELECT 1`);
|
|
return { status: "ok" };
|
|
} catch (error) {
|
|
console.error("Database connection error:", error);
|
|
throw error;
|
|
}
|
|
}),
|
|
checkInfrastructureHealth: adminProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return {
|
|
postgres: { status: "healthy" as const },
|
|
redis: { status: "healthy" as const },
|
|
traefik: { status: "healthy" as const },
|
|
};
|
|
}
|
|
|
|
const [postgres, redis, traefik] = await Promise.all([
|
|
checkPostgresHealth(),
|
|
checkRedisHealth(),
|
|
checkTraefikHealth(),
|
|
]);
|
|
|
|
return { postgres, redis, traefik };
|
|
}),
|
|
setupGPU: adminProcedure
|
|
.input(
|
|
z.object({
|
|
serverId: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD && !input.serverId) {
|
|
throw new Error("Select a server to enable the GPU Setup");
|
|
}
|
|
|
|
try {
|
|
await setupGPUSupport(input.serverId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "setup-gpu",
|
|
});
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("GPU Setup Error:", error);
|
|
throw error;
|
|
}
|
|
}),
|
|
checkGPUStatus: adminProcedure
|
|
.input(
|
|
z.object({
|
|
serverId: z.string().optional(),
|
|
}),
|
|
)
|
|
.query(async ({ input }) => {
|
|
if (IS_CLOUD && !input.serverId) {
|
|
return {
|
|
driverInstalled: false,
|
|
driverVersion: undefined,
|
|
gpuModel: undefined,
|
|
runtimeInstalled: false,
|
|
runtimeConfigured: false,
|
|
cudaSupport: undefined,
|
|
cudaVersion: undefined,
|
|
memoryInfo: undefined,
|
|
availableGPUs: 0,
|
|
swarmEnabled: false,
|
|
gpuResources: 0,
|
|
};
|
|
}
|
|
|
|
try {
|
|
return await checkGPUStatus(input.serverId || "");
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Failed to check GPU status";
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message,
|
|
});
|
|
}
|
|
}),
|
|
updateTraefikPorts: adminProcedure
|
|
.input(
|
|
z.object({
|
|
serverId: z.string().optional(),
|
|
additionalPorts: z.array(
|
|
z.object({
|
|
targetPort: z.number(),
|
|
publishedPort: z.number(),
|
|
protocol: z.enum(["tcp", "udp", "sctp"]),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
if (IS_CLOUD && !input.serverId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Please set a serverId to update Traefik ports",
|
|
});
|
|
}
|
|
const env = await readEnvironmentVariables(
|
|
"dokploy-traefik",
|
|
input?.serverId,
|
|
);
|
|
|
|
for (const port of input.additionalPorts) {
|
|
const portCheck = await checkPortInUse(
|
|
port.publishedPort,
|
|
input.serverId,
|
|
);
|
|
if (portCheck.isInUse) {
|
|
throw new TRPCError({
|
|
code: "CONFLICT",
|
|
message: `Port ${port.targetPort} is already in use by ${portCheck.conflictingContainer}`,
|
|
});
|
|
}
|
|
}
|
|
const preparedEnv = prepareEnvironmentVariables(env);
|
|
|
|
// Run in background so the request returns immediately; client polls /api/health.
|
|
void writeTraefikSetup({
|
|
env: preparedEnv,
|
|
additionalPorts: input.additionalPorts,
|
|
serverId: input.serverId,
|
|
}).catch((err) => {
|
|
console.error(
|
|
"updateTraefikPorts background writeTraefikSetup:",
|
|
err,
|
|
);
|
|
});
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "traefik-ports",
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: "Error updating Traefik ports",
|
|
cause: error,
|
|
});
|
|
}
|
|
}),
|
|
getTraefikPorts: adminProcedure
|
|
.input(apiServerSchema)
|
|
.query(async ({ input }) => {
|
|
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
|
return ports;
|
|
}),
|
|
updateLogCleanup: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
cronExpression: z.string().nullable(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (IS_CLOUD) {
|
|
return true;
|
|
}
|
|
let result: boolean;
|
|
if (input.cronExpression) {
|
|
result = await startLogCleanup(input.cronExpression);
|
|
} else {
|
|
result = await stopLogCleanup();
|
|
}
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "settings",
|
|
resourceName: "log-cleanup",
|
|
});
|
|
return result;
|
|
}),
|
|
|
|
getLogCleanupStatus: protectedProcedure.query(async () => {
|
|
return getLogCleanupStatus();
|
|
}),
|
|
|
|
getDokployCloudIps: adminProcedure.query(async () => {
|
|
if (!IS_CLOUD) {
|
|
return [];
|
|
}
|
|
const ips = process.env.DOKPLOY_CLOUD_IPS?.split(",");
|
|
return ips;
|
|
}),
|
|
});
|