mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into kucherenko/canary
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
updateMariadbById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -155,6 +156,31 @@ export const mariadbRouter = createTRPCRouter({
|
||||
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/mariadb-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployMariaDB)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (mariadb.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
return observable<string>((emit) => {
|
||||
deployMariadb(input.mariadbId, (log) => {
|
||||
emit.next(log);
|
||||
});
|
||||
});
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
updateMongoById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -167,6 +168,31 @@ export const mongoRouter = createTRPCRouter({
|
||||
}
|
||||
return deployMongo(input.mongoId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/mongo-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployMongo)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (mongo.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
});
|
||||
}
|
||||
return observable<string>((emit) => {
|
||||
deployMongo(input.mongoId, (log) => {
|
||||
emit.next(log);
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMySqlById,
|
||||
} from "@dokploy/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
export const mysqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -166,6 +167,31 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
return deployMySql(input.mysqlId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/mysql-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployMySql)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (mysql.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
});
|
||||
}
|
||||
|
||||
return observable<string>((emit) => {
|
||||
deployMySql(input.mysqlId, (log) => {
|
||||
emit.next(log);
|
||||
});
|
||||
});
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -2,20 +2,24 @@ import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateDiscord,
|
||||
apiCreateEmail,
|
||||
apiCreateGotify,
|
||||
apiCreateSlack,
|
||||
apiCreateTelegram,
|
||||
apiFindOneNotification,
|
||||
apiTestDiscordConnection,
|
||||
apiTestEmailConnection,
|
||||
apiTestGotifyConnection,
|
||||
apiTestSlackConnection,
|
||||
apiTestTelegramConnection,
|
||||
apiUpdateDiscord,
|
||||
apiUpdateEmail,
|
||||
apiUpdateGotify,
|
||||
apiUpdateSlack,
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
@@ -24,16 +28,19 @@ import {
|
||||
IS_CLOUD,
|
||||
createDiscordNotification,
|
||||
createEmailNotification,
|
||||
createGotifyNotification,
|
||||
createSlackNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
removeNotificationById,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
updateDiscordNotification,
|
||||
updateEmailNotification,
|
||||
updateGotifyNotification,
|
||||
updateSlackNotification,
|
||||
updateTelegramNotification,
|
||||
} from "@dokploy/server";
|
||||
@@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
gotify: true,
|
||||
},
|
||||
orderBy: desc(notifications.createdAt),
|
||||
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
|
||||
// TODO: Remove this line when the cloud version is ready
|
||||
});
|
||||
}),
|
||||
createGotify: adminProcedure
|
||||
.input(apiCreateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGotifyNotification(input, ctx.user.adminId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateGotify: adminProcedure
|
||||
.input(apiUpdateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateGotifyNotification({
|
||||
...input,
|
||||
adminId: ctx.user.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testGotifyConnection: adminProcedure
|
||||
.input(apiTestGotifyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendGotifyNotification(
|
||||
input,
|
||||
"Test Notification",
|
||||
"Hi, From Dokploy 👋",
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error testing the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { EventEmitter } from "node:events";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
apiCreatePostgres,
|
||||
@@ -27,6 +32,10 @@ import {
|
||||
updatePostgresById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
const ee = new EventEmitter();
|
||||
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -166,6 +175,32 @@ export const postgresRouter = createTRPCRouter({
|
||||
}
|
||||
return deployPostgres(input.postgresId);
|
||||
}),
|
||||
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/postgres-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployPostgres)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (postgres.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Postgres",
|
||||
});
|
||||
}
|
||||
return observable<string>((emit) => {
|
||||
deployPostgres(input.postgresId, (log) => {
|
||||
emit.next(log);
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
const { accesedServices } = await findUserByAuthId(ctx.user.authId);
|
||||
const { accessedServices } = await findUserByAuthId(ctx.user.authId);
|
||||
|
||||
await checkProjectAccess(ctx.user.authId, "access", input.projectId);
|
||||
|
||||
@@ -79,28 +79,28 @@ export const projectRouter = createTRPCRouter({
|
||||
),
|
||||
with: {
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
},
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accesedServices,
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accesedServices),
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accesedServices),
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(postgres.postgresId, accesedServices),
|
||||
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accesedServices),
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -125,18 +125,18 @@ export const projectRouter = createTRPCRouter({
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.rol === "user") {
|
||||
const { accesedProjects, accesedServices } = await findUserByAuthId(
|
||||
const { accessedProjects, accessedServices } = await findUserByAuthId(
|
||||
ctx.user.authId,
|
||||
);
|
||||
|
||||
if (accesedProjects.length === 0) {
|
||||
if (accessedProjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = await db.query.projects.findMany({
|
||||
where: and(
|
||||
sql`${projects.projectId} IN (${sql.join(
|
||||
accesedProjects.map((projectId) => sql`${projectId}`),
|
||||
accessedProjects.map((projectId) => sql`${projectId}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
eq(projects.adminId, ctx.user.adminId),
|
||||
@@ -145,27 +145,27 @@ export const projectRouter = createTRPCRouter({
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accesedServices,
|
||||
accessedServices,
|
||||
),
|
||||
with: { domains: true },
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accesedServices),
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accesedServices),
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(postgres.postgresId, accesedServices),
|
||||
where: buildServiceFilter(postgres.postgresId, accessedServices),
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accesedServices),
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accesedServices),
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
with: { domains: true },
|
||||
},
|
||||
},
|
||||
@@ -239,10 +239,10 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
});
|
||||
function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
|
||||
return accesedServices.length > 0
|
||||
function buildServiceFilter(fieldName: AnyPgColumn, accessedServices: string[]) {
|
||||
return accessedServices.length > 0
|
||||
? sql`${fieldName} IN (${sql.join(
|
||||
accesedServices.map((serviceId) => sql`${serviceId}`),
|
||||
accessedServices.map((serviceId) => sql`${serviceId}`),
|
||||
sql`, `,
|
||||
)})`
|
||||
: sql`1 = 0`;
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
stopServiceRemote,
|
||||
updateRedisById,
|
||||
} from "@dokploy/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -189,6 +190,30 @@ export const redisRouter = createTRPCRouter({
|
||||
}
|
||||
return deployRedis(input.redisId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/redis-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployRedis)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (redis.project.adminId !== ctx.user.adminId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Redis",
|
||||
});
|
||||
}
|
||||
return observable<string>((emit) => {
|
||||
deployRedis(input.redisId, (log) => {
|
||||
emit.next(log);
|
||||
});
|
||||
});
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -96,8 +96,14 @@ export const registryRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error Registry:", error);
|
||||
return false;
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error testing the registry",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
updateServerById,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||
|
||||
export const serverRouter = createTRPCRouter({
|
||||
@@ -126,6 +127,34 @@ export const serverRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
setupWithLogs: protectedProcedure
|
||||
.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.adminId !== ctx.user.adminId) {
|
||||
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: protectedProcedure
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import next from "next";
|
||||
import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-logs";
|
||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||
import { setupDrawerLogsWebSocketServer } from "./wss/drawer-logs";
|
||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||
import { setupTerminalWebSocketServer } from "./wss/terminal";
|
||||
|
||||
@@ -33,6 +34,7 @@ void app.prepare().then(async () => {
|
||||
});
|
||||
|
||||
// WEBSOCKET
|
||||
setupDrawerLogsWebSocketServer(server);
|
||||
setupDeploymentLogsWebSocketServer(server);
|
||||
setupDockerContainerLogsWebSocketServer(server);
|
||||
setupDockerContainerTerminalWebSocketServer(server);
|
||||
|
||||
40
apps/dokploy/server/utils/docker.ts
Normal file
40
apps/dokploy/server/utils/docker.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { execAsync } from "@dokploy/server";
|
||||
|
||||
/** Returns if the current operating system is Windows Subsystem for Linux (WSL). */
|
||||
export const isWSL = async () => {
|
||||
try {
|
||||
const { stdout } = await execAsync("uname -r");
|
||||
const isWSL = stdout.includes("microsoft");
|
||||
return isWSL;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns the Docker host IP address. */
|
||||
export const getDockerHost = async (): Promise<string> => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.platform === "linux" && !(await isWSL())) {
|
||||
try {
|
||||
// Try to get the Docker bridge IP first
|
||||
const { stdout } = await execAsync(
|
||||
"ip route | awk '/default/ {print $3}'",
|
||||
);
|
||||
|
||||
const hostIp = stdout.trim();
|
||||
if (!hostIp) {
|
||||
throw new Error("Failed to get Docker host IP");
|
||||
}
|
||||
|
||||
return hostIp;
|
||||
} catch (error) {
|
||||
console.error("Failed to get Docker host IP:", error);
|
||||
return "172.17.0.1"; // Default Docker bridge network IP
|
||||
}
|
||||
}
|
||||
|
||||
return "host.docker.internal";
|
||||
}
|
||||
|
||||
return "localhost";
|
||||
};
|
||||
36
apps/dokploy/server/wss/drawer-logs.ts
Normal file
36
apps/dokploy/server/wss/drawer-logs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type http from "node:http";
|
||||
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { appRouter } from "../api/root";
|
||||
import { createTRPCContext } from "../api/trpc";
|
||||
|
||||
export const setupDrawerLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/drawer-logs",
|
||||
});
|
||||
|
||||
applyWSSHandler({
|
||||
wss: wssTerm,
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext as any,
|
||||
});
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/drawer-logs") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
});
|
||||
};
|
||||
@@ -1,14 +1,31 @@
|
||||
import type http from "node:http";
|
||||
import {
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
findServerById,
|
||||
validateWebSocketRequest,
|
||||
} from "@dokploy/server";
|
||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { getDockerHost } from "../utils/docker";
|
||||
import { setupLocalServerSSHKey } from "./utils";
|
||||
|
||||
const COMMAND_TO_ALLOW_LOCAL_ACCESS = `
|
||||
# ----------------------------------------
|
||||
mkdir -p $HOME/.ssh && \\
|
||||
chmod 700 $HOME/.ssh && \\
|
||||
touch $HOME/.ssh/authorized_keys && \\
|
||||
chmod 600 $HOME/.ssh/authorized_keys && \\
|
||||
cat /etc/dokploy/ssh/auto_generated-dokploy-local.pub >> $HOME/.ssh/authorized_keys && \\
|
||||
echo "✓ Dokploy SSH key added successfully. Reopen the terminal in Dokploy to reconnect."
|
||||
# ----------------------------------------`;
|
||||
|
||||
const COMMAND_TO_GRANT_PERMISSION_ACCESS = `
|
||||
# ----------------------------------------
|
||||
sudo chown -R $USER:$USER /etc/dokploy/ssh
|
||||
# ----------------------------------------
|
||||
`;
|
||||
|
||||
export const getPublicIpWithFallback = async () => {
|
||||
// @ts-ignore
|
||||
let ip = null;
|
||||
@@ -73,20 +90,41 @@ export const setupTerminalWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send("Setting up private SSH key...\n");
|
||||
const privateKey = await setupLocalServerSSHKey();
|
||||
try {
|
||||
ws.send("Setting up private SSH key...\n");
|
||||
const privateKey = await setupLocalServerSSHKey();
|
||||
|
||||
if (!privateKey) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const dockerHost = await getDockerHost();
|
||||
|
||||
ws.send(`Found Docker host: ${dockerHost}\n`);
|
||||
|
||||
connectionDetails = {
|
||||
host: dockerHost,
|
||||
port,
|
||||
username,
|
||||
privateKey,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error setting up private SSH key: ${error}`);
|
||||
ws.send(`Error setting up private SSH key: ${error}\n`);
|
||||
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("Permission denied")
|
||||
) {
|
||||
ws.send(
|
||||
`Please run the following command on your server to grant permission access and then reopen this window to reconnect:${COMMAND_TO_GRANT_PERMISSION_ACCESS}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!privateKey) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
connectionDetails = {
|
||||
host: "localhost",
|
||||
port,
|
||||
username,
|
||||
privateKey,
|
||||
};
|
||||
} else {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
@@ -161,9 +199,15 @@ export const setupTerminalWebSocketServer = (
|
||||
})
|
||||
.on("error", (err) => {
|
||||
if (err.level === "client-authentication") {
|
||||
ws.send(
|
||||
`Authentication failed: Unauthorized ${isLocalServer ? "" : "private SSH key or "}username.\n❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
if (isLocalServer) {
|
||||
ws.send(
|
||||
`Authentication failed: Please run the command below on your server to allow access. Make sure to run it as the same user as the one configured in connection settings:${COMMAND_TO_ALLOW_LOCAL_ACCESS}\nAfter running the command, reopen this window to reconnect. This procedure is required only once.`,
|
||||
);
|
||||
} else {
|
||||
ws.send(
|
||||
`Authentication failed: Unauthorized private SSH key or username.\n❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ws.send(`SSH connection error: ${err.message} ❌ `);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { execAsync } from "@dokploy/server";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
const HOME_PATH = process.env.HOME || process.env.USERPROFILE || "/";
|
||||
|
||||
const LOCAL_SSH_KEY_PATH = path.join(
|
||||
HOME_PATH,
|
||||
".ssh",
|
||||
"auto_generated-dokploy-local",
|
||||
);
|
||||
|
||||
const AUTHORIZED_KEYS_PATH = path.join(HOME_PATH, ".ssh", "authorized_keys");
|
||||
import { execAsync, paths } from "@dokploy/server";
|
||||
|
||||
export const getShell = () => {
|
||||
switch (os.platform()) {
|
||||
@@ -24,39 +14,20 @@ export const getShell = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns private SSH key for dokploy local server terminal. Uses already created SSH key or generates a new SSH key, also automatically appends the public key to `authorized_keys`, creating the file if needed. */
|
||||
/** Returns private SSH key for dokploy local server terminal. Uses already created SSH key or generates a new SSH key.
|
||||
*/
|
||||
export const setupLocalServerSSHKey = async () => {
|
||||
try {
|
||||
if (!fs.existsSync(LOCAL_SSH_KEY_PATH)) {
|
||||
// Generate new SSH key if it hasn't been created yet
|
||||
await execAsync(
|
||||
`ssh-keygen -t rsa -b 4096 -f ${LOCAL_SSH_KEY_PATH} -N ""`,
|
||||
);
|
||||
}
|
||||
const { SSH_PATH } = paths(true);
|
||||
const sshKeyPath = path.join(SSH_PATH, "auto_generated-dokploy-local");
|
||||
|
||||
const privateKey = fs.readFileSync(LOCAL_SSH_KEY_PATH, "utf8");
|
||||
const publicKey = fs.readFileSync(`${LOCAL_SSH_KEY_PATH}.pub`, "utf8");
|
||||
const authKeyContent = `${publicKey}\n`;
|
||||
|
||||
if (!fs.existsSync(AUTHORIZED_KEYS_PATH)) {
|
||||
// Create authorized_keys if it doesn't exist yet
|
||||
fs.writeFileSync(AUTHORIZED_KEYS_PATH, authKeyContent, { mode: 0o600 });
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
const existingAuthKeys = fs.readFileSync(AUTHORIZED_KEYS_PATH, "utf8");
|
||||
if (existingAuthKeys.includes(publicKey)) {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
// Append the public key to authorized_keys
|
||||
fs.appendFileSync(AUTHORIZED_KEYS_PATH, authKeyContent, {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
return privateKey;
|
||||
} catch (error) {
|
||||
console.error("Error getting private SSH key for local terminal:", error);
|
||||
return "";
|
||||
if (!fs.existsSync(sshKeyPath)) {
|
||||
// Generate new SSH key if it hasn't been created yet
|
||||
await execAsync(
|
||||
`ssh-keygen -t rsa -b 4096 -f ${sshKeyPath} -N "" -C "dokploy-local-access"`,
|
||||
);
|
||||
}
|
||||
|
||||
const privateKey = fs.readFileSync(sshKeyPath, "utf8");
|
||||
|
||||
return privateKey;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user