From 4ab9f25274af8d7b873508e94924df3e444dacf8 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 6 Sep 2026 09:47:35 +0200 Subject: [PATCH] fix(integrations): gate mock integration server-side --- .env.example | 4 +-- .../advanced/environment-variables/index.mdx | 7 ++-- .../[locale]/manage/integrations/new/page.tsx | 3 ++ packages/api/src/env.ts | 2 ++ .../router/integration/integration-router.ts | 33 +++++++++++++++---- .../api/src/router/onboard/onboard-router.ts | 7 ++++ 6 files changed, 44 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index e05862c3c..958af4ccf 100644 --- a/.env.example +++ b/.env.example @@ -58,8 +58,8 @@ TURBO_TELEMETRY_DISABLED=1 # Enable kubernetes tool # ENABLE_KUBERNETES=true -# Enable mock integration -UNSAFE_ENABLE_MOCK_INTEGRATION=true +# Enable the development-only mock integration. Never enable this in production. +# UNSAFE_ENABLE_MOCK_INTEGRATION=true # Public URL contract. Values must be absolute HTTP(S) URLs without credentials, # query strings, or fragments. Trailing slashes are normalized at startup. diff --git a/apps/docs/docs/advanced/environment-variables/index.mdx b/apps/docs/docs/advanced/environment-variables/index.mdx index 364545e27..7d2189dde 100644 --- a/apps/docs/docs/advanced/environment-variables/index.mdx +++ b/apps/docs/docs/advanced/environment-variables/index.mdx @@ -133,9 +133,10 @@ External redis is currently not supported with Proxmox Community Scripts The advanced deployments environment variables should only be used if you know what you are doing. ::: -| Environment Variable | Description | -| ------------------------ | --------------------------------------------------- | -| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. | +| Environment Variable | Description | Default | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- | +| `DB_MIGRATIONS_DISABLED` | Disable database migrations. For example, when migrations are managed separately in Helm deployments. | `false` | +| `UNSAFE_ENABLE_MOCK_INTEGRATION` | Enable the development-only Mock integration. This exposes fabricated data and must not be used in production. | `false` | All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize trailing slashes when Homarr or Docusaurus starts. diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx index 3f7cc3e6b..8cbf820b5 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx @@ -47,6 +47,9 @@ export default async function IntegrationsNewPage(props: NewIntegrationPageProps const tCreate = await getI18n("integration.page.create"); const currentKind = result.data; + if (currentKind === "mock" && !env.UNSAFE_ENABLE_MOCK_INTEGRATION) { + notFound(); + } return ( <> diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index c5c7a2e68..23ea7e233 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -19,6 +19,7 @@ const publicHttpUrl = (variableName: string) => export const env = createEnv({ server: { KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(), + UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false), DEMO_MODE: createBooleanSchema(false), DEMO_READ_ONLY: createBooleanSchema(true), HOMARR_WEBSITE_URL: publicHttpUrl("HOMARR_WEBSITE_URL").default("https://homarr.dev"), @@ -27,6 +28,7 @@ export const env = createEnv({ }, runtimeEnv: { KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, + UNSAFE_ENABLE_MOCK_INTEGRATION: process.env.UNSAFE_ENABLE_MOCK_INTEGRATION, DEMO_MODE: process.env.DEMO_MODE, DEMO_READ_ONLY: process.env.DEMO_READ_ONLY, HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL, diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index a36ff5c6e..1f103fe7c 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -18,7 +18,12 @@ import { integrationSecrets, integrationUserPermissions, } from "@homarr/db/schema"; -import type { GroupPermissionKey, IntegrationPermission, IntegrationSecretKind } from "@homarr/definitions"; +import type { + GroupPermissionKey, + IntegrationKind, + IntegrationPermission, + IntegrationSecretKind, +} from "@homarr/definitions"; import { getIntegrationKindsByCategory, getPermissionsWithParents, @@ -39,6 +44,7 @@ import { } from "@homarr/validation/integration"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; +import { env } from "../../env"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; import { throwIfActionForbiddenAsync } from "./integration-access"; @@ -48,6 +54,15 @@ import { mapTestConnectionError } from "./map-test-connection-error"; const logger = createLogger({ module: "integrationRouter" }); const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch"); +const throwIfMockIntegrationIsDisabled = (kind: IntegrationKind) => { + if (kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION) return; + + throw new TRPCError({ + code: "FORBIDDEN", + message: "Mock integrations are disabled", + }); +}; + export const integrationRouter = createTRPCRouter({ getKinds: publicProcedure .meta({ @@ -58,12 +73,14 @@ export const integrationRouter = createTRPCRouter({ }, }) .query(() => { - return objectEntries(integrationDefs).map(([kind, def]) => ({ - kind, - name: def.name, - category: def.category, - requiredSecrets: def.secretKinds, - })); + return objectEntries(integrationDefs) + .filter(([kind]) => kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION) + .map(([kind, def]) => ({ + kind, + name: def.name, + category: def.category, + requiredSecrets: def.secretKinds, + })); }), all: protectedProcedure .meta({ @@ -229,6 +246,8 @@ export const integrationRouter = createTRPCRouter({ }) .input(integrationCreateSchema) .mutation(async ({ ctx, input }) => { + throwIfMockIntegrationIsDisabled(input.kind); + logger.info("Creating integration", { name: input.name, kind: input.kind, diff --git a/packages/api/src/router/onboard/onboard-router.ts b/packages/api/src/router/onboard/onboard-router.ts index 1a41f5755..d1834693b 100644 --- a/packages/api/src/router/onboard/onboard-router.ts +++ b/packages/api/src/router/onboard/onboard-router.ts @@ -321,6 +321,13 @@ export const onboardRouter = createTRPCRouter({ .requiresStep("setup") .input(onboardingCompleteSetupSchema) .mutation(async ({ ctx, input }) => { + if ( + !env.UNSAFE_ENABLE_MOCK_INTEGRATION && + input.integrations.some((integration) => integration.kind === "mock") + ) { + throw new TRPCError({ code: "FORBIDDEN", message: "Mock integrations are disabled" }); + } + const boardList = await ctx.db.query.boards.findMany({ with: { sections: { with: { layouts: true } },