Skip to content

Commit

Permalink
chore: Use updater in saveUser file (#32993)
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusbsilva137 authored Jan 2, 2025
1 parent 5506c40 commit b04342c
Show file tree
Hide file tree
Showing 17 changed files with 127 additions and 86 deletions.
12 changes: 5 additions & 7 deletions apps/meteor/app/lib/server/functions/saveUser/handleBio.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings';
import type { UpdateFilter } from 'mongodb';
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 = (updateUser: DeepWritable<UpdateFilter<DeepPartial<IUser>>>, bio: SaveUserData['bio']) => {
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',
});
}
updateUser.$set = updateUser.$set || {};
updateUser.$set.bio = bio;
userUpdater.set('bio', bio);
} else {
updateUser.$unset = updateUser.$unset || {};
updateUser.$unset.bio = 1;
userUpdater.unset('bio');
}
};
12 changes: 5 additions & 7 deletions apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings';
import type { UpdateFilter } from 'mongodb';
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 = (updateUser: DeepWritable<UpdateFilter<DeepPartial<IUser>>>, nickname: SaveUserData['nickname']) => {
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',
});
}
updateUser.$set = updateUser.$set || {};
updateUser.$set.nickname = nickname;
userUpdater.set('nickname', nickname);
} else {
updateUser.$unset = updateUser.$unset || {};
updateUser.$unset.nickname = 1;
userUpdater.unset('nickname');
}
};
24 changes: 11 additions & 13 deletions apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { DeepPartial, DeepWritable, IUser, RequiredField } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import Gravatar from 'gravatar';
import type { UpdateFilter } from 'mongodb';

import { getNewUserRoles } from '../../../../../server/services/user/lib/getNewUserRoles';
import { settings } from '../../../../settings/server';
Expand Down Expand Up @@ -34,25 +32,25 @@ export const saveNewUser = async function (userData: SaveUserData, sendPassword:

const _id = await Accounts.createUserAsync(createUser);

const updateUser: RequiredField<DeepWritable<UpdateFilter<DeepPartial<IUser>>>, '$set'> = {
$set: {
...(typeof userData.name !== 'undefined' && { name: userData.name }),
settings: userData.settings || {},
},
};
const updater = Users.getUpdater();

updater.set('settings', userData.settings || {});
if (typeof userData.name !== 'undefined') {
updater.set('name', userData.name);
}

if (typeof userData.requirePasswordChange !== 'undefined') {
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
updater.set('requirePasswordChange', userData.requirePasswordChange);
}

if (typeof userData.verified === 'boolean') {
updateUser.$set['emails.0.verified'] = userData.verified;
updater.set('emails.0.verified', userData.verified);
}

handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
handleBio(updater, userData.bio);
handleNickname(updater, userData.nickname);

await Users.updateOne({ _id }, updateUser as UpdateFilter<IUser>);
await Users.updateFromUpdater({ _id }, updater);

if (userData.sendWelcomeEmail) {
await sendWelcomeEmail(userData);
Expand Down
41 changes: 20 additions & 21 deletions apps/meteor/app/lib/server/functions/saveUser/saveUser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { isUserFederated } from '@rocket.chat/core-typings';
import type { DeepWritable, DeepPartial, IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings';
import type { IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import type { UpdateFilter } from 'mongodb';

import { callbacks } from '../../../../../lib/callbacks';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
Expand Down Expand Up @@ -46,6 +45,8 @@ export type SaveUserData = {
joinDefaultChannels?: boolean;
sendWelcomeEmail?: boolean;
};
export type UpdateUserData = RequiredField<SaveUserData, '_id'>;
export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id;

export const saveUser = async function (userId: IUser['_id'], userData: SaveUserData) {
const oldUserData = userData._id && (await Users.findOneById(userData._id));
Expand All @@ -72,20 +73,23 @@ export const saveUser = async function (userId: IUser['_id'], userData: SaveUser
delete userData.setRandomPassword;
}

if (!userData._id) {
if (!isUpdateUserData(userData)) {
return saveNewUser(userData, sendPassword);
}

await validateUserEditing(userId, userData as RequiredField<SaveUserData, '_id'>);
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,
}))
) {
throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', {
Expand All @@ -95,12 +99,12 @@ export const saveUser = async function (userId: IUser['_id'], userData: SaveUser
}

if (typeof userData.statusText === 'string') {
await setStatusText(userData._id, userData.statusText);
await setStatusText(userData._id, userData.statusText, updater);
}

if (userData.email) {
const shouldSendVerificationEmailToUser = userData.verified !== true;
await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser);
await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser, userData.verified === true, updater);
}

if (
Expand All @@ -113,37 +117,32 @@ export const saveUser = async function (userId: IUser['_id'], userData: SaveUser
sendPassword = false;
}

const updateUser: RequiredField<DeepWritable<UpdateFilter<DeepPartial<IUser>>>, '$set' | '$unset'> = {
$set: {},
$unset: {},
};

handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
handleBio(updater, userData.bio);
handleNickname(updater, userData.nickname);

if (userData.roles) {
updateUser.$set.roles = userData.roles;
updater.set('roles', userData.roles);
}
if (userData.settings) {
updateUser.$set.settings = { preferences: userData.settings.preferences };
updater.set('settings', { preferences: userData.settings.preferences });
}

if (userData.language) {
updateUser.$set.language = userData.language;
updater.set('language', userData.language);
}

if (typeof userData.requirePasswordChange !== 'undefined') {
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
updater.set('requirePasswordChange', userData.requirePasswordChange);
if (!userData.requirePasswordChange) {
updateUser.$unset.requirePasswordChangeReason = 1;
updater.unset('requirePasswordChangeReason');
}
}

if (typeof userData.verified === 'boolean') {
updateUser.$set['emails.0.verified'] = userData.verified;
if (typeof userData.verified === 'boolean' && !userData.email) {
updater.set('emails.0.verified', userData.verified);
}

await Users.updateOne({ _id: userData._id }, updateUser as UpdateFilter<IUser>);
await Users.updateFromUpdater({ _id: userData._id }, updater);

// App IPostUserUpdated event hook
const userUpdated = await Users.findOneById(userData._id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { settings } from '../../../../settings/server';
import { checkEmailAvailability } from '../checkEmailAvailability';
import { checkUsernameAvailability } from '../checkUsernameAvailability';
import type { SaveUserData } from './saveUser';
import { isUpdateUserData } from './saveUser';

export const validateUserData = makeFunction(async (userId: IUser['_id'], userData: SaveUserData): Promise<void> => {
const existingRoles = await getRoleIds();
Expand All @@ -21,14 +22,14 @@ export const validateUserData = makeFunction(async (userId: IUser['_id'], userDa
});
}

if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) {
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 (!userData._id && !(await hasPermissionAsync(userId, 'create-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',
Expand All @@ -52,14 +53,14 @@ export const validateUserData = makeFunction(async (userId: IUser['_id'], userDa
});
}

if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) {
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 (!userData._id && !trim(userData.username)) {
if (!isUpdateUserData(userData) && !trim(userData.username)) {
throw new MeteorError('error-the-field-is-required', 'The field Username is required', {
method: 'insertOrUpdateUser',
field: 'Username',
Expand All @@ -82,14 +83,14 @@ export const validateUserData = makeFunction(async (userId: IUser['_id'], userDa
});
}

if (!userData._id && !userData.password && !userData.setRandomPassword) {
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 (!userData._id) {
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',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { IUser, RequiredField } from '@rocket.chat/core-typings';
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';

import type { SaveUserData } from './saveUser';
import type { UpdateUserData } from './saveUser';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { settings } from '../../../../settings/server';

Expand All @@ -17,7 +17,7 @@ const isEditingField = (previousValue?: string, newValue?: string) => typeof new
* @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: RequiredField<SaveUserData, '_id'>): Promise<void> {
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');
Expand Down
7 changes: 5 additions & 2 deletions apps/meteor/app/lib/server/functions/saveUserIdentity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users } from '@rocket.chat/models';

import { _setRealName } from './setRealName';
Expand All @@ -23,11 +24,13 @@ export async function saveUserIdentity({
name: rawName,
username: rawUsername,
updateUsernameInBackground = false,
updater,
}: {
_id: string;
name?: string;
username?: string;
updateUsernameInBackground?: boolean; // TODO: remove this
updater?: Updater<IUser>;
}) {
if (!_id) {
return false;
Expand All @@ -51,14 +54,14 @@ export async function saveUserIdentity({
return false;
}

if (!(await _setUsername(_id, username, user))) {
if (!(await _setUsername(_id, username, user, updater))) {
return false;
}
user.username = username;
}

if (typeof rawName !== 'undefined' && nameChanged) {
if (!(await _setRealName(_id, name, user))) {
if (!(await _setRealName(_id, name, user, updater))) {
return false;
}
}
Expand Down
17 changes: 15 additions & 2 deletions apps/meteor/app/lib/server/functions/setEmail.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { Users } from '@rocket.chat/models';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -37,7 +39,13 @@ const _sendEmailChangeNotification = async function (to: string, newEmail: strin
}
};

const _setEmail = async function (userId: string, email: string, shouldSendVerificationEmail = true) {
const _setEmail = async function (
userId: string,
email: string,
shouldSendVerificationEmail = true,
verified = false,
updater?: Updater<IUser>,
) {
email = email.trim();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: '_setEmail' });
Expand Down Expand Up @@ -74,7 +82,12 @@ const _setEmail = async function (userId: string, email: string, shouldSendVerif
}

// Set new email
await Users.setEmail(user?._id, email);
if (updater) {
updater.set('emails', [{ address: email, verified }]);
} else {
await Users.setEmail(user?._id, email, verified);
}

const result = {
...user,
email,
Expand Down
16 changes: 14 additions & 2 deletions apps/meteor/app/lib/server/functions/setRealName.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { api } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { settings } from '../../../settings/server';
import { RateLimiter } from '../lib';

export const _setRealName = async function (userId: string, name: string, fullUser?: IUser): Promise<IUser | undefined> {
export const _setRealName = async function (
userId: string,
name: string,
fullUser?: IUser,
updater?: Updater<IUser>,
): Promise<IUser | undefined> {
name = name.trim();

if (!userId || (settings.get('Accounts_RequireNameForSignUp') && !name)) {
Expand All @@ -27,7 +33,13 @@ export const _setRealName = async function (userId: string, name: string, fullUs

// Set new name
if (name) {
await Users.setName(user._id, name);
if (updater) {
updater.set('name', name);
} else {
await Users.setName(user._id, name);
}
} else if (updater) {
updater.unset('name');
} else {
await Users.unsetName(user._id);
}
Expand Down
Loading

0 comments on commit b04342c

Please sign in to comment.