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
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:
261
apps/meteor/server/lib/users/saveUser/saveUser.ts
Normal file
261
apps/meteor/server/lib/users/saveUser/saveUser.ts
Normal 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);
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user