mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge pull request #3055 from n8n-io/n8n-3172-add-post-users-endpoint
⚡ Add POST /users endpoint
This commit is contained in:
commit
1eb5c4dacf
|
@ -1,4 +1,7 @@
|
||||||
import * as querystring from 'querystring';
|
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 {
|
interface IPaginationOffsetDecoded {
|
||||||
offset: number;
|
offset: number;
|
||||||
|
@ -45,3 +48,7 @@ export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
|
||||||
export const connectionName = (): string => {
|
export const connectionName = (): string => {
|
||||||
return 'default';
|
return 'default';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clean = (users: User[]): Array<Partial<User>> => {
|
||||||
|
return users.map((user) => pick(user, getSelectableProperties('user')));
|
||||||
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import path = require('path');
|
||||||
import express = require('express');
|
import express = require('express');
|
||||||
|
|
||||||
import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
|
import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
|
||||||
|
import * as bodyParser from 'body-parser';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { Db } from '../..';
|
import { Db } from '../..';
|
||||||
import config = require('../../../config');
|
import config = require('../../../config');
|
||||||
|
@ -17,6 +18,8 @@ export interface N8nApp {
|
||||||
|
|
||||||
export const publicApiController = express.Router();
|
export const publicApiController = express.Router();
|
||||||
|
|
||||||
|
publicApiController.use(bodyParser.json());
|
||||||
|
|
||||||
publicApiController.use(
|
publicApiController.use(
|
||||||
`/v1`,
|
`/v1`,
|
||||||
OpenApiValidator.middleware({
|
OpenApiValidator.middleware({
|
||||||
|
@ -44,7 +47,13 @@ publicApiController.use(
|
||||||
if (!config.get('userManagement.isInstanceOwnerSetUp')) {
|
if (!config.get('userManagement.isInstanceOwnerSetUp')) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
throw {
|
throw {
|
||||||
message: 'asasasas',
|
status: 400,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.get('userManagement.disabled')) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
|
throw {
|
||||||
status: 400,
|
status: 400,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,11 +241,13 @@ components:
|
||||||
maxLength: 32
|
maxLength: 32
|
||||||
type: string
|
type: string
|
||||||
description: User's first name
|
description: User's first name
|
||||||
|
readOnly: true
|
||||||
example: jhon
|
example: jhon
|
||||||
lastName:
|
lastName:
|
||||||
maxLength: 32
|
maxLength: 32
|
||||||
type: string
|
type: string
|
||||||
description: User's last name
|
description: User's last name
|
||||||
|
readOnly: true
|
||||||
example: doe
|
example: doe
|
||||||
finishedSetup:
|
finishedSetup:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
|
@ -1,20 +1,159 @@
|
||||||
import express = require('express');
|
import express = require('express');
|
||||||
import { getConnection } from 'typeorm';
|
import { getConnection, In } from 'typeorm';
|
||||||
|
|
||||||
import { validate as uuidValidate } from 'uuid';
|
import { validate as uuidValidate } from 'uuid';
|
||||||
|
import validator from 'validator';
|
||||||
import { UserRequest } from '../../../../requests';
|
import { UserRequest } from '../../../../requests';
|
||||||
|
|
||||||
import { User } from '../../../../databases/entities/User';
|
import { User } from '../../../../databases/entities/User';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
clean,
|
||||||
connectionName,
|
connectionName,
|
||||||
decodeCursor,
|
decodeCursor,
|
||||||
getNextCursor,
|
getNextCursor,
|
||||||
getSelectableProperties,
|
getSelectableProperties,
|
||||||
} from '../../../helpers';
|
} 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 = {
|
export = {
|
||||||
createUsers: async (req: UserRequest.Invite, res: express.Response): Promise<void> => {
|
createUsers: async (req: UserRequest.Invite, res: express.Response): Promise<void> => {
|
||||||
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<User>(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<void> => {
|
deleteUser: async (req: UserRequest.Delete, res: express.Response): Promise<void> => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|
Loading…
Reference in a new issue