From e5d8d18b5b388ef1abd4bd503afab77cd69bc686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 8 Jan 2024 15:00:48 +0100 Subject: [PATCH 1/2] fix(editor,core): Fix user reinvites --- packages/cli/src/services/user.service.ts | 2 +- packages/editor-ui/src/Interface.ts | 2 ++ packages/editor-ui/src/api/invitation.ts | 9 +++++++-- packages/editor-ui/src/stores/users.store.ts | 11 ++++++----- packages/editor-ui/src/utils/apiUtils.ts | 4 ++-- packages/editor-ui/src/views/SettingsUsersView.vue | 12 +++++++++--- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index b429e992d23b0..1d3306d4be345 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -270,7 +270,7 @@ export class UserService { const usersInvited = await this.sendEmails( owner, Object.fromEntries(createdUsers), - toCreateUsers[0].role, // same role for all invited users + attributes[0].role, // same role for all invited users ); return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) }; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 560b28dbf18c6..1908d774746ad 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -687,6 +687,8 @@ export type IPersonalizationSurveyVersions = export type IRole = 'default' | 'owner' | 'member' | 'admin'; +export type InvitableRoleName = 'member' | 'admin'; + export interface IUserResponse { id: string; firstName?: string; diff --git a/packages/editor-ui/src/api/invitation.ts b/packages/editor-ui/src/api/invitation.ts index 499149836177e..b9e2b8a1638af 100644 --- a/packages/editor-ui/src/api/invitation.ts +++ b/packages/editor-ui/src/api/invitation.ts @@ -1,4 +1,9 @@ -import type { CurrentUserResponse, IInviteResponse, IRestApiContext, IRole } from '@/Interface'; +import type { + CurrentUserResponse, + IInviteResponse, + IRestApiContext, + InvitableRoleName, +} from '@/Interface'; import type { IDataObject } from 'n8n-workflow'; import { makeRestApiRequest } from '@/utils/apiUtils'; @@ -12,7 +17,7 @@ type AcceptInvitationParams = { export async function inviteUsers( context: IRestApiContext, - params: Array<{ email: string; role: IRole }>, + params: Array<{ email: string; role: InvitableRoleName }>, ) { return makeRestApiRequest(context, 'POST', '/invitations', params); } diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index ab27063e5d80d..2959fd2ede543 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -29,6 +29,7 @@ import type { IUserResponse, IUsersState, CurrentUserResponse, + InvitableRoleName, } from '@/Interface'; import { getCredentialPermissions } from '@/permissions'; import { getPersonalizedNodeTypes, ROLE } from '@/utils/userUtils'; @@ -302,7 +303,9 @@ export const useUsersStore = defineStore(STORES.USERS, { const users = await getUsers(rootStore.getRestApiContext); this.addUsers(users); }, - async inviteUsers(params: Array<{ email: string; role: IRole }>): Promise { + async inviteUsers( + params: Array<{ email: string; role: InvitableRoleName }>, + ): Promise { const rootStore = useRootStore(); const users = await inviteUsers(rootStore.getRestApiContext, params); this.addUsers( @@ -314,11 +317,9 @@ export const useUsersStore = defineStore(STORES.USERS, { ); return users; }, - async reinviteUser(params: { email: string }): Promise { + async reinviteUser({ email, role }: { email: string; role: InvitableRoleName }): Promise { const rootStore = useRootStore(); - const invitationResponse = await inviteUsers(rootStore.getRestApiContext, [ - { email: params.email }, - ]); + const invitationResponse = await inviteUsers(rootStore.getRestApiContext, [{ email, role }]); if (!invitationResponse[0].user.emailSent) { throw Error(invitationResponse[0].error); } diff --git a/packages/editor-ui/src/utils/apiUtils.ts b/packages/editor-ui/src/utils/apiUtils.ts index d8ef8719f3f6d..d7075465703cd 100644 --- a/packages/editor-ui/src/utils/apiUtils.ts +++ b/packages/editor-ui/src/utils/apiUtils.ts @@ -63,7 +63,7 @@ export async function request(config: { baseURL: string; endpoint: string; headers?: IDataObject; - data?: IDataObject; + data?: IDataObject | IDataObject[]; withCredentials?: boolean; }) { const { method, baseURL, endpoint, headers, data } = config; @@ -119,7 +119,7 @@ export async function makeRestApiRequest( context: IRestApiContext, method: Method, endpoint: string, - data?: IDataObject, + data?: IDataObject | IDataObject[], ) { const response = await request({ method, diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index d35f9b539075f..53095882e4506 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -89,7 +89,7 @@ import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants'; -import type { IUser, IUserListAction } from '@/Interface'; +import type { IUser, IUserListAction, InvitableRoleName } from '@/Interface'; import { useToast } from '@/composables/useToast'; import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; @@ -207,9 +207,15 @@ export default defineComponent({ }, async onReinvite(userId: string) { const user = this.usersStore.getUserById(userId); - if (user?.email) { + if (user?.email && user?.globalRole) { + if (!['admin', 'member'].includes(user.globalRole.name)) { + throw new Error('Invalid role name on reinvite'); + } try { - await this.usersStore.reinviteUser({ email: user.email }); + await this.usersStore.reinviteUser({ + email: user.email, + role: user.globalRole.name as InvitableRoleName, + }); this.showToast({ type: 'success', title: this.$locale.baseText('settings.users.inviteResent'), From ca01f479cd9e5b62ac160cd6664a58513f353303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 8 Jan 2024 15:09:27 +0100 Subject: [PATCH 2/2] Add tests --- .../test/integration/invitations.api.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/cli/test/integration/invitations.api.test.ts b/packages/cli/test/integration/invitations.api.test.ts index a87ddde5e5ea8..2ac4d4c1d601b 100644 --- a/packages/cli/test/integration/invitations.api.test.ts +++ b/packages/cli/test/integration/invitations.api.test.ts @@ -346,6 +346,29 @@ describe('POST /invitations', () => { assertInvitedUsersOnDb(storedUser); }); + test('should reinvite member', async () => { + mailer.invite.mockResolvedValue({ emailSent: false }); + + await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'member' }]); + + await ownerAgent + .post('/invitations') + .send([{ email: randomEmail(), role: 'member' }]) + .expect(200); + }); + + test('should reinvite admin if licensed', async () => { + license.isAdvancedPermissionsLicensed.mockReturnValue(true); + mailer.invite.mockResolvedValue({ emailSent: false }); + + await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'admin' }]); + + await ownerAgent + .post('/invitations') + .send([{ email: randomEmail(), role: 'admin' }]) + .expect(200); + }); + test('should fail to create admin shell if not licensed', async () => { license.isAdvancedPermissionsLicensed.mockReturnValue(false); mailer.invite.mockResolvedValue({ emailSent: false });