fix: Fix user reinvites on FE and BE (#8261)

This commit is contained in:
Iván Ovejero 2024-01-09 13:52:34 +01:00 committed by GitHub
parent b1c1372bc2
commit 0dabe5c74e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 50 additions and 13 deletions

View file

@ -270,7 +270,7 @@ export class UserService {
const usersInvited = await this.sendEmails( const usersInvited = await this.sendEmails(
owner, owner,
Object.fromEntries(createdUsers), 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) }; return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) };

View file

@ -346,6 +346,29 @@ describe('POST /invitations', () => {
assertInvitedUsersOnDb(storedUser); 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 () => { test('should fail to create admin shell if not licensed', async () => {
license.isAdvancedPermissionsLicensed.mockReturnValue(false); license.isAdvancedPermissionsLicensed.mockReturnValue(false);
mailer.invite.mockResolvedValue({ emailSent: false }); mailer.invite.mockResolvedValue({ emailSent: false });

View file

@ -687,6 +687,8 @@ export type IPersonalizationSurveyVersions =
export type IRole = 'default' | 'owner' | 'member' | 'admin'; export type IRole = 'default' | 'owner' | 'member' | 'admin';
export type InvitableRoleName = 'member' | 'admin';
export interface IUserResponse { export interface IUserResponse {
id: string; id: string;
firstName?: string; firstName?: string;

View file

@ -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 type { IDataObject } from 'n8n-workflow';
import { makeRestApiRequest } from '@/utils/apiUtils'; import { makeRestApiRequest } from '@/utils/apiUtils';
@ -12,7 +17,7 @@ type AcceptInvitationParams = {
export async function inviteUsers( export async function inviteUsers(
context: IRestApiContext, context: IRestApiContext,
params: Array<{ email: string; role: IRole }>, params: Array<{ email: string; role: InvitableRoleName }>,
) { ) {
return makeRestApiRequest<IInviteResponse[]>(context, 'POST', '/invitations', params); return makeRestApiRequest<IInviteResponse[]>(context, 'POST', '/invitations', params);
} }

View file

@ -29,6 +29,7 @@ import type {
IUserResponse, IUserResponse,
IUsersState, IUsersState,
CurrentUserResponse, CurrentUserResponse,
InvitableRoleName,
} from '@/Interface'; } from '@/Interface';
import { getCredentialPermissions } from '@/permissions'; import { getCredentialPermissions } from '@/permissions';
import { getPersonalizedNodeTypes, ROLE } from '@/utils/userUtils'; import { getPersonalizedNodeTypes, ROLE } from '@/utils/userUtils';
@ -302,7 +303,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
const users = await getUsers(rootStore.getRestApiContext); const users = await getUsers(rootStore.getRestApiContext);
this.addUsers(users); this.addUsers(users);
}, },
async inviteUsers(params: Array<{ email: string; role: IRole }>): Promise<IInviteResponse[]> { async inviteUsers(
params: Array<{ email: string; role: InvitableRoleName }>,
): Promise<IInviteResponse[]> {
const rootStore = useRootStore(); const rootStore = useRootStore();
const users = await inviteUsers(rootStore.getRestApiContext, params); const users = await inviteUsers(rootStore.getRestApiContext, params);
this.addUsers( this.addUsers(
@ -314,11 +317,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
); );
return users; return users;
}, },
async reinviteUser(params: { email: string }): Promise<void> { async reinviteUser({ email, role }: { email: string; role: InvitableRoleName }): Promise<void> {
const rootStore = useRootStore(); const rootStore = useRootStore();
const invitationResponse = await inviteUsers(rootStore.getRestApiContext, [ const invitationResponse = await inviteUsers(rootStore.getRestApiContext, [{ email, role }]);
{ email: params.email },
]);
if (!invitationResponse[0].user.emailSent) { if (!invitationResponse[0].user.emailSent) {
throw Error(invitationResponse[0].error); throw Error(invitationResponse[0].error);
} }

View file

@ -63,7 +63,7 @@ export async function request(config: {
baseURL: string; baseURL: string;
endpoint: string; endpoint: string;
headers?: IDataObject; headers?: IDataObject;
data?: IDataObject; data?: IDataObject | IDataObject[];
withCredentials?: boolean; withCredentials?: boolean;
}) { }) {
const { method, baseURL, endpoint, headers, data } = config; const { method, baseURL, endpoint, headers, data } = config;
@ -119,7 +119,7 @@ export async function makeRestApiRequest<T>(
context: IRestApiContext, context: IRestApiContext,
method: Method, method: Method,
endpoint: string, endpoint: string,
data?: IDataObject, data?: IDataObject | IDataObject[],
) { ) {
const response = await request({ const response = await request({
method, method,

View file

@ -89,7 +89,7 @@ import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants'; 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 { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
@ -207,9 +207,15 @@ export default defineComponent({
}, },
async onReinvite(userId: string) { async onReinvite(userId: string) {
const user = this.usersStore.getUserById(userId); 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 { try {
await this.usersStore.reinviteUser({ email: user.email }); await this.usersStore.reinviteUser({
email: user.email,
role: user.globalRole.name as InvitableRoleName,
});
this.showToast({ this.showToast({
type: 'success', type: 'success',
title: this.$locale.baseText('settings.users.inviteResent'), title: this.$locale.baseText('settings.users.inviteResent'),