You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
13 KiB
441 lines
13 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { Accounts } from 'meteor/accounts-base';
|
|
import _ from 'underscore';
|
|
import Gravatar from 'gravatar';
|
|
import { isUserFederated } from '@rocket.chat/core-typings';
|
|
import { Users } from '@rocket.chat/models';
|
|
|
|
import * as Mailer from '../../../mailer/server/api';
|
|
import { getRoles } from '../../../authorization/server';
|
|
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
|
|
import { settings } from '../../../settings/server';
|
|
import { passwordPolicy } from '../lib/passwordPolicy';
|
|
import { validateEmailDomain } from '../lib';
|
|
import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles';
|
|
import { saveUserIdentity } from './saveUserIdentity';
|
|
import { checkEmailAvailability, setUserAvatar, setEmail } from '.';
|
|
import { setStatusText } from './setStatusText';
|
|
import { checkUsernameAvailability } from './checkUsernameAvailability';
|
|
import { callbacks } from '../../../../lib/callbacks';
|
|
import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator';
|
|
import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser';
|
|
import { trim } from '../../../../lib/utils/stringUtils';
|
|
|
|
const MAX_BIO_LENGTH = 260;
|
|
const MAX_NICKNAME_LENGTH = 120;
|
|
|
|
let html = '';
|
|
let passwordChangedHtml = '';
|
|
Meteor.startup(() => {
|
|
Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => {
|
|
html = template;
|
|
});
|
|
|
|
Mailer.getTemplate('Password_Changed_Email', (template) => {
|
|
passwordChangedHtml = template;
|
|
});
|
|
});
|
|
|
|
async function _sendUserEmail(subject, html, userData) {
|
|
const email = {
|
|
to: userData.email,
|
|
from: settings.get('From_Email'),
|
|
subject,
|
|
html,
|
|
data: {
|
|
email: userData.email,
|
|
password: userData.password,
|
|
},
|
|
};
|
|
|
|
if (typeof userData.name !== 'undefined') {
|
|
email.data.name = userData.name;
|
|
}
|
|
|
|
try {
|
|
await Mailer.send(email);
|
|
} catch (error) {
|
|
throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, {
|
|
function: 'RocketChat.saveUser',
|
|
message: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function validateUserData(userId, userData) {
|
|
const existingRoles = _.pluck(await getRoles(), '_id');
|
|
|
|
if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Editing_user',
|
|
});
|
|
}
|
|
|
|
if (!userData._id && !(await hasPermissionAsync(userId, 'create-user'))) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Adding user is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Adding_user',
|
|
});
|
|
}
|
|
|
|
if (userData.roles && _.difference(userData.roles, existingRoles).length > 0) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'The field Roles consist invalid role id', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Assign_role',
|
|
});
|
|
}
|
|
|
|
if (userData.roles && userData.roles.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Assign_admin',
|
|
});
|
|
}
|
|
|
|
if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) {
|
|
throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', {
|
|
method: 'insertOrUpdateUser',
|
|
field: 'Name',
|
|
});
|
|
}
|
|
|
|
if (!userData._id && !trim(userData.username)) {
|
|
throw new Meteor.Error('error-the-field-is-required', 'The field Username is required', {
|
|
method: 'insertOrUpdateUser',
|
|
field: 'Username',
|
|
});
|
|
}
|
|
|
|
if (userData.roles) {
|
|
await callbacks.run('validateUserRoles', userData);
|
|
}
|
|
|
|
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 Meteor.Error('error-input-is-not-a-valid-field', `${_.escape(userData.username)} is not a valid username`, {
|
|
method: 'insertOrUpdateUser',
|
|
input: userData.username,
|
|
field: 'Username',
|
|
});
|
|
}
|
|
|
|
if (!userData._id && !userData.password && !userData.setRandomPassword) {
|
|
throw new Meteor.Error('error-the-field-is-required', 'The field Password is required', {
|
|
method: 'insertOrUpdateUser',
|
|
field: 'Password',
|
|
});
|
|
}
|
|
|
|
if (!userData._id) {
|
|
if (!(await checkUsernameAvailability(userData.username))) {
|
|
throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.username)} is already in use :(`, {
|
|
method: 'insertOrUpdateUser',
|
|
field: userData.username,
|
|
});
|
|
}
|
|
|
|
if (userData.email && !(await checkEmailAvailability(userData.email))) {
|
|
throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.email)} is already in use :(`, {
|
|
method: 'insertOrUpdateUser',
|
|
field: userData.email,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, userData) {
|
|
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);
|
|
|
|
const isEditingUserRoles = (previousRoles, newRoles) =>
|
|
typeof newRoles !== 'undefined' && !_.isEqual(_.sortBy(previousRoles), _.sortBy(newRoles));
|
|
const isEditingField = (previousValue, newValue) => typeof newValue !== 'undefined' && newValue !== previousValue;
|
|
|
|
if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Assign roles is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Assign_role',
|
|
});
|
|
}
|
|
|
|
if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) {
|
|
throw new Meteor.Error('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') &&
|
|
(!canEditOtherUserInfo || editingMyself)
|
|
) {
|
|
throw new Meteor.Error('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 Meteor.Error('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 Meteor.Error('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 Meteor.Error('error-action-not-allowed', 'Edit user email is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Update_user',
|
|
});
|
|
}
|
|
|
|
if (userData.password && !settings.get('Accounts_AllowPasswordChange') && (!canEditOtherUserPassword || editingMyself)) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Edit user password is not allowed', {
|
|
method: 'insertOrUpdateUser',
|
|
action: 'Update_user',
|
|
});
|
|
}
|
|
}
|
|
|
|
const handleBio = (updateUser, bio) => {
|
|
if (bio && bio.trim()) {
|
|
if (typeof bio !== 'string' || bio.length > MAX_BIO_LENGTH) {
|
|
throw new Meteor.Error('error-invalid-field', 'bio', {
|
|
method: 'saveUserProfile',
|
|
});
|
|
}
|
|
updateUser.$set = updateUser.$set || {};
|
|
updateUser.$set.bio = bio;
|
|
} else {
|
|
updateUser.$unset = updateUser.$unset || {};
|
|
updateUser.$unset.bio = 1;
|
|
}
|
|
};
|
|
|
|
const handleNickname = (updateUser, nickname) => {
|
|
if (nickname && nickname.trim()) {
|
|
if (typeof nickname !== 'string' || nickname.length > MAX_NICKNAME_LENGTH) {
|
|
throw new Meteor.Error('error-invalid-field', 'nickname', {
|
|
method: 'saveUserProfile',
|
|
});
|
|
}
|
|
updateUser.$set = updateUser.$set || {};
|
|
updateUser.$set.nickname = nickname;
|
|
} else {
|
|
updateUser.$unset = updateUser.$unset || {};
|
|
updateUser.$unset.nickname = 1;
|
|
}
|
|
};
|
|
|
|
const saveNewUser = async function (userData, sendPassword) {
|
|
await validateEmailDomain(userData.email);
|
|
|
|
const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles();
|
|
const isGuest = roles && roles.length === 1 && roles.includes('guest');
|
|
|
|
// insert user
|
|
const createUser = {
|
|
username: userData.username,
|
|
password: userData.password,
|
|
joinDefaultChannels: userData.joinDefaultChannels,
|
|
isGuest,
|
|
};
|
|
if (userData.email) {
|
|
createUser.email = userData.email;
|
|
}
|
|
|
|
const _id = await Accounts.createUserAsync(createUser);
|
|
|
|
const updateUser = {
|
|
$set: {
|
|
roles,
|
|
...(typeof userData.name !== 'undefined' && { name: userData.name }),
|
|
settings: userData.settings || {},
|
|
},
|
|
};
|
|
|
|
if (typeof userData.requirePasswordChange !== 'undefined') {
|
|
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
|
|
}
|
|
|
|
if (typeof userData.verified === 'boolean') {
|
|
updateUser.$set['emails.0.verified'] = userData.verified;
|
|
}
|
|
|
|
handleBio(updateUser, userData.bio);
|
|
handleNickname(updateUser, userData.nickname);
|
|
|
|
await Users.updateOne({ _id }, updateUser);
|
|
|
|
if (userData.sendWelcomeEmail) {
|
|
await _sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData);
|
|
}
|
|
|
|
if (sendPassword) {
|
|
await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
|
|
}
|
|
|
|
userData._id = _id;
|
|
|
|
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) {
|
|
const gravatarUrl = Gravatar.url(userData.email, {
|
|
default: '404',
|
|
size: '200',
|
|
protocol: 'https',
|
|
});
|
|
|
|
try {
|
|
await setUserAvatar(userData, gravatarUrl, '', 'url');
|
|
} catch (e) {
|
|
// Ignore this error for now, as it not being successful isn't bad
|
|
}
|
|
}
|
|
|
|
return _id;
|
|
};
|
|
|
|
export const saveUser = async function (userId, userData) {
|
|
const oldUserData = 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);
|
|
let sendPassword = false;
|
|
|
|
if (userData.hasOwnProperty('setRandomPassword')) {
|
|
if (userData.setRandomPassword) {
|
|
userData.password = passwordPolicy.generatePassword();
|
|
userData.requirePasswordChange = true;
|
|
sendPassword = true;
|
|
}
|
|
|
|
delete userData.setRandomPassword;
|
|
}
|
|
|
|
if (!userData._id) {
|
|
return saveNewUser(userData, sendPassword);
|
|
}
|
|
|
|
await validateUserEditing(userId, userData);
|
|
|
|
// update user
|
|
if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) {
|
|
if (
|
|
!(await saveUserIdentity({
|
|
_id: userData._id,
|
|
username: userData.username,
|
|
name: userData.name,
|
|
}))
|
|
) {
|
|
throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', {
|
|
method: 'saveUser',
|
|
});
|
|
}
|
|
}
|
|
|
|
if (typeof userData.statusText === 'string') {
|
|
await setStatusText(userData._id, userData.statusText);
|
|
}
|
|
|
|
if (userData.email) {
|
|
const shouldSendVerificationEmailToUser = userData.verified !== true;
|
|
await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser);
|
|
}
|
|
|
|
if (
|
|
userData.password &&
|
|
userData.password.trim() &&
|
|
(await hasPermissionAsync(userId, 'edit-other-user-password')) &&
|
|
passwordPolicy.validate(userData.password)
|
|
) {
|
|
await Accounts.setPasswordAsync(userData._id, userData.password.trim());
|
|
} else {
|
|
sendPassword = false;
|
|
}
|
|
|
|
const updateUser = {
|
|
$set: {},
|
|
};
|
|
|
|
handleBio(updateUser, userData.bio);
|
|
handleNickname(updateUser, userData.nickname);
|
|
|
|
if (userData.roles) {
|
|
updateUser.$set.roles = userData.roles;
|
|
}
|
|
if (userData.settings) {
|
|
updateUser.$set.settings = { preferences: userData.settings.preferences };
|
|
}
|
|
|
|
if (userData.language) {
|
|
updateUser.$set.language = userData.language;
|
|
}
|
|
|
|
if (typeof userData.requirePasswordChange !== 'undefined') {
|
|
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
|
|
}
|
|
|
|
if (typeof userData.verified === 'boolean') {
|
|
updateUser.$set['emails.0.verified'] = userData.verified;
|
|
}
|
|
|
|
await Users.updateOne({ _id: userData._id }, updateUser);
|
|
|
|
// App IPostUserUpdated event hook
|
|
const userUpdated = await Users.findOneById(userId);
|
|
|
|
await callbacks.run('afterSaveUser', {
|
|
user: userUpdated,
|
|
oldUser: oldUserData,
|
|
});
|
|
|
|
await Apps.triggerEvent(AppEvents.IPostUserUpdated, {
|
|
user: userUpdated,
|
|
previousUser: oldUserData,
|
|
performedBy: await safeGetMeteorUser(),
|
|
});
|
|
|
|
if (sendPassword) {
|
|
await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|