mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
3a9c65e1cb
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
186 lines
6.2 KiB
TypeScript
186 lines
6.2 KiB
TypeScript
import { Response } from 'express';
|
|
import validator from 'validator';
|
|
|
|
import { AuthService } from '@/auth/auth.service';
|
|
import config from '@/config';
|
|
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
|
import type { User } from '@/databases/entities/user';
|
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
|
import { Post, GlobalScope, RestController } from '@/decorators';
|
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
|
import { EventService } from '@/events/event.service';
|
|
import { ExternalHooks } from '@/external-hooks';
|
|
import { License } from '@/license';
|
|
import { Logger } from '@/logging/logger.service';
|
|
import { PostHogClient } from '@/posthog';
|
|
import { UserRequest } from '@/requests';
|
|
import { PasswordUtility } from '@/services/password.utility';
|
|
import { UserService } from '@/services/user.service';
|
|
import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers';
|
|
|
|
@RestController('/invitations')
|
|
export class InvitationController {
|
|
constructor(
|
|
private readonly logger: Logger,
|
|
private readonly externalHooks: ExternalHooks,
|
|
private readonly authService: AuthService,
|
|
private readonly userService: UserService,
|
|
private readonly license: License,
|
|
private readonly passwordUtility: PasswordUtility,
|
|
private readonly userRepository: UserRepository,
|
|
private readonly postHog: PostHogClient,
|
|
private readonly eventService: EventService,
|
|
) {}
|
|
|
|
/**
|
|
* Send email invite(s) to one or multiple users and create user shell(s).
|
|
*/
|
|
|
|
@Post('/', { rateLimit: { limit: 10 } })
|
|
@GlobalScope('user:create')
|
|
async inviteUser(req: UserRequest.Invite) {
|
|
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
|
|
|
if (isSamlLicensedAndEnabled()) {
|
|
this.logger.debug(
|
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
|
);
|
|
throw new BadRequestError(
|
|
'SAML is enabled, so users are managed by the Identity Provider and cannot be added through invites',
|
|
);
|
|
}
|
|
|
|
if (!isWithinUsersLimit) {
|
|
this.logger.debug(
|
|
'Request to send email invite(s) to user(s) failed because the user limit quota has been reached',
|
|
);
|
|
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
|
}
|
|
|
|
if (!config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
|
this.logger.debug(
|
|
'Request to send email invite(s) to user(s) failed because the owner account is not set up',
|
|
);
|
|
throw new BadRequestError('You must set up your own account before inviting others');
|
|
}
|
|
|
|
if (!Array.isArray(req.body)) {
|
|
this.logger.debug(
|
|
'Request to send email invite(s) to user(s) failed because the payload is not an array',
|
|
{
|
|
payload: req.body,
|
|
},
|
|
);
|
|
throw new BadRequestError('Invalid payload');
|
|
}
|
|
|
|
if (!req.body.length) return [];
|
|
|
|
req.body.forEach((invite) => {
|
|
if (typeof invite !== 'object' || !invite.email) {
|
|
throw new BadRequestError(
|
|
'Request to send email invite(s) to user(s) failed because the payload is not an array shaped Array<{ email: string }>',
|
|
);
|
|
}
|
|
|
|
if (!validator.isEmail(invite.email)) {
|
|
this.logger.debug('Invalid email in payload', { invalidEmail: invite.email });
|
|
throw new BadRequestError(
|
|
`Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`,
|
|
);
|
|
}
|
|
|
|
if (invite.role && !['global:member', 'global:admin'].includes(invite.role)) {
|
|
throw new BadRequestError(
|
|
`Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'global:member' or 'global:admin'.`,
|
|
);
|
|
}
|
|
|
|
if (invite.role === 'global:admin' && !this.license.isAdvancedPermissionsLicensed()) {
|
|
throw new ForbiddenError(
|
|
'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.',
|
|
);
|
|
}
|
|
});
|
|
|
|
const attributes = req.body.map(({ email, role }) => ({
|
|
email,
|
|
role: role ?? 'global:member',
|
|
}));
|
|
|
|
const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes);
|
|
|
|
await this.externalHooks.run('user.invited', [usersCreated]);
|
|
|
|
return usersInvited;
|
|
}
|
|
|
|
/**
|
|
* Fill out user shell with first name, last name, and password.
|
|
*/
|
|
@Post('/:id/accept', { skipAuth: true })
|
|
async acceptInvitation(req: UserRequest.Update, res: Response) {
|
|
const { id: inviteeId } = req.params;
|
|
|
|
const { inviterId, firstName, lastName, password } = req.body;
|
|
|
|
if (!inviterId || !inviteeId || !firstName || !lastName || !password) {
|
|
this.logger.debug(
|
|
'Request to fill out a user shell failed because of missing properties in payload',
|
|
{ payload: req.body },
|
|
);
|
|
throw new BadRequestError('Invalid payload');
|
|
}
|
|
|
|
const validPassword = this.passwordUtility.validate(password);
|
|
|
|
const users = await this.userRepository.findManyByIds([inviterId, inviteeId]);
|
|
|
|
if (users.length !== 2) {
|
|
this.logger.debug(
|
|
'Request to fill out a user shell failed because the inviter ID and/or invitee ID were not found in database',
|
|
{
|
|
inviterId,
|
|
inviteeId,
|
|
},
|
|
);
|
|
throw new BadRequestError('Invalid payload or URL');
|
|
}
|
|
|
|
const invitee = users.find((user) => user.id === inviteeId) as User;
|
|
|
|
if (invitee.password) {
|
|
this.logger.debug(
|
|
'Request to fill out a user shell failed because the invite had already been accepted',
|
|
{ inviteeId },
|
|
);
|
|
throw new BadRequestError('This invite has been accepted already');
|
|
}
|
|
|
|
invitee.firstName = firstName;
|
|
invitee.lastName = lastName;
|
|
invitee.password = await this.passwordUtility.hash(validPassword);
|
|
|
|
const updatedUser = await this.userRepository.save(invitee, { transaction: false });
|
|
|
|
this.authService.issueCookie(res, updatedUser, req.browserId);
|
|
|
|
this.eventService.emit('user-signed-up', {
|
|
user: updatedUser,
|
|
userType: 'email',
|
|
wasDisabledLdapUser: false,
|
|
});
|
|
|
|
const publicInvitee = await this.userService.toPublic(invitee);
|
|
|
|
await this.externalHooks.run('user.profile.update', [invitee.email, publicInvitee]);
|
|
await this.externalHooks.run('user.password.update', [invitee.email, invitee.password]);
|
|
|
|
return await this.userService.toPublic(updatedUser, {
|
|
posthog: this.postHog,
|
|
withScopes: true,
|
|
});
|
|
}
|
|
}
|