n8n/packages/cli/src/controllers/invitation.controller.ts
Iván Ovejero 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
refactor(core): Modernize logger service (#11031)
2024-10-01 12:16:09 +02:00

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