diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 13ff2d6e4..e4af05376 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -190,7 +190,7 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => { Automatic process diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx index a0312562e..5924bba50 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx @@ -172,7 +172,7 @@ export const CreateSSHKey = () => { etc.)

diff --git a/apps/dokploy/components/layouts/dashboard-layout.tsx b/apps/dokploy/components/layouts/dashboard-layout.tsx index 727649922..222f69b9e 100644 --- a/apps/dokploy/components/layouts/dashboard-layout.tsx +++ b/apps/dokploy/components/layouts/dashboard-layout.tsx @@ -11,20 +11,19 @@ interface Props { export const DashboardLayout = ({ children }: Props) => { const { data: haveRootAccess } = api.user.haveRootAccess.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery( - undefined, - { - enabled: isCloud === true, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - }, - ); + const { data: currentPlan } = api.stripe.getCurrentPlan.useQuery(undefined, { + enabled: isCloud === true, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const isChatEnabled = isCloud === true && currentPlan === "startup"; return ( <> {children} - {isCloud === true && isUserSubscribed === true && ( + {isChatEnabled && ( <> diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index c910b1d58..109f2aac3 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -21,9 +21,51 @@ import { STARTUP_PRODUCT_ID, WEBSITE_URL, } from "@/server/utils/stripe"; -import { adminProcedure, createTRPCRouter } from "../trpc"; +import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; export const stripeRouter = createTRPCRouter({ + /** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */ + getCurrentPlan: protectedProcedure.query(async ({ ctx }) => { + if (!IS_CLOUD) return null; + const owner = await findUserById(ctx.user.ownerId); + if (!owner?.stripeCustomerId) return null; + + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2024-09-30.acacia", + }); + const subscriptions = await stripe.subscriptions.list({ + customer: owner.stripeCustomerId, + status: "active", + expand: ["data.items.data.price"], + }); + const activeSub = subscriptions.data[0]; + if (!activeSub) return null; + + const priceIds = activeSub.items.data.map( + (item) => (item.price as Stripe.Price).id, + ); + if ( + priceIds.some( + (id) => + id === STARTUP_BASE_PRICE_MONTHLY_ID || + id === STARTUP_BASE_PRICE_ANNUAL_ID, + ) + ) { + return "startup" as const; + } + if ( + priceIds.some( + (id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID, + ) + ) { + return "hobby" as const; + } + if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) { + return "legacy" as const; + } + return null; + }), + getProducts: adminProcedure.query(async ({ ctx }) => { const user = await findUserById(ctx.user.ownerId); const stripeCustomerId = user.stripeCustomerId; diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 32e5e4a7e..c15856dc3 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -281,17 +281,43 @@ const installRequirements = async ( .on("error", (err) => { client.end(); if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); + const technicalDetail = `Error: ${err.message} ${err.level}`; + const friendlyMessage = [ + "", + "❌ Couldn't connect to your server — the SSH key was not accepted.", + "", + "This usually means the key doesn't match what's on the server, or the key format is invalid.", + "", + `Technical details: ${technicalDetail}`, + "", + "💡 Hints:", + " • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).", + " • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.", + " • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab", + ].join("\n"); + onData?.(friendlyMessage); reject( new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + `Authentication failed: Invalid SSH private key. ${technicalDetail}`, ), ); } else { - onData?.(`SSH connection error: ${err.message} ${err.level}`); - reject(new Error(`SSH connection error: ${err.message}`)); + const technicalDetail = `${err.message} ${err.level ?? ""}`.trim(); + const friendlyMessage = [ + "", + "❌ Couldn't connect to your server.", + "", + "The connection failed before setup could run. Common causes: wrong IP or port, firewall blocking access, or the server is offline.", + "", + `Technical details: ${technicalDetail}`, + "", + "💡 Hints:", + " • Check that the server IP address and SSH port are correct and the server is powered on.", + " • If the server is in a private network, ensure Dokploy can reach it (VPN, firewall rules, or correct security groups).", + " • Make sure the SSH port (usually 22) is open and the SSH service is running on the server.", + ].join("\n"); + onData?.(friendlyMessage); + reject(new Error(`SSH connection error: ${technicalDetail}`)); } }) .connect({ diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index cd0249000..d44e3ccaf 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -201,14 +201,31 @@ export const execAsyncRemote = async ( .on("error", (err) => { conn.end(); if (err.level === "client-authentication") { + const technicalDetail = `Error: ${err.message} ${err.level}`; + const friendlyMessage = [ + "", + "❌ Couldn't connect to your server — the SSH key was not accepted.", + "", + "This usually means the key doesn't match what's on the server, or the key format is invalid.", + "", + `Technical details: ${technicalDetail}`, + "", + "💡 Hints:", + " • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).", + " • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.", + " • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab and then click on deployments tab and check the logs for more details.", + ].join("\n"); const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`; - onData?.(errorMsg); + onData?.(friendlyMessage); reject( - new ExecError(errorMsg, { - command, - serverId, - originalError: err, - }), + new ExecError( + `Authentication failed: Invalid SSH private key. ${friendlyMessage}`, + { + command, + serverId, + originalError: err, + }, + ), ); } else { const errorMsg = `SSH connection error: ${err.message}`;