diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts
index a1dd63713747862158eff86f1dc8bc64ba5ca7d4..ebc4871610e3308847b2d347a4258a1953607d5d 100644
--- a/apps/meteor/app/api/server/api.d.ts
+++ b/apps/meteor/app/api/server/api.d.ts
@@ -9,7 +9,7 @@ import type {
 } from '@rocket.chat/rest-typings';
 import type { IUser, IMethodConnection, IRoom } from '@rocket.chat/core-typings';
 import type { ValidateFunction } from 'ajv';
-import type { Request } from 'express';
+import type { Request, Response } from 'express';
 
 import { ITwoFactorOptions } from '../../2fa/server/code';
 
@@ -73,11 +73,13 @@ type Options = (
 
 type PartialThis = {
 	readonly request: Request & { query: Record<string, string> };
+	readonly response: Response;
 };
 
 type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptions> = {
 	readonly requestIp: string;
 	urlParams: UrlParams<TPathPattern>;
+	readonly response: Response;
 	// TODO make it unsafe
 	readonly queryParams: TMethod extends 'GET'
 		? TOptions extends { validateParams: ValidateFunction<infer T> }
@@ -91,6 +93,9 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio
 		? T
 		: Partial<OperationParams<TMethod, TPathPattern>>;
 	readonly request: Request;
+
+	readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never;
+
 	/* @deprecated */
 	requestParams(): OperationParams<TMethod, TPathPattern>;
 	getLoggedInUser(): TOptions extends { authRequired: true } ? IUser : IUser | undefined;
@@ -106,6 +111,8 @@ type ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptio
 	/* @deprecated */
 	getUserFromParams(): IUser;
 	/* @deprecated */
+	isUserFromParams(): boolean;
+	/* @deprecated */
 	getUserInfo(me: IUser): TOptions extends { authRequired: true }
 		? IUser & {
 				email?: string;
@@ -153,6 +160,8 @@ type Operations<TPathPattern extends PathPattern, TOptions extends Options = {}>
 declare class APIClass<TBasePath extends string = '/'> {
 	fieldSeparator: string;
 
+	updateRateLimiterDictionaryForRoute(route: string, rateLimiterDictionary: number): void;
+
 	limitedUserFieldsToExclude(fields: { [x: string]: unknown }, limitedUserFieldsToExclude: unknown): { [x: string]: unknown };
 
 	limitedUserFieldsToExcludeIfIsPrivilegedUser(
diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts
index 8ff1737cc6924b7ef642d269e2919affec7c5861..7762b9b20a18391363753fd4120b1221c0f08764 100644
--- a/apps/meteor/app/api/server/lib/users.ts
+++ b/apps/meteor/app/api/server/lib/users.ts
@@ -16,7 +16,7 @@ export async function findUsersToAutocomplete({
 		term: string;
 	};
 }): Promise<{
-	items: IUser[];
+	items: Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>[];
 }> {
 	if (!(await hasPermissionAsync(uid, 'view-outside-room'))) {
 		return { items: [] };
@@ -69,16 +69,7 @@ export function getInclusiveFields(query: { [k: string]: 1 }): {} {
  * get the default fields if **fields** are empty (`{}`) or `undefined`/`null`
  * @param {Object|null|undefined} fields the fields from parsed jsonQuery
  */
-export function getNonEmptyFields(fields: {}): {
-	name: number;
-	username: number;
-	emails: number;
-	roles: number;
-	status: number;
-	active: number;
-	avatarETag: number;
-	lastLogin: number;
-} {
+export function getNonEmptyFields(fields: { [k: string]: 1 | 0 }): { [k: string]: 1 } {
 	const defaultFields = {
 		name: 1,
 		username: 1,
@@ -88,7 +79,7 @@ export function getNonEmptyFields(fields: {}): {
 		active: 1,
 		avatarETag: 1,
 		lastLogin: 1,
-	};
+	} as const;
 
 	if (!fields || Object.keys(fields).length === 0) {
 		return defaultFields;
diff --git a/apps/meteor/app/api/server/v1/users.js b/apps/meteor/app/api/server/v1/users.ts
similarity index 66%
rename from apps/meteor/app/api/server/v1/users.js
rename to apps/meteor/app/api/server/v1/users.ts
index 094570518e2d04847afa8fef884d45a5d36d4ac7..8e6808fd34b42cdd2d1d9753cb6f630a3d320ed5 100644
--- a/apps/meteor/app/api/server/v1/users.js
+++ b/apps/meteor/app/api/server/v1/users.ts
@@ -1,57 +1,236 @@
+import {
+	isUserCreateParamsPOST,
+	isUserSetActiveStatusParamsPOST,
+	isUserDeactivateIdleParamsPOST,
+	isUsersInfoParamsGetProps,
+	isUserRegisterParamsPOST,
+	isUserLogoutParamsPOST,
+	isUsersListTeamsProps,
+	isUsersAutocompleteProps,
+	isUsersSetAvatarProps,
+	isUsersUpdateParamsPOST,
+	isUsersUpdateOwnBasicInfoParamsPOST,
+	isUsersSetPreferencesParamsPOST,
+} from '@rocket.chat/rest-typings';
 import { Meteor } from 'meteor/meteor';
 import { Accounts } from 'meteor/accounts-base';
 import { Match, check } from 'meteor/check';
 import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-import _ from 'underscore';
+import { IExportOperation, IPersonalAccessToken, IUser } from '@rocket.chat/core-typings';
 
 import { Users, Subscriptions } from '../../../models/server';
 import { Users as UsersRaw } from '../../../models/server/raw';
-import { hasPermission } from '../../../authorization';
+import { hasPermission } from '../../../authorization/server';
 import { settings } from '../../../settings/server';
-import { getURL } from '../../../utils';
 import {
 	validateCustomFields,
 	saveUser,
 	saveCustomFieldsWithoutValidation,
 	checkUsernameAvailability,
+	setStatusText,
 	setUserAvatar,
 	saveCustomFields,
-	setStatusText,
 } from '../../../lib/server';
 import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData';
 import { API } from '../api';
-import { getUploadFormData } from '../lib/getUploadFormData';
 import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users';
 import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
 import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
-import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers';
 import { resetTOTP } from '../../../2fa/server/functions/resetTOTP';
 import { Team } from '../../../../server/sdk';
 import { isValidQuery } from '../lib/isValidQuery';
+import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers';
+import { getURL } from '../../../utils/server';
+import { getUploadFormData } from '../lib/getUploadFormData';
 
 API.v1.addRoute(
-	'users.create',
-	{ authRequired: true },
+	'users.getAvatar',
+	{ authRequired: false },
+	{
+		get() {
+			const user = this.getUserFromParams();
+
+			const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true });
+			this.response.setHeader('Location', url);
+
+			return {
+				statusCode: 307,
+				body: url,
+			};
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.update',
+	{ authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST },
+	{
+		post() {
+			const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data };
+
+			Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData));
+
+			if (this.bodyParams.data.customFields) {
+				saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields);
+			}
+
+			if (typeof this.bodyParams.data.active !== 'undefined') {
+				const {
+					userId,
+					data: { active },
+					confirmRelinquish,
+				} = this.bodyParams;
+
+				Meteor.call('setUserActiveStatus', userId, active, Boolean(confirmRelinquish));
+			}
+			const { fields } = this.parseJsonQuery();
+
+			return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) });
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.updateOwnBasicInfo',
+	{ authRequired: true, validateParams: isUsersUpdateOwnBasicInfoParamsPOST },
+	{
+		post() {
+			const userData = {
+				email: this.bodyParams.data.email,
+				realname: this.bodyParams.data.name,
+				username: this.bodyParams.data.username,
+				nickname: this.bodyParams.data.nickname,
+				statusText: this.bodyParams.data.statusText,
+				newPassword: this.bodyParams.data.newPassword,
+				typedPassword: this.bodyParams.data.currentPassword,
+			};
+
+			// saveUserProfile now uses the default two factor authentication procedures, so we need to provide that
+			const twoFactorOptions = !userData.typedPassword
+				? null
+				: {
+						twoFactorCode: userData.typedPassword,
+						twoFactorMethod: 'password',
+				  };
+
+			Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions);
+
+			return API.v1.success({
+				user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }),
+			});
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.setPreferences',
+	{ authRequired: true, validateParams: isUsersSetPreferencesParamsPOST },
 	{
 		post() {
-			check(this.bodyParams, {
-				email: String,
-				name: String,
-				password: String,
-				username: String,
-				active: Match.Maybe(Boolean),
-				bio: Match.Maybe(String),
-				nickname: Match.Maybe(String),
-				statusText: Match.Maybe(String),
-				roles: Match.Maybe(Array),
-				joinDefaultChannels: Match.Maybe(Boolean),
-				requirePasswordChange: Match.Maybe(Boolean),
-				setRandomPassword: Match.Maybe(Boolean),
-				sendWelcomeEmail: Match.Maybe(Boolean),
-				verified: Match.Maybe(Boolean),
-				customFields: Match.Maybe(Object),
+			if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) {
+				throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed');
+			}
+			const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId;
+			if (!Users.findOneById(userId)) {
+				throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users');
+			}
+
+			Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data));
+			const user = Users.findOneById(userId, {
+				fields: {
+					'settings.preferences': 1,
+					'language': 1,
+				},
 			});
+			return API.v1.success({
+				user: {
+					_id: user._id,
+					settings: {
+						preferences: {
+							...user.settings.preferences,
+							language: user.language,
+						},
+					},
+				},
+			});
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.setAvatar',
+	{ authRequired: true, validateParams: isUsersSetAvatarProps },
+	{
+		async post() {
+			const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar');
+
+			if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) {
+				throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', {
+					method: 'users.setAvatar',
+				});
+			}
+
+			let user = ((): IUser | undefined => {
+				if (this.isUserFromParams()) {
+					return Meteor.users.findOne(this.userId) as IUser | undefined;
+				}
+				if (canEditOtherUserAvatar) {
+					return this.getUserFromParams();
+				}
+			})();
+
+			if (!user) {
+				return API.v1.unauthorized();
+			}
+
+			if (this.bodyParams.avatarUrl) {
+				setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url');
+				return API.v1.success();
+			}
+
+			const [image, fields] = await getUploadFormData(
+				{
+					request: this.request,
+				},
+				{
+					field: 'image',
+				},
+			);
+
+			if (!image) {
+				return API.v1.failure("The 'image' param is required");
+			}
+
+			const sentTheUserByFormData = fields.userId || fields.username;
+			if (sentTheUserByFormData) {
+				if (fields.userId) {
+					user = Users.findOneById(fields.userId, { fields: { username: 1 } });
+				} else if (fields.username) {
+					user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } });
+				}
+
+				if (!user) {
+					throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users');
+				}
+
+				const isAnotherUser = this.userId !== user._id;
+				if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) {
+					throw new Meteor.Error('error-not-allowed', 'Not allowed');
+				}
+			}
 
+			setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest');
+
+			return API.v1.success();
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.create',
+	{ authRequired: true, validateParams: isUserCreateParamsPOST },
+	{
+		post() {
 			// New change made by pull request #5152
 			if (typeof this.bodyParams.joinDefaultChannels === 'undefined') {
 				this.bodyParams.joinDefaultChannels = true;
@@ -68,9 +247,7 @@ API.v1.addRoute(
 			}
 
 			if (typeof this.bodyParams.active !== 'undefined') {
-				Meteor.runAsUser(this.userId, () => {
-					Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active);
-				});
+				Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active);
 			}
 
 			const { fields } = this.parseJsonQuery();
@@ -92,9 +269,7 @@ API.v1.addRoute(
 			const user = this.getUserFromParams();
 			const { confirmRelinquish = false } = this.requestParams();
 
-			Meteor.runAsUser(this.userId, () => {
-				Meteor.call('deleteUser', user._id, confirmRelinquish);
-			});
+			Meteor.call('deleteUser', user._id, confirmRelinquish);
 
 			return API.v1.success();
 		},
@@ -116,52 +291,24 @@ API.v1.addRoute(
 
 			const { confirmRelinquish = false } = this.requestParams();
 
-			Meteor.runAsUser(this.userId, () => {
-				Meteor.call('deleteUserOwnAccount', password, confirmRelinquish);
-			});
+			Meteor.call('deleteUserOwnAccount', password, confirmRelinquish);
 
 			return API.v1.success();
 		},
 	},
 );
 
-API.v1.addRoute(
-	'users.getAvatar',
-	{ authRequired: false },
-	{
-		get() {
-			const user = this.getUserFromParams();
-
-			const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true });
-			this.response.setHeader('Location', url);
-
-			return {
-				statusCode: 307,
-				body: url,
-			};
-		},
-	},
-);
-
 API.v1.addRoute(
 	'users.setActiveStatus',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUserSetActiveStatusParamsPOST },
 	{
 		post() {
-			check(this.bodyParams, {
-				userId: String,
-				activeStatus: Boolean,
-				confirmRelinquish: Match.Maybe(Boolean),
-			});
-
 			if (!hasPermission(this.userId, 'edit-other-user-active-status')) {
 				return API.v1.unauthorized();
 			}
 
-			Meteor.runAsUser(this.userId, () => {
-				const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams;
-				Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish);
-			});
+			const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams;
+			Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish);
 			return API.v1.success({
 				user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }),
 			});
@@ -171,14 +318,9 @@ API.v1.addRoute(
 
 API.v1.addRoute(
 	'users.deactivateIdle',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUserDeactivateIdleParamsPOST },
 	{
 		post() {
-			check(this.bodyParams, {
-				daysIdle: Match.Integer,
-				role: Match.Optional(String),
-			});
-
 			if (!hasPermission(this.userId, 'edit-other-user-active-status')) {
 				return API.v1.unauthorized();
 			}
@@ -197,68 +339,41 @@ API.v1.addRoute(
 	},
 );
 
-API.v1.addRoute(
-	'users.getPresence',
-	{ authRequired: true },
-	{
-		get() {
-			if (this.isUserFromParams()) {
-				const user = Users.findOneById(this.userId);
-				return API.v1.success({
-					presence: user.status,
-					connectionStatus: user.statusConnection,
-					lastLogin: user.lastLogin,
-				});
-			}
-
-			const user = this.getUserFromParams();
-
-			return API.v1.success({
-				presence: user.status,
-			});
-		},
-	},
-);
-
 API.v1.addRoute(
 	'users.info',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUsersInfoParamsGetProps },
 	{
-		get() {
-			const { username, userId } = this.requestParams();
+		async get() {
 			const { fields } = this.parseJsonQuery();
 
-			check(userId, Match.Maybe(String));
-			check(username, Match.Maybe(String));
-
-			if (userId !== undefined && username !== undefined) {
-				throw new Meteor.Error('invalid-filter', 'Cannot filter by id and username at once');
-			}
-
-			if (!userId && !username) {
-				throw new Meteor.Error('invalid-filter', 'Must filter by id or username');
-			}
-
-			const user = getFullUserDataByIdOrUsername({ userId: this.userId, filterId: userId, filterUsername: username });
+			const user = await getFullUserDataByIdOrUsername(this.userId, {
+				filterId: (this.queryParams as any).userId,
+				filterUsername: (this.queryParams as any).username,
+			});
 
 			if (!user) {
 				return API.v1.failure('User not found.');
 			}
 			const myself = user._id === this.userId;
 			if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) {
-				user.rooms = Subscriptions.findByUserId(user._id, {
-					fields: {
-						rid: 1,
-						name: 1,
-						t: 1,
-						roles: 1,
-						unread: 1,
-					},
-					sort: {
-						t: 1,
-						name: 1,
+				return API.v1.success({
+					user: {
+						...user,
+						rooms: Subscriptions.findByUserId(user._id, {
+							projection: {
+								rid: 1,
+								name: 1,
+								t: 1,
+								roles: 1,
+								unread: 1,
+							},
+							sort: {
+								t: 1,
+								name: 1,
+							},
+						}).fetch(),
 					},
-				}).fetch();
+				});
 			}
 
 			return API.v1.success({
@@ -298,14 +413,14 @@ API.v1.addRoute(
 						inclusiveFieldsKeys.includes('emails') && 'emails.address.*',
 						inclusiveFieldsKeys.includes('username') && 'username.*',
 						inclusiveFieldsKeys.includes('name') && 'name.*',
-					].filter(Boolean),
+					].filter(Boolean) as string[],
 					this.queryOperations,
 				)
 			) {
 				throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n'));
 			}
 
-			const actualSort = sort && sort.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 };
+			const actualSort = sort?.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 };
 
 			const limit =
 				count !== 0
@@ -373,6 +488,7 @@ API.v1.addRoute(
 			numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser'),
 			intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'),
 		},
+		validateParams: isUserRegisterParamsPOST,
 	},
 	{
 		post() {
@@ -380,306 +496,40 @@ API.v1.addRoute(
 				return API.v1.failure('Logged in users can not register again.');
 			}
 
-			// We set their username here, so require it
-			// The `registerUser` checks for the other requirements
-			check(
-				this.bodyParams,
-				Match.ObjectIncluding({
-					username: String,
-				}),
-			);
-
 			if (!checkUsernameAvailability(this.bodyParams.username)) {
 				return API.v1.failure('Username is already in use');
 			}
 
 			// Register the user
-			const userId = Meteor.call('registerUser', this.bodyParams);
-
-			// Now set their username
-			Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username));
-			const { fields } = this.parseJsonQuery();
-
-			return API.v1.success({ user: Users.findOneById(userId, { fields }) });
-		},
-	},
-);
-
-API.v1.addRoute(
-	'users.resetAvatar',
-	{ authRequired: true },
-	{
-		post() {
-			const user = this.getUserFromParams();
-
-			if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) {
-				Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar'));
-			} else if (hasPermission(this.userId, 'edit-other-user-avatar')) {
-				Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id));
-			} else {
-				throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', {
-					method: 'users.resetAvatar',
-				});
-			}
-
-			return API.v1.success();
-		},
-	},
-);
-
-API.v1.addRoute(
-	'users.setAvatar',
-	{ authRequired: true },
-	{
-		async post() {
-			check(
-				this.bodyParams,
-				Match.ObjectIncluding({
-					avatarUrl: Match.Maybe(String),
-					userId: Match.Maybe(String),
-					username: Match.Maybe(String),
-				}),
-			);
-			const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar');
-
-			if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) {
-				throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', {
-					method: 'users.setAvatar',
-				});
-			}
-
-			let user;
-			if (this.isUserFromParams()) {
-				user = Meteor.users.findOne(this.userId);
-			} else if (canEditOtherUserAvatar) {
-				user = this.getUserFromParams();
-			} else {
-				return API.v1.unauthorized();
-			}
-
-			if (this.bodyParams.avatarUrl) {
-				setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url');
-				return API.v1.success();
-			}
-
-			const [image, fields] = await getUploadFormData(
-				{
-					request: this.request,
-				},
-				{ field: 'image' },
-			);
-
-			if (!image) {
-				return API.v1.failure("The 'image' param is required");
-			}
-
-			const sentTheUserByFormData = fields.userId || fields.username;
-			if (sentTheUserByFormData) {
-				if (fields.userId) {
-					user = Users.findOneById(fields.userId, { fields: { username: 1 } });
-				} else if (fields.username) {
-					user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } });
-				}
-
-				if (!user) {
-					throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users');
-				}
-
-				const isAnotherUser = this.userId !== user._id;
-				if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) {
-					throw new Meteor.Error('error-not-allowed', 'Not allowed');
-				}
-			}
-
-			setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest');
-
-			return API.v1.success();
-		},
-	},
-);
-
-API.v1.addRoute(
-	'users.getStatus',
-	{ authRequired: true },
-	{
-		get() {
-			if (this.isUserFromParams()) {
-				const user = Users.findOneById(this.userId);
-				return API.v1.success({
-					_id: user._id,
-					message: user.statusText,
-					connectionStatus: user.statusConnection,
-					status: user.status,
-				});
-			}
-
-			const user = this.getUserFromParams();
-
-			return API.v1.success({
-				_id: user._id,
-				message: user.statusText,
-				status: user.status,
-			});
-		},
-	},
-);
-
-API.v1.addRoute(
-	'users.setStatus',
-	{ authRequired: true },
-	{
-		post() {
-			check(
-				this.bodyParams,
-				Match.ObjectIncluding({
-					status: Match.Maybe(String),
-					message: Match.Maybe(String),
-				}),
-			);
-
-			if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
-				throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', {
-					method: 'users.setStatus',
-				});
-			}
-
-			let user;
-			if (this.isUserFromParams()) {
-				user = Meteor.users.findOne(this.userId);
-			} else if (hasPermission(this.userId, 'edit-other-user-info')) {
-				user = this.getUserFromParams();
-			} else {
-				return API.v1.unauthorized();
-			}
-
-			Meteor.runAsUser(user._id, () => {
-				if (this.bodyParams.message || this.bodyParams.message === '') {
-					setStatusText(user._id, this.bodyParams.message);
-				}
-				if (this.bodyParams.status) {
-					const validStatus = ['online', 'away', 'offline', 'busy'];
-					if (validStatus.includes(this.bodyParams.status)) {
-						const { status } = this.bodyParams;
-
-						if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) {
-							throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', {
-								method: 'users.setStatus',
-							});
-						}
-
-						Meteor.users.update(user._id, {
-							$set: {
-								status,
-								statusDefault: status,
-							},
-						});
-
-						setUserStatus(user, status);
-					} else {
-						throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', {
-							method: 'users.setStatus',
-						});
-					}
-				}
-			});
-
-			return API.v1.success();
-		},
-	},
-);
-
-API.v1.addRoute(
-	'users.update',
-	{ authRequired: true, twoFactorRequired: true },
-	{
-		post() {
-			check(this.bodyParams, {
-				userId: String,
-				data: Match.ObjectIncluding({
-					email: Match.Maybe(String),
-					name: Match.Maybe(String),
-					password: Match.Maybe(String),
-					username: Match.Maybe(String),
-					bio: Match.Maybe(String),
-					nickname: Match.Maybe(String),
-					statusText: Match.Maybe(String),
-					active: Match.Maybe(Boolean),
-					roles: Match.Maybe(Array),
-					joinDefaultChannels: Match.Maybe(Boolean),
-					requirePasswordChange: Match.Maybe(Boolean),
-					sendWelcomeEmail: Match.Maybe(Boolean),
-					verified: Match.Maybe(Boolean),
-					customFields: Match.Maybe(Object),
-				}),
-			});
-
-			const userData = _.extend({ _id: this.bodyParams.userId }, this.bodyParams.data);
-
-			Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData));
-
-			if (this.bodyParams.data.customFields) {
-				saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields);
-			}
-
-			if (typeof this.bodyParams.data.active !== 'undefined') {
-				const {
-					userId,
-					data: { active },
-					confirmRelinquish = false,
-				} = this.bodyParams;
+			const userId = Meteor.call('registerUser', this.bodyParams);
 
-				Meteor.runAsUser(this.userId, () => {
-					Meteor.call('setUserActiveStatus', userId, active, confirmRelinquish);
-				});
-			}
+			// Now set their username
+			Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username));
 			const { fields } = this.parseJsonQuery();
 
-			return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) });
+			return API.v1.success({ user: Users.findOneById(userId, { fields }) });
 		},
 	},
 );
 
 API.v1.addRoute(
-	'users.updateOwnBasicInfo',
+	'users.resetAvatar',
 	{ authRequired: true },
 	{
 		post() {
-			check(this.bodyParams, {
-				data: Match.ObjectIncluding({
-					email: Match.Maybe(String),
-					name: Match.Maybe(String),
-					username: Match.Maybe(String),
-					nickname: Match.Maybe(String),
-					statusText: Match.Maybe(String),
-					currentPassword: Match.Maybe(String),
-					newPassword: Match.Maybe(String),
-				}),
-				customFields: Match.Maybe(Object),
-			});
-
-			const userData = {
-				email: this.bodyParams.data.email,
-				realname: this.bodyParams.data.name,
-				username: this.bodyParams.data.username,
-				nickname: this.bodyParams.data.nickname,
-				statusText: this.bodyParams.data.statusText,
-				newPassword: this.bodyParams.data.newPassword,
-				typedPassword: this.bodyParams.data.currentPassword,
-			};
-
-			// saveUserProfile now uses the default two factor authentication procedures, so we need to provide that
-			const twoFactorOptions = !userData.typedPassword
-				? null
-				: {
-						twoFactorCode: userData.typedPassword,
-						twoFactorMethod: 'password',
-				  };
+			const user = this.getUserFromParams();
 
-			Meteor.runAsUser(this.userId, () => Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions));
+			if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) {
+				Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar'));
+			} else if (hasPermission(this.userId, 'edit-other-user-avatar')) {
+				Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id));
+			} else {
+				throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', {
+					method: 'users.resetAvatar',
+				});
+			}
 
-			return API.v1.success({
-				user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }),
-			});
+			return API.v1.success();
 		},
 	},
 );
@@ -690,10 +540,7 @@ API.v1.addRoute(
 	{
 		post() {
 			const user = this.getUserFromParams();
-			let data;
-			Meteor.runAsUser(this.userId, () => {
-				data = Meteor.call('createToken', user._id);
-			});
+			const data = Meteor.call('createToken', user._id);
 			return data ? API.v1.success({ data }) : API.v1.unauthorized();
 		},
 	},
@@ -718,77 +565,6 @@ API.v1.addRoute(
 	},
 );
 
-API.v1.addRoute(
-	'users.setPreferences',
-	{ authRequired: true },
-	{
-		post() {
-			check(this.bodyParams, {
-				userId: Match.Maybe(String),
-				data: Match.ObjectIncluding({
-					newRoomNotification: Match.Maybe(String),
-					newMessageNotification: Match.Maybe(String),
-					clockMode: Match.Maybe(Number),
-					useEmojis: Match.Maybe(Boolean),
-					convertAsciiEmoji: Match.Maybe(Boolean),
-					saveMobileBandwidth: Match.Maybe(Boolean),
-					collapseMediaByDefault: Match.Maybe(Boolean),
-					autoImageLoad: Match.Maybe(Boolean),
-					emailNotificationMode: Match.Maybe(String),
-					unreadAlert: Match.Maybe(Boolean),
-					notificationsSoundVolume: Match.Maybe(Number),
-					desktopNotifications: Match.Maybe(String),
-					pushNotifications: Match.Maybe(String),
-					enableAutoAway: Match.Maybe(Boolean),
-					highlights: Match.Maybe(Array),
-					desktopNotificationRequireInteraction: Match.Maybe(Boolean),
-					messageViewMode: Match.Maybe(Number),
-					showMessageInMainThread: Match.Maybe(Boolean),
-					hideUsernames: Match.Maybe(Boolean),
-					hideRoles: Match.Maybe(Boolean),
-					displayAvatars: Match.Maybe(Boolean),
-					hideFlexTab: Match.Maybe(Boolean),
-					sendOnEnter: Match.Maybe(String),
-					language: Match.Maybe(String),
-					sidebarShowFavorites: Match.Optional(Boolean),
-					sidebarShowUnread: Match.Optional(Boolean),
-					sidebarSortby: Match.Optional(String),
-					sidebarViewMode: Match.Optional(String),
-					sidebarDisplayAvatar: Match.Optional(Boolean),
-					sidebarGroupByType: Match.Optional(Boolean),
-					muteFocusedConversations: Match.Optional(Boolean),
-				}),
-			});
-			if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) {
-				throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed');
-			}
-			const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId;
-			if (!Users.findOneById(userId)) {
-				throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users');
-			}
-
-			Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data));
-			const user = Users.findOneById(userId, {
-				fields: {
-					'settings.preferences': 1,
-					'language': 1,
-				},
-			});
-			return API.v1.success({
-				user: {
-					_id: user._id,
-					settings: {
-						preferences: {
-							...user.settings.preferences,
-							language: user.language,
-						},
-					},
-				},
-			});
-		},
-	},
-);
-
 API.v1.addRoute(
 	'users.forgotPassword',
 	{ authRequired: false },
@@ -810,7 +586,7 @@ API.v1.addRoute(
 	{ authRequired: true },
 	{
 		get() {
-			const result = Meteor.runAsUser(this.userId, () => Meteor.call('getUsernameSuggestion'));
+			const result = Meteor.call('getUsernameSuggestion');
 
 			return API.v1.success({ result });
 		},
@@ -826,7 +602,7 @@ API.v1.addRoute(
 			if (!tokenName) {
 				return API.v1.failure("The 'tokenName' param is required");
 			}
-			const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }));
+			const token = Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor });
 
 			return API.v1.success({ token });
 		},
@@ -842,7 +618,7 @@ API.v1.addRoute(
 			if (!tokenName) {
 				return API.v1.failure("The 'tokenName' param is required");
 			}
-			const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:regenerateToken', { tokenName }));
+			const token = Meteor.call('personalAccessTokens:regenerateToken', { tokenName });
 
 			return API.v1.success({ token });
 		},
@@ -857,19 +633,19 @@ API.v1.addRoute(
 			if (!hasPermission(this.userId, 'create-personal-access-tokens')) {
 				throw new Meteor.Error('not-authorized', 'Not Authorized');
 			}
-			const loginTokens = Users.getLoginTokensByUserId(this.userId).fetch()[0];
-			const getPersonalAccessTokens = () =>
-				loginTokens.services.resume.loginTokens
-					.filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken')
-					.map((loginToken) => ({
-						name: loginToken.name,
-						createdAt: loginToken.createdAt,
-						lastTokenPart: loginToken.lastTokenPart,
-						bypassTwoFactor: loginToken.bypassTwoFactor,
-					}));
+
+			const user = Users.getLoginTokensByUserId(this.userId).fetch()[0] as IUser | undefined;
 
 			return API.v1.success({
-				tokens: loginTokens ? getPersonalAccessTokens() : [],
+				tokens:
+					user?.services?.resume?.loginTokens
+						?.filter((loginToken: any) => loginToken.type === 'personalAccessToken')
+						.map((loginToken: IPersonalAccessToken) => ({
+							name: loginToken.name,
+							createdAt: loginToken.createdAt.toISOString(),
+							lastTokenPart: loginToken.lastTokenPart,
+							bypassTwoFactor: Boolean(loginToken.bypassTwoFactor),
+						})) || [],
 			});
 		},
 	},
@@ -884,11 +660,9 @@ API.v1.addRoute(
 			if (!tokenName) {
 				return API.v1.failure("The 'tokenName' param is required");
 			}
-			Meteor.runAsUser(this.userId, () =>
-				Meteor.call('personalAccessTokens:removeToken', {
-					tokenName,
-				}),
-			);
+			Meteor.call('personalAccessTokens:removeToken', {
+				tokenName,
+			});
 
 			return API.v1.success();
 		},
@@ -931,7 +705,7 @@ API.v1.addRoute('users.2fa.sendEmailCode', {
 		const userId = this.userId || Users[method](emailOrUsername, { fields: { _id: 1 } })?._id;
 
 		if (!userId) {
-			this.logger.error('[2fa] User was not found when requesting 2fa email code');
+			// this.logger.error('[2fa] User was not found when requesting 2fa email code');
 			return API.v1.success();
 		}
 
@@ -968,7 +742,7 @@ API.v1.addRoute(
 
 			if (from) {
 				const ts = new Date(from);
-				const diff = (Date.now() - ts) / 1000 / 60;
+				const diff = (Date.now() - Number(ts)) / 1000 / 60;
 
 				if (diff < 10) {
 					return API.v1.success({
@@ -992,10 +766,13 @@ API.v1.addRoute(
 	{
 		get() {
 			const { fullExport = false } = this.queryParams;
-			const result = Meteor.runAsUser(this.userId, () => Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' }));
+			const result = Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' }) as {
+				requested: boolean;
+				exportOperation: IExportOperation;
+			};
 
 			return API.v1.success({
-				requested: result.requested,
+				requested: Boolean(result.requested),
 				exportOperation: result.exportOperation,
 			});
 		},
@@ -1007,48 +784,43 @@ API.v1.addRoute(
 	{ authRequired: true },
 	{
 		async post() {
-			try {
-				const hashedToken = Accounts._hashLoginToken(this.request.headers['x-auth-token']);
+			const xAuthToken = this.request.headers['x-auth-token'] as string;
 
-				if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) {
-					throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
-				}
+			if (!xAuthToken) {
+				throw new Meteor.Error('error-parameter-required', 'x-auth-token is required');
+			}
+			const hashedToken = Accounts._hashLoginToken(xAuthToken);
 
-				const me = await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } });
+			if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) {
+				throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
+			}
 
-				const token = me.services.resume.loginTokens.find((token) => token.hashedToken === hashedToken);
+			const me = (await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick<IUser, 'services'>;
 
-				const tokenExpires = new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000);
+			const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken);
 
-				return API.v1.success({
-					token: this.request.headers['x-auth-token'],
-					tokenExpires,
-				});
-			} catch (error) {
-				return API.v1.failure(error);
-			}
+			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+			const tokenExpires = new Date(token!.when.getTime() + settings.get<number>('Accounts_LoginExpiration') * 1000);
+
+			return API.v1.success({
+				token: xAuthToken,
+				tokenExpires: tokenExpires.toISOString() || '',
+			});
 		},
 	},
 );
 
 API.v1.addRoute(
 	'users.autocomplete',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUsersAutocompleteProps },
 	{
-		get() {
+		async get() {
 			const { selector } = this.queryParams;
-
-			if (!selector) {
-				return API.v1.failure("The 'selector' param is required");
-			}
-
 			return API.v1.success(
-				Promise.await(
-					findUsersToAutocomplete({
-						uid: this.userId,
-						selector: JSON.parse(selector),
-					}),
-				),
+				await findUsersToAutocomplete({
+					uid: this.userId,
+					selector: JSON.parse(selector),
+				}),
 			);
 		},
 	},
@@ -1059,7 +831,7 @@ API.v1.addRoute(
 	{ authRequired: true },
 	{
 		post() {
-			API.v1.success(Meteor.call('removeOtherTokens'));
+			return API.v1.success(Meteor.call('removeOtherTokens'));
 		},
 	},
 );
@@ -1069,30 +841,28 @@ API.v1.addRoute(
 	{ authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } },
 	{
 		post() {
-			// reset own keys
-			if (this.isUserFromParams()) {
-				resetUserE2EEncriptionKey(this.userId, false);
-				return API.v1.success();
-			}
+			if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) {
+				// reset other user keys
+				const user = this.getUserFromParams();
+				if (!user) {
+					throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
+				}
 
-			// reset other user keys
-			const user = this.getUserFromParams();
-			if (!user) {
-				throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
-			}
+				if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) {
+					throw new Meteor.Error('error-not-allowed', 'Not allowed');
+				}
 
-			if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) {
-				throw new Meteor.Error('error-not-allowed', 'Not allowed');
-			}
+				if (!hasPermission(this.userId, 'edit-other-user-e2ee')) {
+					throw new Meteor.Error('error-not-allowed', 'Not allowed');
+				}
 
-			if (!hasPermission(Meteor.userId(), 'edit-other-user-e2ee')) {
-				throw new Meteor.Error('error-not-allowed', 'Not allowed');
-			}
+				if (!resetUserE2EEncriptionKey(user._id, true)) {
+					return API.v1.failure();
+				}
 
-			if (!resetUserE2EEncriptionKey(user._id, true)) {
-				return API.v1.failure();
+				return API.v1.success();
 			}
-
+			resetUserE2EEncriptionKey(this.userId, false);
 			return API.v1.success();
 		},
 	},
@@ -1102,29 +872,28 @@ API.v1.addRoute(
 	'users.resetTOTP',
 	{ authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } },
 	{
-		post() {
-			// reset own keys
-			if (this.isUserFromParams()) {
-				Promise.await(resetTOTP(this.userId, false));
-				return API.v1.success();
-			}
-
-			// reset other user keys
-			const user = this.getUserFromParams();
-			if (!user) {
-				throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
-			}
+		async post() {
+			// // reset own keys
+			if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) {
+				// reset other user keys
+				if (!hasPermission(this.userId, 'edit-other-user-totp')) {
+					throw new Meteor.Error('error-not-allowed', 'Not allowed');
+				}
 
-			if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) {
-				throw new Meteor.Error('error-not-allowed', 'Not allowed');
-			}
+				if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) {
+					throw new Meteor.Error('error-not-allowed', 'Not allowed');
+				}
 
-			if (!hasPermission(Meteor.userId(), 'edit-other-user-totp')) {
-				throw new Meteor.Error('error-not-allowed', 'Not allowed');
-			}
+				const user = this.getUserFromParams();
+				if (!user) {
+					throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
+				}
 
-			Promise.await(resetTOTP(user._id, true));
+				await resetTOTP(user._id, true);
 
+				return API.v1.success();
+			}
+			await resetTOTP(this.userId, false);
 			return API.v1.success();
 		},
 	},
@@ -1132,25 +901,22 @@ API.v1.addRoute(
 
 API.v1.addRoute(
 	'users.listTeams',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUsersListTeamsProps },
 	{
-		get() {
+		async get() {
 			check(
 				this.queryParams,
 				Match.ObjectIncluding({
 					userId: Match.Maybe(String),
 				}),
 			);
-			const { userId } = this.queryParams;
 
-			if (!userId) {
-				throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
-			}
+			const { userId } = this.queryParams;
 
 			// If the caller has permission to view all teams, there's no need to filter the teams
 			const adminId = hasPermission(this.userId, 'view-all-teams') ? undefined : this.userId;
 
-			const teams = Promise.await(Team.findBySubscribedUserIds(userId, adminId));
+			const teams = await Team.findBySubscribedUserIds(userId, adminId);
 
 			return API.v1.success({
 				teams,
@@ -1161,7 +927,7 @@ API.v1.addRoute(
 
 API.v1.addRoute(
 	'users.logout',
-	{ authRequired: true },
+	{ authRequired: true, validateParams: isUserLogoutParamsPOST },
 	{
 		post() {
 			const userId = this.bodyParams.userId || this.userId;
@@ -1182,7 +948,130 @@ API.v1.addRoute(
 	},
 );
 
-settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => {
+API.v1.addRoute(
+	'users.getPresence',
+	{ authRequired: true },
+	{
+		get() {
+			if (this.isUserFromParams()) {
+				const user = Users.findOneById(this.userId);
+				return API.v1.success({
+					presence: user.status || 'offline',
+					connectionStatus: user.statusConnection || 'offline',
+					...(user.lastLogin && { lastLogin: user.lastLogin }),
+				});
+			}
+
+			const user = this.getUserFromParams();
+
+			return API.v1.success({
+				presence: user.status || 'offline',
+			});
+		},
+	},
+);
+
+API.v1.addRoute(
+	'users.setStatus',
+	{ authRequired: true },
+	{
+		post() {
+			check(
+				this.bodyParams,
+				Match.ObjectIncluding({
+					status: Match.Maybe(String),
+					message: Match.Maybe(String),
+				}),
+			);
+
+			if (!settings.get('Accounts_AllowUserStatusMessageChange')) {
+				throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', {
+					method: 'users.setStatus',
+				});
+			}
+
+			const user = ((): IUser | undefined => {
+				if (this.isUserFromParams()) {
+					return Meteor.users.findOne(this.userId) as IUser;
+				}
+				if (hasPermission(this.userId, 'edit-other-user-info')) {
+					return this.getUserFromParams();
+				}
+			})();
+
+			if (user === undefined) {
+				return API.v1.unauthorized();
+			}
+
+			Meteor.runAsUser(user._id, () => {
+				if (this.bodyParams.message || this.bodyParams.message === '') {
+					setStatusText(user._id, this.bodyParams.message);
+				}
+				if (this.bodyParams.status) {
+					const validStatus = ['online', 'away', 'offline', 'busy'];
+					if (validStatus.includes(this.bodyParams.status)) {
+						const { status } = this.bodyParams;
+
+						if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) {
+							throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', {
+								method: 'users.setStatus',
+							});
+						}
+
+						Meteor.users.update(user._id, {
+							$set: {
+								status,
+								statusDefault: status,
+							},
+						});
+
+						setUserStatus(user, status);
+					} else {
+						throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', {
+							method: 'users.setStatus',
+						});
+					}
+				}
+			});
+
+			return API.v1.success();
+		},
+	},
+);
+
+// status: 'online' | 'offline' | 'away' | 'busy';
+// message?: string;
+// _id: string;
+// connectionStatus?: 'online' | 'offline' | 'away' | 'busy';
+// };
+
+API.v1.addRoute(
+	'users.getStatus',
+	{ authRequired: true },
+	{
+		get() {
+			if (this.isUserFromParams()) {
+				const user = Users.findOneById(this.userId);
+				return API.v1.success({
+					_id: user._id,
+					// message: user.statusText,
+					connectionStatus: (user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy',
+					status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy',
+				});
+			}
+
+			const user = this.getUserFromParams();
+
+			return API.v1.success({
+				_id: user._id,
+				// message: user.statusText,
+				status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy',
+			});
+		},
+	},
+);
+
+settings.watch<number>('Rate_Limiter_Limit_RegisterUser', (value) => {
 	const userRegisterRoute = '/api/v1/users.registerpost';
 
 	API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value);
diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.js b/apps/meteor/app/lib/server/functions/getFullUserData.ts
similarity index 65%
rename from apps/meteor/app/lib/server/functions/getFullUserData.js
rename to apps/meteor/app/lib/server/functions/getFullUserData.ts
index d328f3b05832484efd6cfa9f3db225ea4492426b..74bf410aee9beea7970abb36b75580dfd89c1f32 100644
--- a/apps/meteor/app/lib/server/functions/getFullUserData.js
+++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts
@@ -1,7 +1,9 @@
-import { Logger } from '../../../logger';
+import { IUser } from '@rocket.chat/core-typings';
+
+import { Logger } from '../../../logger/server';
 import { settings } from '../../../settings/server';
 import { Users } from '../../../models/server';
-import { hasPermission } from '../../../authorization';
+import { hasPermission } from '../../../authorization/server';
 
 const logger = new Logger('getFullUserData');
 
@@ -18,7 +20,7 @@ const defaultFields = {
 	statusText: 1,
 	avatarETag: 1,
 	extension: 1,
-};
+} as const;
 
 const fullFields = {
 	emails: 1,
@@ -31,12 +33,12 @@ const fullFields = {
 	requirePasswordChange: 1,
 	requirePasswordChangeReason: 1,
 	roles: 1,
-};
+} as const;
 
-let publicCustomFields = {};
-let customFields = {};
+let publicCustomFields: Record<string, 0 | 1> = {};
+let customFields: Record<string, 0 | 1> = {};
 
-settings.watch('Accounts_CustomFields', (value) => {
+settings.watch<string>('Accounts_CustomFields', (value) => {
 	publicCustomFields = {};
 	customFields = {};
 
@@ -58,29 +60,23 @@ settings.watch('Accounts_CustomFields', (value) => {
 	}
 });
 
-const getCustomFields = (canViewAllInfo) => (canViewAllInfo ? customFields : publicCustomFields);
+const getCustomFields = (canViewAllInfo: boolean): Record<string, 0 | 1> => (canViewAllInfo ? customFields : publicCustomFields);
 
-const getFields = (canViewAllInfo) => ({
+const getFields = (canViewAllInfo: boolean): Record<string, 0 | 1> => ({
 	...defaultFields,
 	...(canViewAllInfo && fullFields),
 	...getCustomFields(canViewAllInfo),
 });
 
-const removePasswordInfo = (user) => {
-	if (user && user.services) {
-		delete user.services.password;
-		delete user.services.email;
-		delete user.services.resume;
-		delete user.services.emailCode;
-		delete user.services.cloud;
-		delete user.services.email2fa;
-		delete user.services.totp;
-	}
-
-	return user;
+const removePasswordInfo = (user: IUser): Omit<IUser, 'services'> => {
+	const { services, ...result } = user;
+	return result;
 };
 
-export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername }) {
+export async function getFullUserDataByIdOrUsername(
+	userId: string,
+	{ filterId, filterUsername }: { filterId: string; filterUsername?: undefined } | { filterId?: undefined; filterUsername: string },
+): Promise<IUser | null> {
 	const caller = Users.findOneById(userId, { fields: { username: 1 } });
 	const targetUser = filterId || filterUsername;
 	const myself = (filterId && targetUser === userId) || (filterUsername && targetUser === caller.username);
diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts
index e72b847c5cee2aba0d56eb0de9213126d87a7a4b..4718303b21caa0f9ecf6e8ce2441c8a9423bd330 100644
--- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts
+++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts
@@ -8,12 +8,26 @@ import { SystemLogger } from '../../../../server/lib/logger/system';
 import { api } from '../../../../server/sdk/api';
 import { fetch } from '../../../../server/lib/http/fetch';
 
-export const setUserAvatar = function (
+export function setUserAvatar(
+	user: Pick<IUser, '_id' | 'username'>,
+	dataURI: Buffer,
+	contentType: string,
+	service: 'rest',
+	etag?: string,
+): void;
+export function setUserAvatar(
 	user: Pick<IUser, '_id' | 'username'>,
 	dataURI: string,
 	contentType: string,
 	service: 'initials' | 'url' | 'rest' | string,
 	etag?: string,
+): void;
+export function setUserAvatar(
+	user: Pick<IUser, '_id' | 'username'>,
+	dataURI: string | Buffer,
+	contentType: string,
+	service: 'initials' | 'url' | 'rest' | string,
+	etag?: string,
 ): void {
 	if (service === 'initials') {
 		Users.setAvatarData(user._id, service, null);
@@ -22,7 +36,7 @@ export const setUserAvatar = function (
 
 	const { buffer, type } = Promise.await(
 		(async (): Promise<{ buffer: Buffer; type: string }> => {
-			if (service === 'url') {
+			if (service === 'url' && typeof dataURI === 'string') {
 				let response: Response;
 				try {
 					response = await fetch(dataURI);
@@ -69,7 +83,7 @@ export const setUserAvatar = function (
 
 			if (service === 'rest') {
 				return {
-					buffer: Buffer.from(dataURI, 'binary'),
+					buffer: dataURI instanceof Buffer ? dataURI : Buffer.from(dataURI, 'binary'),
 					type: contentType,
 				};
 			}
@@ -103,4 +117,4 @@ export const setUserAvatar = function (
 			avatarETag,
 		});
 	}, 500);
-};
+}
diff --git a/apps/meteor/client/lib/presence.ts b/apps/meteor/client/lib/presence.ts
index 5c81820e7e4e2864e3c33fd2b6ba1a8723845196..4a14c1478b2838c327ff66bf68756976ad8982c7 100644
--- a/apps/meteor/client/lib/presence.ts
+++ b/apps/meteor/client/lib/presence.ts
@@ -27,11 +27,6 @@ export type UserPresence = Readonly<
 	Partial<Pick<IUser, 'name' | 'status' | 'utcOffset' | 'statusText' | 'avatarETag' | 'roles' | 'username'>> & Required<Pick<IUser, '_id'>>
 >;
 
-type UsersPresencePayload = {
-	users: UserPresence[];
-	full: boolean;
-};
-
 const isUid = (eventType: keyof Events): eventType is UserPresence['_id'] =>
 	Boolean(eventType) && typeof eventType === 'string' && !['reset', 'restart', 'remove'].includes(eventType);
 
@@ -51,15 +46,6 @@ const notify = (presence: UserPresence): void => {
 	}
 };
 
-declare module '@rocket.chat/rest-typings' {
-	// eslint-disable-next-line @typescript-eslint/interface-name-prefix
-	export interface Endpoints {
-		'/v1/users.presence': {
-			GET: (params: { ids: string[] }) => UsersPresencePayload;
-		};
-	}
-}
-
 const getPresence = ((): ((uid: UserPresence['_id']) => void) => {
 	let timer: ReturnType<typeof setTimeout>;
 
diff --git a/apps/meteor/server/sdk/types/ITeamService.ts b/apps/meteor/server/sdk/types/ITeamService.ts
index 1564ae5c6cdd01c64a882f20f9b7ddbccb491848..607413f82889ecaa7403bc2ddaa0857e93aeaddc 100644
--- a/apps/meteor/server/sdk/types/ITeamService.ts
+++ b/apps/meteor/server/sdk/types/ITeamService.ts
@@ -112,4 +112,5 @@ export interface ITeamService {
 	removeAllMembersFromTeam(teamId: string): Promise<void>;
 	removeRolesFromMember(teamId: string, userId: string, roles: Array<IRole['_id']>): Promise<boolean>;
 	getStatistics(): Promise<ITeamStats>;
+	findBySubscribedUserIds(userId: string, callerId?: string): Promise<ITeam[]>;
 }
diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js
index 6efd6989c96934932a2f7b80ab5cf24ca774c8dc..77949612333b156b87b3c316abb86fceba6973de 100644
--- a/apps/meteor/tests/end-to-end/api/01-users.js
+++ b/apps/meteor/tests/end-to-end/api/01-users.js
@@ -328,6 +328,7 @@ describe('[Users]', function () {
 					email,
 					name: 'name',
 					username,
+					pass: 'test',
 				})
 				.expect('Content-Type', 'application/json')
 				.expect(400)
@@ -3117,7 +3118,6 @@ describe('[Users]', function () {
 					.expect(400)
 					.expect((res) => {
 						expect(res.body).to.have.property('success', false);
-						expect(res.body.error).to.be.equal("The 'selector' param is required");
 					})
 					.end(done);
 			});
diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts
index 0dde24a2d7ba99e8de299397c06cffedb9adbb87..15cb0d6bf9ee9508046fc6d8af950619c00c9509 100644
--- a/packages/core-typings/src/IUser.ts
+++ b/packages/core-typings/src/IUser.ts
@@ -54,6 +54,7 @@ export interface IUserServices {
 	resume?: {
 		loginTokens?: LoginToken[];
 	};
+	cloud?: unknown;
 	google?: any;
 	facebook?: any;
 	github?: any;
diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts
index e50250dbb060d568d31e9e75873975f125234b3c..8ee8f93a7c502d6dbce9ed2e89abc91c9db83fd1 100644
--- a/packages/rest-typings/src/index.ts
+++ b/packages/rest-typings/src/index.ts
@@ -176,3 +176,9 @@ export * from './helpers/PaginatedRequest';
 export * from './helpers/PaginatedResult';
 export * from './helpers/ReplacePlaceholders';
 export * from './v1/emojiCustom';
+
+export * from './v1/users';
+export * from './v1/users/UsersSetAvatarParamsPOST';
+export * from './v1/users/UsersSetPreferenceParamsPOST';
+export * from './v1/users/UsersUpdateOwnBasicInfoParamsPOST';
+export * from './v1/users/UsersUpdateParamsPOST';
diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts
index 9d9413034dde5d159d5ec8a1b9b0a0c4961a69f4..3a49e8c38ceba2f8dfd3fb37001732fd3da767d9 100644
--- a/packages/rest-typings/src/v1/users.ts
+++ b/packages/rest-typings/src/v1/users.ts
@@ -1,6 +1,14 @@
-import type { ITeam, IUser } from '@rocket.chat/core-typings';
+import type { IExportOperation, ISubscription, ITeam, IUser } from '@rocket.chat/core-typings';
 import Ajv from 'ajv';
 
+import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST';
+import type { UserDeactivateIdleParamsPOST } from './users/UserDeactivateIdleParamsPOST';
+import type { UserLogoutParamsPOST } from './users/UserLogoutParamsPOST';
+import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST';
+import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET';
+import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusParamsPOST';
+import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet';
+import type { UsersListTeamsParamsGET } from './users/UsersListTeamsParamsGET';
 import type { PaginatedRequest } from '../helpers/PaginatedRequest';
 import type { PaginatedResult } from '../helpers/PaginatedResult';
 
@@ -43,36 +51,6 @@ const Users2faSendEmailCodeSchema = {
 
 export const isUsers2faSendEmailCodeProps = ajv.compile<Users2faSendEmailCode>(Users2faSendEmailCodeSchema);
 
-type UsersAutocomplete = { selector: string };
-
-const UsersAutocompleteSchema = {
-	type: 'object',
-	properties: {
-		selector: {
-			type: 'string',
-		},
-	},
-	required: ['selector'],
-	additionalProperties: false,
-};
-
-export const isUsersAutocompleteProps = ajv.compile<UsersAutocomplete>(UsersAutocompleteSchema);
-
-type UsersListTeams = { userId: IUser['_id'] };
-
-const UsersListTeamsSchema = {
-	type: 'object',
-	properties: {
-		userId: {
-			type: 'string',
-		},
-	},
-	required: ['userId'],
-	additionalProperties: false,
-};
-
-export const isUsersListTeamsProps = ajv.compile<UsersListTeams>(UsersListTeamsSchema);
-
 type UsersSetAvatar = { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string };
 
 const UsersSetAvatarSchema = {
@@ -127,31 +105,208 @@ export type UserPresence = Readonly<
 >;
 
 export type UsersEndpoints = {
-	'/v1/users.info': {
-		GET: (params: UsersInfo) => {
-			user: IUser;
-		};
+	'/v1/users.2fa.enableEmail': {
+		POST: () => void;
+	};
+
+	'/v1/users.2fa.disableEmail': {
+		POST: () => void;
 	};
+
 	'/v1/users.2fa.sendEmailCode': {
 		POST: (params: Users2faSendEmailCode) => void;
 	};
+
+	'/v1/users.listTeams': {
+		GET: (params: UsersListTeamsParamsGET) => { teams: ITeam[] };
+	};
 	'/v1/users.autocomplete': {
-		GET: (params: UsersAutocomplete) => {
+		GET: (params: UsersAutocompleteParamsGET) => {
 			items: Required<Pick<IUser, '_id' | 'name' | 'username' | 'nickname' | 'status' | 'avatarETag'>>[];
 		};
 	};
+
 	'/v1/users.list': {
 		GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{
 			users: Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'roles' | 'emails' | 'active' | 'avatarETag'>[];
 		}>;
 	};
-	'/v1/users.listTeams': {
-		GET: (params: UsersListTeams) => { teams: Array<ITeam> };
-	};
+
 	'/v1/users.setAvatar': {
 		POST: (params: UsersSetAvatar) => void;
 	};
 	'/v1/users.resetAvatar': {
 		POST: (params: UsersResetAvatar) => void;
 	};
+
+	'/v1/users.requestDataDownload': {
+		GET: (params: { fullExport?: 'true' | 'false' }) => {
+			requested: boolean;
+			exportOperation: IExportOperation;
+		};
+	};
+	'/v1/users.logoutOtherClients': {
+		POST: () => {
+			token: string;
+			tokenExpires: string;
+		};
+	};
+	'/v1/users.removeOtherTokens': {
+		POST: () => void;
+	};
+	'/v1/users.resetE2EKey': {
+		POST: (
+			params:
+				| {
+						userId: string;
+				  }
+				| {
+						username: string;
+				  }
+				| {
+						user: string;
+				  },
+		) => void;
+	};
+	'/v1/users.resetTOTP': {
+		POST: (
+			params:
+				| {
+						userId: string;
+				  }
+				| {
+						username: string;
+				  }
+				| {
+						user: string;
+				  },
+		) => void;
+	};
+
+	'/v1/users.presence': {
+		GET: (params: { from?: string; ids: string | string[] }) => UsersPresencePayload;
+	};
+
+	'/v1/users.removePersonalAccessToken': {
+		POST: (params: { tokenName: string }) => void;
+	};
+
+	'/v1/users.getPersonalAccessTokens': {
+		GET: () => {
+			tokens: {
+				name?: string;
+				createdAt: string;
+				lastTokenPart: string;
+				bypassTwoFactor: boolean;
+			}[];
+		};
+	};
+	'/v1/users.regeneratePersonalAccessToken': {
+		POST: (params: { tokenName: string }) => {
+			token: string;
+		};
+	};
+	'/v1/users.generatePersonalAccessToken': {
+		POST: (params: { tokenName: string; bypassTwoFactor: boolean }) => {
+			token: string;
+		};
+	};
+	'/v1/users.getUsernameSuggestion': {
+		GET: () => {
+			result: string;
+		};
+	};
+	'/v1/users.forgotPassword': {
+		POST: (params: { email: string }) => void;
+	};
+	'/v1/users.getPreferences': {
+		GET: () => {
+			preferences: Required<IUser>['settings']['preferences'];
+		};
+	};
+	'/v1/users.createToken': {
+		POST: () => {
+			data: {
+				userId: string;
+				authToken: string;
+			};
+		};
+	};
+
+	'/v1/users.create': {
+		POST: (params: UserCreateParamsPOST) => {
+			user: IUser;
+		};
+	};
+
+	'/v1/users.setActiveStatus': {
+		POST: (params: UserSetActiveStatusParamsPOST) => {
+			user: IUser;
+		};
+	};
+
+	'/v1/users.deactivateIdle': {
+		POST: (params: UserDeactivateIdleParamsPOST) => {
+			count: number;
+		};
+	};
+
+	'/v1/users.getPresence': {
+		GET: (
+			params:
+				| {
+						userId: string;
+				  }
+				| {
+						username: string;
+				  }
+				| {
+						user: string;
+				  },
+		) => {
+			presence: 'online' | 'offline' | 'away' | 'busy';
+			connectionStatus?: 'online' | 'offline' | 'away' | 'busy';
+			lastLogin?: string;
+		};
+	};
+
+	'/v1/users.setStatus': {
+		POST: (params: { message?: string; status?: 'online' | 'offline' | 'away' | 'busy' }) => void;
+	};
+
+	'/v1/users.getStatus': {
+		GET: () => {
+			status: 'online' | 'offline' | 'away' | 'busy';
+			message?: string;
+			_id: string;
+			connectionStatus?: 'online' | 'offline' | 'away' | 'busy';
+		};
+	};
+
+	'/v1/users.info': {
+		GET: (params: UsersInfoParamsGet) => {
+			user: IUser & { rooms?: Pick<ISubscription, 'rid' | 'name' | 't' | 'roles' | 'unread'>[] };
+		};
+	};
+
+	'/v1/users.register': {
+		POST: (params: UserRegisterParamsPOST) => {
+			user: Partial<IUser>;
+		};
+	};
+
+	'/v1/users.logout': {
+		POST: (params: UserLogoutParamsPOST) => {
+			message: string;
+		};
+	};
 };
+
+export * from './users/UserCreateParamsPOST';
+export * from './users/UserSetActiveStatusParamsPOST';
+export * from './users/UserDeactivateIdleParamsPOST';
+export * from './users/UsersInfoParamsGet';
+export * from './users/UserRegisterParamsPOST';
+export * from './users/UserLogoutParamsPOST';
+export * from './users/UsersListTeamsParamsGET';
+export * from './users/UsersAutocompleteParamsGET';
diff --git a/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..347498999011755e04b7e42562f6003a832e6324
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts
@@ -0,0 +1,51 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserCreateParamsPOST = {
+	email: string;
+	name: string;
+	password: string;
+	username: string;
+	active?: boolean;
+	bio?: string;
+	nickname?: string;
+	statusText?: string;
+	roles?: string[];
+	joinDefaultChannels?: boolean;
+	requirePasswordChange?: boolean;
+	setRandomPassword?: boolean;
+	sendWelcomeEmail?: boolean;
+	verified?: boolean;
+	customFields?: object;
+	/* @deprecated */
+	fields: string;
+};
+
+const userCreateParamsPostSchema = {
+	type: 'object',
+	properties: {
+		email: { type: 'string' },
+		name: { type: 'string' },
+		password: { type: 'string' },
+		username: { type: 'string' },
+		active: { type: 'boolean', nullable: true },
+		bio: { type: 'string', nullable: true },
+		nickname: { type: 'string', nullable: true },
+		statusText: { type: 'string', nullable: true },
+		roles: { type: 'array', items: { type: 'string' } },
+		joinDefaultChannels: { type: 'boolean', nullable: true },
+		requirePasswordChange: { type: 'boolean', nullable: true },
+		setRandomPassword: { type: 'boolean', nullable: true },
+		sendWelcomeEmail: { type: 'boolean', nullable: true },
+		verified: { type: 'boolean', nullable: true },
+		customFields: { type: 'object' },
+		fields: { type: 'string', nullable: true },
+	},
+	additionalProperties: false,
+	required: ['email', 'name', 'password', 'username'],
+};
+
+export const isUserCreateParamsPOST = ajv.compile<UserCreateParamsPOST>(userCreateParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UserDeactivateIdleParamsPOST.ts b/packages/rest-typings/src/v1/users/UserDeactivateIdleParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b7fb03ed5d1ca8587f39d95f5aa11d4232bf8c8
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UserDeactivateIdleParamsPOST.ts
@@ -0,0 +1,26 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserDeactivateIdleParamsPOST = {
+	daysIdle: number;
+	role?: string;
+};
+
+const userDeactivateIdleSchema = {
+	type: 'object',
+	properties: {
+		daysIdle: {
+			type: 'number',
+		},
+		role: {
+			type: 'string',
+		},
+	},
+	required: ['daysIdle'],
+	additionalProperties: false,
+};
+
+export const isUserDeactivateIdleParamsPOST = ajv.compile<UserDeactivateIdleParamsPOST>(userDeactivateIdleSchema);
diff --git a/packages/rest-typings/src/v1/users/UserLogoutParamsPOST.ts b/packages/rest-typings/src/v1/users/UserLogoutParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd51b37495d655fd594064461c4f0ffb6873af23
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UserLogoutParamsPOST.ts
@@ -0,0 +1,22 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserLogoutParamsPOST = {
+	userId?: string;
+};
+
+const UserLogoutParamsPostSchema = {
+	type: 'object',
+	properties: {
+		userId: {
+			type: 'string',
+			nullable: true,
+		},
+	},
+	required: [],
+};
+
+export const isUserLogoutParamsPOST = ajv.compile<UserLogoutParamsPOST>(UserLogoutParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UserRegisterParamsPOST.ts b/packages/rest-typings/src/v1/users/UserRegisterParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0551e7839206b71f8892bbb94815e6a67c4288dc
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UserRegisterParamsPOST.ts
@@ -0,0 +1,47 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserRegisterParamsPOST = {
+	username: string;
+	name?: string;
+	email: string;
+	pass: string;
+	secret?: string;
+	reason?: string;
+};
+
+const UserRegisterParamsPostSchema = {
+	type: 'object',
+	properties: {
+		username: {
+			type: 'string',
+			minLength: 3,
+		},
+
+		name: {
+			type: 'string',
+			nullable: true,
+		},
+		email: {
+			type: 'string',
+		},
+		pass: {
+			type: 'string',
+		},
+		secret: {
+			type: 'string',
+			nullable: true,
+		},
+		reason: {
+			type: 'string',
+			nullable: true,
+		},
+	},
+	required: ['username', 'email', 'pass'],
+	additionalProperties: false,
+};
+
+export const isUserRegisterParamsPOST = ajv.compile<UserRegisterParamsPOST>(UserRegisterParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UserSetActiveStatusParamsPOST.ts b/packages/rest-typings/src/v1/users/UserSetActiveStatusParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ad503f5d29848f368342974d72fe64b85e3aa305
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UserSetActiveStatusParamsPOST.ts
@@ -0,0 +1,24 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserSetActiveStatusParamsPOST = {
+	userId: string;
+	activeStatus: boolean;
+	confirmRelinquish?: boolean;
+};
+
+const UserCreateParamsPostSchema = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string' },
+		activeStatus: { type: 'boolean' },
+		confirmRelinquish: { type: 'boolean', nullable: true },
+	},
+	required: ['userId', 'activeStatus'],
+	additionalProperties: false,
+};
+
+export const isUserSetActiveStatusParamsPOST = ajv.compile<UserSetActiveStatusParamsPOST>(UserCreateParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersAutocompleteParamsGET.ts b/packages/rest-typings/src/v1/users/UsersAutocompleteParamsGET.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bdb6578db24d903f15d6cc2005e71eda9b77c375
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersAutocompleteParamsGET.ts
@@ -0,0 +1,20 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersAutocompleteParamsGET = { selector: string };
+
+const UsersAutocompleteParamsGetSchema = {
+	type: 'object',
+	properties: {
+		selector: {
+			type: 'string',
+		},
+	},
+	required: ['selector'],
+	additionalProperties: false,
+};
+
+export const isUsersAutocompleteProps = ajv.compile<UsersAutocompleteParamsGET>(UsersAutocompleteParamsGetSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersInfoParamsGet.ts b/packages/rest-typings/src/v1/users/UsersInfoParamsGet.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ddae73750ea8688dcb7a4a05c4de0672ad806bfd
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersInfoParamsGet.ts
@@ -0,0 +1,44 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersInfoParamsGet = ({ userId: string } | { username: string }) & {
+	fields?: string;
+};
+
+const UsersInfoParamsGetSchema = {
+	anyOf: [
+		{
+			type: 'object',
+			properties: {
+				userId: {
+					type: 'string',
+				},
+				fields: {
+					type: 'string',
+					nullable: true,
+				},
+			},
+			required: ['userId'],
+			additionalProperties: false,
+		},
+		{
+			type: 'object',
+			properties: {
+				username: {
+					type: 'string',
+				},
+				fields: {
+					type: 'string',
+					nullable: true,
+				},
+			},
+			required: ['username'],
+			additionalProperties: false,
+		},
+	],
+};
+
+export const isUsersInfoParamsGetProps = ajv.compile<UsersInfoParamsGet>(UsersInfoParamsGetSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersListTeamsParamsGET.ts b/packages/rest-typings/src/v1/users/UsersListTeamsParamsGET.ts
new file mode 100644
index 0000000000000000000000000000000000000000..336fd6b3144a34b005829d23e8b9afde9a066a15
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersListTeamsParamsGET.ts
@@ -0,0 +1,21 @@
+import Ajv from 'ajv';
+import type { IUser } from '@rocket.chat/core-typings';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersListTeamsParamsGET = { userId: IUser['_id'] };
+
+const UsersListTeamsParamsGetSchema = {
+	type: 'object',
+	properties: {
+		userId: {
+			type: 'string',
+		},
+	},
+	required: ['userId'],
+	additionalProperties: false,
+};
+
+export const isUsersListTeamsProps = ajv.compile<UsersListTeamsParamsGET>(UsersListTeamsParamsGetSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersSetAvatarParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetAvatarParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9a43dc7b07f35c4c90247c6cb7542f89bc219ba1
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersSetAvatarParamsPOST.ts
@@ -0,0 +1,33 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UserSetAvatarParamsPOST = {
+	avatarUrl?: string;
+	userId?: string;
+	username?: string;
+};
+
+const UserSetAvatarParamsPostSchema = {
+	type: 'object',
+	properties: {
+		avatarUrl: {
+			type: 'string',
+			nullable: true,
+		},
+		userId: {
+			type: 'string',
+			nullable: true,
+		},
+		username: {
+			type: 'string',
+			nullable: true,
+		},
+	},
+	required: [],
+	additionalProperties: false,
+};
+
+export const isUserSetAvatarParamsPOST = ajv.compile<UserSetAvatarParamsPOST>(UserSetAvatarParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5379276b4dee6758d498337cfe0847bed5806803
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts
@@ -0,0 +1,190 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersSetPreferencesParamsPOST = {
+	userId?: string;
+	data: {
+		newRoomNotification?: string;
+		newMessageNotification?: string;
+		clockMode?: number;
+		useEmojis?: boolean;
+		convertAsciiEmoji?: boolean;
+		saveMobileBandwidth?: boolean;
+		collapseMediaByDefault?: boolean;
+		autoImageLoad?: boolean;
+		emailNotificationMode?: string;
+		unreadAlert?: boolean;
+		notificationsSoundVolume?: number;
+		desktopNotifications?: string;
+		pushNotifications?: string;
+		enableAutoAway?: boolean;
+		highlights?: string[];
+		desktopNotificationRequireInteraction?: boolean;
+		messageViewMode?: number;
+		showMessageInMainThread?: boolean;
+		hideUsernames?: boolean;
+		hideRoles?: boolean;
+		displayAvatars?: boolean;
+		hideFlexTab?: boolean;
+		sendOnEnter?: string;
+		language?: string;
+		sidebarShowFavorites?: boolean;
+		sidebarShowUnread?: boolean;
+		sidebarSortby?: string;
+		sidebarViewMode?: string;
+		sidebarDisplayAvatar?: boolean;
+		sidebarGroupByType?: boolean;
+		muteFocusedConversations?: boolean;
+	};
+};
+
+const UsersSetPreferencesParamsPostSchema = {
+	type: 'object',
+	properties: {
+		userId: {
+			type: 'string',
+			nullable: true,
+		},
+		data: {
+			type: 'object',
+			properties: {
+				newRoomNotification: {
+					type: 'string',
+					nullable: true,
+				},
+				newMessageNotification: {
+					type: 'string',
+					nullable: true,
+				},
+				clockMode: {
+					type: 'number',
+					nullable: true,
+				},
+				useEmojis: {
+					type: 'boolean',
+					nullable: true,
+				},
+				convertAsciiEmoji: {
+					type: 'boolean',
+					nullable: true,
+				},
+				saveMobileBandwidth: {
+					type: 'boolean',
+					nullable: true,
+				},
+				collapseMediaByDefault: {
+					type: 'boolean',
+					nullable: true,
+				},
+				autoImageLoad: {
+					type: 'boolean',
+					nullable: true,
+				},
+				emailNotificationMode: {
+					type: 'string',
+					nullable: true,
+				},
+				unreadAlert: {
+					type: 'boolean',
+					nullable: true,
+				},
+				notificationsSoundVolume: {
+					type: 'number',
+					nullable: true,
+				},
+				desktopNotifications: {
+					type: 'string',
+					nullable: true,
+				},
+				pushNotifications: {
+					type: 'string',
+					nullable: true,
+				},
+				enableAutoAway: {
+					type: 'boolean',
+					nullable: true,
+				},
+				highlights: {
+					type: 'array',
+					items: {
+						type: 'string',
+					},
+					nullable: true,
+				},
+				desktopNotificationRequireInteraction: {
+					type: 'boolean',
+					nullable: true,
+				},
+				messageViewMode: {
+					type: 'number',
+					nullable: true,
+				},
+				showMessageInMainThread: {
+					type: 'boolean',
+					nullable: true,
+				},
+				hideUsernames: {
+					type: 'boolean',
+					nullable: true,
+				},
+				hideRoles: {
+					type: 'boolean',
+					nullable: true,
+				},
+				displayAvatars: {
+					type: 'boolean',
+					nullable: true,
+				},
+				hideFlexTab: {
+					type: 'boolean',
+					nullable: true,
+				},
+				sendOnEnter: {
+					type: 'string',
+					nullable: true,
+				},
+				language: {
+					type: 'string',
+					nullable: true,
+				},
+				sidebarShowFavorites: {
+					type: 'boolean',
+					nullable: true,
+				},
+				sidebarShowUnread: {
+					type: 'boolean',
+					nullable: true,
+				},
+				sidebarSortby: {
+					type: 'string',
+					nullable: true,
+				},
+				sidebarViewMode: {
+					type: 'string',
+					nullable: true,
+				},
+				sidebarDisplayAvatar: {
+					type: 'boolean',
+					nullable: true,
+				},
+				sidebarGroupByType: {
+					type: 'boolean',
+					nullable: true,
+				},
+				muteFocusedConversations: {
+					type: 'boolean',
+					nullable: true,
+				},
+			},
+			required: [],
+			additionalProperties: false,
+		},
+	},
+	required: ['data'],
+	additionalProperties: false,
+};
+
+export const isUsersSetPreferencesParamsPOST = ajv.compile<UsersSetPreferencesParamsPOST>(UsersSetPreferencesParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90f80dedc13039ef05ab77e01c09da71a1a76dca
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts
@@ -0,0 +1,67 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersUpdateOwnBasicInfoParamsPOST = {
+	data: {
+		email?: string;
+		name?: string;
+		username?: string;
+		nickname?: string;
+		statusText?: string;
+		currentPassword?: string;
+		newPassword?: string;
+	};
+	customFields?: {};
+};
+
+const UsersUpdateOwnBasicInfoParamsPostSchema = {
+	type: 'object',
+	properties: {
+		data: {
+			type: 'object',
+			properties: {
+				email: {
+					type: 'string',
+					nullable: true,
+				},
+				name: {
+					type: 'string',
+					nullable: true,
+				},
+				username: {
+					type: 'string',
+					nullable: true,
+				},
+				nickname: {
+					type: 'string',
+					nullable: true,
+				},
+				statusText: {
+					type: 'string',
+					nullable: true,
+				},
+				currentPassword: {
+					type: 'string',
+					nullable: true,
+				},
+				newPassword: {
+					type: 'string',
+					nullable: true,
+				},
+			},
+			required: [],
+			additionalProperties: false,
+		},
+		customFields: {
+			type: 'object',
+			nullable: true,
+		},
+	},
+	required: ['data'],
+	additionalProperties: false,
+};
+
+export const isUsersUpdateOwnBasicInfoParamsPOST = ajv.compile<UsersUpdateOwnBasicInfoParamsPOST>(UsersUpdateOwnBasicInfoParamsPostSchema);
diff --git a/packages/rest-typings/src/v1/users/UsersUpdateParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersUpdateParamsPOST.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3d0d6c07b5becb0fb442536e824d5f570e86f518
--- /dev/null
+++ b/packages/rest-typings/src/v1/users/UsersUpdateParamsPOST.ts
@@ -0,0 +1,108 @@
+import Ajv from 'ajv';
+
+const ajv = new Ajv({
+	coerceTypes: true,
+});
+
+export type UsersUpdateParamsPOST = {
+	userId: string;
+	data: {
+		email?: string;
+		name?: string;
+		password?: string;
+		username?: string;
+		bio?: string;
+		nickname?: string;
+		statusText?: string;
+		active?: boolean;
+		roles?: string[];
+		joinDefaultChannels?: boolean;
+		requirePasswordChange?: boolean;
+		sendWelcomeEmail?: boolean;
+		verified?: boolean;
+		customFields?: {};
+	};
+	confirmRelinquish?: boolean;
+};
+
+const UsersUpdateParamsPostSchema = {
+	type: 'object',
+	properties: {
+		userId: {
+			type: 'string',
+		},
+		confirmRelinquish: {
+			type: 'boolean',
+		},
+		data: {
+			type: 'object',
+			properties: {
+				email: {
+					type: 'string',
+					nullable: true,
+				},
+				name: {
+					type: 'string',
+					nullable: true,
+				},
+				password: {
+					type: 'string',
+					nullable: true,
+				},
+				username: {
+					type: 'string',
+					nullable: true,
+				},
+				bio: {
+					type: 'string',
+					nullable: true,
+				},
+				nickname: {
+					type: 'string',
+					nullable: true,
+				},
+				statusText: {
+					type: 'string',
+					nullable: true,
+				},
+				active: {
+					type: 'boolean',
+					nullable: true,
+				},
+				roles: {
+					type: 'array',
+					items: {
+						type: 'string',
+					},
+					nullable: true,
+				},
+				joinDefaultChannels: {
+					type: 'boolean',
+					nullable: true,
+				},
+				requirePasswordChange: {
+					type: 'boolean',
+					nullable: true,
+				},
+				sendWelcomeEmail: {
+					type: 'boolean',
+					nullable: true,
+				},
+				verified: {
+					type: 'boolean',
+					nullable: true,
+				},
+				customFields: {
+					type: 'object',
+					nullable: true,
+				},
+			},
+			required: [],
+			additionalProperties: false,
+		},
+	},
+	required: ['userId', 'data'],
+	additionalProperties: false,
+};
+
+export const isUsersUpdateParamsPOST = ajv.compile<UsersUpdateParamsPOST>(UsersUpdateParamsPostSchema);