mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
⚡ Improvements
This commit is contained in:
parent
a083914649
commit
44ec5c6cfe
|
@ -2,11 +2,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable import/no-cycle */
|
||||
import * as querystring from 'querystring';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { pick } from 'lodash';
|
||||
import { In } from 'typeorm';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import { OpenAPIV3, Format } from 'express-openapi-validator/dist/framework/types';
|
||||
import express = require('express');
|
||||
import validator from 'validator';
|
||||
import { User } from '../databases/entities/User';
|
||||
import type { Role } from '../databases/entities/Role';
|
||||
import { ActiveWorkflowRunner, Db, InternalHooksManager, ITelemetryUserDeletionData } from '..';
|
||||
|
@ -23,11 +25,10 @@ interface IPaginationOffsetDecoded {
|
|||
export type OperationID = 'getUsers' | 'getUser';
|
||||
|
||||
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
|
||||
const data = JSON.parse(Buffer.from(cursor, 'base64').toString()) as string;
|
||||
const unserializedData = querystring.decode(data) as { offset: string; limit: string };
|
||||
const { offset, limit } = JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||
return {
|
||||
offset: parseInt(unserializedData.offset, 10),
|
||||
limit: parseInt(unserializedData.limit, 10),
|
||||
offset,
|
||||
limit,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -213,7 +214,7 @@ export async function transferWorkflowsAndCredentials(data: {
|
|||
{ user: data.fromUser },
|
||||
{ user: data.toUser },
|
||||
);
|
||||
await transactionManager.delete(User, { id: data.fromUser });
|
||||
await transactionManager.delete(User, { id: data.fromUser.id });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -258,7 +259,7 @@ async function deleteWorkflowsAndCredentials(data: { fromUser: User }): Promise<
|
|||
const ownedWorkflows = await Promise.all(sharedWorkflows.map(desactiveWorkflow));
|
||||
await transactionManager.remove(ownedWorkflows);
|
||||
await transactionManager.remove(sharedCredentials.map(({ credentials }) => credentials));
|
||||
await transactionManager.delete(User, { id: data.fromUser });
|
||||
await transactionManager.delete(User, { id: data.fromUser.id });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -311,3 +312,41 @@ export function clean(
|
|||
getSelectableProperties('user').concat(options?.includeRole ? ['globalRole'] : []),
|
||||
);
|
||||
}
|
||||
|
||||
export async function authenticationHandler(
|
||||
req: express.Request,
|
||||
scopes: any,
|
||||
schema: OpenAPIV3.ApiKeySecurityScheme,
|
||||
): Promise<boolean> {
|
||||
const apiKey = req.headers[schema.name.toLowerCase()];
|
||||
const user = await Db.collections.User?.findOne({
|
||||
where: {
|
||||
apiKey,
|
||||
},
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function specFormats(): Format[] {
|
||||
return [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
validate: (email: string) => validator.isEmail(email),
|
||||
},
|
||||
{
|
||||
name: 'identifier',
|
||||
type: 'string',
|
||||
validate: (identifier: string) =>
|
||||
validator.isUUID(identifier) || validator.isEmail(identifier),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// eslint-disable-next-line import/no-cycle
|
||||
import { publicApiController as publicApiControllerV1 } from './v1';
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { publicApiControllerV1 } from './v1';
|
||||
import { publicApiControllerV2 } from './v2';
|
||||
|
||||
export const publicApi = [publicApiControllerV1()];
|
||||
export const publicApi = [publicApiControllerV1, publicApiControllerV2];
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
import express = require('express');
|
||||
import config = require('../../config');
|
||||
import type { UserRequest } from '../requests';
|
||||
import { decodeCursor } from './helpers';
|
||||
import * as UserManagementMailer from '../UserManagement/email/UserManagementMailer';
|
||||
|
||||
import { decodeCursor, getGlobalMemberRole } from './helpers';
|
||||
|
||||
type Role = 'owner' | 'member';
|
||||
|
||||
|
@ -48,23 +50,6 @@ const authorize =
|
|||
});
|
||||
};
|
||||
|
||||
// move this to open api validator
|
||||
// const validEmail = (
|
||||
// req: UserRequest.Invite,
|
||||
// res: express.Response,
|
||||
// next: express.NextFunction,
|
||||
// ): any => {
|
||||
// // eslint-disable-next-line no-restricted-syntax
|
||||
// for (const { email } of req.body) {
|
||||
// if (!validator.isEmail(email)) {
|
||||
// return res.status(400).json({
|
||||
// message: `Request to send email invite(s) to user(s) failed because of an invalid email address: ${email}`,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// next();
|
||||
// };
|
||||
|
||||
const deletingOwnUser = (
|
||||
req: UserRequest.Delete,
|
||||
res: express.Response,
|
||||
|
@ -96,27 +81,64 @@ const validCursor = (
|
|||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): any => {
|
||||
let offset = 0;
|
||||
let limit = 10;
|
||||
if (req.query.cursor) {
|
||||
const { cursor } = req.query;
|
||||
try {
|
||||
({ offset, limit } = decodeCursor(cursor));
|
||||
const { offset, limit } = decodeCursor(cursor);
|
||||
req.query.offset = offset;
|
||||
req.query.limit = limit;
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
message: `invalid cursor`,
|
||||
message: 'An invalid cursor was used',
|
||||
});
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
req.query.offset = offset;
|
||||
// @ts-ignore
|
||||
req.query.limit = limit;
|
||||
next();
|
||||
};
|
||||
|
||||
const getMailerInstance = async (
|
||||
req: UserRequest.Invite,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): Promise<any> => {
|
||||
let mailer: UserManagementMailer.UserManagementMailer | undefined;
|
||||
try {
|
||||
mailer = await UserManagementMailer.getInstance();
|
||||
req.mailer = mailer;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.status(500).json({
|
||||
message: 'Email sending must be set up in order to request a password reset email',
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
const globalMemberRoleSetup = async (
|
||||
req: UserRequest.Invite,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
): Promise<any> => {
|
||||
try {
|
||||
const role = await getGlobalMemberRole();
|
||||
req.globalMemberRole = role;
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
message: 'Members role not found in database - inconsistent state',
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
export const middlewares = {
|
||||
createUsers: [instanceOwnerSetup, emailSetup, authorize(['owner'])],
|
||||
createUsers: [
|
||||
instanceOwnerSetup,
|
||||
emailSetup,
|
||||
authorize(['owner']),
|
||||
getMailerInstance,
|
||||
globalMemberRoleSetup,
|
||||
],
|
||||
deleteUsers: [
|
||||
instanceOwnerSetup,
|
||||
deletingOwnUser,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
@ -8,13 +9,11 @@ import express = require('express');
|
|||
import { UserRequest } from '../../../../requests';
|
||||
|
||||
import { User } from '../../../../databases/entities/User';
|
||||
import { Role } from '../../../../databases/entities/Role';
|
||||
|
||||
import {
|
||||
clean,
|
||||
deleteDataAndSendTelemetry,
|
||||
getAllUsersAndCount,
|
||||
getGlobalMemberRole,
|
||||
encodeNextCursor,
|
||||
getUser,
|
||||
getUsers,
|
||||
|
@ -24,8 +23,6 @@ import {
|
|||
transferWorkflowsAndCredentials,
|
||||
} from '../../../helpers';
|
||||
|
||||
import * as UserManagementMailer from '../../../../UserManagement/email/UserManagementMailer';
|
||||
|
||||
import { ResponseHelper } from '../../../..';
|
||||
|
||||
import { middlewares } from '../../../middlewares';
|
||||
|
@ -36,31 +33,7 @@ export = {
|
|||
ResponseHelper.send(async (req: UserRequest.Invite, res: express.Response) => {
|
||||
const tokenOwnerId = req.user.id;
|
||||
const emailsInBody = req.body.map((data) => data.email);
|
||||
|
||||
let mailer: UserManagementMailer.UserManagementMailer | undefined;
|
||||
try {
|
||||
mailer = await UserManagementMailer.getInstance();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
'Email sending must be set up in order to request a password reset email',
|
||||
undefined,
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let role: Role | undefined;
|
||||
|
||||
try {
|
||||
role = await getGlobalMemberRole();
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
'Members role not found in database - inconsistent state',
|
||||
undefined,
|
||||
500,
|
||||
);
|
||||
}
|
||||
const { mailer, globalMemberRole: role } = req;
|
||||
|
||||
const { usersToSave, pendingUsers } = await getUsersToSaveAndInvite(emailsInBody);
|
||||
|
||||
|
@ -69,40 +42,45 @@ export = {
|
|||
try {
|
||||
savedUsers = await saveUsersWithRole(usersToSave, role!, tokenOwnerId);
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError('An error occurred during user creation');
|
||||
return res.status(500).json({
|
||||
message: 'An error occurred during user creation',
|
||||
});
|
||||
}
|
||||
|
||||
const userstoInvite = [...savedUsers, ...pendingUsers];
|
||||
|
||||
await inviteUsers(userstoInvite, mailer, tokenOwnerId);
|
||||
|
||||
return clean(userstoInvite);
|
||||
return res.json(clean(userstoInvite));
|
||||
}),
|
||||
],
|
||||
deleteUser: [
|
||||
...middlewares.deleteUsers,
|
||||
async (req: UserRequest.Delete, res: express.Response): Promise<any> => {
|
||||
async (req: UserRequest.Delete, res: express.Response) => {
|
||||
const { identifier: idToDelete } = req.params;
|
||||
const { transferId, includeRole } = req.query;
|
||||
const { transferId = '', includeRole = false } = req.query;
|
||||
const apiKeyUserOwner = req.user;
|
||||
|
||||
const users = await getUsers({
|
||||
withIdentifiers: [idToDelete, transferId ?? ''],
|
||||
withIdentifiers: [idToDelete, transferId],
|
||||
includeRole,
|
||||
});
|
||||
|
||||
if (!users?.length || (transferId && users.length !== 2)) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
'Request to delete a user failed because the ID of the user to delete and/or the ID of the transferee were not found in DB',
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
if (!users?.length || (transferId !== '' && users.length !== 2)) {
|
||||
return res.status(400).json({
|
||||
message:
|
||||
'Request to delete a user failed because the ID of the user to delete and/or the ID of the transferee were not found in DB',
|
||||
});
|
||||
}
|
||||
|
||||
const userToDelete = users?.find((user) => user.id === req.params.identifier) as User;
|
||||
const userToDelete = users?.find(
|
||||
(user) => user.id === req.params.identifier || user.email === req.params.identifier,
|
||||
) as User;
|
||||
|
||||
if (transferId) {
|
||||
const transferee = users?.find((user) => user.id === transferId) as User;
|
||||
const transferee = users?.find(
|
||||
(user) => user.id === transferId || user.email === transferId,
|
||||
) as User;
|
||||
|
||||
await transferWorkflowsAndCredentials({
|
||||
fromUser: userToDelete,
|
||||
|
@ -118,34 +96,30 @@ export = {
|
|||
transferId,
|
||||
});
|
||||
|
||||
return clean(userToDelete);
|
||||
return clean(userToDelete, { includeRole });
|
||||
},
|
||||
],
|
||||
getUser: [
|
||||
...middlewares.getUser,
|
||||
// @ts-ignore
|
||||
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
|
||||
const { includeRole } = req.query;
|
||||
async (req: UserRequest.Get, res: express.Response) => {
|
||||
const { includeRole = false } = req.query;
|
||||
const { identifier } = req.params;
|
||||
|
||||
const user = await getUser({ withIdentifier: identifier, includeRole });
|
||||
|
||||
if (!user) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Could not find user with identifier: ${identifier}`,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
return res.status(404).json({
|
||||
message: `Could not find user with identifier: ${identifier}`,
|
||||
});
|
||||
}
|
||||
|
||||
return clean(user, { includeRole });
|
||||
}, true),
|
||||
return res.json(clean(user, { includeRole }));
|
||||
},
|
||||
],
|
||||
getUsers: [
|
||||
...middlewares.getUsers,
|
||||
// @ts-ignore
|
||||
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
|
||||
const { offset, limit, includeRole = false } = req.query;
|
||||
async (req: UserRequest.Get, res: express.Response) => {
|
||||
const { offset = 0, limit = 100, includeRole = false } = req.query;
|
||||
|
||||
const [users, count] = await getAllUsersAndCount({
|
||||
includeRole,
|
||||
|
@ -153,10 +127,10 @@ export = {
|
|||
offset,
|
||||
});
|
||||
|
||||
return {
|
||||
users: clean(users, { includeRole }),
|
||||
return res.json({
|
||||
data: clean(users, { includeRole }),
|
||||
nextCursor: encodeNextCursor(offset, limit, count),
|
||||
};
|
||||
}, true),
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
|
@ -8,62 +10,39 @@ import path = require('path');
|
|||
|
||||
import express = require('express');
|
||||
|
||||
import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
|
||||
import { HttpError } from 'express-openapi-validator/dist/framework/types';
|
||||
|
||||
import { Db } from '../..';
|
||||
import { authenticationHandler, specFormats } from '../helpers';
|
||||
|
||||
export const publicApiController = (): express.Router => {
|
||||
const openApiSpec = path.join(__dirname, 'openapi.yml');
|
||||
export const publicApiControllerV1 = express.Router();
|
||||
|
||||
const apiController = express.Router();
|
||||
const openApiSpec = path.join(__dirname, 'openapi.yml');
|
||||
|
||||
apiController.use('/v1/spec', express.static(openApiSpec));
|
||||
publicApiControllerV1.use('/v1/spec', express.static(openApiSpec));
|
||||
|
||||
apiController.use('/v1', express.json());
|
||||
publicApiControllerV1.use('/v1', express.json());
|
||||
|
||||
apiController.use(
|
||||
'/v1',
|
||||
OpenApiValidator.middleware({
|
||||
apiSpec: openApiSpec,
|
||||
operationHandlers: path.join(__dirname),
|
||||
validateRequests: true,
|
||||
validateApiSpec: true,
|
||||
validateSecurity: {
|
||||
handlers: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ApiKeyAuth: async (req, scopes, schema: OpenAPIV3.ApiKeySecurityScheme) => {
|
||||
const apiKey = req.headers[schema.name.toLowerCase()];
|
||||
const user = await Db.collections.User?.findOne({
|
||||
where: {
|
||||
apiKey,
|
||||
},
|
||||
relations: ['globalRole'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
publicApiControllerV1.use(
|
||||
'/v1',
|
||||
OpenApiValidator.middleware({
|
||||
apiSpec: openApiSpec,
|
||||
operationHandlers: path.join(__dirname, '..'),
|
||||
validateRequests: true,
|
||||
validateApiSpec: true,
|
||||
formats: specFormats(),
|
||||
validateSecurity: {
|
||||
handlers: {
|
||||
ApiKeyAuth: authenticationHandler,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// add error handler
|
||||
// @ts-ignore
|
||||
apiController.use(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return res.status(error.status || 400).json({
|
||||
message: error.message,
|
||||
// errors: error.errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return apiController;
|
||||
};
|
||||
// error handler
|
||||
publicApiControllerV1.use(
|
||||
(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return res.status(error.status || 400).json({
|
||||
message: error.message,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ paths:
|
|||
/users:
|
||||
get:
|
||||
x-eov-operation-id: getUsers
|
||||
x-eov-operation-handler: routes/Users
|
||||
x-eov-operation-handler: v1/handlers/Users
|
||||
tags:
|
||||
- users
|
||||
summary: Retrieve all users
|
||||
|
@ -73,7 +73,7 @@ paths:
|
|||
$ref: '#/components/schemas/Error'
|
||||
post:
|
||||
x-eov-operation-id: createUsers
|
||||
x-eov-operation-handler: routes/Users
|
||||
x-eov-operation-handler: v1/handlers/Users
|
||||
tags:
|
||||
- users
|
||||
summary: Invite a user
|
||||
|
@ -108,7 +108,7 @@ paths:
|
|||
/users/{identifier}:
|
||||
get:
|
||||
x-eov-operation-id: getUser
|
||||
x-eov-operation-handler: routes/Users
|
||||
x-eov-operation-handler: v1/handlers/Users
|
||||
tags:
|
||||
- users
|
||||
summary: Get user by ID/Email
|
||||
|
@ -123,6 +123,7 @@ paths:
|
|||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
format: identifier
|
||||
- name: includeRole
|
||||
in: query
|
||||
required: false
|
||||
|
@ -146,7 +147,7 @@ paths:
|
|||
$ref: '#/components/schemas/Error'
|
||||
delete:
|
||||
x-eov-operation-id: deleteUser
|
||||
x-eov-operation-handler: routes/Users
|
||||
x-eov-operation-handler: v1/handlers/Users
|
||||
tags:
|
||||
- users
|
||||
summary: Delete user by ID/Email
|
||||
|
@ -161,6 +162,7 @@ paths:
|
|||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
format: identifier
|
||||
- name: transferId
|
||||
in: query
|
||||
description: ID of the user to transfer workflows and credentials to.
|
||||
|
@ -175,7 +177,7 @@ paths:
|
|||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
type: boolean
|
||||
example: true
|
||||
responses:
|
||||
"200":
|
||||
|
@ -235,6 +237,7 @@ components:
|
|||
example: 123e4567-e89b-12d3-a456-426614174000
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: jhon.doe@company.com
|
||||
firstName:
|
||||
maxLength: 32
|
||||
|
|
37
packages/cli/src/PublicApi/v2/handlers/Users/index.ts
Normal file
37
packages/cli/src/PublicApi/v2/handlers/Users/index.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import express = require('express');
|
||||
|
||||
import { UserRequest } from '../../../../requests';
|
||||
|
||||
import { getUser } from '../../../helpers';
|
||||
|
||||
import { ResponseHelper } from '../../../..';
|
||||
|
||||
import { middlewares } from '../../../middlewares';
|
||||
|
||||
export = {
|
||||
getUser: [
|
||||
...middlewares.getUser,
|
||||
// @ts-ignore
|
||||
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
|
||||
const { includeRole = false } = req.query;
|
||||
const { identifier } = req.params;
|
||||
|
||||
const user = await getUser({ withIdentifier: identifier, includeRole });
|
||||
|
||||
if (!user) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Could not find user with identifier: ${identifier}`,
|
||||
undefined,
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
}, true),
|
||||
],
|
||||
};
|
48
packages/cli/src/PublicApi/v2/index.ts
Normal file
48
packages/cli/src/PublicApi/v2/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable import/no-cycle */
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
|
||||
import path = require('path');
|
||||
|
||||
import express = require('express');
|
||||
|
||||
import { HttpError } from 'express-openapi-validator/dist/framework/types';
|
||||
|
||||
import { authenticationHandler } from '../helpers';
|
||||
|
||||
export const publicApiControllerV2 = express.Router();
|
||||
|
||||
const openApiSpec = path.join(__dirname, 'openapi.yml');
|
||||
|
||||
publicApiControllerV2.use('/v2/spec', express.static(openApiSpec));
|
||||
|
||||
publicApiControllerV2.use('/v2', express.json());
|
||||
|
||||
publicApiControllerV2.use(
|
||||
'/v2',
|
||||
OpenApiValidator.middleware({
|
||||
apiSpec: openApiSpec,
|
||||
operationHandlers: path.join(__dirname, '..'),
|
||||
validateRequests: true,
|
||||
validateApiSpec: true,
|
||||
validateSecurity: {
|
||||
handlers: {
|
||||
ApiKeyAuth: authenticationHandler,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// error handler
|
||||
publicApiControllerV2.use(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return res.status(error.status || 400).json({
|
||||
message: error.message,
|
||||
// errors: error.errors,
|
||||
});
|
||||
},
|
||||
);
|
231
packages/cli/src/PublicApi/v2/openapi.yml
Normal file
231
packages/cli/src/PublicApi/v2/openapi.yml
Normal file
|
@ -0,0 +1,231 @@
|
|||
---
|
||||
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/v2
|
||||
tags:
|
||||
- name: users
|
||||
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: v1/handlers/Users
|
||||
tags:
|
||||
- users
|
||||
summary: Retrieve all users
|
||||
description: Retrieve all users from your instance. Only available for the instance owner.
|
||||
parameters:
|
||||
- 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
|
||||
- name: includeRole
|
||||
in: query
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: boolean
|
||||
example: true
|
||||
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'
|
||||
/users/{identifier}:
|
||||
get:
|
||||
x-eov-operation-id: getUser
|
||||
x-eov-operation-handler: v2/handlers/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: identifier
|
||||
in: path
|
||||
description: The ID or email of the user
|
||||
required: true
|
||||
style: simple
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
- name: includeRole
|
||||
in: query
|
||||
required: false
|
||||
style: form
|
||||
explode: true
|
||||
schema:
|
||||
type: boolean
|
||||
example: true
|
||||
responses:
|
||||
"200":
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
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
|
||||
readOnly: true
|
||||
example: jhon
|
||||
lastName:
|
||||
maxLength: 32
|
||||
type: string
|
||||
description: User's last name
|
||||
readOnly: true
|
||||
example: doe
|
||||
pending:
|
||||
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: []
|
6
packages/cli/src/requests.d.ts
vendored
6
packages/cli/src/requests.d.ts
vendored
|
@ -11,8 +11,10 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
|
||||
import { User } from './databases/entities/User';
|
||||
import { Role } from './databases/entities/Role';
|
||||
import type { IExecutionDeleteFilter, IWorkflowDb } from '.';
|
||||
import type { PublicUser } from './UserManagement/Interfaces';
|
||||
import * as UserManagementMailer from './UserManagement/email/UserManagementMailer';
|
||||
|
||||
export type AuthlessRequest<
|
||||
RouteParams = {},
|
||||
|
@ -28,6 +30,8 @@ export type AuthenticatedRequest<
|
|||
RequestQuery = {},
|
||||
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
||||
user: User;
|
||||
mailer?: UserManagementMailer.UserManagementMailer;
|
||||
globalMemberRole?: Role;
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
|
@ -209,7 +213,7 @@ export declare namespace UserRequest {
|
|||
{ id: string; email: string; identifier: string },
|
||||
{},
|
||||
{},
|
||||
{ limit: number; offset: number; cursor: string; includeRole: boolean }
|
||||
{ limit?: number; offset?: number; cursor?: string; includeRole?: boolean }
|
||||
>;
|
||||
|
||||
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
||||
|
|
|
@ -10,7 +10,6 @@ import { Role } from '../../../src/databases/entities/Role';
|
|||
import {
|
||||
randomApiKey,
|
||||
randomEmail,
|
||||
randomInvalidPassword,
|
||||
randomName,
|
||||
randomValidPassword,
|
||||
} from '../shared/random';
|
||||
|
@ -18,8 +17,6 @@ import {
|
|||
import * as utils from '../shared/utils';
|
||||
import * as testDb from '../shared/testDb';
|
||||
|
||||
// import * from './../../../src/PublicApi/helpers'
|
||||
|
||||
let app: express.Application;
|
||||
let testDbName = '';
|
||||
let globalOwnerRole: Role;
|
||||
|
@ -55,14 +52,6 @@ beforeEach(async () => {
|
|||
await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName);
|
||||
await testDb.truncate(['User', 'Workflow', 'Credentials'], testDbName);
|
||||
|
||||
// jest.isolateModules(() => {
|
||||
// jest.mock('../../../config');
|
||||
// jest.mock('./../../../src/PublicApi/helpers', () => ({
|
||||
// ...jest.requireActual('./../../../src/PublicApi/helpers'),
|
||||
// connectionName: jest.fn(() => testDbName),
|
||||
// }));
|
||||
// });
|
||||
|
||||
await testDb.createUser({
|
||||
id: INITIAL_TEST_USER.id,
|
||||
email: INITIAL_TEST_USER.email,
|
||||
|
@ -107,7 +96,6 @@ test('GET /users should fail due to invalid API Key', async () => {
|
|||
});
|
||||
|
||||
test('GET /users should fail due to member trying to access owner only endpoint', async () => {
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
|
||||
const member = await testDb.createUser();
|
||||
|
||||
|
@ -125,16 +113,7 @@ test('GET /users should fail due no instance owner not setup', async () => {
|
|||
|
||||
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: true, user: owner });
|
||||
|
||||
// console.log(authOwnerAgent);
|
||||
|
||||
const response = await authOwnerAgent.get('/v1/users');
|
||||
// const response2 = await authOwnerAgent.get('/v1/spec');
|
||||
// const response3 = await authOwnerAgent.get('/v1/hello');
|
||||
|
||||
// console.log(response.body);
|
||||
// console.log(response.statusCode);
|
||||
|
||||
// console.log(authOwnerAgent.app);
|
||||
|
||||
expect(response.statusCode).toBe(500);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue