mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 21:31:11 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d313ae3ded |
@@ -45,8 +45,8 @@ export const UpdateServer = ({
|
||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
||||
!!updateData?.updateAvailable,
|
||||
);
|
||||
const { mutateAsync: getUpdateData, isPending } =
|
||||
api.settings.getUpdateData.useMutation();
|
||||
const utils = api.useUtils();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
||||
const [latestVersion, setLatestVersion] = useState(
|
||||
@@ -55,8 +55,9 @@ export const UpdateServer = ({
|
||||
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
||||
|
||||
const handleCheckUpdates = async () => {
|
||||
setIsPending(true);
|
||||
try {
|
||||
const updateData = await getUpdateData();
|
||||
const updateData = await utils.settings.getUpdateData.fetch();
|
||||
const versionToUpdate = updateData.latestVersion || "";
|
||||
setHasCheckedUpdate(true);
|
||||
setIsUpdateAvailable(updateData.updateAvailable);
|
||||
@@ -76,6 +77,8 @@ export const UpdateServer = ({
|
||||
toast.error(
|
||||
"An error occurred while checking for updates, please try again.",
|
||||
);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ export const UpdateServerButton = () => {
|
||||
updateAvailable: false,
|
||||
});
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { mutateAsync: getUpdateData } =
|
||||
api.settings.getUpdateData.useMutation();
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
||||
@@ -48,7 +47,7 @@ export const UpdateServerButton = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchedUpdateData = await getUpdateData();
|
||||
const fetchedUpdateData = await utils.settings.getUpdateData.fetch();
|
||||
|
||||
if (fetchedUpdateData?.updateAvailable) {
|
||||
// Stop interval when update is available
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
type Bitbucket,
|
||||
getBitbucketHeaders,
|
||||
IS_CLOUD,
|
||||
IS_DEMO,
|
||||
shouldDeploy,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
@@ -38,6 +39,10 @@ export default async function handler(
|
||||
) {
|
||||
const { refreshToken } = req.query;
|
||||
try {
|
||||
if (IS_DEMO) {
|
||||
res.status(403).json({ message: "This is a read-only demo instance." });
|
||||
return;
|
||||
}
|
||||
if (req.headers["x-github-event"] === "ping") {
|
||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
|
||||
import { IS_CLOUD, IS_DEMO, shouldDeploy } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
@@ -21,6 +21,10 @@ export default async function handler(
|
||||
) {
|
||||
const { refreshToken } = req.query;
|
||||
try {
|
||||
if (IS_DEMO) {
|
||||
res.status(403).json({ message: "This is a read-only demo instance." });
|
||||
return;
|
||||
}
|
||||
if (req.headers["x-github-event"] === "ping") {
|
||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findPreviewDeploymentByApplicationId,
|
||||
findPreviewDeploymentsByPullRequestId,
|
||||
IS_CLOUD,
|
||||
IS_DEMO,
|
||||
removePreviewDeployment,
|
||||
shouldDeploy,
|
||||
} from "@dokploy/server";
|
||||
@@ -30,6 +31,11 @@ export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (IS_DEMO) {
|
||||
res.status(403).json({ message: "This is a read-only demo instance." });
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = req.headers["x-hub-signature-256"];
|
||||
if (!signature) {
|
||||
res.status(401).json({ message: "Missing signature header" });
|
||||
|
||||
@@ -543,7 +543,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
getUpdateData: protectedProcedure.mutation(async () => {
|
||||
getUpdateData: protectedProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
// import { getServerAuthSession } from "@/server/auth";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/index";
|
||||
import { hasValidLicense, IS_DEMO } from "@dokploy/server/index";
|
||||
import type { statements } from "@dokploy/server/lib/access-control";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
@@ -141,6 +141,23 @@ const t = initTRPC
|
||||
*/
|
||||
export const createTRPCRouter = t.router;
|
||||
|
||||
/**
|
||||
* Every procedure below is built on this instead of `t.procedure` directly, so
|
||||
* demo-mode blocking applies everywhere in one place. It checks `type !==
|
||||
* "query"` rather than `type === "mutation"` because some real deploy/restore
|
||||
* actions (e.g. deployWithLogs, restoreBackupWithLogs) are implemented as
|
||||
* `.subscription()` generators, not `.mutation()`.
|
||||
*/
|
||||
const baseProcedure = t.procedure.use(({ type, next }) => {
|
||||
if (IS_DEMO && type !== "query") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "This is a read-only demo instance.",
|
||||
});
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Public (unauthenticated) procedure
|
||||
*
|
||||
@@ -148,7 +165,7 @@ export const createTRPCRouter = t.router;
|
||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||
* are logged in.
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
export const publicProcedure = baseProcedure;
|
||||
|
||||
/**
|
||||
* Protected (authenticated) procedure
|
||||
@@ -158,7 +175,7 @@ export const publicProcedure = t.procedure;
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
export const protectedProcedure = baseProcedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
@@ -172,7 +189,7 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
export const cliProcedure = baseProcedure.use(({ ctx, next }) => {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
@@ -190,7 +207,7 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
export const adminProcedure = baseProcedure.use(({ ctx, next }) => {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
@@ -213,7 +230,7 @@ export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
* Does NOT call the license server on every request; full validation (haveValidLicenseKey)
|
||||
* is used in the UI gate and when activating/validating keys.
|
||||
*/
|
||||
export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => {
|
||||
export const enterpriseProcedure = baseProcedure.use(async ({ ctx, next }) => {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type http from "node:http";
|
||||
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import {
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
IS_DEMO,
|
||||
validateRequest,
|
||||
} from "@dokploy/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
@@ -74,6 +79,13 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
ws.close(4003, "Not authorized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_DEMO) {
|
||||
ws.send("Terminal access is disabled on this read-only demo instance.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
execAsync,
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
IS_DEMO,
|
||||
validateRequest,
|
||||
} from "@dokploy/server";
|
||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
@@ -107,6 +108,12 @@ export const setupTerminalWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_DEMO) {
|
||||
ws.send("Terminal access is disabled on this read-only demo instance.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let connectionDetails: ConnectConfig = {};
|
||||
|
||||
const isLocalServer = serverId === "local";
|
||||
|
||||
@@ -4,6 +4,11 @@ import Docker from "dockerode";
|
||||
|
||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
|
||||
// Public read-only demo instance: every non-query tRPC procedure, the
|
||||
// terminal/exec websockets, and the deploy webhooks are blocked so visitors
|
||||
// can browse seeded data without being able to change anything.
|
||||
export const IS_DEMO = process.env.IS_DEMO === "true";
|
||||
|
||||
export const DOKPLOY_DOCKER_API_VERSION =
|
||||
process.env.DOKPLOY_DOCKER_API_VERSION;
|
||||
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError, createAuthMiddleware } from "better-auth/api";
|
||||
import { admin, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { IS_CLOUD, IS_DEMO } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import {
|
||||
@@ -32,6 +32,20 @@ import { getPublicIpWithFallback } from "../wss/utils";
|
||||
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
|
||||
import { betterAuthSecret } from "./auth-secret";
|
||||
|
||||
// Default-deny allowlist for demo mode: block every better-auth endpoint
|
||||
// except the ones a shared read-only login needs (sign-in, session check,
|
||||
// sign-out). This is deliberately an allowlist, not a blocklist of the
|
||||
// mutating paths (sign-up, change-password, invite-member, api-key, sso,
|
||||
// scim, ...) — better-auth plugins add new endpoints over time, and a
|
||||
// blocklist silently stops covering them.
|
||||
const DEMO_ALLOWED_AUTH_PATHS = new Set([
|
||||
"/sign-in/email",
|
||||
"/get-session",
|
||||
"/sign-out",
|
||||
"/ok",
|
||||
"/error",
|
||||
]);
|
||||
|
||||
const resolveTrustedOrigins = async () => {
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
@@ -125,6 +139,12 @@ const createBetterAuth = () =>
|
||||
...(ctx.context.baseURL ? [new URL(ctx.context.baseURL).origin] : []),
|
||||
...(await resolveTrustedOrigins()),
|
||||
].filter(Boolean);
|
||||
|
||||
if (IS_DEMO && !DEMO_ALLOWED_AUTH_PATHS.has(ctx.path)) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "This is a read-only demo instance.",
|
||||
});
|
||||
}
|
||||
}),
|
||||
},
|
||||
emailVerification: {
|
||||
|
||||
Reference in New Issue
Block a user