feat(core): Allow admin creation (#7837)
https://linear.app/n8n/issue/PAY-1038
This commit is contained in:
@@ -281,6 +281,7 @@ export class Server extends AbstractServer {
|
||||
activeWorkflowRunner,
|
||||
Container.get(RoleService),
|
||||
userService,
|
||||
Container.get(License),
|
||||
),
|
||||
Container.get(SamlController),
|
||||
Container.get(SourceControlController),
|
||||
@@ -296,6 +297,7 @@ export class Server extends AbstractServer {
|
||||
internalHooks,
|
||||
externalHooks,
|
||||
Container.get(UserService),
|
||||
Container.get(License),
|
||||
postHog,
|
||||
),
|
||||
Container.get(VariablesController),
|
||||
|
||||
@@ -28,6 +28,7 @@ export class InvitationController {
|
||||
private readonly internalHooks: IInternalHooksClass,
|
||||
private readonly externalHooks: IExternalHooksClass,
|
||||
private readonly userService: UserService,
|
||||
private readonly license: License,
|
||||
private readonly postHog?: PostHogClient,
|
||||
) {}
|
||||
|
||||
@@ -88,11 +89,26 @@ export class InvitationController {
|
||||
`Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (invite.role && !['member', 'admin'].includes(invite.role)) {
|
||||
throw new BadRequestError(
|
||||
`Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'member' or 'admin'.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (invite.role === 'admin' && !this.license.isAdvancedPermissionsLicensed()) {
|
||||
throw new UnauthorizedError(
|
||||
'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const emails = req.body.map((e) => e.email);
|
||||
const attributes = req.body.map(({ email, role }) => ({
|
||||
email,
|
||||
role: role ?? 'member',
|
||||
}));
|
||||
|
||||
const { usersInvited, usersCreated } = await this.userService.inviteMembers(req.user, emails);
|
||||
const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes);
|
||||
|
||||
await this.externalHooks.run('user.invited', [usersCreated]);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Logger } from '@/Logger';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { License } from '@/License';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/users')
|
||||
@@ -32,6 +33,7 @@ export class UsersController {
|
||||
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly userService: UserService,
|
||||
private readonly license: License,
|
||||
) {}
|
||||
|
||||
static ERROR_MESSAGES = {
|
||||
@@ -43,6 +45,7 @@ export class UsersController {
|
||||
NO_ADMIN_ON_OWNER: 'Admin cannot change role on global owner',
|
||||
NO_OWNER_ON_OWNER: 'Owner cannot change role on global owner',
|
||||
NO_USER_TO_OWNER: 'Cannot promote user to global owner',
|
||||
NO_ADMIN_IF_UNLICENSED: 'Admin role is not available without a license',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -336,6 +339,7 @@ export class UsersController {
|
||||
NO_USER_TO_OWNER,
|
||||
NO_USER,
|
||||
NO_OWNER_ON_OWNER,
|
||||
NO_ADMIN_IF_UNLICENSED,
|
||||
} = UsersController.ERROR_MESSAGES.CHANGE_ROLE;
|
||||
|
||||
if (req.user.globalRole.scope === 'global' && req.user.globalRole.name === 'member') {
|
||||
@@ -364,6 +368,14 @@ export class UsersController {
|
||||
throw new NotFoundError(NO_USER);
|
||||
}
|
||||
|
||||
if (
|
||||
newRole.scope === 'global' &&
|
||||
newRole.name === 'admin' &&
|
||||
!this.license.isAdvancedPermissionsLicensed()
|
||||
) {
|
||||
throw new UnauthorizedError(NO_ADMIN_IF_UNLICENSED);
|
||||
}
|
||||
|
||||
if (
|
||||
req.user.globalRole.scope === 'global' &&
|
||||
req.user.globalRole.name === 'admin' &&
|
||||
|
||||
@@ -296,7 +296,11 @@ export declare namespace PasswordResetRequest {
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace UserRequest {
|
||||
export type Invite = AuthenticatedRequest<{}, {}, Array<{ email: string }>>;
|
||||
export type Invite = AuthenticatedRequest<
|
||||
{},
|
||||
{},
|
||||
Array<{ email: string; role?: 'member' | 'admin' }>
|
||||
>;
|
||||
|
||||
export type InviteResponse = {
|
||||
user: { id: string; email: string; inviteAcceptUrl?: string; emailSent: boolean };
|
||||
|
||||
@@ -238,18 +238,19 @@ export class UserService {
|
||||
);
|
||||
}
|
||||
|
||||
public async inviteMembers(owner: User, emails: string[]) {
|
||||
async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) {
|
||||
const memberRole = await this.roleService.findGlobalMemberRole();
|
||||
const adminRole = await this.roleService.findGlobalAdminRole();
|
||||
|
||||
const existingUsers = await this.findMany({
|
||||
where: { email: In(emails) },
|
||||
where: { email: In(attributes.map(({ email }) => email)) },
|
||||
relations: ['globalRole'],
|
||||
select: ['email', 'password', 'id'],
|
||||
});
|
||||
|
||||
const existUsersEmails = existingUsers.map((user) => user.email);
|
||||
|
||||
const toCreateUsers = emails.filter((email) => !existUsersEmails.includes(email));
|
||||
const toCreateUsers = attributes.filter(({ email }) => !existUsersEmails.includes(email));
|
||||
|
||||
const pendingUsersToInvite = existingUsers.filter((email) => email.isPending);
|
||||
|
||||
@@ -264,10 +265,10 @@ export class UserService {
|
||||
try {
|
||||
await this.getManager().transaction(async (transactionManager) =>
|
||||
Promise.all(
|
||||
toCreateUsers.map(async (email) => {
|
||||
toCreateUsers.map(async ({ email, role }) => {
|
||||
const newUser = Object.assign(new User(), {
|
||||
email,
|
||||
globalRole: memberRole,
|
||||
globalRole: role === 'member' ? memberRole : adminRole,
|
||||
});
|
||||
const savedUser = await transactionManager.save<User>(newUser);
|
||||
createdUsers.set(email, savedUser.id);
|
||||
@@ -285,6 +286,6 @@ export class UserService {
|
||||
|
||||
const usersInvited = await this.sendEmails(owner, Object.fromEntries(createdUsers));
|
||||
|
||||
return { usersInvited, usersCreated: toCreateUsers };
|
||||
return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user