mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-20 18:49:27 -08:00
Merge branch 'feature/public-api-initial-setup-N8N-3170' into feature/n8n-public-api
This commit is contained in:
commit
e1bd9fc510
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
39
packages/cli/src/PublicApi/v1/index.ts
Normal file
39
packages/cli/src/PublicApi/v1/index.ts
Normal file
|
@ -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;
|
||||
};
|
292
packages/cli/src/PublicApi/v1/openapi.yml
Normal file
292
packages/cli/src/PublicApi/v1/openapi.yml
Normal file
|
@ -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: []
|
19
packages/cli/src/PublicApi/v1/routes/Users/index.ts
Normal file
19
packages/cli/src/PublicApi/v1/routes/Users/index.ts
Normal file
|
@ -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 });
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
);
|
||||
|
|
4
packages/cli/src/requests.d.ts
vendored
4
packages/cli/src/requests.d.ts
vendored
|
@ -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 }>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue