Improvements

This commit is contained in:
ricardo 2022-04-13 21:15:11 -04:00
parent a083914649
commit 44ec5c6cfe
11 changed files with 491 additions and 174 deletions

View file

@ -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),
},
];
}

View file

@ -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];

View file

@ -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,

View file

@ -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), },
], ],
}; };

View file

@ -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 publicApiControllerV1 = express.Router();
export const publicApiController = (): express.Router => {
const openApiSpec = path.join(__dirname, 'openapi.yml'); const openApiSpec = path.join(__dirname, 'openapi.yml');
const apiController = express.Router(); publicApiControllerV1.use('/v1/spec', express.static(openApiSpec));
apiController.use('/v1/spec', express.static(openApiSpec)); publicApiControllerV1.use('/v1', express.json());
apiController.use('/v1', express.json()); publicApiControllerV1.use(
apiController.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;
};

View file

@ -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

View 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),
],
};

View 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,
});
},
);

View 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: []

View file

@ -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 }>;

View file

@ -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);
}); });