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