mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
- Added FormDescription to clarify user requirements in the server handling component. - Updated alert messages to inform users about connecting as root or non-root with passwordless sudo access. - Introduced new status rows in the validation component to display privilege mode and Docker group membership. - Implemented validation functions for sudo access and Docker group membership in the server setup scripts, ensuring proper permissions are checked during setup.
506 lines
14 KiB
TypeScript
506 lines
14 KiB
TypeScript
import {
|
|
createServer,
|
|
defaultCommand,
|
|
deleteServer,
|
|
findServerById,
|
|
findServersByUserId,
|
|
findUserById,
|
|
getPublicIpWithFallback,
|
|
haveActiveServices,
|
|
IS_CLOUD,
|
|
removeDeploymentsByServerId,
|
|
serverAudit,
|
|
serverSetup,
|
|
serverValidate,
|
|
setupMonitoring,
|
|
updateServerById,
|
|
} from "@dokploy/server";
|
|
import { db } from "@dokploy/server/db";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { observable } from "@trpc/server/observable";
|
|
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
import {
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
withPermission,
|
|
} from "@/server/api/trpc";
|
|
import {
|
|
apiCreateServer,
|
|
apiFindOneServer,
|
|
apiRemoveServer,
|
|
apiUpdateServer,
|
|
apiUpdateServerMonitoring,
|
|
applications,
|
|
compose,
|
|
mariadb,
|
|
mongo,
|
|
mysql,
|
|
organization,
|
|
postgres,
|
|
redis,
|
|
server,
|
|
} from "@/server/db/schema";
|
|
|
|
export const serverRouter = createTRPCRouter({
|
|
create: withPermission("server", "create")
|
|
.input(apiCreateServer)
|
|
.mutation(async ({ ctx, input }) => {
|
|
try {
|
|
const user = await findUserById(ctx.user.ownerId);
|
|
const servers = await findServersByUserId(user.id);
|
|
if (IS_CLOUD && servers.length >= user.serversQuantity) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "You cannot create more servers",
|
|
});
|
|
}
|
|
const project = await createServer(
|
|
input,
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
await audit(ctx, {
|
|
action: "create",
|
|
resourceType: "server",
|
|
resourceId: project.serverId,
|
|
resourceName: project.name,
|
|
});
|
|
return project;
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Error creating the server",
|
|
cause: error,
|
|
});
|
|
}
|
|
}),
|
|
|
|
one: withPermission("server", "read")
|
|
.input(apiFindOneServer)
|
|
.query(async ({ input, ctx }) => {
|
|
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",
|
|
});
|
|
}
|
|
|
|
return server;
|
|
}),
|
|
getDefaultCommand: withPermission("server", "read")
|
|
.input(apiFindOneServer)
|
|
.query(async ({ input }) => {
|
|
const server = await findServerById(input.serverId);
|
|
const isBuildServer = server.serverType === "build";
|
|
return defaultCommand(isBuildServer);
|
|
}),
|
|
all: withPermission("server", "read").query(async ({ ctx }) => {
|
|
const result = await db
|
|
.select({
|
|
...getTableColumns(server),
|
|
totalSum: sql<number>`cast(count(${applications.applicationId}) + count(${compose.composeId}) + count(${redis.redisId}) + count(${mariadb.mariadbId}) + count(${mongo.mongoId}) + count(${mysql.mysqlId}) + count(${postgres.postgresId}) as integer)`,
|
|
})
|
|
.from(server)
|
|
.leftJoin(applications, eq(applications.serverId, server.serverId))
|
|
.leftJoin(compose, eq(compose.serverId, server.serverId))
|
|
.leftJoin(redis, eq(redis.serverId, server.serverId))
|
|
.leftJoin(mariadb, eq(mariadb.serverId, server.serverId))
|
|
.leftJoin(mongo, eq(mongo.serverId, server.serverId))
|
|
.leftJoin(mysql, eq(mysql.serverId, server.serverId))
|
|
.leftJoin(postgres, eq(postgres.serverId, server.serverId))
|
|
.where(eq(server.organizationId, ctx.session.activeOrganizationId))
|
|
.orderBy(desc(server.createdAt))
|
|
.groupBy(server.serverId);
|
|
|
|
return result;
|
|
}),
|
|
count: protectedProcedure.query(async ({ ctx }) => {
|
|
const organizations = await db.query.organization.findMany({
|
|
where: eq(organization.ownerId, ctx.user.id),
|
|
with: {
|
|
servers: true,
|
|
},
|
|
});
|
|
|
|
const servers = organizations.flatMap((org) => org.servers);
|
|
|
|
return servers.length ?? 0;
|
|
}),
|
|
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
|
|
const result = await db.query.server.findMany({
|
|
orderBy: desc(server.createdAt),
|
|
where: IS_CLOUD
|
|
? and(
|
|
isNotNull(server.sshKeyId),
|
|
eq(server.organizationId, ctx.session.activeOrganizationId),
|
|
eq(server.serverStatus, "active"),
|
|
eq(server.serverType, "deploy"),
|
|
)
|
|
: and(
|
|
isNotNull(server.sshKeyId),
|
|
eq(server.organizationId, ctx.session.activeOrganizationId),
|
|
eq(server.serverType, "deploy"),
|
|
),
|
|
});
|
|
return result;
|
|
}),
|
|
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
|
|
const result = await db.query.server.findMany({
|
|
orderBy: desc(server.createdAt),
|
|
where: IS_CLOUD
|
|
? and(
|
|
isNotNull(server.sshKeyId),
|
|
eq(server.organizationId, ctx.session.activeOrganizationId),
|
|
eq(server.serverStatus, "active"),
|
|
eq(server.serverType, "build"),
|
|
)
|
|
: and(
|
|
isNotNull(server.sshKeyId),
|
|
eq(server.organizationId, ctx.session.activeOrganizationId),
|
|
eq(server.serverType, "build"),
|
|
),
|
|
});
|
|
return result;
|
|
}),
|
|
setup: withPermission("server", "create")
|
|
.input(apiFindOneServer)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to setup this server",
|
|
});
|
|
}
|
|
const currentServer = await serverSetup(input.serverId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "server",
|
|
resourceId: input.serverId,
|
|
resourceName: server.name,
|
|
});
|
|
return currentServer;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
setupWithLogs: withPermission("server", "create")
|
|
.meta({
|
|
openapi: {
|
|
path: "/deploy/server-with-logs",
|
|
method: "POST",
|
|
override: true,
|
|
enabled: false,
|
|
},
|
|
})
|
|
.input(apiFindOneServer)
|
|
.subscription(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to setup this server",
|
|
});
|
|
}
|
|
return observable<string>((emit) => {
|
|
serverSetup(input.serverId, (log) => {
|
|
emit.next(log);
|
|
});
|
|
});
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
validate: withPermission("server", "read")
|
|
.input(apiFindOneServer)
|
|
.query(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to validate this server",
|
|
});
|
|
}
|
|
const response = await serverValidate(input.serverId);
|
|
return response as unknown as {
|
|
docker: {
|
|
enabled: boolean;
|
|
version: string;
|
|
};
|
|
rclone: {
|
|
enabled: boolean;
|
|
version: string;
|
|
};
|
|
nixpacks: {
|
|
enabled: boolean;
|
|
version: string;
|
|
};
|
|
buildpacks: {
|
|
enabled: boolean;
|
|
version: string;
|
|
};
|
|
railpack: {
|
|
enabled: boolean;
|
|
version: string;
|
|
};
|
|
isDokployNetworkInstalled: boolean;
|
|
isSwarmInstalled: boolean;
|
|
isMainDirectoryInstalled: boolean;
|
|
privilegeMode: string;
|
|
dockerGroupMember: boolean;
|
|
};
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
cause: error as Error,
|
|
});
|
|
}
|
|
}),
|
|
|
|
security: withPermission("server", "read")
|
|
.input(apiFindOneServer)
|
|
.query(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to validate this server",
|
|
});
|
|
}
|
|
const response = await serverAudit(input.serverId);
|
|
return response as unknown as {
|
|
ufw: {
|
|
installed: boolean;
|
|
active: boolean;
|
|
defaultIncoming: string;
|
|
};
|
|
ssh: {
|
|
enabled: boolean;
|
|
keyAuth: boolean;
|
|
permitRootLogin: string;
|
|
passwordAuth: string;
|
|
usePam: string;
|
|
};
|
|
nonRootUser: {
|
|
hasValidSudoUser: boolean;
|
|
};
|
|
unattendedUpgrades: {
|
|
installed: boolean;
|
|
active: boolean;
|
|
updateEnabled: number;
|
|
upgradeEnabled: number;
|
|
};
|
|
fail2ban: {
|
|
installed: boolean;
|
|
enabled: boolean;
|
|
active: boolean;
|
|
sshEnabled: string;
|
|
sshMode: string;
|
|
};
|
|
};
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
cause: error as Error,
|
|
});
|
|
}
|
|
}),
|
|
setupMonitoring: withPermission("server", "create")
|
|
.input(apiUpdateServerMonitoring)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to setup this server",
|
|
});
|
|
}
|
|
|
|
await updateServerById(input.serverId, {
|
|
metricsConfig: {
|
|
server: {
|
|
type: "Remote",
|
|
refreshRate: input.metricsConfig.server.refreshRate,
|
|
retentionDays: input.metricsConfig.server.retentionDays,
|
|
port: input.metricsConfig.server.port,
|
|
token: input.metricsConfig.server.token,
|
|
urlCallback: input.metricsConfig.server.urlCallback,
|
|
cronJob: input.metricsConfig.server.cronJob,
|
|
thresholds: {
|
|
cpu: input.metricsConfig.server.thresholds.cpu,
|
|
memory: input.metricsConfig.server.thresholds.memory,
|
|
},
|
|
},
|
|
containers: {
|
|
refreshRate: input.metricsConfig.containers.refreshRate,
|
|
services: {
|
|
include: input.metricsConfig.containers.services.include || [],
|
|
exclude: input.metricsConfig.containers.services.exclude || [],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const currentServer = await setupMonitoring(input.serverId);
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "server",
|
|
resourceId: input.serverId,
|
|
resourceName: server.name,
|
|
});
|
|
return currentServer;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
remove: withPermission("server", "delete")
|
|
.input(apiRemoveServer)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const activeServers = await haveActiveServices(input.serverId);
|
|
|
|
if (activeServers) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Server has active services, please delete them first",
|
|
});
|
|
}
|
|
const currentServer = await findServerById(input.serverId);
|
|
await audit(ctx, {
|
|
action: "delete",
|
|
resourceType: "server",
|
|
resourceId: currentServer.serverId,
|
|
resourceName: currentServer.name,
|
|
});
|
|
await removeDeploymentsByServerId(currentServer);
|
|
await deleteServer(input.serverId);
|
|
|
|
if (IS_CLOUD) {
|
|
const admin = await findUserById(ctx.user.ownerId);
|
|
|
|
await updateServersBasedOnQuantity(admin.id, admin.serversQuantity);
|
|
}
|
|
|
|
return currentServer;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
update: withPermission("server", "create")
|
|
.input(apiUpdateServer)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const server = await findServerById(input.serverId);
|
|
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You are not authorized to update this server",
|
|
});
|
|
}
|
|
|
|
if (server.serverStatus === "inactive") {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Server is inactive",
|
|
});
|
|
}
|
|
const currentServer = await updateServerById(input.serverId, {
|
|
...input,
|
|
});
|
|
|
|
await audit(ctx, {
|
|
action: "update",
|
|
resourceType: "server",
|
|
resourceId: input.serverId,
|
|
resourceName: server.name,
|
|
});
|
|
return currentServer;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
publicIp: protectedProcedure.query(async () => {
|
|
if (IS_CLOUD) {
|
|
return "";
|
|
}
|
|
const ip = await getPublicIpWithFallback();
|
|
return ip;
|
|
}),
|
|
getServerTime: protectedProcedure.query(() => {
|
|
if (IS_CLOUD) {
|
|
return null;
|
|
}
|
|
return {
|
|
time: new Date(),
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
};
|
|
}),
|
|
getServerMetrics: withPermission("monitoring", "read")
|
|
.input(
|
|
z.object({
|
|
url: z.string(),
|
|
token: z.string(),
|
|
dataPoints: z.string(),
|
|
}),
|
|
)
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const url = new URL(input.url);
|
|
url.searchParams.append("limit", input.dataPoints);
|
|
const response = await fetch(url.toString(), {
|
|
headers: {
|
|
Authorization: `Bearer ${input.token}`,
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`,
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
throw new Error(
|
|
[
|
|
"No monitoring data available. This could be because:",
|
|
"",
|
|
"1. You don't have setup the monitoring service, you can do in web server section.",
|
|
"2. If you already have setup the monitoring service, wait a few minutes and refresh the page.",
|
|
].join("\n"),
|
|
);
|
|
}
|
|
return data as {
|
|
cpu: string;
|
|
cpuModel: string;
|
|
cpuCores: number;
|
|
cpuPhysicalCores: number;
|
|
cpuSpeed: number;
|
|
os: string;
|
|
distro: string;
|
|
kernel: string;
|
|
arch: string;
|
|
memUsed: string;
|
|
memUsedGB: string;
|
|
memTotal: string;
|
|
uptime: number;
|
|
diskUsed: string;
|
|
totalDisk: string;
|
|
networkIn: string;
|
|
networkOut: string;
|
|
timestamp: string;
|
|
}[];
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}),
|
|
});
|