import { api } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { isUserNativeFederated } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Invites, Users, Subscriptions } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; import _ from 'underscore'; import { checkUsernameAvailability } from './checkUsernameAvailability'; import { getAvatarSuggestionForUser } from './getAvatarSuggestionForUser'; import { saveUserIdentity } from './saveUserIdentity'; import { setUserAvatar } from './setUserAvatar'; import { validateUsername } from './validateUsername'; import { onceTransactionCommitedSuccessfully } from '../../database/utils'; import { settings } from '../../settings'; import { callbacks } from '../callbacks'; import { SystemLogger } from '../logger/system'; import { notifyOnUserChange } from '../notifyListener'; import { addUserToRoom } from '../rooms/addUserToRoom'; import { joinDefaultChannels } from '../rooms/joinDefaultChannels'; const isUserInFederatedRooms = async (userId: string): Promise => { const cursor = Subscriptions.findUserFederatedRoomIds(userId); const hasAny = await cursor.hasNext(); await cursor.close(); return hasAny; }; export const setUsernameWithValidation = async (userId: string, username: string): Promise => { if (!username) { throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'setUsername' }); } const user = await Users.findOneById(userId); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setUsername' }); } if (isUserNativeFederated(user) || (await isUserInFederatedRooms(userId))) { throw new Meteor.Error('error-not-allowed', 'Cannot change username for federated users or users in federated rooms', { method: 'setUsername', }); } if (user.username && !settings.get('Accounts_AllowUsernameChange')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } if (user.username === username || (user.username && user.username.toLowerCase() === username.toLowerCase())) { return; } if (!validateUsername(username)) { throw new Meteor.Error( 'username-invalid', `${_.escape(username)} is not a valid username, use only letters, numbers, dots, hyphens and underscores`, ); } if (!(await checkUsernameAvailability(username))) { throw new Meteor.Error('error-field-unavailable', `${_.escape(username)} is already in use :(`, { method: 'setUsername', field: username, }); } if (!(await saveUserIdentity({ _id: user._id, username }))) { throw new Meteor.Error('error-could-not-change-username', 'Could not change username', { method: 'setUsername', }); } void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { username } }); }; export const _setUsername = async function ( userId: string, u: string, fullUser: IUser, updater?: Updater, session?: ClientSession, ): Promise { const username = u.trim(); if (!userId || !username) { return false; } if (!validateUsername(username)) { return false; } if (isUserNativeFederated(fullUser) || (await isUserInFederatedRooms(userId))) { throw new Meteor.Error('error-not-allowed', 'Cannot change username for federated users or users in federated rooms', { method: 'setUsername', }); } const user = fullUser || (await Users.findOneById(userId, { session })); // User already has desired username, return if (user.username === username) { return user; } const previousUsername = user.username; // Check username availability or if the user already owns a different casing of the name if (!previousUsername || !(username.toLowerCase() === previousUsername.toLowerCase())) { if (!(await checkUsernameAvailability(username))) { return false; } } // If first time setting username, send Enrollment Email if (!previousUsername && user.emails && user.emails.length > 0 && settings.get('Accounts_Enrollment_Email')) { await onceTransactionCommitedSuccessfully(() => { try { setImmediate(() => { Accounts.sendEnrollmentEmail(user._id); }); } catch (err: any) { SystemLogger.error({ err }); } }, session); } // Set new username* // TODO: use updater for setting the username and handle possible side effects in addUserToRoom await Users.setUsername(user._id, username, { session }); user.username = username; if (!previousUsername && settings.get('Accounts_SetDefaultAvatar') === true) { const avatarSuggestions = await getAvatarSuggestionForUser(user); let avatarData; let serviceName = 'gravatar'; for (const service of Object.keys(avatarSuggestions)) { avatarData = avatarSuggestions[service]; if (service !== 'gravatar') { serviceName = service; break; } } if (avatarData) { await setUserAvatar(user, avatarData.blob, avatarData.contentType, serviceName, undefined, updater, session); } } await onceTransactionCommitedSuccessfully(async () => { if (!previousUsername) { await joinDefaultChannels(user._id); callbacks.runAsync('afterCreateUser', user); } // If it's the first username and the user has an invite Token, then join the invite room if (!previousUsername && user.inviteToken) { const inviteData = await Invites.findOneById(user.inviteToken); if (inviteData?.rid) { await addUserToRoom(inviteData.rid, user); } } void api.broadcast('user.nameChanged', { _id: user._id, name: user.name, username: user.username, }); }, session); return user; };