General improvements

 asasas
This commit is contained in:
ricardo 2022-04-11 21:15:19 -04:00
parent e80c04e28b
commit b47056ca4e
9 changed files with 197 additions and 269 deletions

View file

@ -30,7 +30,7 @@
"start:default": "cd bin && ./n8n", "start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n", "start:windows": "cd bin && n8n",
"test": "npm run test:sqlite", "test": "npm run test:sqlite",
"test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest", "test:sqlite": "export N8N_LOG_LEVEL='debug'; export DB_TYPE=sqlite; jest --detectOpenHandles",
"test:postgres": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=postgresdb && jest", "test:postgres": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=postgresdb && jest",
"test:mysql": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=mysqldb && jest", "test:mysql": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=mysqldb && jest",
"watch": "tsc --watch", "watch": "tsc --watch",
@ -92,7 +92,6 @@
"typescript": "~4.6.0" "typescript": "~4.6.0"
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@rudderstack/rudder-sdk-node": "1.0.6", "@rudderstack/rudder-sdk-node": "1.0.6",
@ -138,7 +137,6 @@
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"passport": "^0.5.0", "passport": "^0.5.0",
"passport-cookie": "^1.0.9", "passport-cookie": "^1.0.9",
"passport-http-header-strategy": "^1.1.0",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"pg": "^8.3.0", "pg": "^8.3.0",
"prom-client": "^13.1.0", "prom-client": "^13.1.0",

View file

@ -5,8 +5,6 @@
import * as querystring from 'querystring'; 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 express = require('express');
import SwaggerParser from '@apidevtools/swagger-parser';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { validate as uuidValidate } from 'uuid'; import { validate as uuidValidate } from 'uuid';
import { User } from '../databases/entities/User'; import { User } from '../databases/entities/User';
@ -22,13 +20,6 @@ interface IPaginationOffsetDecoded {
offset: number; offset: number;
limit: number; limit: number;
} }
export interface IMiddlewares {
[key: string]: [IMiddleware];
}
interface IMiddleware {
(req: express.Request, res: express.Response, next: express.NextFunction): void;
}
export type OperationID = 'getUsers' | 'getUser'; export type OperationID = 'getUsers' | 'getUser';
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => { export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
@ -40,7 +31,7 @@ export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
}; };
}; };
export const getNextCursor = ( export const encodeNextCursor = (
offset: number, offset: number,
limit: number, limit: number,
numberOfRecords: number, numberOfRecords: number,
@ -49,12 +40,10 @@ export const getNextCursor = (
if (retrieveRecordsLength < numberOfRecords) { if (retrieveRecordsLength < numberOfRecords) {
return Buffer.from( return Buffer.from(
JSON.stringify( JSON.stringify({
querystring.encode({
limit, limit,
offset: offset + limit, offset: offset + limit,
}), }),
),
).toString('base64'); ).toString('base64');
} }
@ -68,59 +57,6 @@ export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
}[table]; }[table];
}; };
export const connectionName = (): string => {
return 'default';
};
const middlewareDefined = (operationId: OperationID, middlewares: IMiddlewares) =>
operationId && middlewares[operationId];
export const addMiddlewares = (
router: express.Router,
method: string,
routePath: string,
operationId: OperationID,
middlewares: IMiddlewares,
): void => {
if (middlewareDefined(operationId, middlewares)) {
const expressPath = routePath.replace(/\{([^}]+)}/g, ':$1');
switch (method) {
case 'get':
router.get(expressPath, ...middlewares[operationId]);
break;
case 'post':
router.post(expressPath, ...middlewares[operationId]);
break;
case 'put':
router.put(expressPath, ...middlewares[operationId]);
break;
case 'delete':
router.delete(expressPath, ...middlewares[operationId]);
break;
default:
break;
}
}
};
export const addCustomMiddlewares = async (
apiController: express.Router,
openApiSpec: string,
middlewares: IMiddlewares,
): Promise<void> => {
const { paths = {} } = await SwaggerParser.parse(openApiSpec);
Object.entries(paths).forEach(([routePath, methods]) => {
Object.entries(methods).forEach(([method, data]) => {
const operationId: OperationID = (
data as {
'x-eov-operation-id': OperationID;
}
)['x-eov-operation-id'];
addMiddlewares(apiController, method, routePath, operationId, middlewares);
});
});
};
export async function getGlobalMemberRole(): Promise<Role | undefined> { export async function getGlobalMemberRole(): Promise<Role | undefined> {
return Db.collections.Role?.findOneOrFail({ return Db.collections.Role?.findOneOrFail({
name: 'member', name: 'member',
@ -227,7 +163,7 @@ export async function inviteUsers(
export async function getUser(data: { export async function getUser(data: {
withIdentifier: string; withIdentifier: string;
includeRole: boolean; includeRole?: boolean;
}): Promise<User | undefined> { }): Promise<User | undefined> {
return Db.collections.User?.findOne({ return Db.collections.User?.findOne({
where: { where: {

View file

@ -0,0 +1,4 @@
// eslint-disable-next-line import/no-cycle
import { publicApiController as publicApiControllerV1 } from './v1';
export const publicApi = [publicApiControllerV1()];

View file

@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import express = require('express'); import express = require('express');
import validator from 'validator';
import config = require('../../config'); import config = require('../../config');
import type { UserRequest } from '../requests'; import type { UserRequest } from '../requests';
import { decodeCursor } from './helpers'; import { decodeCursor } from './helpers';
@ -15,10 +17,10 @@ const instanceOwnerSetup = (
res: express.Response, res: express.Response,
next: express.NextFunction, next: express.NextFunction,
): any => { ): any => {
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) { if (!config.getEnv('userManagement.isInstanceOwnerSetUp')) {
return next(); return res.status(404).json({ message: 'asasas' });
} }
return res.status(400).json({ message: 'asasas' }); next();
}; };
const emailSetup = ( const emailSetup = (
@ -26,10 +28,10 @@ const emailSetup = (
res: express.Response, res: express.Response,
next: express.NextFunction, next: express.NextFunction,
): any => { ): any => {
if (config.getEnv('userManagement.emails.mode')) { if (!config.getEnv('userManagement.emails.mode')) {
return next(); return res.status(500).json({ message: 'asasas' });
} }
return res.status(400).json({ message: 'asasas' }); next();
}; };
const authorize = const authorize =
@ -41,26 +43,27 @@ const authorize =
if (role.includes(userRole)) { if (role.includes(userRole)) {
return next(); return next();
} }
return res.status(400).json({ return res.status(403).json({
message: 'asasas', message: 'asasas',
}); });
}; };
const validEmail = ( // move this to open api validator
req: UserRequest.Invite, // const validEmail = (
res: express.Response, // req: UserRequest.Invite,
next: express.NextFunction, // res: express.Response,
): any => { // next: express.NextFunction,
// eslint-disable-next-line no-restricted-syntax // ): any => {
for (const { email } of req.body) { // // eslint-disable-next-line no-restricted-syntax
if (!validator.isEmail(email)) { // for (const { email } of req.body) {
return res.status(400).json({ // if (!validator.isEmail(email)) {
message: `Request to send email invite(s) to user(s) failed because of an invalid email address: ${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(); // }
}; // next();
// };
const deletingOwnUser = ( const deletingOwnUser = (
req: UserRequest.Delete, req: UserRequest.Delete,
@ -95,9 +98,6 @@ const validCursor = (
): any => { ): any => {
let offset = 0; let offset = 0;
let limit = 10; let limit = 10;
if (req.query?.limit) {
limit = parseInt(req.query?.limit, 10) || 10;
}
if (req.query.cursor) { if (req.query.cursor) {
const { cursor } = req.query; const { cursor } = req.query;
try { try {
@ -108,31 +108,21 @@ const validCursor = (
}); });
} }
} }
req.limit = limit; // @ts-ignore
req.offset = offset; req.query.offset = offset;
next(); // @ts-ignore
}; req.query.limit = limit;
const parseIncludeRole = (
req: UserRequest.Get,
res: express.Response,
next: express.NextFunction,
): any => {
req.includeRole = false;
if (req.query?.includeRole) {
req.includeRole = req.query.includeRole === 'true';
}
next(); next();
}; };
export const middlewares = { export const middlewares = {
createUsers: [instanceOwnerSetup, emailSetup, validEmail, authorize(['owner'])], createUsers: [instanceOwnerSetup, emailSetup, authorize(['owner'])],
deleteUsers: [ deleteUsers: [
instanceOwnerSetup, instanceOwnerSetup,
deletingOwnUser, deletingOwnUser,
transferingToDeletedUser, transferingToDeletedUser,
authorize(['owner']), authorize(['owner']),
], ],
getUsers: [instanceOwnerSetup, parseIncludeRole, validCursor, authorize(['owner'])], getUsers: [instanceOwnerSetup, validCursor, authorize(['owner'])],
getUser: [instanceOwnerSetup, parseIncludeRole, authorize(['owner'])], getUser: [instanceOwnerSetup, authorize(['owner'])],
}; };

View file

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import * as OpenApiValidator from 'express-openapi-validator'; import * as OpenApiValidator from 'express-openapi-validator';
@ -5,55 +8,48 @@ import path = require('path');
import express = require('express'); import express = require('express');
import { HttpError } from 'express-openapi-validator/dist/framework/types'; import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
import passport = require('passport');
import { Strategy } from 'passport-http-header-strategy';
import { VerifiedCallback } from 'passport-jwt';
import { Db } from '../..';
import { middlewares } from '../middlewares';
import { addCustomMiddlewares, IMiddlewares } from '../helpers';
export const publicApiController = (async (): Promise<express.Router> => { import { Db } from '../..';
export const publicApiController = (): express.Router => {
const openApiSpec = path.join(__dirname, 'openapi.yml'); const openApiSpec = path.join(__dirname, 'openapi.yml');
const apiController = express.Router(); const apiController = express.Router();
apiController.use('/spec', express.static(openApiSpec)); apiController.use('/v1/spec', express.static(openApiSpec));
apiController.use(express.json()); apiController.use('/v1', express.json());
passport.use(
new Strategy(
{ header: 'X-N8N-API-KEY', passReqToCallback: false },
async (token: string, done: VerifiedCallback) => {
const user = await Db.collections.User?.findOne({
where: {
apiKey: token,
},
relations: ['globalRole'],
});
if (!user) {
return done(null, false);
}
return done(null, user);
},
),
);
// add authentication middlewlares
apiController.use('/', passport.authenticate('header', { session: false }));
await addCustomMiddlewares(apiController, openApiSpec, middlewares as unknown as IMiddlewares);
apiController.use( apiController.use(
'/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,
validateSecurity: false, 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;
},
},
},
}), }),
); );
@ -62,12 +58,12 @@ export const publicApiController = (async (): Promise<express.Router> => {
apiController.use( apiController.use(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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 || 500).json({ return res.status(error.status || 400).json({
message: error.message, message: error.message,
errors: error.errors, // errors: error.errors,
}); });
}, },
); );
return apiController; return apiController;
})(); };

View file

@ -16,7 +16,7 @@ externalDocs:
servers: servers:
- url: /api/v1 - url: /api/v1
tags: tags:
- name: user - name: users
description: Operations about user description: Operations about user
externalDocs: externalDocs:
description: Find out more about our store description: Find out more about our store
@ -31,15 +31,6 @@ paths:
summary: Retrieve all users summary: Retrieve all users
description: Retrieve all users from your instance. Only available for the instance owner. description: Retrieve all users from your instance. Only available for the instance owner.
parameters: 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 - name: limit
in: query in: query
description: The maximum number of items to return description: The maximum number of items to return
@ -65,7 +56,7 @@ paths:
style: form style: form
explode: true explode: true
schema: schema:
type: string type: boolean
example: true example: true
responses: responses:
"200": "200":
@ -84,7 +75,7 @@ paths:
x-eov-operation-id: createUsers x-eov-operation-id: createUsers
x-eov-operation-handler: routes/Users x-eov-operation-handler: routes/Users
tags: tags:
- user - users
summary: Invite a user summary: Invite a user
description: Invites a user to your instance. Only available for the instance owner. description: Invites a user to your instance. Only available for the instance owner.
operationId: createUser operationId: createUser
@ -138,7 +129,7 @@ paths:
style: form style: form
explode: true explode: true
schema: schema:
type: string type: boolean
example: true example: true
responses: responses:
"200": "200":
@ -257,7 +248,7 @@ components:
description: User's last name description: User's last name
readOnly: true readOnly: true
example: doe example: doe
finishedSetup: pending:
type: boolean type: boolean
description: Whether the user finished setting up the invitation or not description: Whether the user finished setting up the invitation or not
readOnly: true readOnly: true

View file

@ -1,8 +1,10 @@
/* 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 */ /* eslint-disable @typescript-eslint/no-unused-vars */
import express = require('express'); import express = require('express');
import { getConnection, In } from 'typeorm';
import { validate as uuidValidate } from 'uuid';
import { UserRequest } from '../../../../requests'; import { UserRequest } from '../../../../requests';
import { User } from '../../../../databases/entities/User'; import { User } from '../../../../databases/entities/User';
@ -10,11 +12,10 @@ import { Role } from '../../../../databases/entities/Role';
import { import {
clean, clean,
decodeCursor,
deleteDataAndSendTelemetry, deleteDataAndSendTelemetry,
getAllUsersAndCount, getAllUsersAndCount,
getGlobalMemberRole, getGlobalMemberRole,
getNextCursor, encodeNextCursor,
getUser, getUser,
getUsers, getUsers,
getUsersToSaveAndInvite, getUsersToSaveAndInvite,
@ -25,10 +26,14 @@ import {
import * as UserManagementMailer from '../../../../UserManagement/email/UserManagementMailer'; import * as UserManagementMailer from '../../../../UserManagement/email/UserManagementMailer';
import { Db, ResponseHelper } from '../../../..'; import { ResponseHelper } from '../../../..';
import { middlewares } from '../../../middlewares';
export = { export = {
createUsers: ResponseHelper.send(async (req: UserRequest.Invite, res: express.Response) => { createUsers: [
...middlewares.createUsers,
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);
@ -73,14 +78,18 @@ export = {
return clean(userstoInvite); return clean(userstoInvite);
}), }),
// eslint-disable-next-line consistent-return ],
deleteUser: async (req: UserRequest.Delete, res: express.Response): Promise<any> => { deleteUser: [
...middlewares.deleteUsers,
async (req: UserRequest.Delete, res: express.Response): Promise<any> => {
const { identifier: idToDelete } = req.params; const { identifier: idToDelete } = req.params;
const { transferId } = req.query; const { transferId, includeRole } = req.query;
const apiKeyUserOwner = req.user; const apiKeyUserOwner = req.user;
const includeRole = req.query?.includeRole?.toLowerCase() === 'true' || false;
const users = await getUsers({ withIdentifiers: [idToDelete, transferId ?? ''], includeRole }); const users = await getUsers({
withIdentifiers: [idToDelete, transferId ?? ''],
includeRole,
});
if (!users?.length || (transferId && users.length !== 2)) { if (!users?.length || (transferId && users.length !== 2)) {
throw new ResponseHelper.ResponseError( throw new ResponseHelper.ResponseError(
@ -111,16 +120,19 @@ export = {
return clean(userToDelete); return clean(userToDelete);
}, },
// eslint-disable-next-line consistent-return ],
getUser: ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => { getUser: [
const { includeRole } = req; ...middlewares.getUser,
// @ts-ignore
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
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( throw new ResponseHelper.ResponseError(
`Could not find user with identifier: ${identifier}`, `Could not find user with identifier: ${identifier as string}`,
undefined, undefined,
404, 404,
); );
@ -128,9 +140,12 @@ export = {
return clean(user, { includeRole }); return clean(user, { includeRole });
}, true), }, true),
// eslint-disable-next-line consistent-return ],
getUsers: ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => { getUsers: [
const { offset, limit, includeRole } = req; ...middlewares.getUsers,
// @ts-ignore
ResponseHelper.send(async (req: UserRequest.Get, res: express.Response) => {
const { offset, limit, includeRole = false } = req.query;
const [users, count] = await getAllUsersAndCount({ const [users, count] = await getAllUsersAndCount({
includeRole, includeRole,
@ -140,7 +155,8 @@ export = {
return { return {
users: clean(users, { includeRole }), users: clean(users, { includeRole }),
nextCursor: getNextCursor(offset, limit, count), nextCursor: encodeNextCursor(offset, limit, count),
}; };
}, true), }, true),
],
}; };

View file

@ -169,7 +169,7 @@ import { SharedWorkflow } from './databases/entities/SharedWorkflow';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './api/credentials.api'; import { credentialsController } from './api/credentials.api';
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper'; import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
import { publicApiController as publicApiControllerV1 } from './PublicApi/v1'; import { publicApi } from './PublicApi';
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
@ -580,7 +580,7 @@ class App {
return ResponseHelper.sendSuccessResponse(res, {}, true, 204); return ResponseHelper.sendSuccessResponse(res, {}, true, 204);
}); });
this.app.use(`/${this.publicApiEndpoint}/v1`, await publicApiControllerV1); this.app.use(`/${this.publicApiEndpoint}`, ...publicApi);
// Parse cookies for easier access // Parse cookies for easier access
this.app.use(cookieParser()); this.app.use(cookieParser());

View file

@ -28,9 +28,6 @@ export type AuthenticatedRequest<
RequestQuery = {}, RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & { > = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
user: User; user: User;
limit: number;
offset: number;
includeRole: boolean;
}; };
// ---------------------------------- // ----------------------------------
@ -205,14 +202,14 @@ export declare namespace UserRequest {
{ id: string; email: string; identifier: string }, { id: string; email: string; identifier: string },
{}, {},
{}, {},
{ transferId?: string; includeRole: string } { transferId?: string; includeRole: boolean }
>; >;
export type Get = AuthenticatedRequest< export type Get = AuthenticatedRequest<
{ id: string; email: string; identifier: string }, { id: string; email: string; identifier: string },
{}, {},
{}, {},
{ limit?: string; offset: string; cursor?: string; includeRole?: string } { limit: number; offset: number; cursor: string; includeRole: boolean }
>; >;
export type Reinvite = AuthenticatedRequest<{ id: string }>; export type Reinvite = AuthenticatedRequest<{ id: string }>;