mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
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 {
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
withPermission,
|
|
} from "@/server/api/trpc";
|
|
import { audit } from "@/server/api/utils/audit";
|
|
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;
|
|
}
|
|
}),
|
|
});
|