From 509254782f34918400ac35fc32d20ce1747876f5 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 27 Mar 2022 21:41:44 -0400 Subject: [PATCH] :zap: Add POST /users endpoint --- packages/cli/src/PublicApi/helpers.ts | 7 + packages/cli/src/PublicApi/v1/index.ts | 11 +- packages/cli/src/PublicApi/v1/openapi.yml | 2 + .../src/PublicApi/v1/routes/Users/index.ts | 143 +++++++++++++++++- 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/PublicApi/helpers.ts b/packages/cli/src/PublicApi/helpers.ts index e4b07cd3bc..f159f19068 100644 --- a/packages/cli/src/PublicApi/helpers.ts +++ b/packages/cli/src/PublicApi/helpers.ts @@ -1,4 +1,7 @@ import * as querystring from 'querystring'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { pick } from 'lodash'; +import { User } from '../databases/entities/User'; interface IPaginationOffsetDecoded { offset: number; @@ -45,3 +48,7 @@ export const getSelectableProperties = (table: 'user' | 'role'): string[] => { export const connectionName = (): string => { return 'default'; }; + +export const clean = (users: User[]): Array> => { + return users.map((user) => pick(user, getSelectableProperties('user'))); +}; diff --git a/packages/cli/src/PublicApi/v1/index.ts b/packages/cli/src/PublicApi/v1/index.ts index d7d338ff61..5b11d9a5c1 100644 --- a/packages/cli/src/PublicApi/v1/index.ts +++ b/packages/cli/src/PublicApi/v1/index.ts @@ -7,6 +7,7 @@ import path = require('path'); import express = require('express'); import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types'; +import * as bodyParser from 'body-parser'; // eslint-disable-next-line import/no-cycle import { Db } from '../..'; import config = require('../../../config'); @@ -17,6 +18,8 @@ export interface N8nApp { export const publicApiController = express.Router(); +publicApiController.use(bodyParser.json()); + publicApiController.use( `/v1`, OpenApiValidator.middleware({ @@ -44,7 +47,13 @@ publicApiController.use( if (!config.get('userManagement.isInstanceOwnerSetUp')) { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { - message: 'asasasas', + status: 400, + }; + } + + if (config.get('userManagement.disabled')) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw { status: 400, }; } diff --git a/packages/cli/src/PublicApi/v1/openapi.yml b/packages/cli/src/PublicApi/v1/openapi.yml index 58971bd2a9..ceb7cf99fe 100644 --- a/packages/cli/src/PublicApi/v1/openapi.yml +++ b/packages/cli/src/PublicApi/v1/openapi.yml @@ -241,11 +241,13 @@ components: 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 finishedSetup: type: boolean diff --git a/packages/cli/src/PublicApi/v1/routes/Users/index.ts b/packages/cli/src/PublicApi/v1/routes/Users/index.ts index 8e306d8b19..2b8fbe22aa 100644 --- a/packages/cli/src/PublicApi/v1/routes/Users/index.ts +++ b/packages/cli/src/PublicApi/v1/routes/Users/index.ts @@ -1,20 +1,159 @@ import express = require('express'); -import { getConnection } from 'typeorm'; +import { getConnection, In } from 'typeorm'; import { validate as uuidValidate } from 'uuid'; +import validator from 'validator'; import { UserRequest } from '../../../../requests'; import { User } from '../../../../databases/entities/User'; + import { + clean, connectionName, decodeCursor, getNextCursor, getSelectableProperties, } from '../../../helpers'; +import config = require('../../../../../config'); + +import * as UserManagementMailer from '../../../../UserManagement/email/UserManagementMailer'; + +import { Db, ResponseHelper, InternalHooksManager } from '../../../..'; +import { Role } from '../../../../databases/entities/Role'; +import { getInstanceBaseUrl } from '../../../../UserManagement/UserManagementHelper'; + export = { createUsers: async (req: UserRequest.Invite, res: express.Response): Promise => { - res.json({ success: true }); + if (config.get('userManagement.emails.mode') === '') { + throw new ResponseHelper.ResponseError( + 'Email sending must be set up in order to request a password reset email', + undefined, + 500, + ); + } + + let mailer: UserManagementMailer.UserManagementMailer | undefined; + try { + mailer = await UserManagementMailer.getInstance(); + } catch (error) { + if (error instanceof Error) { + throw new ResponseHelper.ResponseError( + `There is a problem with your SMTP setup! ${error.message}`, + undefined, + 500, + ); + } + } + + const createUsers: { [key: string]: string | null } = {}; + // Validate payload + req.body.forEach((invite) => { + if (typeof invite !== 'object' || !invite.email) { + throw new ResponseHelper.ResponseError( + 'Request to send email invite(s) to user(s) failed because the payload is not an array shaped Array<{ email: string }>', + undefined, + 400, + ); + } + + if (!validator.isEmail(invite.email)) { + throw new ResponseHelper.ResponseError( + `Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`, + undefined, + 400, + ); + } + createUsers[invite.email] = null; + }); + + const role = (await Db.collections.Role?.findOne({ scope: 'global', name: 'member' })) as Role; + + if (!role) { + throw new ResponseHelper.ResponseError( + 'Members role not found in database - inconsistent state', + undefined, + 500, + ); + } + + // remove/exclude existing users from creation + const existingUsers = await Db.collections.User?.find({ + where: { email: In(Object.keys(createUsers)) }, + }); + + existingUsers?.forEach((user) => { + if (user.password) { + delete createUsers[user.email]; + return; + } + createUsers[user.email] = user.id; + }); + + const usersToSetUp = Object.keys(createUsers).filter((email) => createUsers[email] === null); + + let savedUsers = []; + try { + savedUsers = await Db.transaction(async (transactionManager) => { + return Promise.all( + usersToSetUp.map(async (email) => { + const newUser = Object.assign(new User(), { + email, + globalRole: role, + }); + const savedUser = await transactionManager.save(newUser); + createUsers[savedUser.email] = savedUser.id; + return savedUser; + }), + ); + }); + + void InternalHooksManager.getInstance().onUserInvite({ + user_id: req.user.id, + target_user_id: Object.values(createUsers) as string[], + }); + } catch (error) { + throw new ResponseHelper.ResponseError('An error occurred during user creation'); + } + + const baseUrl = getInstanceBaseUrl(); + + const usersPendingSetup = Object.entries(createUsers).filter(([email, id]) => id && email); + + // send invite email to new or not yet setup users + + await Promise.all( + usersPendingSetup.map(async ([email, id]) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const inviteAcceptUrl = `${baseUrl}/signup?inviterId=${req.user.id}&inviteeId=${id}`; + const result = await mailer?.invite({ + email, + inviteAcceptUrl, + domain: baseUrl, + }); + const resp: { user: { id: string | null; email: string }; error?: string } = { + user: { + id, + email, + }, + }; + if (result?.success) { + void InternalHooksManager.getInstance().onUserTransactionalEmail({ + user_id: id!, + message_type: 'New user invite', + }); + } else { + void InternalHooksManager.getInstance().onEmailFailed({ + user_id: req.user.id, + message_type: 'New user invite', + }); + resp.error = `Email could not be sent`; + } + return resp; + }), + ); + + res.json([...clean(existingUsers ?? []), ...clean(savedUsers)]); }, deleteUser: async (req: UserRequest.Delete, res: express.Response): Promise => { res.json({ success: true });