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`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ guilherme.gazzo
+
+
+
+
+
+ currently working on User Card
+
+
+
+
+
+
+ Nickname
+
+
+ gazzo
+
+
+
+
+ Roles
+
+
+
+
+
+
+ admin
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+ Username
+
+
+ guilherme.gazzo
+
+
+
+
+ Bio
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus.
+