diff --git a/.github/workflows/sync-version.yml b/.github/workflows/sync-version.yml index be19a2bb1..5e8ccb706 100644 --- a/.github/workflows/sync-version.yml +++ b/.github/workflows/sync-version.yml @@ -3,6 +3,9 @@ name: Sync version to MCP and CLI repos on: release: types: [published] + push: + tags: + - 'v*' workflow_dispatch: jobs: diff --git a/apps/dokploy/components/dashboard/requests/columns.tsx b/apps/dokploy/components/dashboard/requests/columns.tsx index 997074fde..2ad2455ba 100644 --- a/apps/dokploy/components/dashboard/requests/columns.tsx +++ b/apps/dokploy/components/dashboard/requests/columns.tsx @@ -79,8 +79,11 @@ export const columns: ColumnDef[] = [ : log.RequestPath}
- - Status: {formatStatusLabel(log.OriginStatus)} + + Status:{" "} + {formatStatusLabel(log.OriginStatus || log.DownstreamStatus)} Exec Time: {formatDuration(log.Duration)} diff --git a/apps/dokploy/components/dashboard/requests/requests-table.tsx b/apps/dokploy/components/dashboard/requests/requests-table.tsx index e804b065b..6f7406e19 100644 --- a/apps/dokploy/components/dashboard/requests/requests-table.tsx +++ b/apps/dokploy/components/dashboard/requests/requests-table.tsx @@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
setSearch(event.target.value)} className="md:max-w-sm" diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c7622a3dc..d90e9af32 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.29.1", + "version": "v0.29.2", "private": true, "license": "Apache-2.0", "type": "module", @@ -147,7 +147,7 @@ "shell-quote": "^1.8.1", "slugify": "^1.6.6", "sonner": "^1.7.4", - "ssh2": "1.15.0", + "ssh2": "~1.16.0", "stripe": "17.2.0", "superjson": "^2.2.2", "swagger-ui-react": "^5.31.2", diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index 1a99c3a8e..bb6eb06d3 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -12,6 +12,15 @@ import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; +/** + * Log a webhook handler error server-side without leaking its shape to the HTTP + * response. Drizzle errors carry the raw SQL query, column list and parameters, + * so we never forward the error object to the client. + */ +export const logWebhookError = (context: string, error: unknown) => { + console.error(context, error); +}; + /** * Helper function to get package_version from registry_package events */ @@ -262,14 +271,15 @@ export default async function handler( ); } } catch (error) { - res.status(400).json({ message: "Error deploying Application", error }); + logWebhookError("Error deploying Application:", error); + res.status(400).json({ message: "Error deploying Application" }); return; } res.status(200).json({ message: "Application deployed successfully" }); } catch (error) { - console.log(error); - res.status(400).json({ message: "Error deploying Application", error }); + logWebhookError("Error deploying Application:", error); + res.status(400).json({ message: "Error deploying Application" }); } } diff --git a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts index 640a2531d..85a379eb3 100644 --- a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts @@ -12,6 +12,7 @@ import { extractCommittedPaths, extractHash, getProviderByHeader, + logWebhookError, } from "../[refreshToken]"; export default async function handler( @@ -195,13 +196,14 @@ export default async function handler( ); } } catch (error) { - res.status(400).json({ message: "Error deploying Compose", error }); + logWebhookError("Error deploying Compose:", error); + res.status(400).json({ message: "Error deploying Compose" }); return; } res.status(200).json({ message: "Compose deployed successfully" }); } catch (error) { - console.log(error); - res.status(400).json({ message: "Error deploying Compose", error }); + logWebhookError("Error deploying Compose:", error); + res.status(400).json({ message: "Error deploying Compose" }); } } diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index 4438366f6..293207198 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -17,7 +17,11 @@ import { applications, compose, github } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; -import { extractCommitMessage, extractHash } from "./[refreshToken]"; +import { + extractCommitMessage, + extractHash, + logWebhookError, +} from "./[refreshToken]"; export default async function handler( req: NextApiRequest, @@ -197,10 +201,8 @@ export default async function handler( }); return; } catch (error) { - console.error("Error deploying applications on tag:", error); - res - .status(400) - .json({ message: "Error deploying applications on tag", error }); + logWebhookError("Error deploying applications on tag:", error); + res.status(400).json({ message: "Error deploying applications on tag" }); return; } } @@ -322,7 +324,8 @@ export default async function handler( } res.status(200).json({ message: `Deployed ${totalApps} apps` }); } catch (error) { - res.status(400).json({ message: "Error deploying Application", error }); + logWebhookError("Error deploying Application:", error); + res.status(400).json({ message: "Error deploying Application" }); } } else if (req.headers["x-github-event"] === "pull_request") { const prId = githubBody?.pull_request?.id; diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index c3633b135..75bb60f2c 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -458,9 +458,26 @@ export const backupRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { try { const destination = await findDestinationById(input.destinationId); + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this destination.", + }); + } + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } const rcloneFlags = getS3Credentials(destination); const bucketPath = `:s3:${destination.bucket}`; diff --git a/apps/dokploy/server/api/routers/cluster.ts b/apps/dokploy/server/api/routers/cluster.ts index afd8a0e92..3dc07935e 100644 --- a/apps/dokploy/server/api/routers/cluster.ts +++ b/apps/dokploy/server/api/routers/cluster.ts @@ -18,7 +18,16 @@ export const clusterRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } const docker = await getRemoteDocker(input.serverId); const workers: DockerNode[] = await docker.listNodes(); return workers; @@ -32,6 +41,15 @@ export const clusterRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } try { const drainCommand = `docker node update --availability drain ${input.nodeId}`; const removeCommand = `docker node rm ${input.nodeId} --force`; @@ -65,7 +83,16 @@ export const clusterRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } const docker = await getRemoteDocker(input.serverId); const result = await docker.swarmInspect(); const docker_version = await docker.version(); @@ -88,7 +115,16 @@ export const clusterRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } const docker = await getRemoteDocker(input.serverId); const result = await docker.swarmInspect(); const docker_version = await docker.version(); diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index 03cd3c935..6f3b1d1ae 100644 --- a/apps/dokploy/server/api/routers/deployment.ts +++ b/apps/dokploy/server/api/routers/deployment.ts @@ -16,6 +16,7 @@ import { checkServicePermissionAndAccess, findMemberByUserId, } from "@dokploy/server/services/permission"; +import { findServerById } from "@dokploy/server/services/server"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; import { z } from "zod"; @@ -52,7 +53,14 @@ export const deploymentRouter = createTRPCRouter({ }), allByServer: withPermission("deployment", "read") .input(apiFindAllByServer) - .query(async ({ input }) => { + .query(async ({ input, ctx }) => { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } return await findAllDeploymentsByServerId(input.serverId); }), allCentralized: withPermission("deployment", "read").query( diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index 2f9da6d71..51c1fec5d 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -1,5 +1,5 @@ import { db } from "@dokploy/server/db"; -import { IS_CLOUD } from "@dokploy/server/index"; +import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index"; import { TRPCError } from "@trpc/server"; import { and, desc, eq, exists } from "drizzle-orm"; import { nanoid } from "nanoid"; @@ -325,6 +325,24 @@ export const organizationRouter = createTRPCRouter({ }) .returning(); + if (IS_CLOUD && created) { + const host = + process.env.NODE_ENV === "development" + ? "http://localhost:3000" + : "https://app.dokploy.com"; + const inviteLink = `${host}/invitation?token=${created.id}`; + + const org = await db.query.organization.findFirst({ + where: eq(organization.id, orgId), + }); + + await sendInvitationEmail({ + email, + inviteLink, + organizationName: org?.name || "organization", + }); + } + await audit(ctx, { action: "create", resourceType: "organization", diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index 144f7c74a..7da745b8e 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -7,19 +7,25 @@ import { updateScheduleSchema, } from "@dokploy/server/db/schema/schedule"; import { runCommand } from "@dokploy/server/index"; -import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { + checkPermission, + checkServicePermissionAndAccess, + findMemberByUserId, +} from "@dokploy/server/services/permission"; import { createSchedule, deleteSchedule, findScheduleById, updateSchedule, } from "@dokploy/server/services/schedule"; +import { findServerById } from "@dokploy/server/services/server"; import { TRPCError } from "@trpc/server"; import { asc, desc, eq } from "drizzle-orm"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; import { removeJob, schedule } from "@/server/utils/backup"; import { createTRPCRouter, protectedProcedure } from "../trpc"; + export const scheduleRouter = createTRPCRouter({ create: protectedProcedure .input(createScheduleSchema) @@ -29,6 +35,45 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["create"], }); + } else { + if (input.scheduleType === "dokploy-server" && IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Host-level schedules are not available in the cloud version.", + }); + } + + await checkPermission(ctx, { schedule: ["create"] }); + + if ( + input.scheduleType === "server" || + input.scheduleType === "dokploy-server" + ) { + const member = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (member.role !== "owner" && member.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Only owners and admins can manage server-level schedules.", + }); + } + } + + if (input.scheduleType === "server" && input.serverId) { + const targetServer = await findServerById(input.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } } const newSchedule = await createSchedule(input); @@ -57,12 +102,77 @@ export const scheduleRouter = createTRPCRouter({ .input(updateScheduleSchema) .mutation(async ({ input, ctx }) => { const existingSchedule = await findScheduleById(input.scheduleId); + + if ( + IS_CLOUD && + input.scheduleType && + input.scheduleType !== existingSchedule.scheduleType + ) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Changing scheduleType is not allowed in the cloud version.", + }); + } + const serviceId = existingSchedule.applicationId || existingSchedule.composeId; if (serviceId) { await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["update"], }); + } else { + if (existingSchedule.scheduleType === "dokploy-server" && IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Host-level schedules are not available in the cloud version.", + }); + } + + await checkPermission(ctx, { schedule: ["update"] }); + + if ( + existingSchedule.scheduleType === "server" || + existingSchedule.scheduleType === "dokploy-server" + ) { + const member = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (member.role !== "owner" && member.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Only owners and admins can manage server-level schedules.", + }); + } + } + + if ( + existingSchedule.scheduleType === "server" && + existingSchedule.serverId + ) { + const targetServer = await findServerById(existingSchedule.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } + + if ( + existingSchedule.scheduleType === "dokploy-server" && + existingSchedule.userId && + existingSchedule.userId !== ctx.user.id + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You can only manage your own host-level schedules.", + }); + } } const updatedSchedule = await updateSchedule(input); @@ -107,6 +217,56 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["delete"], }); + } else { + if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Host-level schedules are not available in the cloud version.", + }); + } + + await checkPermission(ctx, { schedule: ["delete"] }); + + if ( + scheduleItem.scheduleType === "server" || + scheduleItem.scheduleType === "dokploy-server" + ) { + const member = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (member.role !== "owner" && member.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Only owners and admins can manage server-level schedules.", + }); + } + } + + if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) { + const targetServer = await findServerById(scheduleItem.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } + + if ( + scheduleItem.scheduleType === "dokploy-server" && + scheduleItem.userId && + scheduleItem.userId !== ctx.user.id + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You can only manage your own host-level schedules.", + }); + } } await deleteSchedule(input.scheduleId); @@ -148,6 +308,30 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, input.id, { schedule: ["read"], }); + } else { + await checkPermission(ctx, { schedule: ["read"] }); + + if (input.scheduleType === "server") { + const targetServer = await findServerById(input.id); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } + + if ( + input.scheduleType === "dokploy-server" && + input.id !== ctx.user.id + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You can only list your own host-level schedules.", + }); + } } const where = { application: eq(schedules.applicationId, input.id), @@ -178,6 +362,31 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["read"], }); + } else { + await checkPermission(ctx, { schedule: ["read"] }); + + if (schedule.scheduleType === "server" && schedule.serverId) { + const targetServer = await findServerById(schedule.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this schedule.", + }); + } + } + + if ( + schedule.scheduleType === "dokploy-server" && + schedule.userId && + schedule.userId !== ctx.user.id + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this schedule.", + }); + } } return schedule; }), @@ -191,6 +400,56 @@ export const scheduleRouter = createTRPCRouter({ await checkServicePermissionAndAccess(ctx, serviceId, { schedule: ["create"], }); + } else { + if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Host-level schedules are not available in the cloud version.", + }); + } + + await checkPermission(ctx, { schedule: ["create"] }); + + if ( + scheduleItem.scheduleType === "server" || + scheduleItem.scheduleType === "dokploy-server" + ) { + const member = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + if (member.role !== "owner" && member.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: + "Only owners and admins can manage server-level schedules.", + }); + } + } + + if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) { + const targetServer = await findServerById(scheduleItem.serverId); + if ( + targetServer.organizationId !== ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } + + if ( + scheduleItem.scheduleType === "dokploy-server" && + scheduleItem.userId && + scheduleItem.userId !== ctx.user.id + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You can only manage your own host-level schedules.", + }); + } } try { await runCommand(input.scheduleId); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 63578b099..93b7e6cf6 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -9,12 +9,12 @@ import { getWebServerSettings, IS_CLOUD, removeUserById, + renderInvitationEmail, sendEmailNotification, sendResendNotification, updateUser, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; -import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { account, apiAssignPermissions, @@ -29,6 +29,7 @@ import { hasPermission, resolvePermissions, } from "@dokploy/server/services/permission"; +import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { and, asc, eq, gt } from "drizzle-orm"; @@ -639,27 +640,26 @@ export const userRouter = createTRPCRouter({ ); try { - const htmlContent = ` -\t\t\t\t

You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: Accept Invitation

-\t\t\t\t`; + const toEmail = currentInvitation?.email || ""; + const orgName = organization?.name || "organization"; + const subject = `You've been invited to join ${orgName} on Dokploy`; + const html = await renderInvitationEmail({ + email: toEmail, + inviteLink, + organizationName: orgName, + }); if (email) { await sendEmailNotification( - { - ...email, - toAddresses: [currentInvitation?.email || ""], - }, - "Invitation to join organization", - htmlContent, + { ...email, toAddresses: [toEmail] }, + subject, + html, ); } else if (resend) { await sendResendNotification( - { - ...resend, - toAddresses: [currentInvitation?.email || ""], - }, - "Invitation to join organization", - htmlContent, + { ...resend, toAddresses: [toEmail] }, + subject, + html, ); } } catch (error) { diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index 5b50219d2..1f589d1e3 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -15,7 +15,9 @@ import { updateVolumeBackupSchema, volumeBackups, } from "@dokploy/server/db/schema"; +import { findDestinationById } from "@dokploy/server/services/destination"; import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { findServerById } from "@dokploy/server/services/server"; import { execAsyncRemote, execAsyncStream, @@ -265,7 +267,23 @@ export const volumeBackupsRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .subscription(async ({ input }) => { + .subscription(async ({ input, ctx }) => { + const destination = await findDestinationById(input.destinationId); + if (destination.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this destination.", + }); + } + if (input.serverId) { + const targetServer = await findServerById(input.serverId); + if (targetServer.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this server.", + }); + } + } return observable((emit) => { const runRestore = async () => { try { diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 159bedaae..ed4541558 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -85,6 +85,11 @@ export const setupDockerContainerLogsWebSocketServer = ( if (serverId) { const server = await findServerById(serverId); + if (server.organizationId !== session.activeOrganizationId) { + ws.close(); + return; + } + if (!server.sshKeyId) return; const client = new Client(); client diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index a2c242d95..e752c0651 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -61,6 +61,12 @@ export const setupDockerContainerTerminalWebSocketServer = ( try { if (serverId) { const server = await findServerById(serverId); + + if (server.organizationId !== session.activeOrganizationId) { + ws.close(); + return; + } + if (!server.sshKeyId) throw new Error("No SSH key available for this server"); diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index c39fa70b7..cd9eefed6 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -57,6 +57,11 @@ export const setupDeploymentLogsWebSocketServer = ( if (serverId) { const server = await findServerById(serverId); + if (server.organizationId !== session.activeOrganizationId) { + ws.close(); + return; + } + if (!server.sshKeyId) { ws.close(); return; diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index 00b0e2c2c..4825f7301 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -154,6 +154,11 @@ export const setupTerminalWebSocketServer = ( return; } + if (server.organizationId !== session.activeOrganizationId) { + ws.close(); + return; + } + const { ipAddress: host, port, username, sshKey, sshKeyId } = server; if (!sshKeyId) { diff --git a/packages/server/package.json b/packages/server/package.json index 7fe2eaba0..1f519b494 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -80,7 +80,7 @@ "semver": "7.7.3", "shell-quote": "^1.8.1", "slugify": "^1.6.6", - "ssh2": "1.15.0", + "ssh2": "~1.16.0", "toml": "3.0.0", "ws": "8.16.0", "yaml": "2.8.1", diff --git a/packages/server/src/emails/emails/invitation.tsx b/packages/server/src/emails/emails/invitation.tsx index 833b77286..dd075aecf 100644 --- a/packages/server/src/emails/emails/invitation.tsx +++ b/packages/server/src/emails/emails/invitation.tsx @@ -14,21 +14,18 @@ import { Text, } from "@react-email/components"; -export type TemplateProps = { - email: string; - name: string; -}; - -interface VercelInviteUserEmailProps { +interface InvitationEmailProps { inviteLink: string; toEmail: string; + organizationName: string; } export const InvitationEmail = ({ inviteLink, toEmail, -}: VercelInviteUserEmailProps) => { - const previewText = "Join to Dokploy"; + organizationName = "an organization", +}: InvitationEmailProps) => { + const previewText = `You've been invited to join ${organizationName} on Dokploy`; return ( @@ -44,50 +41,67 @@ export const InvitationEmail = ({ }, }} > - - -
+ + + {/* Header */} +
Dokploy
- - Join to Dokploy - - - Hello, - - - You have been invited to join Dokploy, a platform - that helps for deploying your apps to the cloud. - -
- + + {/* Body */} +
+ + You've been invited to join {organizationName} + + + You have been invited to join{" "} + {organizationName}{" "} + on Dokploy, the platform for deploying your apps to the cloud. + Click the button below to accept the invitation. + + + {/* CTA Button */} +
+ +
+ + + If the button above doesn't work, copy and paste the following + link into your browser: + + + {inviteLink} + +
+ + {/* Footer */} +
+
+ + This invitation was intended for{" "} + {toEmail}. This invite + was sent from{" "} + + Dokploy Cloud + + . If you were not expecting this invitation, you can safely + ignore this email. +
- - or copy and paste this URL into your browser:{" "} - - https://dokploy.com - - -
- - This invitation was intended for {toEmail}. This invite was sent - from dokploy.com. If you - were not expecting this invitation, you can ignore this email. If - you are concerned about your account's safety, please reply to - diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index e6fd0ba59..717c20246 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup"; export * from "./utils/notifications/dokploy-restart"; export * from "./utils/notifications/server-threshold"; export * from "./utils/notifications/utils"; +export * from "./verification/send-verification-email"; export * from "./utils/process/execAsync"; export * from "./utils/process/spawnAsync"; export * from "./utils/providers/bitbucket"; diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 65dd1b01d..069be48cc 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -409,23 +409,6 @@ const { handler, api } = betterAuth({ enabled: true, maximumRolesPerOrganization: 10, }, - async sendInvitationEmail(data, _request) { - if (IS_CLOUD) { - const host = - process.env.NODE_ENV === "development" - ? "http://localhost:3000" - : "https://app.dokploy.com"; - const inviteLink = `${host}/invitation?token=${data.id}`; - - await sendEmail({ - email: data.email, - subject: "Invitation to join organization", - text: ` -

You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: Accept Invitation

- `, - }); - } - }, }), ...(IS_CLOUD ? [ @@ -481,8 +464,10 @@ export const validateRequest = async (request: IncomingMessage) => { }; } - const organizationId = JSON.parse( - apiKeyRecord.metadata || "{}", + const organizationId = ( + JSON.parse(apiKeyRecord.metadata || "{}") as { + organizationId?: string; + } ).organizationId; if (!organizationId) { diff --git a/packages/server/src/utils/access-log/utils.ts b/packages/server/src/utils/access-log/utils.ts index b89dc2ed2..9b472ee27 100644 --- a/packages/server/src/utils/access-log/utils.ts +++ b/packages/server/src/utils/access-log/utils.ts @@ -120,7 +120,7 @@ export function parseRawConfig( if (search) { parsedLogs = parsedLogs.filter((log) => - log.RequestPath.toLowerCase().includes(search.toLowerCase()), + log.RequestHost.toLowerCase().includes(search.toLowerCase()), ); } diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx index 098789a2b..e6ae0250a 100644 --- a/packages/server/src/verification/send-verification-email.tsx +++ b/packages/server/src/verification/send-verification-email.tsx @@ -1,4 +1,5 @@ import { renderAsync } from "@react-email/components"; +import InvitationEmail from "../emails/emails/invitation"; import VerifyEmailTemplate from "../emails/emails/verify-email"; import { sendEmailNotification } from "../utils/notifications/utils"; @@ -51,3 +52,42 @@ export const sendVerificationEmail = async ({ text: html, }); }; + +export const renderInvitationEmail = async ({ + email, + inviteLink, + organizationName, +}: { + email: string; + inviteLink: string; + organizationName: string; +}) => { + return renderAsync( + InvitationEmail({ + inviteLink, + toEmail: email, + organizationName, + }), + ); +}; + +export const sendInvitationEmail = async ({ + email, + inviteLink, + organizationName, +}: { + email: string; + inviteLink: string; + organizationName: string; +}) => { + const html = await renderInvitationEmail({ + email, + inviteLink, + organizationName, + }); + await sendEmail({ + email, + subject: `You've been invited to join ${organizationName} on Dokploy`, + text: html, + }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c06680b..132d3b20e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -417,8 +417,8 @@ importers: specifier: ^1.7.4 version: 1.7.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) ssh2: - specifier: 1.15.0 - version: 1.15.0 + specifier: ~1.16.0 + version: 1.16.0 stripe: specifier: 17.2.0 version: 17.2.0 @@ -758,8 +758,8 @@ importers: specifier: ^1.6.6 version: 1.6.6 ssh2: - specifier: 1.15.0 - version: 1.15.0 + specifier: ~1.16.0 + version: 1.16.0 toml: specifier: 3.0.0 version: 3.0.0 @@ -4196,6 +4196,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xterm/addon-attach@0.10.0': resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==} @@ -7599,8 +7600,8 @@ packages: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} - ssh2@1.15.0: - resolution: {integrity: sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==} + ssh2@1.16.0: + resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==} engines: {node: '>=10.16.0'} stackback@0.0.2: @@ -13139,7 +13140,7 @@ snapshots: debug: 4.4.3 readable-stream: 3.6.2 split-ca: 1.0.1 - ssh2: 1.15.0 + ssh2: 1.16.0 transitivePeerDependencies: - supports-color @@ -15783,7 +15784,7 @@ snapshots: sqlstring@2.3.3: {} - ssh2@1.15.0: + ssh2@1.16.0: dependencies: asn1: 0.2.6 bcrypt-pbkdf: 1.0.2