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-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable import/no-cycle */
|
/* eslint-disable import/no-cycle */
|
||||||
import * as querystring from 'querystring';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { validate as uuidValidate } from 'uuid';
|
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 { User } from '../databases/entities/User';
|
||||||
import type { Role } from '../databases/entities/Role';
|
import type { Role } from '../databases/entities/Role';
|
||||||
import { ActiveWorkflowRunner, Db, InternalHooksManager, ITelemetryUserDeletionData } from '..';
|
import { ActiveWorkflowRunner, Db, InternalHooksManager, ITelemetryUserDeletionData } from '..';
|
||||||
|
@ -23,11 +25,10 @@ interface IPaginationOffsetDecoded {
|
||||||
export type OperationID = 'getUsers' | 'getUser';
|
export type OperationID = 'getUsers' | 'getUser';
|
||||||
|
|
||||||
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
|
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
|
||||||
const data = JSON.parse(Buffer.from(cursor, 'base64').toString()) as string;
|
const { offset, limit } = JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||||
const unserializedData = querystring.decode(data) as { offset: string; limit: string };
|
|
||||||
return {
|
return {
|
||||||
offset: parseInt(unserializedData.offset, 10),
|
offset,
|
||||||
limit: parseInt(unserializedData.limit, 10),
|
limit,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -213,7 +214,7 @@ export async function transferWorkflowsAndCredentials(data: {
|
||||||
{ user: data.fromUser },
|
{ user: data.fromUser },
|
||||||
{ user: data.toUser },
|
{ 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));
|
const ownedWorkflows = await Promise.all(sharedWorkflows.map(desactiveWorkflow));
|
||||||
await transactionManager.remove(ownedWorkflows);
|
await transactionManager.remove(ownedWorkflows);
|
||||||
await transactionManager.remove(sharedCredentials.map(({ credentials }) => credentials));
|
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'] : []),
|
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
|
/* eslint-disable import/no-cycle */
|
||||||
import { publicApiController as publicApiControllerV1 } from './v1';
|
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 express = require('express');
|
||||||
import config = require('../../config');
|
import config = require('../../config');
|
||||||
import type { UserRequest } from '../requests';
|
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';
|
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 = (
|
const deletingOwnUser = (
|
||||||
req: UserRequest.Delete,
|
req: UserRequest.Delete,
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
|
@ -96,27 +81,64 @@ const validCursor = (
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
next: express.NextFunction,
|
next: express.NextFunction,
|
||||||
): any => {
|
): any => {
|
||||||
let offset = 0;
|
|
||||||
let limit = 10;
|
|
||||||
if (req.query.cursor) {
|
if (req.query.cursor) {
|
||||||
const { cursor } = req.query;
|
const { cursor } = req.query;
|
||||||
try {
|
try {
|
||||||
({ offset, limit } = decodeCursor(cursor));
|
const { offset, limit } = decodeCursor(cursor);
|
||||||
|
req.query.offset = offset;
|
||||||
|
req.query.limit = limit;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
message: `invalid cursor`,
|
message: 'An invalid cursor was used',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
next();
|
||||||
req.query.offset = offset;
|
};
|
||||||
// @ts-ignore
|
|
||||||
req.query.limit = limit;
|
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();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const middlewares = {
|
export const middlewares = {
|
||||||
createUsers: [instanceOwnerSetup, emailSetup, authorize(['owner'])],
|
createUsers: [
|
||||||
|
instanceOwnerSetup,
|
||||||
|
emailSetup,
|
||||||
|
authorize(['owner']),
|
||||||
|
getMailerInstance,
|
||||||
|
globalMemberRoleSetup,
|
||||||
|
],
|
||||||
deleteUsers: [
|
deleteUsers: [
|
||||||
instanceOwnerSetup,
|
instanceOwnerSetup,
|
||||||
deletingOwnUser,
|
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-return */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
@ -8,13 +9,11 @@ import express = require('express');
|
||||||
import { UserRequest } from '../../../../requests';
|
import { UserRequest } from '../../../../requests';
|
||||||
|
|
||||||
import { User } from '../../../../databases/entities/User';
|
import { User } from '../../../../databases/entities/User';
|
||||||
import { Role } from '../../../../databases/entities/Role';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clean,
|
clean,
|
||||||
deleteDataAndSendTelemetry,
|
deleteDataAndSendTelemetry,
|
||||||
getAllUsersAndCount,
|
getAllUsersAndCount,
|
||||||
getGlobalMemberRole,
|
|
||||||
encodeNextCursor,
|
encodeNextCursor,
|
||||||
getUser,
|
getUser,
|
||||||
getUsers,
|
getUsers,
|
||||||
|
@ -24,8 +23,6 @@ import {
|
||||||
transferWorkflowsAndCredentials,
|
transferWorkflowsAndCredentials,
|
||||||
} from '../../../helpers';
|
} from '../../../helpers';
|
||||||
|
|
||||||
import * as UserManagementMailer from '../../../../UserManagement/email/UserManagementMailer';
|
|
||||||
|
|
||||||
import { ResponseHelper } from '../../../..';
|
import { ResponseHelper } from '../../../..';
|
||||||
|
|
||||||
import { middlewares } from '../../../middlewares';
|
import { middlewares } from '../../../middlewares';
|
||||||
|
@ -36,31 +33,7 @@ export = {
|
||||||
ResponseHelper.send(async (req: UserRequest.Invite, res: express.Response) => {
|
ResponseHelper.send(async (req: UserRequest.Invite, res: express.Response) => {
|
||||||
const tokenOwnerId = req.user.id;
|
const tokenOwnerId = req.user.id;
|
||||||
const emailsInBody = req.body.map((data) => data.email);
|
const emailsInBody = req.body.map((data) => data.email);
|
||||||
|
const { mailer, globalMemberRole: role } = req;
|
||||||
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 { usersToSave, pendingUsers } = await getUsersToSaveAndInvite(emailsInBody);
|
const { usersToSave, pendingUsers } = await getUsersToSaveAndInvite(emailsInBody);
|
||||||
|
|
||||||
|
@ -69,40 +42,45 @@ export = {
|
||||||
try {
|
try {
|
||||||
savedUsers = await saveUsersWithRole(usersToSave, role!, tokenOwnerId);
|
savedUsers = await saveUsersWithRole(usersToSave, role!, tokenOwnerId);
|
||||||
} catch (error) {
|
} 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];
|
const userstoInvite = [...savedUsers, ...pendingUsers];
|
||||||
|
|
||||||
await inviteUsers(userstoInvite, mailer, tokenOwnerId);
|
await inviteUsers(userstoInvite, mailer, tokenOwnerId);
|
||||||
|
|
||||||
return clean(userstoInvite);
|
return res.json(clean(userstoInvite));
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
deleteUser: [
|
deleteUser: [
|
||||||
...middlewares.deleteUsers,
|
...middlewares.deleteUsers,
|
||||||
async (req: UserRequest.Delete, res: express.Response): Promise<any> => {
|
async (req: UserRequest.Delete, res: express.Response) => {
|
||||||
const { identifier: idToDelete } = req.params;
|
const { identifier: idToDelete } = req.params;
|
||||||
const { transferId, includeRole } = req.query;
|
const { transferId = '', includeRole = false } = req.query;
|
||||||
const apiKeyUserOwner = req.user;
|
const apiKeyUserOwner = req.user;
|
||||||
|
|
||||||
const users = await getUsers({
|
const users = await getUsers({
|
||||||
withIdentifiers: [idToDelete, transferId ?? ''],
|
withIdentifiers: [idToDelete, transferId],
|
||||||
includeRole,
|
includeRole,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!users?.length || (transferId && users.length !== 2)) {
|
if (!users?.length || (transferId !== '' && users.length !== 2)) {
|
||||||
throw new ResponseHelper.ResponseError(
|
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',
|
'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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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({
|
await transferWorkflowsAndCredentials({
|
||||||
fromUser: userToDelete,
|
fromUser: userToDelete,
|
||||||
|
@ -118,34 +96,30 @@ export = {
|
||||||
transferId,
|
transferId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return clean(userToDelete);
|
return clean(userToDelete, { includeRole });
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
getUser: [
|
getUser: [
|
||||||
...middlewares.getUser,
|
...middlewares.getUser,
|
||||||
// @ts-ignore
|
async (req: UserRequest.Get, res: express.Response) => {
|
||||||
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
|
const { includeRole = false } = req.query;
|
||||||
const { includeRole } = req.query;
|
|
||||||
const { identifier } = req.params;
|
const { identifier } = req.params;
|
||||||
|
|
||||||
const user = await getUser({ withIdentifier: identifier, includeRole });
|
const user = await getUser({ withIdentifier: identifier, includeRole });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ResponseHelper.ResponseError(
|
return res.status(404).json({
|
||||||
`Could not find user with identifier: ${identifier}`,
|
message: `Could not find user with identifier: ${identifier}`,
|
||||||
undefined,
|
});
|
||||||
404,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return clean(user, { includeRole });
|
return res.json(clean(user, { includeRole }));
|
||||||
}, true),
|
},
|
||||||
],
|
],
|
||||||
getUsers: [
|
getUsers: [
|
||||||
...middlewares.getUsers,
|
...middlewares.getUsers,
|
||||||
// @ts-ignore
|
async (req: UserRequest.Get, res: express.Response) => {
|
||||||
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
|
const { offset = 0, limit = 100, includeRole = false } = req.query;
|
||||||
const { offset, limit, includeRole = false } = req.query;
|
|
||||||
|
|
||||||
const [users, count] = await getAllUsersAndCount({
|
const [users, count] = await getAllUsersAndCount({
|
||||||
includeRole,
|
includeRole,
|
||||||
|
@ -153,10 +127,10 @@ export = {
|
||||||
offset,
|
offset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return res.json({
|
||||||
users: clean(users, { includeRole }),
|
data: clean(users, { includeRole }),
|
||||||
nextCursor: encodeNextCursor(offset, limit, count),
|
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 @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
/* eslint-disable import/no-dynamic-require */
|
/* eslint-disable import/no-dynamic-require */
|
||||||
|
@ -8,62 +10,39 @@ import path = require('path');
|
||||||
|
|
||||||
import express = require('express');
|
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 => {
|
export const publicApiControllerV1 = express.Router();
|
||||||
const openApiSpec = path.join(__dirname, 'openapi.yml');
|
|
||||||
|
|
||||||
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(
|
publicApiControllerV1.use(
|
||||||
'/v1',
|
'/v1',
|
||||||
OpenApiValidator.middleware({
|
OpenApiValidator.middleware({
|
||||||
apiSpec: openApiSpec,
|
apiSpec: openApiSpec,
|
||||||
operationHandlers: path.join(__dirname),
|
operationHandlers: path.join(__dirname, '..'),
|
||||||
validateRequests: true,
|
validateRequests: true,
|
||||||
validateApiSpec: true,
|
validateApiSpec: true,
|
||||||
|
formats: specFormats(),
|
||||||
validateSecurity: {
|
validateSecurity: {
|
||||||
handlers: {
|
handlers: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
ApiKeyAuth: authenticationHandler,
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// add error handler
|
// error handler
|
||||||
// @ts-ignore
|
publicApiControllerV1.use(
|
||||||
apiController.use(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
(error: HttpError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
return res.status(error.status || 400).json({
|
return res.status(error.status || 400).json({
|
||||||
message: error.message,
|
message: error.message,
|
||||||
// errors: error.errors,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return apiController;
|
|
||||||
};
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ paths:
|
||||||
/users:
|
/users:
|
||||||
get:
|
get:
|
||||||
x-eov-operation-id: getUsers
|
x-eov-operation-id: getUsers
|
||||||
x-eov-operation-handler: routes/Users
|
x-eov-operation-handler: v1/handlers/Users
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Retrieve all users
|
summary: Retrieve all users
|
||||||
|
@ -73,7 +73,7 @@ paths:
|
||||||
$ref: '#/components/schemas/Error'
|
$ref: '#/components/schemas/Error'
|
||||||
post:
|
post:
|
||||||
x-eov-operation-id: createUsers
|
x-eov-operation-id: createUsers
|
||||||
x-eov-operation-handler: routes/Users
|
x-eov-operation-handler: v1/handlers/Users
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Invite a user
|
summary: Invite a user
|
||||||
|
@ -108,7 +108,7 @@ paths:
|
||||||
/users/{identifier}:
|
/users/{identifier}:
|
||||||
get:
|
get:
|
||||||
x-eov-operation-id: getUser
|
x-eov-operation-id: getUser
|
||||||
x-eov-operation-handler: routes/Users
|
x-eov-operation-handler: v1/handlers/Users
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Get user by ID/Email
|
summary: Get user by ID/Email
|
||||||
|
@ -123,6 +123,7 @@ paths:
|
||||||
explode: false
|
explode: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
format: identifier
|
||||||
- name: includeRole
|
- name: includeRole
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
|
@ -146,7 +147,7 @@ paths:
|
||||||
$ref: '#/components/schemas/Error'
|
$ref: '#/components/schemas/Error'
|
||||||
delete:
|
delete:
|
||||||
x-eov-operation-id: deleteUser
|
x-eov-operation-id: deleteUser
|
||||||
x-eov-operation-handler: routes/Users
|
x-eov-operation-handler: v1/handlers/Users
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Delete user by ID/Email
|
summary: Delete user by ID/Email
|
||||||
|
@ -161,6 +162,7 @@ paths:
|
||||||
explode: false
|
explode: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
format: identifier
|
||||||
- name: transferId
|
- name: transferId
|
||||||
in: query
|
in: query
|
||||||
description: ID of the user to transfer workflows and credentials to.
|
description: ID of the user to transfer workflows and credentials to.
|
||||||
|
@ -175,7 +177,7 @@ paths:
|
||||||
style: form
|
style: form
|
||||||
explode: true
|
explode: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: boolean
|
||||||
example: true
|
example: true
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
@ -235,6 +237,7 @@ components:
|
||||||
example: 123e4567-e89b-12d3-a456-426614174000
|
example: 123e4567-e89b-12d3-a456-426614174000
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
format: email
|
||||||
example: jhon.doe@company.com
|
example: jhon.doe@company.com
|
||||||
firstName:
|
firstName:
|
||||||
maxLength: 32
|
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';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { User } from './databases/entities/User';
|
import { User } from './databases/entities/User';
|
||||||
|
import { Role } from './databases/entities/Role';
|
||||||
import type { IExecutionDeleteFilter, IWorkflowDb } from '.';
|
import type { IExecutionDeleteFilter, IWorkflowDb } from '.';
|
||||||
import type { PublicUser } from './UserManagement/Interfaces';
|
import type { PublicUser } from './UserManagement/Interfaces';
|
||||||
|
import * as UserManagementMailer from './UserManagement/email/UserManagementMailer';
|
||||||
|
|
||||||
export type AuthlessRequest<
|
export type AuthlessRequest<
|
||||||
RouteParams = {},
|
RouteParams = {},
|
||||||
|
@ -28,6 +30,8 @@ export type AuthenticatedRequest<
|
||||||
RequestQuery = {},
|
RequestQuery = {},
|
||||||
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
|
||||||
user: User;
|
user: User;
|
||||||
|
mailer?: UserManagementMailer.UserManagementMailer;
|
||||||
|
globalMemberRole?: Role;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -209,7 +213,7 @@ export declare namespace UserRequest {
|
||||||
{ id: string; email: string; identifier: string },
|
{ 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 }>;
|
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Role } from '../../../src/databases/entities/Role';
|
||||||
import {
|
import {
|
||||||
randomApiKey,
|
randomApiKey,
|
||||||
randomEmail,
|
randomEmail,
|
||||||
randomInvalidPassword,
|
|
||||||
randomName,
|
randomName,
|
||||||
randomValidPassword,
|
randomValidPassword,
|
||||||
} from '../shared/random';
|
} from '../shared/random';
|
||||||
|
@ -18,8 +17,6 @@ import {
|
||||||
import * as utils from '../shared/utils';
|
import * as utils from '../shared/utils';
|
||||||
import * as testDb from '../shared/testDb';
|
import * as testDb from '../shared/testDb';
|
||||||
|
|
||||||
// import * from './../../../src/PublicApi/helpers'
|
|
||||||
|
|
||||||
let app: express.Application;
|
let app: express.Application;
|
||||||
let testDbName = '';
|
let testDbName = '';
|
||||||
let globalOwnerRole: Role;
|
let globalOwnerRole: Role;
|
||||||
|
@ -55,14 +52,6 @@ beforeEach(async () => {
|
||||||
await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName);
|
await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName);
|
||||||
await testDb.truncate(['User', 'Workflow', 'Credentials'], 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({
|
await testDb.createUser({
|
||||||
id: INITIAL_TEST_USER.id,
|
id: INITIAL_TEST_USER.id,
|
||||||
email: INITIAL_TEST_USER.email,
|
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 () => {
|
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();
|
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 });
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: true, user: owner });
|
||||||
|
|
||||||
// console.log(authOwnerAgent);
|
|
||||||
|
|
||||||
const response = await authOwnerAgent.get('/v1/users');
|
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);
|
expect(response.statusCode).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue