Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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