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
262 lines
7.9 KiB
TypeScript
262 lines
7.9 KiB
TypeScript
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);
|
|
};
|
|
})();
|