diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 1e5b25f933..59dea619e0 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -580,6 +580,15 @@ const config = convict({ }, }, + publicApiEndpoints: { + path: { + format: String, + default: 'api', + env: 'N8N_PUBLIC_API_ENDPOINT', + doc: 'Path for the public api endpoints', + }, + }, + workflowTagsDisabled: { format: Boolean, default: false, diff --git a/packages/cli/package.json b/packages/cli/package.json index 1ba3ed0fda..eb0d1a7dcc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -19,7 +19,7 @@ "bin": "n8n" }, "scripts": { - "build": "tsc && cp -r ./src/UserManagement/email/templates ./dist/src/UserManagement/email", + "build": "tsc && cp -r ./src/UserManagement/email/templates ./dist/src/UserManagement/email && rsync -a --include='*/' --include='*.yml' --exclude='*' ./src/PublicApi/ ./dist/src/PublicApi/", "dev": "concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch\" \"nodemon\"", "format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/cli/**/**.ts --write", "lint": "cd ../.. && node_modules/eslint/bin/eslint.js packages/cli", @@ -114,6 +114,7 @@ "csrf": "^3.1.0", "dotenv": "^8.0.0", "express": "^4.16.4", + "express-openapi-validator": "^4.13.6", "fast-glob": "^3.2.5", "flatted": "^3.2.4", "google-timezones-json": "^1.0.2", diff --git a/packages/cli/src/PublicApi/v1/index.ts b/packages/cli/src/PublicApi/v1/index.ts new file mode 100644 index 0000000000..2c601905e4 --- /dev/null +++ b/packages/cli/src/PublicApi/v1/index.ts @@ -0,0 +1,39 @@ + +import { + Application, +} from 'express'; + +import * as OpenApiValidator from 'express-openapi-validator'; + +import path = require('path'); + +import express = require('express'); + +export interface N8nApp { + app: Application; +} + +const publicApiController = express.Router(); + +export const getRoutes = (): express.Router => { + + publicApiController.use(`/v1`, + OpenApiValidator.middleware({ + apiSpec: path.join(__dirname, 'openapi.yml'), + operationHandlers: path.join(__dirname), + validateRequests: true, // (default) + validateApiSpec: true, + })); + + //add error handler + //@ts-ignore + publicApiController.use((err, req, res, next) => { + res.status(err.status || 500).json({ + message: err.message, + errors: err.errors, + }); + + }); + + return publicApiController; +}; \ No newline at end of file diff --git a/packages/cli/src/PublicApi/v1/openapi.yml b/packages/cli/src/PublicApi/v1/openapi.yml new file mode 100644 index 0000000000..7221a17e75 --- /dev/null +++ b/packages/cli/src/PublicApi/v1/openapi.yml @@ -0,0 +1,292 @@ +--- +openapi: 3.0.0 +info: + title: Public n8n API + description: n8n Public API + termsOfService: https://n8n.io/legal/terms + contact: + email: hello@n8n.io + license: + name: Apache 2.0 with Commons Clause + url: https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: /api/v1 +tags: +- name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /users: + get: + x-eov-operation-id: getUsers + x-eov-operation-handler: routes/Users + tags: + - users + summary: Retrieve all users + description: Retrieve all users from your instance. Only available for the instance owner. + parameters: + - name: select + in: query + required: false + style: form + explode: true + schema: + type: string + description: Comma-separted list of the properties to return. Use a to return all properties. Dot notation be use for nested properties + example: email,firstName + - name: limit + in: query + description: The maximum number of items to return + required: false + style: form + explode: true + schema: + type: number + example: 100 + default: 100 + - name: cursor + in: query + description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request's response_metadata. Default value fetches the first "page" of the collection. See pagination for more detail. + required: false + style: form + explode: true + schema: + type: string + example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/inline_response_200' + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + x-eov-operation-id: createUsers + x-eov-operation-handler: routes/Users + tags: + - user + summary: Invite a user + description: Invites a user to your instance. Only available for the instance owner. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/Users' + required: true + responses: + "200": + description: A User object + content: + application/json: + schema: + $ref: '#/components/schemas/Users' + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + "422": + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationError' + /users/{userId}: + get: + x-eov-operation-id: getUser + x-eov-operation-handler: routes/Users + tags: + - users + summary: Get user by ID/Email + description: Retrieve a user from your instance. Only available for the instance owner. + operationId: getUser + parameters: + - name: userId + in: path + description: The ID or email of the user + required: true + style: simple + explode: false + schema: + type: string + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + x-eov-operation-id: deleteUser + x-eov-operation-handler: routes/Users + tags: + - users + summary: Delete user by ID/Email + description: Deletes a user from your instance. Only available for the instance owner. + operationId: deleteUser + parameters: + - name: userId + in: path + description: The name that needs to be deleted + required: true + style: simple + explode: false + schema: + type: string + - name: transferId + in: query + description: ID of the user to transfer workflows and credentials to. + required: true + style: form + explode: true + schema: + type: string + responses: + "200": + description: User deleted successfully + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + "404": + description: User not found +components: + schemas: + InputValidationError: + required: + - code + - description + - message + type: object + properties: + code: + type: string + message: + type: string + errors: + $ref: '#/components/schemas/Errors' + Error: + required: + - code + - description + - message + type: object + properties: + code: + type: string + message: + type: string + description: + type: string + Errors: + type: array + items: + $ref: '#/components/schemas/Error' + Users: + type: array + items: + $ref: '#/components/schemas/User' + User: + required: + - email + type: object + properties: + id: + type: string + readOnly: true + example: 123e4567-e89b-12d3-a456-426614174000 + email: + type: string + example: jhon.doe@company.com + firstName: + maxLength: 32 + type: string + description: User's first name + example: jhon + lastName: + maxLength: 32 + type: string + description: User's last name + example: doe + finishedSetup: + type: boolean + description: Whether the user finished setting up the invitation or not + readOnly: true + createdAt: + type: string + description: Time the user was created + format: date-time + readOnly: true + updatedAt: + type: string + description: Last time the user was updaded + format: date-time + readOnly: true + inline_response_200: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + nextCursor: + type: string + description: Paginate through users by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection. + nullable: true + example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA + responses: + NotFound: + description: The specified resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Forbidden: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnprocessableEntity: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationError' + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-N8N-API-KEY + +security: + - ApiKeyAuth: [] \ No newline at end of file diff --git a/packages/cli/src/PublicApi/v1/routes/Users/index.ts b/packages/cli/src/PublicApi/v1/routes/Users/index.ts new file mode 100644 index 0000000000..baab16d0cd --- /dev/null +++ b/packages/cli/src/PublicApi/v1/routes/Users/index.ts @@ -0,0 +1,19 @@ +import express = require('express'); + +import { UserRequest } from '../../../../requests'; + +export = { + createUsers: async (req: UserRequest.Invite, res: express.Response) => { + res.json({ success: true}); + }, + deleteUser: async (req: UserRequest.Delete, res: express.Response) => { + console.log('aja') + res.json({ success: true }); + }, + getUser: async (req: UserRequest.Get, res: express.Response) => { + res.json({ success: true }); + }, + getUsers: async (req: UserRequest.Get, res: express.Response) => { + res.json({ success: true }); + }, +}; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index e87b36e81f..45b9109546 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -111,9 +111,11 @@ import { Db, ExternalHooks, GenericHelpers, + getCredentialForUser, ICredentialsDb, ICredentialsOverwrite, ICustomRequest, + IDiagnosticInfo, IExecutionFlattedDb, IExecutionFlattedResponse, IExecutionPushResponse, @@ -122,7 +124,6 @@ import { IExecutionsStopData, IExecutionsSummary, IExternalHooksClass, - IDiagnosticInfo, IN8nUISettings, IPackageVersions, ITagWithCountDb, @@ -139,7 +140,6 @@ import { WorkflowExecuteAdditionalData, WorkflowHelpers, WorkflowRunner, - getCredentialForUser, } from '.'; import * as config from '../config'; @@ -158,13 +158,13 @@ import { resolveJwt } from './UserManagement/auth/jwt'; import { User } from './databases/entities/User'; import { CredentialsEntity } from './databases/entities/CredentialsEntity'; import type { + AuthenticatedRequest, CredentialRequest, ExecutionRequest, - WorkflowRequest, NodeParameterOptionsRequest, OAuthRequest, - AuthenticatedRequest, TagsRequest, + WorkflowRequest, } from './requests'; import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers'; import { ExecutionEntity } from './databases/entities/ExecutionEntity'; @@ -172,6 +172,7 @@ import { SharedWorkflow } from './databases/entities/SharedWorkflow'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants'; import { credentialsController } from './api/credentials.api'; import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper'; +import * as publicApiv1Routes from './PublicApi/v1'; require('body-parser-xml')(bodyParser); @@ -220,6 +221,8 @@ class App { restEndpoint: string; + publicApiEndpoint: string; + frontendSettings: IN8nUISettings; protocol: string; @@ -252,6 +255,7 @@ class App { this.payloadSizeMax = config.get('endpoints.payloadSizeMax') as number; this.timezone = config.get('generic.timezone') as string; this.restEndpoint = config.get('endpoints.rest') as string; + this.publicApiEndpoint = config.get('publicApiEndpoints.path') as string; this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); this.testWebhooks = TestWebhooks.getInstance(); @@ -386,6 +390,7 @@ class App { this.endpointWebhook, this.endpointWebhookTest, this.endpointPresetCredentials, + this.publicApiEndpoint, ]; // eslint-disable-next-line prefer-spread ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':')); @@ -495,8 +500,9 @@ class App { // eslint-disable-next-line no-inner-declarations function isTenantAllowed(decodedToken: object): boolean { - if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') + if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') { return true; + } for (const [k, v] of Object.entries(decodedToken)) { if (k === jwtNamespace) { @@ -554,6 +560,12 @@ class App { }); } + // ---------------------------------------- + // Public API + // ---------------------------------------- + + this.app.use(`/${this.publicApiEndpoint}`, publicApiv1Routes.getRoutes()); + // Parse cookies for easier access this.app.use(cookieParser()); @@ -3081,7 +3093,7 @@ async function getExecutionsCount( try { // Get an estimate of rows count. const estimateRowsNumberSql = - "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';"; + 'SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = \'execution_entity\';'; const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query( estimateRowsNumberSql, ); diff --git a/packages/cli/src/requests.d.ts b/packages/cli/src/requests.d.ts index 72e1ffe0e2..b344d02a14 100644 --- a/packages/cli/src/requests.d.ts +++ b/packages/cli/src/requests.d.ts @@ -196,7 +196,9 @@ export declare namespace UserRequest { { inviterId?: string; inviteeId?: string } >; - export type Delete = AuthenticatedRequest<{ id: string }, {}, {}, { transferId?: string }>; + export type Delete = AuthenticatedRequest<{ id: string, email: string }, {}, {}, { transferId?: string }>; + + export type Get = AuthenticatedRequest<{ id: string, email: string }, {}, {}, { limit: string, cursor: string }>; export type Reinvite = AuthenticatedRequest<{ id: string }>;