diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 3ae49bd00d..22de687fdf 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -531,6 +531,9 @@ export interface IPublicApiSettings { enabled: boolean; latestVersion: number; path: string; + swaggerUi: { + enabled: boolean; + }; } export interface IPackageVersions { diff --git a/packages/cli/src/PublicApi/index.ts b/packages/cli/src/PublicApi/index.ts index 104ad15856..d8344a8ec7 100644 --- a/packages/cli/src/PublicApi/index.ts +++ b/packages/cli/src/PublicApi/index.ts @@ -3,27 +3,25 @@ import express, { Router } from 'express'; import fs from 'fs/promises'; import path from 'path'; -import * as OpenApiValidator from 'express-openapi-validator'; -import { HttpError } from 'express-openapi-validator/dist/framework/types'; -import { OpenAPIV3 } from 'openapi-types'; -import swaggerUi from 'swagger-ui-express'; -import validator from 'validator'; -import YAML from 'yamljs'; +import type { HttpError } from 'express-openapi-validator/dist/framework/types'; +import type { OpenAPIV3 } from 'openapi-types'; +import type { JsonObject } from 'swagger-ui-express'; import config from '@/config'; import * as Db from '@/Db'; import { InternalHooksManager } from '@/InternalHooksManager'; import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; -function createApiRouter( +async function createApiRouter( version: string, openApiSpecPath: string, handlersDirectory: string, swaggerThemeCss: string, publicApiEndpoint: string, -): Router { +): Promise { const n8nPath = config.getEnv('path'); - const swaggerDocument = YAML.load(openApiSpecPath) as swaggerUi.JsonObject; + const YAML = await import('yamljs'); + const swaggerDocument = YAML.load(openApiSpecPath) as JsonObject; // add the server depending on the config so the user can interact with the API // from the Swagger UI swaggerDocument.server = [ @@ -33,21 +31,26 @@ function createApiRouter( ]; const apiController = express.Router(); - apiController.use( - `/${publicApiEndpoint}/${version}/docs`, - swaggerUi.serveFiles(swaggerDocument), - swaggerUi.setup(swaggerDocument, { - customCss: swaggerThemeCss, - customSiteTitle: 'n8n Public API UI', - customfavIcon: `${n8nPath}favicon.ico`, - }), - ); + if (!config.getEnv('publicApi.swaggerUi.disabled')) { + const { serveFiles, setup } = await import('swagger-ui-express'); - apiController.use(`/${publicApiEndpoint}/${version}`, express.json()); + apiController.use( + `/${publicApiEndpoint}/${version}/docs`, + serveFiles(swaggerDocument), + setup(swaggerDocument, { + customCss: swaggerThemeCss, + customSiteTitle: 'n8n Public API UI', + customfavIcon: `${n8nPath}favicon.ico`, + }), + ); + } + const { default: validator } = await import('validator'); + const { middleware } = await import('express-openapi-validator'); apiController.use( `/${publicApiEndpoint}/${version}`, - OpenApiValidator.middleware({ + express.json(), + middleware({ apiSpec: openApiSpecPath, operationHandlers: handlersDirectory, validateRequests: true, @@ -131,10 +134,12 @@ export const loadPublicApiVersions = async ( const css = (await fs.readFile(swaggerThemePath)).toString(); const versions = folders.filter((folderName) => folderName.startsWith('v')); - const apiRouters = versions.map((version) => { - const openApiPath = path.join(__dirname, version, 'openapi.yml'); - return createApiRouter(version, openApiPath, __dirname, css, publicApiEndpoint); - }); + const apiRouters = await Promise.all( + versions.map(async (version) => { + const openApiPath = path.join(__dirname, version, 'openapi.yml'); + return createApiRouter(version, openApiPath, __dirname, css, publicApiEndpoint); + }), + ); return { apiRouters, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 12461cb052..1e312f796a 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -332,9 +332,12 @@ class App { smtpSetup: isEmailSetUp(), }, publicApi: { - enabled: config.getEnv('publicApi.disabled') === false, + enabled: !config.getEnv('publicApi.disabled'), latestVersion: 1, path: config.getEnv('publicApi.path'), + swaggerUi: { + enabled: !config.getEnv('publicApi.swaggerUi.disabled'), + }, }, workflowTagsDisabled: config.getEnv('workflowTagsDisabled'), logLevel: config.getEnv('logs.level'), diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index e36b637831..68a11628ad 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -19,6 +19,9 @@ if (inE2ETests) { EXTERNAL_FRONTEND_HOOKS_URLS: '', N8N_PERSONALIZATION_ENABLED: 'false', }; +} +if (inTest) { + process.env.N8N_PUBLIC_API_DISABLED = 'true'; } else { dotenv.config(); } diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 29dcaa85a7..ee459209a0 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -637,6 +637,14 @@ export const schema = { env: 'N8N_PUBLIC_API_ENDPOINT', doc: 'Path for the public api endpoints', }, + swaggerUi: { + disabled: { + format: Boolean, + default: false, + env: 'N8N_PUBLIC_API_SWAGGERUI_DISABLED', + doc: 'Whether to disable the Swagger UI for the Public API', + }, + }, }, workflowTagsDisabled: { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 65cd0d0985..5c83b589ef 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -796,6 +796,9 @@ export interface IN8nUISettings { enabled: boolean; latestVersion: number; path: string; + swaggerUi: { + enabled: boolean; + }; }; onboardingCallPromptEnabled: boolean; allowedModules: { @@ -1204,6 +1207,9 @@ export interface ISettingsState { enabled: boolean; latestVersion: number; path: string; + swaggerUi: { + enabled: boolean; + }; }; onboardingCallPromptEnabled: boolean; saveDataErrorExecution: string; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index c7be218a25..148efb4289 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1127,6 +1127,8 @@ "settings.api.view.info.webhook": "webhook node", "settings.api.view.myKey": "My API Key", "settings.api.view.tryapi": "Try it out using the", + "settings.api.view.more-details": "You can find more details in", + "settings.api.view.external-docs": "the API documentation", "settings.api.view.error": "Could not check if an api key already exists.", "settings.version": "Version", "settings.usageAndPlan.title": "Usage and plan", diff --git a/packages/editor-ui/src/stores/settings.ts b/packages/editor-ui/src/stores/settings.ts index 94ee0bfb16..96dffdd23a 100644 --- a/packages/editor-ui/src/stores/settings.ts +++ b/packages/editor-ui/src/stores/settings.ts @@ -38,6 +38,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { enabled: false, latestVersion: 0, path: '/', + swaggerUi: { + enabled: false, + }, }, onboardingCallPromptEnabled: false, saveDataErrorExecution: 'all', @@ -57,6 +60,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { isPublicApiEnabled(): boolean { return this.api.enabled; }, + isSwaggerUIEnabled(): boolean { + return this.api.swaggerUi.enabled; + }, publicApiLatestVersion(): number { return this.api.latestVersion; }, @@ -139,9 +145,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { this.userManagement.enabled = settings.userManagement.enabled; this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad; this.userManagement.smtpSetup = settings.userManagement.smtpSetup; - this.api.enabled = settings.publicApi.enabled; - this.api.latestVersion = settings.publicApi.latestVersion; - this.api.path = settings.publicApi.path; + this.api = settings.publicApi; this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled; }, async getSettings(): Promise { diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue index cc86f6cd9c..21c7ce3aba 100644 --- a/packages/editor-ui/src/views/SettingsApiView.vue +++ b/packages/editor-ui/src/views/SettingsApiView.vue @@ -48,10 +48,16 @@
- {{ $locale.baseText('settings.api.view.tryapi') }} + {{ + $locale.baseText(`settings.api.view.${swaggerUIEnabled ? 'tryapi' : 'more-details'}`) + }} - - {{ $locale.baseText('settings.api.view.apiPlayground') }} + + {{ + $locale.baseText( + `settings.api.view.${swaggerUIEnabled ? 'apiPlayground' : 'external-docs'}`, + ) + }}
@@ -78,9 +84,10 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useRootStore } from '@/stores/n8nRootStore'; import { useUsersStore } from '@/stores/users'; +import { DOCS_DOMAIN } from '@/constants'; export default mixins(showMessage).extend({ - name: 'SettingsPersonalView', + name: 'SettingsApiView', components: { CopyInput, }, @@ -89,7 +96,8 @@ export default mixins(showMessage).extend({ loading: false, mounted: false, apiKey: '', - apiPlaygroundPath: '', + swaggerUIEnabled: false, + apiDocsURL: '', }; }, mounted() { @@ -97,7 +105,10 @@ export default mixins(showMessage).extend({ const baseUrl = this.rootStore.baseUrl; const apiPath = this.settingsStore.publicApiPath; const latestVersion = this.settingsStore.publicApiLatestVersion; - this.apiPlaygroundPath = `${baseUrl}${apiPath}/v${latestVersion}/docs`; + this.swaggerUIEnabled = this.settingsStore.isSwaggerUIEnabled; + this.apiDocsURL = this.swaggerUIEnabled + ? `${baseUrl}${apiPath}/v${latestVersion}/docs` + : `https://${DOCS_DOMAIN}/api/api-reference/`; }, computed: { ...mapStores(useRootStore, useSettingsStore, useUsersStore),