snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/model-typings';
import type { SaveUserData } from './saveUser';
const MAX_BIO_LENGTH = 260;
export const handleBio = (userUpdater: Updater<IUser>, bio: SaveUserData['bio']) => {
if (bio?.trim()) {
if (bio.length > MAX_BIO_LENGTH) {
throw new MeteorError('error-bio-size-exceeded', `Bio size exceeds ${MAX_BIO_LENGTH} characters`, {
method: 'saveUserProfile',
});
}
userUpdater.set('bio', bio);
} else {
userUpdater.unset('bio');
}
};

View File

@@ -0,0 +1,20 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/model-typings';
import type { SaveUserData } from './saveUser';
const MAX_NICKNAME_LENGTH = 120;
export const handleNickname = (userUpdater: Updater<IUser>, nickname: SaveUserData['nickname']) => {
if (nickname?.trim()) {
if (nickname.length > MAX_NICKNAME_LENGTH) {
throw new MeteorError('error-nickname-size-exceeded', `Nickname size exceeds ${MAX_NICKNAME_LENGTH} characters`, {
method: 'saveUserProfile',
});
}
userUpdater.set('nickname', nickname);
} else {
userUpdater.unset('nickname');
}
};

View File

@@ -0,0 +1,2 @@
export { saveUser } from './saveUser';
export { validateUserEditing } from './validateUserEditing';

View File

@@ -0,0 +1,96 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Users } from '@rocket.chat/models';
import Gravatar from 'gravatar';
import { Accounts } from 'meteor/accounts-base';
import { notifyOnUserChangeById } from '../../notifyListener';
import { validateEmailDomain } from '../../validateEmailDomain';
import { warnGravatarDeprecation } from '../gravatarDeprecation';
import { setUserAvatar } from '../setUserAvatar';
import { handleBio } from './handleBio';
import { handleNickname } from './handleNickname';
import type { SaveUserData } from './saveUser';
import { sendPasswordEmail, sendWelcomeEmail } from './sendUserEmail';
import { getNewUserRoles } from '../../../services/user/lib/getNewUserRoles';
import { settings } from '../../../settings';
export const saveNewUser = async function (userData: SaveUserData, sendPassword: boolean, performedBy: IUser) {
await validateEmailDomain(userData.email);
const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles();
const isGuest = roles?.length === 1 && roles.includes('guest');
// insert user
const createUser: Record<string, any> = {
username: userData.username,
password: userData.password,
...(userData.name && { name: userData.name }),
joinDefaultChannels: userData.joinDefaultChannels,
isGuest,
globalRoles: roles,
skipNewUserRolesSetting: true,
performedBy,
};
if (userData.email) {
createUser.email = userData.email;
}
const _id = await Accounts.createUserAsync(createUser);
const updater = Users.getUpdater();
updater.set('settings', userData.settings || {});
if (typeof userData.name !== 'undefined') {
updater.set('name', userData.name);
}
if (typeof userData.requirePasswordChange !== 'undefined') {
updater.set('requirePasswordChange', userData.requirePasswordChange);
}
if (typeof userData.verified === 'boolean') {
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);
await Users.updateFromUpdater({ _id }, updater);
if (userData.sendWelcomeEmail) {
await sendWelcomeEmail(userData);
}
if (sendPassword) {
await sendPasswordEmail(userData);
}
userData._id = _id;
// Offline (air-gapped) licenses suppress the default Gravatar fetch — a
// default-on outbound call the workspace must never initiate on its own.
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email && !License.hasOfflineLicense()) {
warnGravatarDeprecation();
const gravatarUrl = Gravatar.url(userData.email, {
default: '404',
size: '200',
protocol: 'https',
});
try {
await setUserAvatar({ ...userData, _id }, gravatarUrl, '', 'url');
} catch (e) {
// Ignore this error for now, as it not being successful isn't bad
}
}
void notifyOnUserChangeById({ clientAction: 'inserted', id: _id });
return _id;
};

View File

@@ -0,0 +1,261 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { MeteorError } from '@rocket.chat/core-services';
import { isUserFederated } from '@rocket.chat/core-typings';
import type { IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import type { ClientSession } from 'mongodb';
import { handleBio } from './handleBio';
import { handleNickname } from './handleNickname';
import { saveNewUser } from './saveNewUser';
import { sendPasswordEmail } from './sendUserEmail';
import { setPasswordUpdater } from './setPasswordUpdater';
import { validateUserData } from './validateUserData';
import { validateUserEditing } from './validateUserEditing';
import { wrapInSessionTransaction, onceTransactionCommitedSuccessfully } from '../../../database/utils';
import type { UserChangedAuditStore } from '../../auditServerEvents/userChanged';
import { generatePassword } from '../../auth/generatePassword';
import { passwordPolicy } from '../../auth/passwordPolicy';
import { hasPermissionAsync } from '../../authorization/hasPermission';
import { callbacks } from '../../callbacks';
import { notifyOnUserChange } from '../../notifyListener';
import { shouldBreakInVersion } from '../../shouldBreakInVersion';
import { saveCustomFields } from '../saveCustomFields';
import { saveUserIdentity } from '../saveUserIdentity';
import { setEmail } from '../setEmail';
import { setStatusText } from '../setStatusText';
export type SaveUserData = {
_id?: IUser['_id'];
setRandomPassword?: boolean;
password?: string;
requirePasswordChange?: boolean;
username?: string;
name?: string;
statusText?: string;
email?: string;
verified?: boolean;
bio?: string;
nickname?: string;
roles?: IRole['_id'][];
settings?: Partial<IUserSettings>;
language?: string;
joinDefaultChannels?: boolean;
sendWelcomeEmail?: boolean;
customFields?: Record<string, any>;
active?: boolean;
freeSwitchExtension?: string;
};
export type UpdateUserData = RequiredField<SaveUserData, '_id'>;
export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id;
type SaveUserOptions = {
auditStore?: UserChangedAuditStore;
};
const findUserById = async (uid: IUser['_id']): Promise<IUser> => {
const user = await Users.findOneById(uid);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
return user;
};
const _saveUser = (session?: ClientSession) =>
async function (userId: IUser['_id'], userData: SaveUserData, options?: SaveUserOptions) {
const performedBy = await findUserById(userId);
const oldUserData = userData._id && (await Users.findOneById(userData._id));
if (oldUserData && isUserFederated(oldUserData)) {
throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user');
}
await validateUserData(userId, userData);
await callbacks.run('beforeSaveUser', {
user: userData,
oldUser: oldUserData,
});
let sendPassword = false;
if (userData.hasOwnProperty('setRandomPassword')) {
if (userData.setRandomPassword) {
userData.password = generatePassword();
userData.requirePasswordChange = true;
sendPassword = true;
}
delete userData.setRandomPassword;
}
if (!isUpdateUserData(userData)) {
// TODO audit new users
return saveNewUser(userData, sendPassword, performedBy);
}
if (!oldUserData) {
throw new MeteorError('error-user-not-found', 'User not found', {
method: 'saveUser',
});
}
options?.auditStore?.setOriginalUser(oldUserData);
await validateUserEditing(userId, userData);
// update user
const updater = Users.getUpdater();
if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) {
if (
!(await saveUserIdentity({
_id: userData._id,
username: userData.username,
name: userData.name,
updateUsernameInBackground: true,
updater,
session,
}))
) {
throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', {
method: 'saveUser',
});
}
}
if (typeof userData.statusText === 'string') {
await setStatusText(oldUserData, userData.statusText, { updater, session });
}
if (userData.email) {
const shouldSendVerificationEmailToUser = userData.verified !== true;
await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser, userData.verified === true, updater);
}
if (
userData.password?.trim() &&
(await hasPermissionAsync(userId, 'edit-other-user-password')) &&
passwordPolicy.validate(userData.password)
) {
await setPasswordUpdater(updater, userData.password.trim());
} else {
sendPassword = false;
}
handleBio(updater, userData.bio);
handleNickname(updater, userData.nickname);
if (userData.roles) {
updater.set('roles', userData.roles);
}
if (userData.settings) {
updater.set('settings', { preferences: userData.settings.preferences });
}
if (userData.language) {
updater.set('language', userData.language);
}
if (typeof userData.requirePasswordChange !== 'undefined') {
updater.set('requirePasswordChange', userData.requirePasswordChange);
if (!userData.requirePasswordChange) {
updater.unset('requirePasswordChangeReason');
}
}
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);
updater.set(`emails.${index}.verified`, userData.verified);
}
if (!userData.email) {
updater.set(`emails.0.verified`, userData.verified);
}
}
if (userData.customFields) {
await saveCustomFields(userData._id, userData.customFields, { _updater: updater, session });
}
await Users.updateFromUpdater({ _id: userData._id }, updater, { session });
await onceTransactionCommitedSuccessfully(async () => {
if (session && options?.auditStore) {
// setting this inside here to avoid moving `executeSetUserActiveStatus` from the endpoint fn
// updater will be commited by this point, so it won't affect the external user activation/deactivation
if (userData.active !== undefined) {
updater.set('active', userData.active);
}
options.auditStore.setUpdateFilter(updater.getRawUpdateFilter());
void options.auditStore.commitAuditEvent();
}
// App IPostUserUpdated event hook
// We need to pass the session here to ensure this record is fetched
// with the uncommited transaction data.
const userUpdated = await Users.findOneById(userData._id);
await callbacks.run('afterSaveUser', {
user: userUpdated,
oldUser: oldUserData,
});
await Apps.self?.triggerEvent(AppEvents.IPostUserUpdated, {
user: userUpdated,
previousUser: oldUserData,
performedBy,
});
if (sendPassword) {
await sendPasswordEmail(userData);
}
if (typeof userData.verified === 'boolean') {
delete userData.verified;
}
void notifyOnUserChange({
clientAction: 'updated',
id: userData._id,
diff: {
...userData,
emails: userUpdated?.emails,
},
});
}, session);
return true;
};
const isBroken = shouldBreakInVersion('9.0.0');
export const saveUser = (() => {
if (!process.env.DEBUG_DISABLE_USER_AUDIT) {
return wrapInSessionTransaction(_saveUser);
}
if (isBroken) {
throw new Error('DEBUG_DISABLE_USER_AUDIT flag is deprecated and should be removed');
}
const saveUserNoSession = _saveUser();
return function saveUser(userId: IUser['_id'], userData: SaveUserData, _options?: any) {
return saveUserNoSession(userId, userData);
};
})();

View File

@@ -0,0 +1,55 @@
import { MeteorError } from '@rocket.chat/core-services';
import { Meteor } from 'meteor/meteor';
import type { SaveUserData } from './saveUser';
import { settings } from '../../../settings';
import * as Mailer from '../../notifications/email/api';
let html = '';
let passwordChangedHtml = '';
Meteor.startup(() => {
Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => {
html = template;
});
Mailer.getTemplate('Password_Changed_Email', (template) => {
passwordChangedHtml = template;
});
});
export async function sendUserEmail(subject: string, html: string, userData: SaveUserData): Promise<void> {
if (!userData.email) {
return;
}
const email = {
to: userData.email,
from: settings.get<string>('From_Email'),
subject,
html,
data: {
email: userData.email,
password: userData.password ?? '******',
...(typeof userData.name !== 'undefined' ? { name: userData.name } : {}),
},
};
try {
await Mailer.send(email);
} catch (error) {
const errorMessage = typeof error === 'object' && error && 'message' in error ? error.message : '';
throw new MeteorError('error-email-send-failed', `Error trying to send email: ${errorMessage}`, {
function: 'RocketChat.saveUser',
message: errorMessage,
});
}
}
export async function sendWelcomeEmail(userData: Pick<SaveUserData, 'email' | 'name' | 'password'>) {
return sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData);
}
export async function sendPasswordEmail(userData: Pick<SaveUserData, 'email' | 'name' | 'password'>) {
return sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
}

View File

@@ -0,0 +1,25 @@
import crypto from 'node:crypto';
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/model-typings';
import bcrypt from 'bcrypt';
import { Accounts } from 'meteor/accounts-base';
const hashPassword = async (password: string) => {
const hash = crypto.createHash('sha256');
hash.update(password);
const hashedPassword = hash.digest('hex');
return bcrypt.hash(hashedPassword, Accounts._bcryptRounds());
};
export async function setPasswordUpdater(
updater: Updater<IUser>,
newPlaintextPassword: string,
options: { logout?: boolean } = { logout: true },
) {
updater.set('services.password.bcrypt', await hashPassword(newPlaintextPassword));
if (options.logout) {
updater.unset('services.resume.loginTokens');
}
}

View File

@@ -0,0 +1,108 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { makeFunction } from '@rocket.chat/patch-injection';
import escape from 'lodash.escape';
import type { SaveUserData } from './saveUser';
import { isUpdateUserData } from './saveUser';
import { trim } from '../../../../lib/utils/stringUtils';
import { settings } from '../../../settings';
import { getRoleIds } from '../../authorization/getRoles';
import { hasPermissionAsync } from '../../authorization/hasPermission';
import { checkEmailAvailability } from '../checkEmailAvailability';
import { checkUsernameAvailability } from '../checkUsernameAvailability';
export const validateUserData = makeFunction(async (userId: IUser['_id'], userData: SaveUserData): Promise<void> => {
const existingRoles = await getRoleIds();
if (userData.verified && userData._id && userId === userData._id) {
throw new MeteorError('error-action-not-allowed', 'Editing email verification is not allowed', {
method: 'insertOrUpdateUser',
action: 'Editing_user',
});
}
if (isUpdateUserData(userData) && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) {
throw new MeteorError('error-action-not-allowed', 'Editing user is not allowed', {
method: 'insertOrUpdateUser',
action: 'Editing_user',
});
}
if (!isUpdateUserData(userData) && !(await hasPermissionAsync(userId, 'create-user'))) {
throw new MeteorError('error-action-not-allowed', 'Adding user is not allowed', {
method: 'insertOrUpdateUser',
action: 'Adding_user',
});
}
if (userData.roles) {
const newRoles = userData.roles.filter((roleId) => !existingRoles.includes(roleId));
if (newRoles.length > 0) {
throw new MeteorError('error-action-not-allowed', 'The field Roles consist invalid role id', {
method: 'insertOrUpdateUser',
action: 'Assign_role',
});
}
}
if (userData.roles?.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) {
throw new MeteorError('error-action-not-allowed', 'Assigning admin is not allowed', {
method: 'insertOrUpdateUser',
action: 'Assign_admin',
});
}
if (settings.get('Accounts_RequireNameForSignUp') && !isUpdateUserData(userData) && !trim(userData.name)) {
throw new MeteorError('error-the-field-is-required', 'The field Name is required', {
method: 'insertOrUpdateUser',
field: 'Name',
});
}
if (!isUpdateUserData(userData) && !trim(userData.username)) {
throw new MeteorError('error-the-field-is-required', 'The field Username is required', {
method: 'insertOrUpdateUser',
field: 'Username',
});
}
let nameValidation;
try {
nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`);
} catch (e) {
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
}
if (userData.username && !nameValidation.test(userData.username)) {
throw new MeteorError('error-input-is-not-a-valid-field', `${escape(userData.username)} is not a valid username`, {
method: 'insertOrUpdateUser',
input: userData.username,
field: 'Username',
});
}
if (!isUpdateUserData(userData) && !userData.password && !userData.setRandomPassword) {
throw new MeteorError('error-the-field-is-required', 'The field Password is required', {
method: 'insertOrUpdateUser',
field: 'Password',
});
}
if (!isUpdateUserData(userData)) {
if (userData.username && !(await checkUsernameAvailability(userData.username))) {
throw new MeteorError('error-field-unavailable', `${escape(userData.username)} is already in use :(`, {
method: 'insertOrUpdateUser',
field: userData.username,
});
}
if (userData.email && !(await checkEmailAvailability(userData.email))) {
throw new MeteorError('error-field-unavailable', `${escape(userData.email)} is already in use :(`, {
method: 'insertOrUpdateUser',
field: userData.email,
});
}
}
});

View File

@@ -0,0 +1,123 @@
/* eslint-disable complexity */
import { MeteorError } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import type { UpdateUserData } from './saveUser';
import { settings } from '../../../settings';
import { hasPermissionAsync } from '../../authorization/hasPermission';
const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['roles']) =>
newRoles !== undefined &&
(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 (newExtension?: string) => {
if (!settings.get('VoIP_TeamCollab_SIP_Integration_Enabled')) {
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
*
* @param {string} userId
* @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData
*/
export async function validateUserEditing(userId: IUser['_id'], userData: UpdateUserData): Promise<void> {
const editingMyself = userData._id && userId === userData._id;
const canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info');
const canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password');
const user = await Users.findOneById(userData._id);
if (!user) {
throw new MeteorError('error-invalid-user', 'Invalid user');
}
if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) {
throw new MeteorError('error-action-not-allowed', 'Assign roles is not allowed', {
method: 'insertOrUpdateUser',
action: 'Assign_role',
});
}
if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) {
throw new MeteorError('error-action-not-allowed', 'Edit user profile is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
isEditingField(user.username, userData.username) &&
!settings.get('Accounts_AllowUsernameChange') &&
(editingMyself ? user.username : !canEditOtherUserInfo)
) {
throw new MeteorError('error-action-not-allowed', 'Edit username is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
isEditingField(user.statusText, userData.statusText) &&
!settings.get('Accounts_AllowUserStatusMessageChange') &&
(!canEditOtherUserInfo || editingMyself)
) {
throw new MeteorError('error-action-not-allowed', 'Edit user status is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
isEditingField(user.name, userData.name) &&
!settings.get('Accounts_AllowRealNameChange') &&
(!canEditOtherUserInfo || editingMyself)
) {
throw new MeteorError('error-action-not-allowed', 'Edit user real name is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
user.emails?.[0] &&
isEditingField(user.emails[0].address, userData.email) &&
!settings.get('Accounts_AllowEmailChange') &&
(!canEditOtherUserInfo || editingMyself)
) {
throw new MeteorError('error-action-not-allowed', 'Edit user email is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
userData.password &&
!settings.get('Accounts_AllowPasswordChange') &&
(editingMyself ? user.services?.password || !user.requirePasswordChange : !canEditOtherUserPassword)
) {
throw new MeteorError('error-action-not-allowed', 'Edit user password is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
if (
isEditingField(user.freeSwitchExtension ?? '', userData.freeSwitchExtension) &&
!(await canEditExtension(userData.freeSwitchExtension))
) {
throw new MeteorError('error-action-not-allowed', 'Edit user voice call extension is not allowed', {
method: 'insertOrUpdateUser',
action: 'Update_user',
});
}
}