forked from n8n-io/n8n
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Extract Invitation routes to InvitationController (no-chang…
…elog) (n8n-io#7726) This PR: - Creates `InvitationController` - Moves `POST /users` to `POST /invitations` and move related test to `invitations.api.tests` - Moves `POST /users/:id` to `POST /invitations/:id/accept` and move related test to `invitations.api.tests` - Adjusts FE to use new endpoints - Moves all the invitation logic to the `UserService` --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
- Loading branch information
1 parent
e2ffd39
commit 8e0ae3c
Showing
17 changed files
with
713 additions
and
624 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { In } from 'typeorm'; | ||
import Container, { Service } from 'typedi'; | ||
import { Authorized, NoAuthRequired, Post, RestController } from '@/decorators'; | ||
import { BadRequestError, UnauthorizedError } from '@/ResponseHelper'; | ||
import { issueCookie } from '@/auth/jwt'; | ||
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; | ||
import { Response } from 'express'; | ||
import { UserRequest } from '@/requests'; | ||
import { Config } from '@/config'; | ||
import { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; | ||
import { License } from '@/License'; | ||
import { UserService } from '@/services/user.service'; | ||
import { Logger } from '@/Logger'; | ||
import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers'; | ||
import { hashPassword, validatePassword } from '@/UserManagement/UserManagementHelper'; | ||
import { PostHogClient } from '@/posthog'; | ||
import type { User } from '@/databases/entities/User'; | ||
import validator from 'validator'; | ||
|
||
@Service() | ||
@RestController('/invitations') | ||
export class InvitationController { | ||
constructor( | ||
private readonly config: Config, | ||
private readonly logger: Logger, | ||
private readonly internalHooks: IInternalHooksClass, | ||
private readonly externalHooks: IExternalHooksClass, | ||
private readonly userService: UserService, | ||
private readonly postHog?: PostHogClient, | ||
) {} | ||
|
||
/** | ||
* Send email invite(s) to one or multiple users and create user shell(s). | ||
*/ | ||
|
||
@Authorized(['global', 'owner']) | ||
@Post('/') | ||
async inviteUser(req: UserRequest.Invite) { | ||
const isWithinUsersLimit = Container.get(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 UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); | ||
} | ||
|
||
if (!this.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}`, | ||
); | ||
} | ||
}); | ||
|
||
const emails = req.body.map((e) => e.email); | ||
|
||
const { usersInvited, usersCreated } = await this.userService.inviteMembers(req.user, emails); | ||
|
||
await this.externalHooks.run('user.invited', [usersCreated]); | ||
|
||
return usersInvited; | ||
} | ||
|
||
/** | ||
* Fill out user shell with first name, last name, and password. | ||
*/ | ||
@NoAuthRequired() | ||
@Post('/:id/accept') | ||
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 = validatePassword(password); | ||
|
||
const users = await this.userService.findMany({ | ||
where: { id: In([inviterId, inviteeId]) }, | ||
relations: ['globalRole'], | ||
}); | ||
|
||
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 hashPassword(validPassword); | ||
|
||
const updatedUser = await this.userService.save(invitee); | ||
|
||
await issueCookie(res, updatedUser); | ||
|
||
void this.internalHooks.onUserSignup(updatedUser, { | ||
user_type: 'email', | ||
was_disabled_ldap_user: 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 this.userService.toPublic(updatedUser, { posthog: this.postHog }); | ||
} | ||
} |
Oops, something went wrong.