diff --git a/.changeset/shy-bats-worry.md b/.changeset/shy-bats-worry.md new file mode 100644 index 0000000000000..8615105495228 --- /dev/null +++ b/.changeset/shy-bats-worry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Replaces old `Assign Extension` button and modal by introducing a proper input in the user edit form. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index b418d555761fd..048b289b3fcec 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -56,6 +56,7 @@ import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation'; import { saveUser } from '../../../lib/server/functions/saveUser'; import { sendWelcomeEmail } from '../../../lib/server/functions/saveUser/sendUserEmail'; +import { canEditExtension } from '../../../lib/server/functions/saveUser/validateUserEditing'; import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; @@ -325,6 +326,10 @@ API.v1.addRoute( validateCustomFields(this.bodyParams.customFields); } + if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.userId, this.bodyParams.freeSwitchExtension))) { + return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed'); + } + const newUserId = await saveUser(this.userId, this.bodyParams); const userId = typeof newUserId !== 'string' ? this.userId : newUserId; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts index bb9a6a1cac672..508472110fc32 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts @@ -47,6 +47,10 @@ export const saveNewUser = async function (userData: SaveUserData, sendPassword: updater.set('emails.0.verified', userData.verified); } + if (typeof userData.freeSwitchExtension === 'string' && userData.freeSwitchExtension !== '') { + updater.set('freeSwitchExtension', userData.freeSwitchExtension); + } + handleBio(updater, userData.bio); handleNickname(updater, userData.nickname); diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index 13884b6e34b9b..e77e56cc84b9d 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -53,6 +53,8 @@ export type SaveUserData = { customFields?: Record; active?: boolean; + + freeSwitchExtension?: string; }; export type UpdateUserData = RequiredField; export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id; @@ -162,6 +164,14 @@ const _saveUser = (session?: ClientSession) => } } + if (typeof userData.freeSwitchExtension === 'string' && userData.freeSwitchExtension !== (oldUserData?.freeSwitchExtension ?? '')) { + if (userData.freeSwitchExtension.trim() === '') { + updater.unset('freeSwitchExtension'); + } else { + updater.set('freeSwitchExtension', userData.freeSwitchExtension); + } + } + if (typeof userData.verified === 'boolean') { if (oldUserData && 'emails' in oldUserData && oldUserData.emails?.some(({ address }) => address === userData.email)) { const index = oldUserData.emails.findIndex(({ address }) => address === userData.email); diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts index c9f3ddbe296ac..2fd4a3f0e6f42 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { MeteorError } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -11,6 +12,22 @@ const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['rol (newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item))); const isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue; +export const canEditExtension = async (userId: string, newExtension?: string) => { + if (!settings.get('VoIP_TeamCollab_Enabled')) { + return false; + } + + if (!(await hasPermissionAsync(userId, 'manage-voip-extensions'))) { + return false; + } + + if (newExtension && (await Users.findOneByFreeSwitchExtension(newExtension, { projection: { _id: 1 } }))) { + throw new MeteorError('error-extension-not-available', 'Extension is already assigned to another user'); + } + + return true; +}; + /** * Validate permissions to edit user fields * @@ -97,4 +114,14 @@ export async function validateUserEditing(userId: IUser['_id'], userData: Update action: 'Update_user', }); } + + if ( + isEditingField(user.freeSwitchExtension ?? '', userData.freeSwitchExtension) && + !(await canEditExtension(userId, userData.freeSwitchExtension)) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit user voice call extension is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } } diff --git a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index 9e76aa0064590..cf8aa0baad335 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -35,6 +35,11 @@ const Template: StoryFn = (args) => >; @@ -71,6 +72,7 @@ const UserInfo = ({ canViewAllInfo, actions, reason, + freeSwitchExtension, // @ts-expect-error - abacAttributes is not yet implemented in Users properties abacAttributes = null, ...props @@ -180,6 +182,13 @@ const UserInfo = ({ )} + {freeSwitchExtension && ( + + {t('Voice_call_extension')} + {freeSwitchExtension} + + )} + {abacAttributes?.length > 0 && ( {t('ABAC_Attributes')} diff --git a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap index 33c5d0d0d537b..3a519a834fa8f 100644 --- a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap +++ b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap @@ -572,3 +572,278 @@ exports[`renders WithABACAttributes without crashing 1`] = ` `; + +exports[`renders WithVoiceCallExtension without crashing 1`] = ` + +
+